tags:

views:

440

answers:

4

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. :-)

+8  A: 

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.

mghie
Spot on mghie, thanks! I dropped this into a couple of sample key places and gave it a quick test; the screen updates happen just fine but the controls don't generate 'new' events while the form is busy. If I combine this along with explicitly locking-down the form when the job kicks-off, I'm hopeful that this will stop the rogue events from getting though. Many thanks!
robsoft
+5  A: 

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.)

Workshop Alex
Thanks, Workshop Alex - I think that going forwards this is the "right" thing to try and do, particularly where there could be a long period of inactivity. In this particular case however, mghie's 'Update' seems to drop-in and replace ProcessMessages call entirely.
robsoft
Though Alex gave a good explanation, it lacks a pointer on how to get started: Delphi has a thread-unit template in the 'add new' dialog, but in essence it all boils down to creating an derived class from TThread, overriding the Execute method.
Stijn Sanders
Stijn is providing one good way to use threads in Delphi. Basically, the TThread class wraps around the Windows API and provides a base class where you can share data. Personally, I prefer to use the raw API instead, creating a separate thread procedure which calls a specific method of my business logic class again. (Or a method of your main/child form.) But because of the complexity, I would not even dare to use this technique with legacy code, unless I'm going to rewrite it from scratch. (Legacy code often isn't thread-safe.)
Workshop Alex
+1  A: 

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...

Thanks for this Codervish. I must admit I haven't used actionlists much at all in the past but having just taken a closer look it does seem like quite a neat way to control the whole enabled/events side of things. Cheers!
robsoft
+1  A: 

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.

Re0sless
Thanks for this - this approach is how I've tended to do it myself in other apps. I think I'll make a point of trying to use a thread for long-running code in the next app I write, as it seems like something I ought to be comfortable using. :-)
robsoft