How can I terminate a thread that has a seperate message loop?

http://www.techques.com/question/1-10415481/How-can-I-terminate-a-thread-that-has-a-seperate-message-loop

I am writing a utility unit for the SetWindowsHookEx API.

To use it, I'd like to have an interface like this:

复制代码
var
  Thread: TKeyboardHookThread;
begin
  Thread := TKeyboardHookThread.Create(SomeForm.Handle, SomeMessageNumber);
  try
    Thread.Resume;
    SomeForm.ShowModal;
  finally
    Thread.Free; // <-- Application hangs here
  end;
end;
复制代码

In my current implementation of TKeyboardHookThread I am unable to make the thread exit correctly.

The code is:

复制代码
TKeyboardHookThread = class(TThread)
  private
    class var
      FCreated                 : Boolean;
      FKeyReceiverWindowHandle : HWND;
      FMessage                 : Cardinal;
      FHiddenWindow            : TForm;
  public
    constructor Create(AKeyReceiverWindowHandle: HWND; AMessage: Cardinal);
    destructor Destroy; override;
    procedure Execute; override;
  end;

function HookProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  S: KBDLLHOOKSTRUCT;
begin
  if nCode < 0 then begin
    Result := CallNextHookEx(0, nCode, wParam, lParam)
  end else begin
    S := PKBDLLHOOKSTRUCT(lParam)^;
    PostMessage(TKeyboardHookThread.FKeyReceiverWindowHandle, TKeyboardHookThread.FMessage, S.vkCode, 0);
    Result := CallNextHookEx(0, nCode, wParam, lParam);
  end;
end;

constructor TKeyboardHookThread.Create(AKeyReceiverWindowHandle: HWND;
  AMessage: Cardinal);
begin
  if TKeyboardHookThread.FCreated then begin
    raise Exception.Create('Only one keyboard hook supported');
  end;
  inherited Create('KeyboardHook', True);
  FKeyReceiverWindowHandle     := AKeyReceiverWindowHandle;
  FMessage                     := AMessage;
  TKeyboardHookThread.FCreated := True;
end;

destructor TKeyboardHookThread.Destroy;
begin
  PostMessage(FHiddenWindow.Handle, WM_QUIT, 0, 0);
  inherited;
end;

procedure TKeyboardHookThread.Execute;
var
  m: tagMSG;
  hook: HHOOK;
begin
  hook := SetWindowsHookEx(WH_KEYBOARD_LL, @HookProc, HInstance, 0);
  try
    FHiddenWindow := TForm.Create(nil);
    try
      while GetMessage(m, 0, 0, 0) do begin
        TranslateMessage(m);
        DispatchMessage(m);
      end;
    finally
      FHiddenWindow.Free;
    end;
  finally
    UnhookWindowsHookEx(hook);
  end;
end;
复制代码

 

AFAICS the hook procedure only gets called when there is a message loop in the thread.

The problem is I don't know how to correctly exit this message loop.

I tried to do this using a hidden TForm that belongs to the thread,

but the message loop doesn't process messages I'm sending to the window handle of that form.

How to do this right, so that the message loop gets terminated on thread shutdown?

Edit: The solution I'm now using looks like this (and works like a charm):

复制代码
 TKeyboardHookThread = class(TThread)
  private
    class var
      FCreated                 : Boolean;
      FKeyReceiverWindowHandle : HWND;
      FMessage                 : Cardinal;
  public
    constructor Create(AKeyReceiverWindowHandle: HWND; AMessage: Cardinal);
    destructor Destroy; override;
    procedure Execute; override;
  end;

function HookProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  S: KBDLLHOOKSTRUCT;
begin
  if nCode < 0 then begin
    Result := CallNextHookEx(0, nCode, wParam, lParam)
  end else begin
    S := PKBDLLHOOKSTRUCT(lParam)^;
    PostMessage(TKeyboardHookThread.FKeyReceiverWindowHandle, TKeyboardHookThread.FMessage, S.vkCode, 0);
    Result := CallNextHookEx(0, nCode, wParam, lParam);
  end;
end;

constructor TKeyboardHookThread.Create(AKeyReceiverWindowHandle: HWND;
  AMessage: Cardinal);
