正确关闭使用 ATL 在单独线程上创建的窗口

Posted

技术标签:

【中文标题】正确关闭使用 ATL 在单独线程上创建的窗口【英文标题】:Properly closing window created on a separate thread using ATL 【发布时间】:2013-05-22 12:09:48 【问题描述】:

我有一个多线程应用程序,并且在某些线程上,我正在使用 ATL 的CWindowImpl<> 创建窗口。我有一个用作线程过程的静态方法。我需要在线程上创建一个窗口,因为我需要与线程进行一些通信到be synchronous,而PostThreadMessage() 是明确异步的。当我的窗口收到WM_DESTROY消息(由MESSAGE_HANDLER宏定义的处理程序)时,它调用PostQuitMessage(),如下方法所示:

LRESULT MyATLWindowClass::OnDestroy(UINT uMsg,
                                    WPARAM wParam,
                                    LPARAM lParam,
                                    BOOL& bHandled) 
  ::PostQuitMessage(0);
  return 0;

我正在使用PostThreadMessage() 向线程发送自定义消息,以向线程指示是时候自行终止了。处理该自定义消息时,我调用了CWindowImpl::DestroyWindow() 方法,该方法似乎可以正确地破坏窗口,因为我的OnDestroy 消息处理程序正在被调用。但是,拥有线程似乎从未收到WM_QUIT 消息进行处理。下面是我的线程过程的简化版本。

unsigned int WINAPI MyATLWindowClass::ThreadProc(LPVOID lpParameter) 
  // Initialize COM on the thread
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

  // Create the window using ATL
  MyATLWindowClass new_window;
  HWND session_window_handle = new_window.Create(
      /* HWND hWndParent */ HWND_MESSAGE,
      /* _U_RECT rect */ CWindow::rcDefault,
      /* LPCTSTR szWindowName */ NULL,
      /* DWORD dwStyle */ NULL,
      /* DWORD dwExStyle */ NULL,
      /* _U_MENUorID MenuOrID */ 0U,
      /* LPVOID lpCreateParam */ NULL);

  // Initialize the message pump on the thread.
  MSG msg;
  ::PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);

  // Run the message loop
  BOOL get_message_return_value;
  while ((get_message_return_value = ::GetMessage(&msg, NULL, 0, 0)) != 0) 
    if (get_message_return_value == -1) 
      // GetMessage handling logic taken from MSDN documentation
      break;
     else 
      if (msg.message == WD_SIGNAL_THREAD_SHUTDOWN) 
        // Requested thread shutdown, so destroy the window
        new_window.DestroyWindow();
       else if (msg.message == WM_QUIT) 
        // Process the quit message and exit the message loop
        // to terminate the thread
        break;
       else 
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
      
    
  

  // Uninitialize COM on the thread before exiting
  ::CoUninitialize();
  return 0;

请注意,我是否调用DestroyWindow() 或向窗口发送WM_CLOSE 消息似乎并不重要。在这两种情况下,线程的消息泵都没有接收到 WM_QUIT。拥有线程的消息泵应该接收这样的消息吗?我对线程的消息泵和窗口的消息泵如何交互的误解在哪里?或者关于 ATL 的窗口类如何创建和管理窗口,我缺少什么?

【问题讨论】:

WM_QUIT 是终止整个应用程序,而你只是在破坏你的窗口......为什么你期望WM_QUIT 来?除非您有特定原因,否则它不必在那里。如果您的线程仅用于托管窗口,那么只需在线程 proc 上监视 WM_NCDESTROY,一旦您为窗口处理了它,就该关闭线程了。 根据the documentation for PostQuitMessage(),该API 函数应该将WM_QUIT 消息发布到线程的消息队列,并且应该可以在每个线程的基础上使用。我是否误解了文档? GetMessage 为 WM_QUIT 消息返回 0,因此您的 while 循环终止而不执行循环体。所以你不需要测试 == WM_QUIT @ScottMcP-MVP 啊哈!那就是我缺少的那部分。我在GetMessage 文档中忽略了那篇文章。感谢您指出。 【参考方案1】:

GetMessage() 从不返回 WM_QUIT。该消息强制它返回 0,旨在终止您的消息循环。

当心使用 PostThreadMessage() 的相当大的危险。它应该从不在一个也显示窗口的线程上使用,就像你正在使用的那样。问题是它不需要 HWND 参数。所以只有你的消息循环可以看到消息,它不会被发送到任何带有 DispatchMessage() 的窗口。当输入模式消息循环时,这会出错,这种循环不在您的控制范围内。就像使 MessageBox 工作的模态循环一样。或者 Windows 用来允许用户调整窗口大小的那个。或者 DialogBox() 使用的那个。等等。始终使用 PostMessage(),使用您自己的消息号。

【讨论】:

在这里我将简单地指出,我的窗口永远不会向用户显示,只是用作线程之间通信的同步便利。这是我想要的行为。如果该应用程序实际上开始在屏幕上绘制窗口,则无论如何都需要重构此代码。顺便说一句,我搬到PostThreadMessage 是因为简单地将WM_CLOSE 发送到窗口(使用适当的消息处理程序),然后等待线程退出会在C 运行时深处崩溃。另一方面,这种方法很有效。 哦,对于那些在家记分的人来说,有问题的应用程序是开源的,特别是Selenium project 的 IEDriverServer.exe 应用程序。如果有人想尝试解决我在 Windows 编程方面明显的无能,请成为我的客人。 Patches gratefully accepted.【参考方案2】:

一些迟到的额外想法。一旦你发现WD_SIGNAL_THREAD_SHUTDOWN,你就可以安全地终止你的消息循环:

  if (msg.message == WD_SIGNAL_THREAD_SHUTDOWN) 
    // Requested thread shutdown, so destroy the window
    new_window.DestroyWindow();
    break; // exit the message loop
  

DestroyWindow是同步调用,窗口在返回前会被完全销毁,可以退出循环。因此,发布 WM_QUIT 将是多余的。

此外,如果窗口不可见且其唯一目的是处理消息,则可以使用 message-only 窗口。

【讨论】:

以上是关于正确关闭使用 ATL 在单独线程上创建的窗口的主要内容,如果未能解决你的问题,请参考以下文章

使用异步方法等待 Task.Run 不会在正确的线程上引发异常

在单独的线程中运行 Tkinter 表单

关闭已创建 STA COM 对象的线程时避免断开上下文警告

如何在不同的线程上使用静态资源?

创建单独的 MFC GUI 线程,不能移动/调整大小/最大化 CWnd

如何使用线程自动关闭 PyQt/PySide 窗口?