【问题标题】:Creating a Window Inside TThread在 TThread 中创建一个窗口
【发布时间】:2011-04-08 00:10:26
【问题描述】:

我试图在 2 个单独的项目之间发送消息,但我的问题是我试图让接收器在 TThread 对象内运行,但 WndProc 不能从对象内工作,必须是一个函数,无论如何要创建TThread 中的一个窗口,可以处理线程内的消息?

这就是我的意思

function TDataThread.WindowProc(hwnd: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
 Result := 0;
 case uMsg of
   WM_DATA_AVA: MessageBox(0, 'Data Avaibale', 'Test', 0);
  else Result := DefWindowProc(hwnd, uMsg, wParam, lParam);
 end;
end;

Procedure TDataThread.Create(const Title:String);
begin
 HAppInstance := HInstance;
 with WndClass do
 begin
  Style := 0;
  lpfnWndProc := @WindowProc;          //The Error Lies here (Variable Required)
  cbClsExtra := 0;
  cbWndExtra := 0;
  hInstance := HAppInstance;
  hIcon := 0;
  hCursor := LoadCursor(0, IDC_ARROW);
  hbrBackground := COLOR_WINDOW;
  lpszMenuName := nil;
  lpszClassName := 'TDataForm';
 end;
 Windows.RegisterClass(WndClass);
 MainForm := CreateWindow('TDataForm', PAnsiChar(Title), WS_DLGFRAME , XPos, YPos, 698, 517, 0, 0, hInstance, nil);
end;

我需要一个表单,以便我可以从另一个应用程序获取它的句柄,如果需要,使用 FindWindow 和 FindWindowEx

【问题讨论】:

    标签: delphi delphi-7


    【解决方案1】:

    在后台线程中运行 wndproc 可以在 Win32 中完成,但人们普遍认为这是一个坏主意。

    为此,您必须确保您的后台线程包含消息分发循环:GetMessage/TranslateMessage/DispatchMessage。您必须确保要在后台线程中处理消息的窗口句柄是在后台线程(在后台线程的上下文中调用 CreateWindow)及其所有子窗口上创建的。而且你必须确保你的后台线程除了它正在做的任何事情之外还经常调用它的消息循环(这有点违背了使用后台线程的目的!)

    如果您的后台线程没有消息循环,则在后台线程上创建的窗口句柄将永远不会收到任何消息,因此不会发生任何事情。

    那么,为什么不应该这样做:Windows 是消息驱动的,这意味着它们本质上是一个协作式多任务调度系统。每个 GUI windows 应用程序都必须在主线程中有一个消息循环才能完成任何事情。该消息循环将支持几乎任意数量的窗口,所有这些都在主线程上。正确实现的 UI 不会在主线程中做任何事情来阻止执行,因此消息循环将始终准备好并响应。

    因此,如果主线程上的现有消息循环将处理您所有的窗口消息传递需求而不会阻塞或冻结,那么您为什么要通过尝试在后台线程中运行第二个消息循环来使您的生活变得更加复杂呢?使用后台线程没有任何好处。

    【讨论】:

    • 作为建议,当有新数据可供处理时,让主线程获取消息并通知您的工作线程。
    • Windows 中的所有线程都是平等的,没有关于它们的“主要”或“背景”。它们在是否有消息循环方面有所不同,并且有一个是在流程中首先创建的,但就差异而言,仅此而已。与 COM 的交互可能需要线程具有消息循环,在线程中使用窗口需要它具有消息循环。消息循环也是与线程通信的好方法。除了 VCL 与它不匹配之外,一个进程中的多个消息循环没有任何问题。
    • 所以我无法让我的线程与其他进程通信?因为每个进程都必须向其线程发送回复以表明它已准备好映射文件以获取数据!
    • 由于您提到另一个进程发送回复以指示数据已准备好,您还可以考虑为每个进程/线程对使用一个命名互斥锁。线程启动进程,将互斥锁的名称作为参数传递,然后线程阻塞等待互斥锁发出信号。该进程获取命名的互斥体并在工作完成时发出信号。不需要消息循环。 (这假设您可以控制进程和线程的源代码)
    • @mghie:是的,Windows 中的所有线程都是平等的。然而,并非所有程序员都如此。如果有一个解决方案可以在不大量使用线程的情况下完成工作,请使用它。如果有一个解决方案可以在不使用线程绑定窗口句柄、后台线程和 COM 的情况下完成工作,那就更好了。人们被线吸引,就像飞蛾扑火一样,结果相似。
    【解决方案2】:

    您不需要 Window 来接收消息,请尝试以下操作。 在线程中(一次)调用 PeekMessage 以强制创建消息队列,例如:

      // Force Message Queue Creation
      PeekMessage(Msg, 0, WM_USER, WM_USER, PM_NOREMOVE);
    

    然后设置一个消息循环/泵,例如:

      // Run until terminated
      while not Terminated do
      begin
    
        if GetMessage(@Msg, 0, 0, 0) then
        begin
          case Msg.message of
            WM_DATA_AV: MessageBox(0, 'Data Avaibale', 'Test', 0); 
          else begin
            TranslateMessage(@Msg);
            DispatchMessage(@Msg);
          end;
        end;
      end;
    

    【讨论】:

    • 是的,但是我怎么知道这个线程的句柄来发送消息呢?因为发件人来自另一个进程
    • 使用 PostThreadMessage (msdn.microsoft.com/en-us/library/ms644946(VS.85).aspx),它使用 ThreadId 而不是窗口句柄。
    • 但是您遇到了发送应用程序需要定位接收线程 ID 的问题。使用窗口使搜索更容易。
    【解决方案3】:

    如果 TThread 实现了消息循环,并且在与消息循环相同的线程上下文中调用 CreateWindow(),则在 TThread 中创建窗口可以正常工作。换句话说,您必须从 TThread 的 Execute() 方法内部调用 CreateWindow(),而不是从其构造函数内部调用,例如:

    type
      TDataThread = class(TThread)
      private
        FTitle: String;
        FWnd: HWND;
        FWndClass: WNDCLASS;
        FRegistered: boolean;
        class function WindowProc(hwnd: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; static;
      protected
        procedure Execute; override;
        procedure DoTerminate; override;
      public
        constructor Create(const Title:String); reintroduce;
      end;
    
    constructor TDataThread.Create(const Title: String); 
    begin 
      inherited Create(False);
      FTitle := Title;
      with FWndClass do 
      begin 
        Style := 0; 
        lpfnWndProc := @WindowProc;
        cbClsExtra := 0; 
        cbWndExtra := 0; 
        hInstance := HInstance; 
        hIcon := 0; 
        hCursor := LoadCursor(0, IDC_ARROW); 
        hbrBackground := COLOR_WINDOW; 
        lpszMenuName := nil; 
        lpszClassName := 'TDataForm'; 
      end; 
    end; 
    
    procedure TDataThread.Execute; 
    var
      Msg: TMsg;
    begin
      FRegistered := Windows.RegisterClass(FWndClass) <> 0;
      if not FRegistered then Exit;
      FWnd := CreateWindow(FWndClass.lpszClassName, PChar(FTitle), WS_DLGFRAME, XPos, YPos, 698, 517, 0, 0, HInstance, nil); 
      if FWnd = 0 then Exit;
      while GetMessage(Msg, FWnd, 0, 0) > 0 do
      begin
        TranslateMessage(msg);
        DispatchMessage(msg)
      end;
    end;
    
    procedure TDataThread.DoTerminate;
    begin
      if FWnd <> 0 then DestroyWindow(FWnd);
      if FRegistered then Windows.UnregisterClass(FWndClass.lpszClassName, HInstance);
      inherited;
    end;
    
    function TDataThread.WindowProc(hwnd: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
    begin
      Result := 0;
      case uMsg of
        WM_DATA_AVA:
          MessageBox(0, 'Data Available', 'Test', 0);
      else
        Result := DefWindowProc(hwnd, uMsg, wParam, lParam);
      end;
    end; 
    

    【讨论】:

    • +1,这是来自 dthorpe 的回答的重要技术信息,这有点隐藏在警告中。不过没必要有会员FWndClass,把所有的东西都放进Execute(),去掉DoTerminate(),事情就清楚了。如果类名和窗口标题都是构造函数的参数,这将是一个很好的辅助基类。
    • 我更喜欢使用 DoTerminate() 因为它允许线程自行清理,无论 Execute() 是干净退出还是由于未捕获的异常。在整个 Execute() 代码周围放置一个 try/except 对我来说有点难看。
    • 人为地将数据结构的设置、使用和销毁放入不同的方法中会更糟糕。例如,即使 RegisterClass() 失败,您的代码也会愉快地调用 UnregisterClass()
    • 这就是为什么我通常会在取消注册之前添加一个检查以确保它已注册。这只是一个例子。
    • @JerryDodge BOOL 是一个 4 字节整数(在 Delphi 中为 LongBool)。 GetMessage() 能够返回 -1、0 和 > 0,尽管 -1 is rareAllocateHWnd() 不是线程安全的,因此直接使用CreatWindow()GetMessage() 只能返回来自PostMessage()PostThreadMessage() 的已发布消息(尽管跨线程边界分派来自SendMessage() 的已发送消息是必需的)。 WM_POWERBROADCAST 不是贴出的消息,所以消息循环是看不到的,需要窗口过程来处理。
    【解决方案4】:
    TTestLoopThread = class(TThread)
          private
            FWinHandle: HWND;
            procedure DeallocateHWnd(Wnd: HWND);
          protected
            procedure Execute; override;
            procedure WndProc(var msg: TMessage);
          public
            constructor Create;
            destructor Destroy; override;
          end;
    
        implementation
    
        var
          WM_SHUTDOWN_THREADS: Cardinal;
    
        procedure TForm1.FormCreate(Sender: TObject);
        begin
          WM_SHUTDOWN_THREADS := RegisterWindowMessage('TVS_Threads');
        end;
    
        procedure TForm1.Button1Click(Sender: TObject);
        begin
          TTestLoopThread.Create;
        end;
    
        procedure TForm1.Button2Click(Sender: TObject);
        begin
          SendMessage(wnd_broadcast, WM_SHUTDOWN_THREADS, 0, 0);
        end;
    
        { TTestLoopThread }
    
        constructor TTestLoopThread.Create;
        begin
          inherited Create(False);
        end;
    
        destructor TTestLoopThread.Destroy;
        begin
          inherited;
        end;
    
        procedure TTestLoopThread.DeallocateHWnd(Wnd: HWND);
        var
          Instance: Pointer;
        begin
          Instance := Pointer(GetWindowLong(Wnd, GWL_WNDPROC));
          if Instance <> @DefWindowProc then
            // make sure we restore the old, original windows procedure before leaving
            SetWindowLong(Wnd, GWL_WNDPROC, Longint(@DefWindowProc));
          FreeObjectInstance(Instance);
          DestroyWindow(Wnd);
        end;
    
        procedure TTestLoopThread.Execute;
        var
          Msg: TMsg;
        begin
          FreeOnTerminate := True;
          FWinHandle := AllocateHWND(WndProc); //Inside Thread
          try
          while GetMessage(Msg, 0, 0, 0) do
            begin
             TranslateMessage(Msg);
             DispatchMessage(Msg);
            end;
          finally
          DeallocateHWND(FWinHandle);
          end;
        end;
    
        procedure TTestLoopThread.WndProc(var msg: TMessage);
        begin
          if Msg.Msg = WM_SHUTDOWN_THREADS then
          begin
           Form1.Memo1.Lines.Add('Thread ' + IntToStr(ThreadID) + ' shutting down.');
           PostMessage(FWinHandle, WM_QUIT, 0, 0);
          end
          else
           Msg.Result := DefWindowProc(FWinHandle, Msg.Msg, Msg.wParam, Msg.lParam);
        end;
    

    【讨论】:

    • AlocateHWND(), DeallocateHWND(), MakeObjectInstance(), FreeObjectInstance() - 这些函数不是线程安全的,因为它们使用不受跨线程并发访问保护的全局资源。主线程相当广泛地使用了这些函数,因此也使用它们的不安全工作线程真的会把它们搞砸。话虽如此,仍有一些第三方自定义实现是线程安全的。否则,根本不要使用它们,而直接使用 Win32 API 函数调用(CreateWindow()SetWindowLong())在工作线程中工作正常。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-27
    • 1970-01-01
    • 2018-02-28
    • 2022-01-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多