【问题标题】:How to create a modeless dialog from a worker thread when main GUI thread is blocked?当主 GUI 线程被阻塞时,如何从工作线程创建无模式对话框?
【发布时间】:2019-01-24 08:31:00
【问题描述】:

我的目标是编写一个类(我们称它为CProgressDlg),当主 UI 线程中的某些操作需要超过 1 秒才能完成时,该类可用于显示带有进度条的对话框窗口。所以之前写的一个方法:

if(do_work)
{
    for(int i = 0; i < a_lot; i++)
    {
        //Do work...
        ::Sleep(100);     //Use sleep to simulate work
    }
}

可以像这样轻松调整(伪代码):

if(do_work)
{
    CProgressDlg m_progDlg;

    for(int i = 0; i < a_lot; i++)
    {
        //Do work...
        ::Sleep(100);     //Use sleep to simulate work

        if(m_progDlg.UpdateWithProgress(i))
        {
            //User canceled it
            break;
        }
    }
}

所以要实现它,我将从CProgressDlg 构造函数启动一个工作线程:

::CreateThread(0, 0, ThreadProcProgressDlg, (LPVOID)0, 0, 0);

然后我会从一个工作线程创建一个无模式对话框,为用户显示进度条和一个取消按钮:

DWORD WINAPI ThreadProcProgressDlg(
  _In_ LPVOID lpParameter
)
{
    //Wait a little
    ::Sleep(1000);

    HMODULE hModule = AfxGetResourceHandle();
    ASSERT(hModule);

    //Get parent window
    //(Can't use main window, as its UI thread is blocked)
    HWND hParentWnd = NULL;

    const static BYTE dlgTemplate[224] = {
        0x1, 0x0, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc8, 0x0, 0xc8, 0x90, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0xdb, 0x0, 0x4b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x90, 0x1, 0x0, 0x1, 0x4d, 0x0, 0x53, 0x0, 0x20, 0x0, 0x53, 0x0, 0x68, 0x0, 0x65, 0x0, 0x6c, 0x0, 0x6c, 0x0, 0x20, 0x0, 0x44, 0x0, 0x6c, 0x0, 0x67, 0x0, 0x0, 0x0, 
        0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x50, 0x92, 0x0, 0x36, 0x0, 0x42, 0x0, 0xe, 0x0, 0x2, 0x0, 0x0, 0x0, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x81, 0x0, 0x2, 0x50, 0x7, 0x0, 0x7, 0x0, 0xcd, 0x0, 0x19, 0x0, 0xed, 0x3, 0x0, 0x0, 0xff, 0xff, 0x82, 0x0, 0x0, 0x0, 0x0, 0x0, 
        0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x50, 0x7, 0x0, 0x21, 0x0, 0xcd, 0x0, 0x7, 0x0, 0xec, 0x3, 0x0, 0x0, 0x6d, 0x0, 0x73, 0x0, 0x63, 0x0, 0x74, 0x0, 0x6c, 0x0, 0x73, 0x0, 0x5f, 0x0, 0x70, 0x0, 0x72, 0x0, 0x6f, 0x0, 0x67, 0x0, 0x72, 0x0, 0x65, 0x0, 0x73, 0x0, 0x73, 0x0, 0x33, 0x0, 0x32, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 
        0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x81, 0x0, 0x2, 0x50, 0x7, 0x0, 0x29, 0x0, 0xcd, 0x0, 0x8, 0x0, 0xee, 0x3, 0x0, 0x0, 0xff, 0xff, 0x82, 0x0, 0x0, 0x0, 0x0, 0x0, };

    //Show dialog
    HWND hDlgWnd = ::CreateDialogIndirectParam(hModule, (LPCDLGTEMPLATE)dlgTemplate, hParentWnd, DlgWndProc, (LPARAM)0);
    ASSERT(hDlgWnd);
    if(hDlgWnd)
    {
        ::ShowWindow(hDlgWnd, SW_SHOW);
    }

    return 0;
}

最小的对话过程(只是为了显示它)将是这样的:

INT_PTR CALLBACK DlgWndProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(wParam);
    UNREFERENCED_PARAMETER(lParam);

    switch (uMsg)
    {
        case WM_INITDIALOG:
        {
        }
        return TRUE;

        case WM_COMMAND:
        {
            UINT uCmd = LOWORD(wParam);

            if (uCmd == IDOK || 
                uCmd == IDCANCEL)
            {
                ::DestroyWindow(hDlg);

                return (INT_PTR)TRUE;
            }
        }
        break;
    }

    return (INT_PTR)FALSE;
}

但是当我运行这段代码时,我的无模式对话框会显示片刻然后消失。我知道我可能没有做任何事情来从工作线程中正确显示它。

