Jump to content
alank2

ProcessMessages doesn't seem to update controls

Recommended Posts

If I throw 3 buttons onto a form and set button 1's click to this, it seems the ProcessMessages doesn't fully update all the buttons status.  I can run this in bcb6 which is very old, but it behaves as expected, all 3 buttons go to gray/disabled, 3s goes by, then they are become active again, then another 3s delay before the form is available for another click.

 

In cppb10.3 however, something different happens.

 

The first time it is run, the first pass leaves button1 in a non normal state, but not disabled.  buttons2/3 unaffected.  When it gets to the 2nd pass where it sets the buttons to true, now it updates button1 to visible, and it disables button2 (from the earlier set to false!)

The second time it is run, the button1 goes disabled, but buttons 2/3 stay enabled, until the second pass where button1 goes enabled, but buttons 2/3 go disabled.

 

I think ProcessMessages was supposed to process all messages, right?

 

(and yes, the loop where I repeatedly call processmessages every 100ms, does update them more as expected, but not instantaneously)

 


void __fastcall TForm1::Button1Click(TObject *Sender)
{
  //int i1;

  Button1->Enabled=false;
  Button2->Enabled=false;
  Button3->Enabled=false;
  Application->ProcessMessages();
  Sleep(3000);

/*  for (i1=0; i1<30; i1++)
    {
      Application->ProcessMessages();
      Sleep(3000/30);
    }*/

  Button1->Enabled=true;
  Button2->Enabled=true;
  Button3->Enabled=true;
  Application->ProcessMessages();
  Sleep(3000);
}

 

 

Edited by alank2

Share this post


Link to post
6 hours ago, alank2 said:

I think ProcessMessages was supposed to process all messages, right?

Yes, it does.  But it can only handle messages that are already in the message queue (or synthetically generated by the queue) at the moment it is called.  It does not process future messages.  And the behavior you describe sounds like the actions are being delayed such that ProcessMessages() doesn't see all of them right away, which is why calling ProcessMessages() multiple times produces better results.

 

You really should not be using ProcessMessages() in this manner in the first place.  Break up your code logic using TThread::ForceQueue() or a TTimer or equivalent to allow control to flow back into the main message loop so it can continue to process new messages while your desired time interval is waiting to elapse.

 

For example, using TThread::ForceQueue():

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  Button1->Enabled = false;
  Button2->Enabled = false;
  Button3->Enabled = false;
  TThread::ForceQueue(nullptr, &Step2, 3000);
}

void __fastcall TForm1::Step2()
{
  Button1->Enabled = true;
  Button2->Enabled = true;
  Button3->Enabled = true;
  TThread::ForceQueue(&Step3, 3000);
}

void __fastcall TForm1::Step3()
{
  //...
}

// alternatively

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  Button1->Enabled = false;
  Button2->Enabled = false;
  Button3->Enabled = false;
  TThread::ForceQueue(nullptr,
                      [this](){
                        Button1->Enabled = true;
                        Button2->Enabled = true;
                        Button3->Enabled = true;
                        TThread::ForceQueue(nullpr,
                                            [](){
                                              //...
                                            },
                                            3000);
                      },
                      3000);
}

Or, using TTimer:

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  Button1->Enabled = false;
  Button2->Enabled = false;
  Button3->Enabled = false;
  //Timer1->Interval = 3000;
  Timer1->Tag = 1;
  Timer1->Enabled = true;
}

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
  if (Timer1->Tag == 1)
  {
    Button1->Enabled = true;
    Button2->Enabled = true;
    Button3->Enabled = true;
    //Timer1->Interval = 3000;
    Timer1->Tag = 2;
  }
  else
  {
    //...
    Timer1->Enabled = false;
    Timer1->Tag = 0;
  }
}

// alternatively...

void __fastcall TForm1::Button1Click(TObject *Sender)
{
  Button1->Enabled = false;
  Button2->Enabled = false;
  Button3->Enabled = false;
  //Timer1->Interval = 3000;
  Timer1->OnTimer = Step2;
  Timer1->Enabled = true;
}

void __fastcall TForm1::Step2(TObject *Sender)
{
  Button1->Enabled = true;
  Button2->Enabled = true;
  Button3->Enabled = true;
  //Timer1->Interval = 3000;
  Timer1->OnTimer = Step3;
}

void __fastcall TForm1::Step3(TObject *Sender)
{
  //...
  Timer1->Enabled = false;
  Timer1->OnTimer = nullptr;
}

 

 

Edited by Remy Lebeau
  • Like 3

Share this post


Link to post

Thanks Remy; sometimes I have a synchronous process and I want to make sure some VCL elements are updated beforehand.  This always worked fine in bcb6.

 

What could cause the Enabled change to _not_ create an immediate pending message?

Share this post


Link to post

