Link to home
Start Free TrialLog in
Avatar of PeterDelphin
PeterDelphin

asked on

Posting a string with PostMessage

Hello! I use the following method to send a string from a thread to the UI:

unit Unit2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

const
  WM_SETCAPTION = WM_USER;

type
  TForm2 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    procedure WMSetCaption(var msg: TMessage); message WM_SETCAPTION;
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

procedure TForm2.WMSetCaption(var msg: TMessage);
begin
  Self.Caption := PChar(msg.LParam);
end;

procedure TForm2.Button1Click(Sender: TObject);
begin
  System.Classes.TThread.CreateAnonymousThread(
    procedure
    begin
      PostMessage(Handle, WM_SETCAPTION, 0, LParam(PChar('My new caption')));
    end).Start;
end;

end.

Open in new window


See also the attached project.

It works well. BUT: Could this be potentially dangerous?
StringPostMessage.zip
ASKER CERTIFIED SOLUTION
Avatar of Sinisa Vuk
Sinisa Vuk
Flag of Croatia image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of PeterDelphin
PeterDelphin

ASKER

Many thanks for the useful information!
if this is within the same delphi app,
you could create an object to hold the string

the receiver should free the object
const
  WM_SETCAPTION = WM_USER + 3;

type
  TMsgObject = class(TObject)
    fMsg: string;
  public
    constructor Create(aMsg: string);
    property Msg: string read fMsg;
  end;


  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    fMsgId: Integer;
    procedure WMSetCaption(var Msg: TMessage); message WM_SETCAPTION;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

constructor TMsgObject.Create(aMsg: string);
begin
  inherited Create;
  fMsg := aMsg;
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  Inc(fMsgId);
  System.Classes.TThread.CreateAnonymousThread(
    procedure
    begin
      PostMessage(Handle, WM_SETCAPTION, 0, Integer(TMsgObject.Create('New caption ' + IntToStr(fMsgId) )) );
    end).Start;
end;

constructor TForm1.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  fMsgId := 0;
end;

destructor TForm1.Destroy;
begin
  inherited Destroy;
end;

procedure TForm1.WMSetCaption(var Msg: TMessage);
var mo: TMsgObject;
begin
  if Msg.Msg = WM_SETCAPTION then
  begin
    mo := TMsgObject(Msg.LParam);
    Caption := mo.Msg;
    mo.Free;
  end;
end;

end.

Open in new window

Hello Geert Gruwez, this solution is brilliant, thank you! I like this solution the best.

Here I made a more generalized usage example from your solution:

unit SendingStringWithPostMessageUsingObjectMainForm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

const
  WM_STRINGMESSAGE = WM_USER + 3;

type
  TStrMsgObject = class(TObject)
    fMsg: string;
  public
    constructor Create(aMsg: string);
    property Msg: string read fMsg;
  end;

type
  TForm2 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
    fMsgId: Integer;
    procedure WMStringMessage(var Msg: TMessage); message WM_STRINGMESSAGE;
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

const
  ACTION_SETCAPTION    = 0;
  ACTION_CHANGECAPTION = 1;

constructor TStrMsgObject.Create(aMsg: string);
begin
  inherited Create;
  fMsg := aMsg;
end;

procedure TForm2.WMStringMessage(var Msg: TMessage);
var
  mo: TStrMsgObject;
begin
  mo := TStrMsgObject(Msg.LParam);
  try
    case Msg.WParam of
      ACTION_SETCAPTION:
        begin
          Caption := mo.Msg;
        end;
      ACTION_CHANGECAPTION:
        begin
          Caption := Caption + mo.Msg;
        end;
    end
  finally
    mo.Free;
  end;
end;

procedure TForm2.Button1Click(Sender: TObject);
begin
  System.Classes.TThread.CreateAnonymousThread(
    procedure
    begin
      PostMessage(Handle, WM_STRINGMESSAGE, ACTION_SETCAPTION, Integer(TStrMsgObject.Create('My new caption')));
    end).Start;
end;

procedure TForm2.Button2Click(Sender: TObject);
begin
  System.Classes.TThread.CreateAnonymousThread(
    procedure
    begin
      PostMessage(Handle, WM_STRINGMESSAGE, ACTION_CHANGECAPTION, Integer(TStrMsgObject.Create(' changed')));
    end).Start;
end;

end.

Open in new window


I have these two questions:

1. Is there a particular reason have you defined the Msg constant as WM_USER + 3? Wouldn't it be enough to define it as WM_USER?

2. Is PostMessage strictly FIFO? I.e., can I rely on PostMessage messages being processed in the SAME order they are sent?
SendingStringWithPostMessageUsingObj.zip
WM_User +3 is just a brainf*** :)
using wm_user would do just as well


PostMessage is Fifo ... unless you met the same idiot i have
and there is some routine using peekMessage and PopMessage to alter the queue
if you haven't programmed this,  then it's fifo :)

i met an idiot once, who was only interested in specific messages
instead of only processing those specific messages he processed the whole queue like this pseudo code
n := CountMessagesOnQueue;
for I := 1 to n do 
begin
  M := PopMessage;
  if M.Message <> MyMessage then 
    PostMessage(M)  
  else 
    ProcessMessage(M);
end;

Open in new window