知道我错过了什么吗?

【问题讨论】:

  • 为什么要把繁重的工作交给GUI线程,把进度条交给工作线程?不应该反过来吗?
  • @Alex:我明白这一点。就像我说的,这是已经编写的代码的补丁。
  • 示例:用户单击列表列对其进行排序(包含很多项目的列表)。在当前代码中,它可能会使用旋转的鼠标光标阻塞主线程约 5 秒。我想写这门课来解决这样的情况。 (这只是我想到的一个例子。)
  • 也许有一个错误导致destroy函数被无意调用?通过调试断点单步执行代码会留下任何线索吗?
  • 既然你在主线程中做繁重的工作,那么仅仅为了更新GUI而创建一个工作线程是没有意义的。它肯定会产生比解决更多的问题。也就是说,如果您需要从工作线程更新部分GUI,您通常会使用PostMessage 发布消息,并让主线程在繁重的工作循环中处理此消息。同样,这并不比让主线程完成所有工作更好(事实上,它更糟)。

标签: c++ multithreading winapi mfc dialog


【解决方案1】:

线程要显示一个窗口,必须有一个消息循环,以便窗口接收消息。工作线程通常没有消息循环,因此无法显示窗口。否则,您需要定期致电GetMessage(),这是一种不好的做法,但无论如何它都会起作用。收到消息后,使用 TranslateMessage() 和 DispatchMessage()。

另见Worker thread doesn't have message loop (MFC, windows). Can we make it to receive messages?

【讨论】:

  • 好吧,我不能打电话给GetMessagePeekMessage,因为我没有那个工作线程的窗口。加上GetMessage 将阻止执行。
  • 这就是 GUI 线程的重点:阻塞直到你有消息。这就是为什么从一个应该做其他工作的线程调用它是不好的。
【解决方案2】:

正如其他人指出的那样,您不能简单地从非 GUI 线程创建窗口。即使你能做到,你仍然会遇到你说的主线程“挂起”的问题。

您有 2 个解决方案: 1)使用消息泵技术 2) 将工作移入线程并在显示进度窗口的同时等待 GUI

不幸的是,这两种解决方案都要求您根据具体情况进行处理。您需要手动识别 GUI 上所有可能较长的操作,然后修改它们的代码。

在这两种情况下,我都喜欢使用模态对话框来控制进度条,因为模态对话框会阻止访问主 UI。这可以防止用户在当前功能完成之前与其他功能进行交互。

  1. 防止挂起的最基本方法是添加一个查看消息队列并将其抽出的函数:
bool CMyGUIWnd::PumpAppMessages()
{
    MSG msg;
    while (::PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE)) {
        if (!AfxGetApp ()->PumpMessage ()) {
            ::PostQuitMessage (0);
            return false;
        }
    }

    return true;
}

在任何冗长的代码中(例如您的列排序),您都需要找到将这个PumpAppMessages 洒在哪里的地方,以保持程序响应。

当然你不想一直调用它,你可能想确保只在一定的毫秒数后调用它(在我下面的示例中为 250):

bool CMyGUIWnd::PumpMessages()
{
    //
    // Retrieve and dispatch any waiting messages.
    //
    bool do_update = false;
    DWORD cur_time = ::GetTickCount ();

    if (cur_time < m_last_pump_message_time){
        do_update = true; // wrap around occurred
    }else{
        DWORD dt = cur_time - m_last_pump_message_time;
        if (dt > 250){
            do_update = true;
        }
    }

    if (do_update)
    {
        m_last_pump_message_time = cur_time;    
        return PumpAppMessages();
    }

    return true;
}

其中m_last_pump_message_time 初始化为::GetTickCount()

要显示进度条和阻止 UI,您需要编写一个进度对话框类,该类在创建后调用冗长的函数。您将在函数之前实例化此对话框并使用 DoModal 调用阻止 UI。

void CMyGUIWnd::LengthyCallWrapper()
{
    CProgressDlg dlg (&LengthyCall);
    dlg.DoModal();
}

void CMyGUIWnd::LengthyCall()
{
    // Long process with lots of PumpMessages calls to keep UI alive
}
  1. 第二种方法工作量大一些,但使 UI 响应更快,因为它不依赖于消息抽取的频率。

您将再次使用进度对话框类,它采用冗长的函数指针并在工作线程中执行它。一旦线程完成,它应该向对话框发布一条消息。作为对该消息的响应,对话框将自行关闭以解锁 UI。

对于这两种情况下的实际进度报告,您的LengthyCall 应该采用指向进度对话框的指针,以便它可以更新适当的控件。在第二种方法中,您需要在设置任何进度变量时添加诸如CCriticalSection 之类的同步对象,但您不会直接修改任何控件。相反,您将设置一个计时器,并根据提供的变量定期检查和更新进度控件。

