【问题标题】:Thread message loop for a thread with a hidden window?带有隐藏窗口的线程的线程消息循环?
【发布时间】:2011-10-08 23:23:23
【问题描述】:

我有一个 Delphi 6 应用程序,它有一个线程专用于与使用 SendMessage() 和 WM_COPYDATA 消息与外部程序交互的外部应用程序进行通信。因此,我创建了一个带有 AllocateHWND() 的隐藏窗口来服务需要,因为由于 SendMessage() 函数只接受窗口句柄,而不是线程 ID,因此线程消息队列将不起作用。我不确定要在线程 Execute() 方法中放入什么。

我假设如果我使用 GetMessage() 循环或创建一个带有 WaitFor*() 函数调用的循环,线程将阻塞,因此线程的 WndProc() 将永远不会处理来自的 SendMessage() 消息国外的节目对吗?如果是这样,在 Execute() 循环中放入不会不必要地消耗 CPU 周期但一旦收到 WM_QUIT 消息就会退出的正确代码是什么?如有必要,我总是可以使用 Sleep() 进行循环,但我想知道是否有更好的方法。

【问题讨论】:

  • SendMessage 不应该与线程 MQ 一起使用,PostMessage 是。
  • SendMessage() 如果 HWND 属于另一个进程,仍然需要接收线程执行消息检索(即消息循环)。

标签: multithreading delphi blocking sendmessage wm-copydata


【解决方案1】:

AllocateHWnd()(更具体地说,MakeObjectInstance())不是线程安全的,所以你必须小心。最好直接使用CreatWindow/Ex()(或AllocateHWnd() 的线程安全版本,如DSiAllocateHwnd()

无论如何,HWND 与创建它的线程上下文相关联,因此您必须在 Execute() 方法中创建和销毁 HWND,而不是在线程的构造函数/析构函数中。此外,即使SendMessage() 用于向您发送消息,它们来自另一个进程,因此它们不会被您的HWND 处理,直到其拥有的线程执行消息检索操作,因此线程需要自己的消息循环。

您的 Execute() 方法应该如下所示:

procedure TMyThread.Execute;
var
  Message: TMsg;
begin
  FWnd := ...; // create the HWND and tie it to WndProc()...
  try
    while not Terminated do
    begin
      if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLINPUT) = WAIT_OBJECT_0 then
      begin
        while PeekMessage(Message, 0, 0, 0, PM_REMOVE) do
        begin
          TranslateMessage(Message);
          DispatchMessage(Message);
        end;
      end;
    end;
  finally
    // destroy FWnd...
  end;
end;

procedure TMyThread.WndProc(var Message: TMessage);
begin
  if Message.Msg = WM_COPYDATA then
  begin
    ...
    Message.Result := ...;
  end else
    Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam, Message.LParam);
end;

或者:

// In Delphi XE2, a virtual TerminatedSet() method was added to TThread,
// which is called when TThread.Terminate() is called.  In earlier versions,
// use a custom method instead...

type
  TMyThread = class(TThread)
  private
    procedure Execute; override;
    {$IF RTLVersion >= 23}
    procedure TerminatedSet; override;
    {$IFEND}
  public
    {$IF RTLVersion < 23}
    procedure Terminate; reintroduce;
    {$IFEND}
  end;

procedure TMyThread.Execute;
var
  Message: TMsg;
begin
  FWnd := ...; // create the HWND and tie it to WndProc()...
  try
    while not Terminated do
    begin
      if WaitMessage then
      begin
        while PeekMessage(Message, 0, 0, 0, PM_REMOVE) do
        begin
          if Message.Msg = WM_QUIT then Break;
          TranslateMessage(Message);
          DispatchMessage(Message);
        end;
      end;
    end;
  finally
    // destroy FWnd...
  end;
end;

{$IF RTLVersion < 23}
procedure TMyThread.Terminate;
begin
  inherited Terminate;
  PostThreadMessage(ThreadID, WM_QUIT, 0, 0);
end;
{$ELSE}
procedure TMyThread.TerminatedSet;
begin
  PostThreadMessage(ThreadID, WM_QUIT, 0, 0);
end;
{$IFEND}

【讨论】:

  • 感谢@Remy Lebeau。 MsgWaitForMultipleObjects() 是我缺少的关键要素。
  • 您应该使用 DSiAllocateHwnd 而不是 AllocateHwnd。 thedelphigeek.com/2007/06/allocatehwnd-is-not-thread-safe.html
  • @David: WaitMessage() 在新消息到达之前不会返回,从而阻塞调用线程。 MsgWaitForMultipleObjects() 有一个超时,所以线程可以在消息队列空闲时唤醒做其他事情,比如检查Terminated 属性。你可以这样做WaitMessage(),除非你自己发消息。
  • @MaximMasiutin:是的,我知道这一切。我早期的 cmets 是几年前写的。在现代 Delphi 版本中,TThread 有一个虚拟的TerminatedSet() 方法,当调用Terminate() 时,可以覆盖该方法以执行此类发布/信号。但最初的问题是针对没有TerminatedSet() 的Delphi 6,因此需要使用自定义方法。我已经更新了我的答案。
  • @NasreddineAbdelillahGalfout 是的
【解决方案2】:

这是一个不需要 Classes.pas 的循环,仅依赖 System.pas 来实现一些辅助功能,Windows.pas 用于 Win32 API 函数,Messages.pas 用于 WM_ 常量。