I needn't say he didn't really understand windows messages ?
and his apps had very unpredictable behaviour  ... works as designed was a good description
I discourage to use PostMessage (with reason I post). Please read the differences.... Better use SendMessage - cause handler will wait to store right message to Caption.
If you don't believe me  - try to PostMessage:
   PostMessage(Handle, WM_STRINGMESSAGE, ACTION_SETCAPTION, Integer(TStrMsgObject.Create('Caption '+IntToStr(id)));

Open in new window

... where id is your thread id (maybe index if you want) ... and start 100 threads with something like Sleep(Random(10)*500) to simulate some
process.
Your example works - but is not general solution for threads....
Sinisa ...
can you post a project proving it doesn't work ?
anyways ... for threads, i use PostThreadMessage
Geert your example works - cause you create object and send pointer of it - no hassle with strings. But if you look at my example ...
with PostMessage works bad, and with SendMessage better. Note - this is example to show bad behavior ....
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

const
  WM_THREAD_MSG = WM_USER+1;

type
  TWorkerThread = class(TThread)
  private
    FId: Integer;
    FMsgRoot: String;
    FParentHnd: THandle;
  protected
    procedure Execute; override;
  public
    constructor Create(ParentHnd: THandle; MsgRoot: String; Id: Integer); overload;
  end;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    procedure WMThreadMessage(var Msg: TMessage); message WM_THREAD_MSG;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TWorkerThread }

constructor TWorkerThread.Create(ParentHnd: THandle; MsgRoot: String; Id: Integer);
begin
  FMsgRoot := MsgRoot;
  FId := Id;
  FParentHnd := ParentHnd;
  
  inherited Create(False);
end;

procedure TWorkerThread.Execute;
var
  w: Integer;
  s: String;
begin
  FreeOnTerminate := True;
  
  w := Random(10)*500;
  s := FMsgRoot + ' ' + IntToStr(FId);
  Sleep(w);
  //PostMessage(FParentHnd, WM_THREAD_MSG, 0, Integer(PChar(s)));
  SendMessage(FParentHnd, WM_THREAD_MSG, 0, Integer(PChar(s)));
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Randomize;
end;

procedure TForm1.WMThreadMessage(var Msg: TMessage);
var
  s: String;
begin
  s := String(PChar(msg.LParam));
  Memo1.Lines.Add(s);
  Msg.Result := 0;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  i: integer;
begin
  for i := 1 to 100 do
  begin
    TWorkerThread.Create(Self.Handle, 'My thread', i);
  end;
end;

end.

Open in new window

Sinisa, thanks for your remarks. I am aware of the difference between PostMessage and SendMessage. For my specific scenario, I nevertheless prefer PostMessage for the targeted purpose because I can make sure the messages are sent from one single thread in the intended order.
Geert, the MS documentation about PostThreadMessage says:

Posts a message to the message queue of the specified thread.

But when I PostMessage(Handle, ... (where Handle refers to the current form) the message is automatically directed to the UI thread, isn't it?
well ... conceptually it's the same ... :)
the message gets processed by WndProc

The difference is ...
PostMessage uses the handle of the window
PostThreadMessage uses the handle of the thread

The difference is working with visual controls or not

it's rather odd to send WM_SET_CURSOR to a thread
Sinisa ...
that's not really the issue.

You use PostMessage if you want to work ansynchronously
and Sendmessage if not

in my opinion ...
moving a progressbar to indicate progress is best done asynchronously
I know i contradict my article with a progressbar with a callback
SendMessage and callback tecnhiques are synchronous

working with a queue ... like what Peter seems to be doing ... should also be asynchronously
...anyway PostMessage is not good to transfer (as parameter) something like pointer to string itself, but good enough to post pointer to some object instance or integer value for progressbar for ex.
it might not be good ... but that's irrelevant if you have to work asynchronously
Geert, the task I'm working on is a little bit more complicated.

Can I be sure that all single loop threads from a System.Threading.TParallel.For loop are terminated AFTER this loop, right? In other words: The TParallel.For loop returns only AFTER all its loop threads are terminated, right?
i like the words little complicated and thread in the next senctence

sure ?
when are we ever sure ?
the only certainty i have:
if the electric power fails before the processing is finished, then i'm sure all processes are finished
however, i believe you still required a second step ... with the power on ... :)

this is a different ballgame :)
you are going from 1 anonymous thread to waiting for multiple threads to finish

this is a WaitForAll to finish

and since you are using Delphi System ... that should be documented
and it is ... but they created a wrapper for managing threads: The TTask:
http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Using_TTask_from_the_Parallel_Programming_Library

inside that you have the waitforall
Geert, sorry for expanding the question topic. (Should I open a new question?)
As I understand it, TTask.WaitForAll concerns an array of ITask while TParallel.For manages its loop threads itself. Is this correct? So if this is correct then TParallel.For should return only after its loop threads are completely terminated. Is this correct?
i haven't used that PPL yet ... I wrote my own PPL.
I have considered moving to it, but haven't got there yet

Looks like TParallel is the low level implementation
and TTask is the interface which you should be using

to me it looks like you write descendants of TParallel to write your own flavor of such an engine

to give a conclusion I would have to read the docs too
i can't give advice on my own experience with this library as i haven't used it yet

it looks to be very similary to The Delphi Geek's Omni Thread Library
I'm assuming Embarcadero rewrote it somewhat and used the ideas or just hired the guy
looks like this is your answer:

All overloads of TParallel.For return TParallel.TLoopResult. When all the iterations of a TParallel.For iterator event have been executed, the Completed property of the TLoopResult instance returned by TParallel.For is set to True.
Geert, many thanks for the comprehensive and helpful answers.

This has convinced me, so I will buy a Premium Plan.

Unfortunately, I cannot find a DETAILED description of the features of the Premium Plan.
Premium Plan ?
is that for health care ?
Geert, I like your sarcastic humor ;-)

No, I mean the Experts Exchange Premium Plan.