希望这会有所帮助。

【讨论】:

  • 嘿,感谢您的意见。
  • 但是你试过了吗?我在商业软件中使用这两种方法至少 15 年了。正如我所说,Windows 上的 GUI 线程没有灵丹妙药。如果您想在 GUI 上显示长任务的进度,则需要中断它,否则您将其移动到工作线程并与 UI 对话。
  • 不,我没有。我前段时间自己实现了它。我想我在 cmets 的某个地方发布了我的解决方案。基本上,我在我的工作线程中创建了一个消息循环,允许创建自己的弹出窗口,让用户查看进度并在需要时取消。主线程定期调用更新函数,通知工作线程其进度,检查stopNow 事件是否需要退出,并执行此操作:PeekMessage(&amp;msg, hMainThreadWnd, 0, 0, PM_NOREMOVE); 以防止 GUI 线程在标题栏中显示“未响应”用于锁定的 GUI 线程。
  • 我知道这不是一个理想的解决方案,因为它不允许 GUI 线程处理其绘图消息,如果用户开始大量交互,则会产生主窗口变灰的副作用用它。但是,从好的方面来说,这种方法可以包含在已经工作的代码中,只需 2 或 3 行代码,而无需做任何其他事情。因此,我将与它一起去。
【解决方案3】:
CreateDialogIndirectParam(...)
ShowWindow(...)

CreateDialog 立即返回,线程在ShowWindow 之后退出,因此无模式对话框关闭,因为线程已完成。

这与DialogBoxDialogBoxIndirect 不同,它们有自己的消息循环,并且在用户关闭对话框或其他消息之前不会返回。

CreateDialogCreateDialogIndirectParam...的用法如下:

CreateDialogIndirectParam(...)
ShowWindow(...)

MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
    if(hDlgWnd == 0 || !IsDialogMessage(hDlgWnd, &msg))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}


在这种情况下,您可以从 GUI 线程中借用一个窗口。例子:
class CProgressDlg : public CDialog
{
public:
    bool stop;
    CProgressDlg() { stop = false; }
    void OnCancel()
    {
        stop = true;
        CDialog::OnCancel();
    }
};

UINT WorkerThread(LPVOID ptr)
{
    CProgressDlg* dlg = (CProgressDlg*)ptr;
    for(int i = 0; i < 10; i++)
    {
        Sleep(1000);
        if(dlg->stop)
        {
            dlg->MessageBox(L"stopped");
            break;
        }
    }

    dlg->SendMessage(WM_COMMAND, IDCANCEL);
    return 0;
}

void CMyWindow::foo()
{
    m_progress.Create(IDD_PROGRESS, this);
    m_progress.ShowWindow(SW_SHOW);
    AfxBeginThread(WorkerThread, (LPVOID)&m_progress);
}

【讨论】:

  • 谢谢,但这并不容易。似乎发生的事情是这样的。无模式对话框使用其父窗口的消息循环。另一个规定似乎是父窗口的WndProc 必须在同一个线程中。所以在我的情况下,当CreateDialogIndirectParam 被调用时,它没有找到与线程关联的任何消息循环,因此一些内部检查使其自毁。解决方法是创建一个消息循环。不幸的是,说起来容易做起来难。
  • 我基本上最终从工作线程注册了一个自定义类,然后创建了该类的仅消息窗口,因此它在该工作线程中有自己的消息循环。然后,我可以使用主线程中的SendMessage/PostMessage 机制与该线程通信(在它进入长时间处理之前)。然后使用自定义WM_APP+n 消息之一,我能够通知我的线程如上所示,致电CreateDialogIndirectParam。但这仍然有一些与我的主 GUI 线程的消息循环挂起的事实相关的惊喜......
  • 即如果我将“挂起”主线程的HWND 指定为CreateDialogIndirectParamShowWindow 的父窗口,则在尝试显示我的窗口时,这两个函数都会挂起,直到主线程响应为止。 (因此违背了我在这里练习的目的。)我猜这两个 API 都使用 SendMessage 与父窗口进行通信。因此,显示我的无模式窗口的唯一方法是使用NULL 作为父窗口,不幸的是,这有其自身的负面后果,即从其父窗口中分离出来。我还没有克服...
  • 线程已完成,因此与该线程关联的窗口被销毁。您不能拥有与不再存在的线程关联的窗口;或不再存在的进程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-12-20
  • 2021-03-11
  • 1970-01-01
  • 2010-12-30
  • 1970-01-01
  • 1970-01-01
  • 2013-03-30
相关资源
最近更新 更多