请注意,这里的窗口句柄是从工作线程创建和销毁的,但是主线程一直等到工作线程完成初始化。您可以将此等待推迟到稍后,当您真正需要窗口句柄时,这样主线程可能会在此期间做一些工作,而工作线程会自行设置。

unit WorkerThread;

interface

implementation

uses
  Messages,
  Windows;

var
  ExitEvent, ThreadReadyEvent: THandle;
  ThreadId: TThreadID;
  ThreadHandle: THandle;
  WindowHandle: HWND;

function HandleCopyData(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;
begin
  Result := 0; // handle it
end;

function HandleWmUser(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;
// you may handle other messages as well - just an example of the WM_USER handling
begin
  Result := 0; // handle it
end;

function MyWindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
  if Msg = WM_COPYDATA then
  begin
    Result := HandleCopyData(hWnd, Msg, wParam, lParam);
  end else
  if Msg = WM_USER then
  begin
    // you may handle other messages as well - just an example of the WM_USER handling
    // if you have more than 2 differnt messag types, use the "case" switch
    Result := HandleWmUser(hWnd, Msg, wParam, lParam);
  end else
  begin
    Result := DefWindowProc(hWnd, Msg, wParam, lParam);
  end;
end;

const
  WindowClassName = 'MsgHelperWndClass';
  WindowClass: TWndClass = (
    style: 0;
    lpfnWndProc: @MyWindowProc;
    cbClsExtra: 0;
    cbWndExtra: 0;
    hInstance: 0;
    hIcon: 0;
    hCursor: 0;
    hbrBackground: 0;
    lpszMenuName: nil;
    lpszClassName: WindowClassName);

procedure CreateWindowFromThread;
var
  A: ATOM;
begin
  A := RegisterClass(WindowClass);
  WindowHandle := CreateWindowEx(WS_EX_TOOLWINDOW, WindowClassName, 'Message Helper Window', WS_POPUP, 0, 0, 0, 0, 0, 0, hInstance, nil);
end;

procedure FreeWindowFromThread;
var
  H: HWND;
begin
  H := WindowHandle;
  WindowHandle := 0;
  DestroyWindow(H);
  UnregisterClass(WindowClassName, hInstance);
end;

function ThreadFunc(P: Pointer): Integer;  //The worker thread main loop, windows handle initialization and finalization
const
  EventCount = 1;
var
  EventArray: array[0..EventCount-1] of THandle;
  R: Cardinal;
  M: TMsg;
begin
  Result := 0;
  CreateWindowFromThread;
  try
    EventArray[0] := ExitEvent; // you may add other events if you need - just enlarge the Events array
    SetEvent(ThreadReadyEvent);
    repeat
      R := MsgWaitForMultipleObjects(EventCount, EventArray, False, INFINITE, QS_ALLINPUT);
      if R = WAIT_OBJECT_0 + EventCount then
      begin
        while PeekMessage(M, WindowHandle, 0, 0, PM_REMOVE) do
        begin
          case M.Message of
             WM_QUIT:
               Break;
             else
                begin
                  TranslateMessage(M);
                  DispatchMessage(M);
                end;
          end;
        end;
        if M.Message = WM_QUIT then
          Break;
      end else
      if R = WAIT_OBJECT_0 then
      begin
        // we have the ExitEvent signaled - so the thread have to quit
        Break;
      end else
      if R = WAIT_TIMEOUT then
      begin
        // do nothing, the timeout should not have happened since we have the INFINITE timeout
      end else
      begin
        // some errror happened, or the wait was abandoned with WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount– 1)
        // just exit the thread
        Break;
      end;
    until False;
  finally
    FreeWindowFromThread;
  end;
end;

procedure InitializeFromMainThread;
begin
  ExitEvent := CreateEvent(nil, False, False, nil);
  ThreadReadyEvent := CreateEvent(nil, False, False, nil);
  ThreadHandle := BeginThread(nil, 0, @ThreadFunc, nil, 0, ThreadId);
end;

procedure WaitUntilHelperThreadIsReady;
begin
  WaitForSingleObject(ThreadReadyEvent, INFINITE); // wait until the worker thread start running and initialize the main window
  CloseHandle(ThreadReadyEvent); // we won't need it any more
  ThreadReadyEvent := 0;
end;

procedure FinalizeFromMainThread;
begin
  SetEvent(ExitEvent); // we should call it AFTER terminate for the Terminated property would already be True when the tread exits from MsgWaitForMultipleObjects
  WaitForSingleObject(ThreadHandle, INFINITE);
  CloseHandle(ThreadHandle); ThreadHandle := 0;
  CloseHandle(ExitEvent); ExitEvent := 0;
end;

initialization
  InitializeFromMainThread;

  WaitUntilHelperThreadIsReady; // we can call it later, just before we need the window handle
finalization
  FinalizeFromMainThread;
end.

【讨论】:

  • 如果我在我的程序中使用了Halt,则将不会执行终结部分。这样好吗?
  • @NasreddineAbdelillahGalfout 不要使用Halt。除非在极端条件下,否则很少有充分的理由使用它
  • @RemyLebeau 感谢您的回复。我一直在阅读有关 AllocateHWnd() 和其他替代方案的文档。完成部分出现了,当我读到它时,我发现了Halt。我不使用它,但很高兴知道。再次感谢您。
猜你喜欢
  • 2018-10-14
  • 1970-01-01
  • 2012-05-02
  • 2010-09-18
  • 2011-05-19
  • 1970-01-01
  • 1970-01-01
  • 2012-05-14
  • 1970-01-01
相关资源
最近更新 更多