begin
  if TKeyboardHookThread.FCreated then begin
    raise Exception.Create('Only one keyboard hook supported');
  end;
  inherited Create('KeyboardHook', True);
  FKeyReceiverWindowHandle     := AKeyReceiverWindowHandle;
  FMessage                     := AMessage;
  TKeyboardHookThread.FCreated := True;
end;

destructor TKeyboardHookThread.Destroy;
begin
  PostThreadMessage(ThreadId, WM_QUIT, 0, 0);
  inherited;
end;

procedure TKeyboardHookThread.Execute;
var
  m: tagMSG;
  hook: HHOOK;
begin
  hook := SetWindowsHookEx(WH_KEYBOARD_LL, @HookProc, HInstance, 0);
  try
    while GetMessage(m, 0, 0, 0) do begin
      TranslateMessage(m);
      DispatchMessage(m);
    end;
  finally
    UnhookWindowsHookEx(hook);
  end;
end;
复制代码

 

 

You need to send the WM_QUIT message to that thread's message queue to exit the thread. 

GetMessage returns false if the message it pulls from the queue is WM_QUIT, so it will exit the loop on receiving that message.

To do this, use the PostThreadMessage function to send the WM_QUIT message directly to the thread's message queue.

For example:

PostThreadMessage(Thread.Handle, WM_QUIT, 0, 0);

 

 

The message pump never exits and so when you free the thread

it blocks indefinitely waiting for the Execute method to finish.

Call PostQuitMessage, from the thread, to terminate the message pump.

If you wish to invoke this from the main thread then you will need to

post a WM_QUIT to the thread.

Also, your hidden window is a disaster waiting to happen.

You can't create a VCL object outside the main thread.

You will have to create a window handle using raw Win32,

or even better, use DsiAllocateHwnd.

 

http://www.techques.com/question/1-10451535/How-to-exit-a-thread's-message-loop?

 

It turns out that it's better for everyone that you don't use a windowless message queue.

A lot of things can be unintentionally, and subtly, broken if you don't have a window for messages to be dispatched to.

Instead allocate hidden window (e.g. using Delphi's thread-unsafe AllocateHwnd)

and post messages to it using plain old PostMessage:

复制代码
procedure TMyThread.Execute;
var
   msg: TMsg;
begin
   Fhwnd := AllocateHwnd(WindowProc);
   if Fhwnd = 0 then Exit;
   try
      while Longint(GetMessage(msg, 0, 0, 0)) > 0 do // will block until a message arrives on the queue.
      begin
         TranslateMessage(msg);
         DispatchMessage(msg);
      end;
   finally
      DeallocateHwnd(Fhwnd);
      Fhwnd := 0;
   end;
end;
复制代码

 

Where we can have a plain old window procedure to handle the messages:

复制代码
WM_TerminateYourself = WM_APP + 1;

procedure TMyThread.WindowProc(var msg: TMessage);
begin
   case msg.Msg of
   WM_ReadyATractorBeam: ReadyTractorBeam;
   WM_TerminateYourself: PostQuitMessage(0);
   else
      msg.Result := DefWindowProc(Fhwnd, msg.msg, msg.wParam, msg.lParam);
   end;
end;    
复制代码

and when you want the thread to finish, you tell it:

procedure TMyThread.Terminate;
begin
   PostMessage(Fhwnd, WM_TerminateYourself, 0, 0);
end;

PostThreadMessage( FThreadId, WM_QUIT, 0, 0 );

Using PostThreadMessage is not necessarily incorrect. Raymond's article that you linked to says:

PostQuitMessage(0)

Because the system tries not to inject a WM_QUIT message at a "bad time";

instead it waits for things to "settle down" before generating the WM_QUIT message,

thereby reducing the chances that the program might be in the middle of a multi-step

procedure triggered by a sequence of posted messages.

If the concerns outlined here do not apply to your message queue,

then call PostThreadMessage with WM_QUIT and knock yourself out.

Otherwise you'll need to create a special signal, i.e. a user-defined message,

that allows you to call PostQuitMessage from the thread.

posted @   IAmAProgrammer  阅读(607)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示