views:

414

answers:

4

I'm new to threads. I'm using a 3rd party library that uses threads which at times call a procedure I've provided.

How do I update update a TLabel.Caption from my procedure when its called by the thread?

If I've called InitializeCriticalSection elsewhere, is it as simple as

  EnterCriticalSection(CritSect);
  GlobalVariable := 'New TLabel.Caption';
  LeaveCriticalSection(CritSect);

And then in my main thread:

  EnterCriticalSection(CritSect);
    Label1.Caption:= GlobalVariable;
  LeaveCriticalSection(CritSect);

But, how do I get the main thread code to be called? The thread can use SendMessage? Or is there some better/easier way (.OnIdle could check a flag set by the thread?)

Thanks.

+4  A: 

To make your code gets called in the main thread, take a look at TThread.Synchronize. It accepts a method pointer (or, in D2009+, an anonymous method) and takes care of all the messaging behind the scenes to make sure your code will run in the main thread.

Mason Wheeler
The only question is if his callback is part of the thread and therefore he has direct acces to synchroinize. I don't know if the function can be called outsize the scope of TThread. That is why I posted the code with PostMessage
Runner
TThread has a class procedure, also named Synchronize that accepts a thread and callback as arguments and you can pass nil as the thread.
Craig Peterson
Yup, my callback is not part of the thread, so synchronize is not available.
Tom1952
@Craig, cool didn't know they added that. I don't use Synchronize very often ;)
Runner
+4  A: 

You must ensure that the label is updated in a safe manner. Furthermore VCL runs in the application main thread and messing with it from other threads can have weird results. Even if using critical sections.

So my advice is: just use PostMessage. When your callback procedure is called, just call PostMessage from that procedure to the main form window handle. This will ensure that the label caption is set in the context of the main thread.

Code sample:

type
  TForm1 = class(TForm)
  private
    procedure OnWMUpdateLabel(var Msg: TMessage); message WM_UPDATE_LABEL;
    procedure MyCallbackProcedure(const Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.OnWMUpdateLabel(var Msg: TMessage);
begin
  Label1.Caption := SomeVariable;
end;

procedure TForm1.MyCallbackProcedure(const Sender: TObject);
begin
  SomeVariable := 'New Label';
  PostMessage(Handle, WM_UPDATE_LABEL, 0, 0);
end;

But you have to be careful if you pass strings around this way. You have to synchronize the access to such variable. Or you can use GlobalAddAtom (which is a little deprecated), or something similar.

EDIT:

As Mason already said you can also use Synchronize which is easier to use. For your problem it should be perfectly OK.

EDIT 2:

For the reference how to use GlobalAddAtom (yes I misspelled it earlier):

http://www.delphi3000.com/articles/article_574.asp?SK=

Runner
You were right - my callback is not part of the thread, so synchronize is not available. The thread is started by the 3rd party library...I wasn't able to find how to use AddGlobalAtom, so I've decided I'll use your PostMessage solution, and simply pass a word identifying the one of several captions I'll be using.Thanks.
Tom1952
You can still use `Synchronize` since it's a static method.
Smasher
BTW @Runner: what is "a little deprecated"? :)
Smasher
PostMessage can be a significant overhead for inter thread communication. It's not really a problem if you want to update a label caption once in a while, but don't use that for massive communication, it will make your application almost unusable. Been there, done that, didn't like the result.
dummzeuch
@user257188: Since you accepted this answer you should also be aware of the potential pitfalls: Using `Handle` like this may be dangerous, as reading it calls a property getter in a VCL class from a secondary thread. You have to make sure that the handle is valid the *whole* time the thread is alive and may call the callback. That means calling `HandleNeeded` before the thread is created, killing the thread before the form is freed, and *not* causing the form handle to be recreated. Changing the stay-on-topness of a form for example would cause this to happen...
mghie
@Smasher, that is my poor choice of words :) I meant that GlobalAddAtom is not the best way of passing strings around. I used it often in the past, but these days I tend to avoid it. Maybe it is just me :)
Runner
@mghie: True, but I didn't want to go there, because that sort of cases would be verty rare in my opinion. But a valid point still. Even more since the answer is accepted.
Runner
@dummzeuch: If you update your label that often than you have bigger problems ;) And you have to do it in the main thread. Not much choices there. Synchronize is probably even worse performance wise.
Runner
@Runner: Synchronize used to use PostMessage (or was it SendMessage) in the first few versions of Delphi. Nowadays (since at least Delphi 6) it uses a much faster mechanism. I must admit I don't remember exactly how it is implemented, but I did some timing back when I was deciding between using Synchronize and PostMessage and Synchronize came out very far on top.Regarding "bigger problems": The application I am talking about is extracting single frames from a video and storing them as jpegs. It made a hell of a difference when I switched to a queuing mechanism with events and mutexes.
dummzeuch
A: 

Critical Sections are used to serialize accessing to a piece of code. For updating graphical user interface, you should take note that only the main thread should update GUI elements.

So if your thread needs to update a GUI element, it should delegate this to the main thread. To do so, you can use different techniques:

The simplest one is using Synchronize method in your thread code. When Synchronize is called, your thread is paused, the code you provided to Synchronize will be executed in the context of the main thread, and then your thread resumes.

If you don't like your thread get stopped every time that piece of code is called, then you can use Queue method. Queue sends your request to the message queue of the destination thread (here main thread), so your thread will not stop, but the UI might not get updated immediately, depending on how crowded main thread's message queue is.

Another way to achieve this is to send custom Windows messages to the main thread using SendMessage or PostMessage API functions. In that case, you have to define a custom message, and send it to the main thread whenever you need to change a UI element. Your main thread should provide a message handler for that type of message, and handle the received messages. The consequence is something similar to using Queue method.

vcldeveloper
"Your main thread should provide a message handler for that type of message, and handle the received messages."Ah, and there's the complexity. How to safely pass a string from a thread using a message!
Tom1952
@user257188: This is only a problem with `PostMessage()`. With `SendMessage()` the string will be valid while the message is processed.
mghie
A: 

The way I do this is to have the main application thread use a TTimer on the form to check the status of a thread specific value to see if the status has changed, and if so, update the label (or other components). The TThread descendent uses a property with Getter function to protect access with the critical section. This way the work thread is never held back by the main thread (which Synchronize will do), and the user does not experience any delay in the use of the UI.

mj2008