I am using Delphi exclusively but I suppose it'll be similar in CB too.

Forget about ProcessMessages. If you really need, you can call Button1.Repaint for example. This will skip the message queue and simply "force" the control to refresh it's appearance.

  • Like 1

Share this post


Link to post

I don't use c++ to build interfaces, only for device interfacing. I normally use Delphi instead, but the principle is the same:
when you use any property of a component (but also functions, procedures, methods, etc ...) especially graphical components, i.e. which is executed "behind" the property it can be synchronous or asynchronous.

So you cannot in any way expect that your action will have an immediate response, especially with graphics. Normally the graphics have a lower priority level (let's call it that) than the standard code and it is still unlikely that the graphics will be able to follow the code in real time (and it doesn't even make sense normally).
To clarify the situation better, I present the use of the "SendMessage" and the "PostMessage". Both perform the same function, i.e. send messages to the queue.... but while SendMessage "makes sure" that the message is deposited and returns to the caller only after the message has been deposited in the queue (synchronous operation with your code), PostMessage instead sends it to the queue and returns immediately, without making sure that it is actually in the queue (the message will arrive in the queue asynchronously with respect to your code).
With SendMessage at the next line of your code it is certain that the message is present in the queue, with PostMessage it is not certain (and no time prediction can be made) that the message is present at the queue of the next line of your code.

  • Like 1

Share this post


Link to post

The Sleep(x) is just a replacement for some other process that will keep execution in a method.

 

The workflow is something like this:

 

User clicks a button.

I want to disable all buttons while this process runs so they are unable to click any of them, so I change them to disabled.

Then I want to make sure that the screen is updated before I call whatever it is that may take seconds to run.  Previously I could just set them to enabled=false and call ProcessMessages() once and it would update properly.

I realize that putting the "seconds to run" is more ideal in a thread, but that adds creating and managing a thread which I don't want to have to do for every process that takes a few seconds.

 

Essentially I am looking for a function that will make sure the UI is fully updated before a period of time where it won't be updated.

 

I tried using calling Repaint for all 3 buttons before a ProcessMessages, but it didn't work.  Whatever is different between versions, perhaps changing a property from true to false takes a number of things to occur in sequence that rely on each other?

 

Share this post


Link to post

As documentation says:

Quote

Call Repaint to repaint the control.

As implemented in TWinControl, Repaint calls the Invalidate method and then the Update method to repaint the control.

So, is better to use directly Update for each control after every "enabled = false".

I tried and for me is working, but I really don't know if it's OK in all situation (i.e. under heavy load).

 

  • Like 1

Share this post


Link to post

DelphiUdIT - I tried the below (with and without the processmessages calls) and it still has the issue

 


  Button1->Enabled=false;
  Button1->Update();
  Button2->Enabled=false;
  Button2->Update();
  Button3->Enabled=false;
  Button3->Update();

//  Application->ProcessMessages();
  Sleep(3000);

  Button1->Enabled=true;
  Button1->Update();
  Button2->Enabled=true;
  Button2->Update();
  Button3->Enabled=true;
  Button3->Update();

//  Application->ProcessMessages();
  Sleep(3000);

Share this post


Link to post

I've tried again several times and it actually works 1 time in a while.
Saying that there aren't many ways: do you use what @Remy Lebeau told you or do you use a simple timer i.e. you set "enable = false" for all the controls and then call a timer (with a 50 ms interval) and perform your processing in the timer event. At the end of the timer you reactivate the controls.

Edited by DelphiUdIT

Share this post


Link to post
4 hours ago, alank2 said:

sometimes I have a synchronous process and I want to make sure some VCL elements are updated beforehand.  This always worked fine in bcb6.

BCB6 predates Visual Styles and VCL Styles and basically most modern UI practices.

4 hours ago, alank2 said:

What could cause the Enabled change to _not_ create an immediate pending message?

For example, when UI changes are animated by the OS or Styles, it would thus take multiple paint cycles to fully animate the complete state change, and ProcessMessages() or Update() may not (likely will not) catch all of the paint cycles in a single call.

51 minutes ago, alank2 said:

I realize that putting the "seconds to run" is more ideal in a thread, but that adds creating and managing a thread which I don't want to have to do for every process that takes a few seconds.

You can mitigate that somewhat by using thread pools, for instance.

51 minutes ago, alank2 said:

Essentially I am looking for a function that will make sure the UI is fully updated before a period of time where it won't be updated.

There is nothing in UI programming that says like "state change is finished and the UI for it is fully drawn".  Not even back in BCB6's day.  Although back then, drawing was much simpler, and usually completed in 1 paint cycle.  That may not be the case anymore in modern UIs.

51 minutes ago, alank2 said:

I tried using calling Repaint for all 3 buttons before a ProcessMessages, but it didn't work.

