-1

At program start, in the OnActivate event handler, I need to do something which blocks the program for a few seconds. During this time the form's client area is still not completely painted, which looks ugly for the user. (During this blocked time I don't need the program to respond to clicks or other user actions, so there is no need to put the blocking operation into a thread - I just need the form to be completely painted). So I use TForm.Update and Application-ProcessMessages to update the form before the blocking operation which works very well:

procedure TForm1.FormActivate(Sender: TObject);
begin
  Form1.Update;
  Application.ProcessMessages;
  Sleep(7000);
end;

However, I wonder whether there is not another more elegant solution for this problem. This could be for example a OnShown event implemented in a descendant of TForm which will be fired AFTER the form has been completely painted. How could such an event be implemented?

user1580348
  • 5,721
  • 4
  • 43
  • 105
  • 1
    See for instance here http://www.swissdelphicenter.com/torry/showcode.php?id=1276. But I'm fairly sure there is a question about it on Stack Overflow. – TLama Jun 17 '14 at 16:01
  • Posting a message (as in the link you provided) IMO is not completely reliable, as a message (AFAIK) could also get lost on certain circumstances. I need something which is 200% reliable. – user1580348 Jun 17 '14 at 16:22
  • Messages will not be lost. But the `PostMessage` can fail when queued if the queue is full, and you can detect that with the `PostMessage` result parameter. – LU RD Jun 17 '14 at 16:25
  • In .NET there is a Form.Shown Event (see MSDN). So at Windows level the form must know when it is completely painted. Shouldn't it be possible to access that with Windows API? – user1580348 Jun 17 '14 at 16:27
  • 3
    You're blocking in the wrong place. Simply send yourself a message at the end of the FormShow event, and do your blocking operation in response to that message. The form is completely painted by the time you receive the message in its handler. (OnActivate can be fired multiple times when the form/application lose and regain focus; it's typically the wrong place to be doing things anyway.) – Ken White Jun 17 '14 at 16:29
  • Just another note on `PostMessage`. It is used throughout the vcl as the basic way to implement the event driven program flow. – LU RD Jun 17 '14 at 16:43
  • C# fires the [`Shown`](http://referencesource.microsoft.com/#System.Windows.Forms/ndp/fx/src/winforms/Managed/System/WinForms/Form.cs#4910) event from the form's [`OnLoad`](http://referencesource.microsoft.com/#System.Windows.Forms/ndp/fx/src/winforms/Managed/System/WinForms/Form.cs#4833) event which will be based on some internal workflow. I don't think it reacts to any Windows message. Posting a message to yourself is reliable. Yes, it can fail if you'll have 10000 pending messages in the queue, which only indicates that something is wrong with your app. – TLama Jun 17 '14 at 16:45
  • In that case WM_PAINT may get lost too, not to worry. – Sertac Akyuz Jun 17 '14 at 17:48
  • Here are two questions from SO http://stackoverflow.com/questions/6203090/how-can-i-make-a-a-dialog-box-happen-directly-after-my-apps-main-form-is-visibl http://stackoverflow.com/questions/14318807/what-is-the-best-way-to-autostart-an-action-after-onshow-event – Sertac Akyuz Jun 17 '14 at 17:53
  • Thanks to all for the very useful hints! – user1580348 Jun 18 '14 at 13:56

3 Answers3

2

Your real problem is that you are blocking the UI thread. Simply put, you must never do that. Move the long running task onto a different thread and thus allow the UI to remain responsive.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Generally I agree; but in this specific case for my specific purpose this is not a problem. – user1580348 Jun 18 '14 at 13:55
  • 1
    It's obviously up to you if you prefer to have your application fail to pump its UI message queue, and have the system ghost your window as not responding. I would not do so, which is why I wrote the above. But it's clearly your choice if you would rather do that. – David Heffernan Jun 18 '14 at 13:59
0

If you are looking for event which is fired when application finishes loading/repainting you should use TApplication.OnIdle event

http://docwiki.embarcadero.com/Libraries/XE3/en/Vcl.Forms.TApplication.OnIdle

This event is fired once application is read to recieve users input. NOTE this event will be fired every time application becomes idle so you need to implement some controll variable which will tel you when OnIdle even was fired for the first time.

But as David already pointed out it is not good to block your UI (main thread). Why? When you block your main thread the application can't normally process its messages. This could lead to OS recognizing your application as being "Hanged". And aou definitly wanna avoid this becouse it could cause the users to go and forcefully kill your application whihc would probably lead to data loss. Also if you ever wanna design your application for any other platforms than Windows your application might fail the certification proces becouse of that.

SilverWarior
  • 7,372
  • 2
  • 16
  • 22
  • Yuck. OnIdle isn't what the poster is looking for either. It fires far too often, and is even worse than using OnActivate. Not everything is possible to move to a background thread; for instance, CRUD applications that display data in a DBGrid have to connect to the DB first, and the DB connection has to belong to the main thread in order to be accessible. You can't move that connection to a secondary thread, because then the connection isn't accessible to your UI. – Ken White Jun 18 '14 at 19:25
0

In the past a simple PostMessage did the trick. Essentially you fire it during DoShow of the base form:

procedure TBaseForm.DoShow;
begin
  inherited;
  PostMessage(Handle, APP_AFTERSHOW, 0, 0);
end;

then catch the msg and create an AfterShow event for all forms inherited from this base form.

But that no longer works, well not if you are skinning and have a good number of VCL controls.

My next trick was to spawn a simple thread in DoShow and check for IsWindowVisible(Handle) and IsWindowEnabled(Handle). That really sped things up it cut 250ms from load time since db opening and other stuff was already in the AfterShow event.

Then finally I thought of madHooks, easy enough to hook the API ShowWindow for my application and fire APP_AFTERSHOW from that.

function ShowWindowCB(hWnd: HWND; nCmdShow: Integer): BOOL; stdcall;
begin
  Result := ShowWindowNext(hWnd, nCmdShow);
  PostMessage(hWnd, APP_AFTERSHOW, 0, 0);
end;

procedure TBaseForm.Loaded;
begin
  inherited;
  if not Assigned(Application.MainForm) then // Must be Mainform it gets assigned after creation completes
    HookAPI(user32, 'ShowWindow', @ShowWindowCB, @ShowWindowNext);
end;

To get the whole thing to completely paint before AfterShow it still needed a ProcessPaintMessages call

procedure TBaseForm.APPAFTERSHOW(var AMessage: TMessage);
begin
  ProcessPaintMessages;
  AfterShow;
end;

procedure ProcessPaintMessages; // << not tested, pulled out of code
var
  msg: TMsg;
begin
    while PeekMessage(msg, 0, WM_PAINT, WM_PAINT, PM_REMOVE) do 
      DispatchMessage(msg);
end; 

My final test was to add a Sleep to the AfterShow event and see the Form fully painted with empty db containers since the AfterShow events had not yet completed.

procedure TMainForm.AfterShow;
begin
  inherited;
  Sleep(8*1000);
 ......
FredS
  • 680
  • 1
  • 5
  • 6