9
votes

As far as I understand and know the method of the TThread Class, if you synchronize your code, it actually get's executed in the main Application Thread (just like a timer/buttonclick/etc.) I've been playing around and noticed that a MessageBox DOES NOT block the main application, however sleep does just as expected. Why is that?

type
  TTestThread = class(TThread)
  private
    procedure SynchThread;
  protected
    procedure Execute; override;
  public
    constructor Create(CreateSuspended: Boolean);
  end;

procedure TTestThread.SynchThread;
begin
 MessageBoxA (0, 'Hello', 'Test', 0);
end;

procedure TTestThread.Execute;
begin
 Synchronize (SynchThread)
end;

constructor TTestThread.Create(CreateSuspended: Boolean);
begin
  inherited;
  FreeOnTerminate := True;
end;

procedure StartThread;
var
 TestThread : TTestThread;
begin
 TestThread := TTestThread.Create (FALSE);
end;
3
Explain what you mean by "block the main app"David Heffernan

3 Answers

13
votes

There are two parts to this answer.

Part 1 is nicely explained in If MessageBox()/related are synchronous, why doesn't my message loop freeze?. The MessageBox function is not blocking, it merely creates a dialog box with its own message loop.

Part 2 is explained in the MessageBox documentation.

hWnd: A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no owner window.

When you display a modal dialog, Windows disables its owner, but if you pass 0 for the first parameter, there is no owner and nothing to disable. Therefore, your program will continue to process messages (and react to them) while the message box is displayed.

To change this behaviour, pass form's handle as a first parameter. For example:

procedure TTestThread.SynchThread;
begin
  MessageBoxA (Form1.Handle, 'Hello', 'Test', 0);
end;
12
votes

I suspect that the question boils down to what you mean when you say:

A message box does not block the main application.

What I take this to mean is that when you show the message box, your VCL form can still be interacted with. The issue here is unrelated to threads and I suggest we remove them from the equation. Your understanding of what Synchronize does is sound.

The issue is entirely related to the concept of a window's owner, and how modal dialog windows behave with respect to their owners. Note that by owner, I don't mean the Delphi property TComponent.Owner, but I mean the Windows API meaning of owner.

Create a VCL app and drop two buttons on the form. Add the following OnClick handlers.

procedure TForm1.Button1Click(Sender: TObject);
begin
  MessageBox(0, 'Not owned', nil, MB_OK);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  MessageBox(Handle, 'Owned by the VCL form', nil, MB_OK);
end;

Now observe what happens when you click on Button1. The message box shows, but you can still click on the VCL form. And compare with Button2. When it shows the message box, the VCL form cannot be interacted with.

When a modal dialog window is shown, the dialog window disables its owner. In the case of Button2, the owner is the VCL form. And once the form is disabled, you cannot interact with it. In the case of Button1, there is no owner and so the modal dialog window does not disable any other window. That's why the VCL form can be interacted with.

Raymond Chen has a long series on modality at his Old New Thing blog:

4
votes

Synchronize will execute the code in the Mainthread.
A good explanation can be found here Synchronization in Delphi TThread class

You just will have to prevent user from interacting with the forms of your application, eg. by

procedure TTestThread.SynchThread;
begin
MessageBoxA (0, 'Hello', 'Test', MB_TASKMODAL);      
end;

using MessageBoxA as you did, won't prevent the Mainthread from reacting on those events triggerd by ueser interaction with your forms, just try

procedure TForm4.Button2Click(Sender: TObject);
begin
    MessageBoxA (0, 'Hello', 'Test', 0);
   // vs
   //  MessageBoxA (0, 'Hello', 'Test', MB_TASKMODAL);
end;

MessageBoxA

that synchronize will be executed in the main thread can be shown (IMHO) by

type
  TTestThread = class(TThread)
  private
    FSync:Boolean;
    FCalled:TDateTime;
    procedure SynchThread;
  protected
    procedure Execute; override;
  public
    constructor Create(CreateSuspended: Boolean;sync:Boolean);
  end;

procedure TTestThread.SynchThread;
begin
 MessageBox (0,PChar(DateTimeToStr(FCalled)+#13#10+DateTimeToStr(Now)),'Hello' , 0);
end;

procedure TTestThread.Execute;
begin
 sleep(100); // give Caller Time to fell asleep
 if Fsync then Synchronize (SynchThread) else SynchThread;
end;

constructor TTestThread.Create(CreateSuspended: Boolean;sync:Boolean);
begin
  inherited Create(CreateSuspended);
  FSync := Sync;
  FCalled :=Now;
  FreeOnTerminate := True;
end;

procedure StartThread(sync:Boolean);
var
 TestThread : TTestThread;
begin
 TestThread := TTestThread.Create (FALSE,sync);
end;

procedure TForm4.RunUnsynchronizedClick(Sender: TObject);
begin
   StartThread(false);// no sync
   Sleep(5000);       // Stop messageloop
end;

procedure TForm4.RunSynchronizedClick(Sender: TObject);
begin
   StartThread(true); // sync
   Sleep(5000);       // Stop messageloop
end;