Whatever is different between versions, perhaps changing a property from true to false takes a number of things to occur in sequence that rely on each other?

That is not really a factor of the property itself, but in the UI rendering behind the scenes, so more oriented to differ between OS versions rather than compiler/VCL versions.

13 minutes ago, alank2 said:

DelphiUdIT - I tried the below (with and without the processmessages calls) and it still has the issue

Update() only processes paint messages that are already pending in the message queue.  But you also need Invaliidate() to generate those messages in the first place, if they are not already generated for you.  So that is where Repaint() comes into play, to signal that a new paint cycle is needed if it hasn't already been signaled.

  • Like 2

Share this post


Link to post

Seems the easiest solution is to delay an action via timer. Disable buttons, create timer with interval ~200ms and do your action in its OnTimer.

  • Like 1

Share this post


Link to post
14 hours ago, Remy Lebeau said:

For example, using TThread::ForceQueue():


  TThread::ForceQueue(nullptr, &Step2, 3000);

 

@Remy LebeauThanks for pointing into that direction 👍

 

I use ForceQueue and my own implementation of a DelayWorker successfully for years now,

but I never realized that ForceQueue has its own Delay option available too.

Good to know, I will look into it how this behaves.
I see a little problematic part, since it seems to keep the Delay within a TMonitor.Enter(ThreadLock); ...  TMonitor.Exit(ThreadLock); loop, at a first glance.
Anyway, that should be fine for simple, non-critical Delays, like in this case.

 

 

Edited by Rollo62

Share this post


Link to post

Fr0sT.Brutal that works - the optimal delay is above 300ms:

 


  #define DELAY 325

  Label1->Caption="off";
  Button1->Enabled=false;
  Button2->Enabled=false;
  Button3->Enabled=false;
  Sleep(DELAY);
  Application->ProcessMessages();
  Sleep(3000);

 

  Label1->Caption="on";
  Button1->Enabled=true;
  Button2->Enabled=true;
  Button3->Enabled=true;

  Sleep(DELAY);
  Application->ProcessMessages();
  Sleep(3000);

 

  Label1->Caption="";

Share this post


Link to post

This really does have to do with the highlighting and styling of the button.  If I make a loop that disables and enables a button when clicking it, sometimes it won't change states if I am still hovering over it with the mouse.

 

Is there a way to disable the highlighting that happens when you simply hover over a button?

Share this post


Link to post

   Here's a pascal example that simply replaces ProcessMessages with custom "message" calls to allow the sleep delays.

 

   assign this ClickEvent to several buttons onclick Event.  I would look media player source for dealing with updating several buttons state. 

  

implementation

{$R *.dfm}

const
  Clickable = True; // alias for enabled = True

procedure ButtonEnable(button: TButton; const aCaption: string;
  const isEnabled: Boolean); //inline;
begin
  if button.Enabled <> isEnabled then
    button.Enabled := isEnabled;
  if button.Caption <> aCaption then
    button.Caption := aCaption;
//  button.Enabled := isEnabled; order matters
  //Button.Repaint; //
end;

procedure SnoozedButton(button: TButton; MS: Integer); //inline;
begin
  ButtonEnable(button, 'Snoozed', not Clickable);
  Sleep(MS);
end;

var inUse: Boolean = False;

procedure TForm15.DealButtonClick(Sender: TObject);
begin
  if inUse = not False
    then exit;
  inUse := True;//Sender as TButton;

  SnoozedButton(Sender as TButton, 3000);
  ButtonEnable(Sender as TButton, 'Hit me', not Clickable);

  Sleep(1500);
  (Sender as TButton).Caption := 'Hitted';

  sleep(1500);
  sleepControl(Sender, 2000); // Event driven call

  ButtonEnable(Sender as TButton, 'Deal', Clickable);

  inUse := False;//nil;
end;

procedure TForm15.sleepControl(Sender: TObject; const MS: Integer);
begin
  var
  button := Sender as TButton;
  ButtonEnable(button, 'Sleeping', not Clickable);
  Sleep(MS);
  ButtonEnable(button, 'Stay', not Clickable);
  Sleep(MS);
end;

 

3 minutes ago, alank2 said:

Is there a way to disable the highlighting that happens when you simply hover over a button?

I think that's a feature🙂.  I would like the showhint to work when the button is disabled.

  • Like 1

Share this post


Link to post

A colleague of mine suggested changing DoubleBuffered and this seems to change the behavior enough to fix it!

 

edit: well, it fixes it in debug mode, but not always in release mode so it isn't a fix after all.

Edited by alank2

Share this post


Link to post
10 hours ago, Rollo62 said:

I use ForceQueue and my own implementation of a DelayWorker successfully for years now,

but I never realized that ForceQueue has its own Delay option available too.

The Delay option was added in 10.4 Sydney, so it is a relatively recent addition (2020).

  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×