robsoft
robsoft

Reputation: 5585

Get window to refresh (etc) without calling Application.ProcessMessages?

I've got a legacy app here that has a few 'time-consuming' loops that get fired off as a result of various user interaction. The time-consuming code periodically updates something on the screen with progress information (typically a label) and then, seemingly to persuade the visual refresh to happen there-and-then, the code calls Application.ProcessMessages (argh!).

We all know now what kind of trouble this can introduce to a GUI app (being charitable, it was a more innocent time back then) and we're finding that sure as eggs, from time to time we get users achieving the impossible with the program because they're clicking on controls while the program is 'busy'.

What's the best way of periodically refreshing the visuals of the form without taking-on other events/messages etc?

My thoughts were to either;
- disable all of the controls before doing anything time-consuming, and leaving the '...ProcessMessages' calls in place to 'force' the refresh, or
- find another way to refresh a control periodically

I can do the former but it left me wondering - is there a better solution?

example code from legacy;

i:=0;
while FJobToBeDone do
begin
  DoStepOfLoop;
  inc(i);
  if i mod 100 = 0 then
  begin
    UpdateLabelsEtc;
    Application.ProcessMessages;
  end;
end;

I can already hear you all fainting, at the back. :-)

Upvotes: 15

Views: 12065

Answers (5)

Le Minh Hoang
Le Minh Hoang

Reputation: 31

Do not call Application.Processmessages, This is slow and might generate unlimited recursion. For example, to update everything in Panel1 without flick, we can use this method:

procedure TForm1.ForceRepaint;
var
  Cache: TBitmap;
  DC: HDC;
begin
  Cache := TBitmap.Create;
  Cache.SetSize(Panel1.Width, Panel1.Height);
  Cache.Canvas.Lock;
  DC := GetDC(Panel1.Handle);
  try
    Panel1.PaintTo(Cache.Canvas.Handle, 0, 0);
    BitBlt(DC, 0, 0, Panel1.Width, Panel1.Height, Cache.Canvas.Handle, 0, 0, SRCCOPY);
  finally
    ReleaseDC(Panel1.Handle, DC);
    Cache.Canvas.Unlock;
    Cache.Free;
  end;
end;

For better performance, the cache bitmap should be created at first and free when the process has finished

Upvotes: 3

Re0sless
Re0sless

Reputation: 10886

Rather than disable the controls, we have a boolean var in the form FBusy, then simply check this when the user presses a button, we introduced it for the exact reasons you mention, users clicking buttons while they wait for long running code to run (it scary how familiar your code sample is).

So you end up with something like

procedure OnClick(Sender:TObejct);
begin
    if (FBusy) then
    begin
        ShowMessage('Wait for it!!');
        Exit;
    end
    else FBusy := True;
    try
        //long running code
    finally
        FBusy := False;
    end;
end;

Its impotent to remember to rap the long running code up in a try-finally block in case of exits or exception, as you would end up with a form that will not work.

As suggested we do use threads if its for code that will not affect the data, say running a report or data analysis, but some things this is not an options, say if we are updating 20,000 product records, then we don't want anyone trying to sell or other wise altering the records mid flight, so we have to block the application until it is done.

Upvotes: 1

user114285
user114285

Reputation: 513

The technique you're looking for is called threading. It is a diffucult technique in programming. The code should be handled with care, because it is harder to debug. Whether you go with threading or not, you should definitely disable the controls that users should not mess with (I mean the controls that can effect the ongoing process). You should consider using actions to enable/disable buttons etc...

Upvotes: 1

Wim ten Brink
Wim ten Brink

Reputation: 26682

The solution I use for long updates is by doing the calculations in a separate thread. That way, the main thread stays very responsive. Once the thread is done, it sends a Windows message to the main thread, indicating the main thread can process the results.

This has some other, severe drawbacks though. First of all, while the other thread is active, you'll have to disable a few controls because they might restart the thread again. A second drawback is that your code needs to become thread-safe. This can be a real challenge sometimes. If you're working with legacy code, it's very likely that your code won't be thread-safe. Finally, multi-threaded code is harder to debug and should be done by experienced developers.

But the big advantage of multi-threading is that your application stays responsive and the user can just continue to do some other things until the thread is done. Basically, you're translating a synchronous method into an asynchronous function. And the thread can fire several messages indicating certain controls to refresh their own data, which would be updated immediately, on the fly. (And at the moment when you want them to be updated.)

I've used this technique myself quite a few times, because I think it's much better. (But also more complex.)

Upvotes: 8

mghie
mghie

Reputation: 32334

If you call Update() on the controls after you have changed properties you will force them to redraw. Another way is to call Repaint() instead of Refresh(), which implies a call to Update().

You may need to call Update() on parent controls or frames as well, but this could allow you to eliminate the ProcessMessages() call completely.

Upvotes: 18

Related Questions