简体   繁体   中英

How to create a modeless dialog from a worker thread when main GUI thread is blocked?

My goal is to write a class (let's call it CProgressDlg ) that can be used to display a dialog window with a progress bar when some operation in the main UI thread takes longer than, say 1 second, to finish. So a previously written method:

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

can be easily adjusted as something like this (pseudo-code):

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;
        }
    }
}

So to implement it I would start a worker thread from the CProgressDlg constructor:

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

And then from a worker thread I would create a modeless dialog that will display the progress bar and a cancel button for the user:

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;
}

Where the minimal dialog procedure (just to display it) will be something like this:

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;
}

But when I run this code, my modeless dialog is shown for a split second and then disappears. I understand that I probably haven't done something to display it properly from a worker thread.

Any idea what am I missing?

For a thread to display a window, there must be a message loop so the window receives messages. Worker threads typically do not have message loops, so no window can be displayed. Otherwise, you need to call GetMessage() periodically, a bad practise but it will anyway work. After you get a message, Use TranslateMessage() and DispatchMessage().

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

As others pointed out you cannot simply create windows from non GUI thread. Even if you were able to, you would still have a problem of main thread 'hanging' as you said.

Your have 2 solutions here: 1) Use message pumping technique 2) Move the work into a thread and wait on GUI while showing progress window

Unfortunately both solutions require you to deal with this on case by case basis. You need to manually identify all potentially long operations on GUI and then modify their code.

In both cases I like to use modal dialog for progress bar control because modal dialog blocks access to main UI. This prevents user from interacting with other features until current one is completed.

  1. The most basic way to prevent hanging is to add a function that peeks at message queue and pumps it:
bool CMyGUIWnd::PumpAppMessages()
{
    MSG msg;
    while (::PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE)) {
        if (!AfxGetApp ()->PumpMessage ()) {
            ::PostQuitMessage (0);
            return false;
        }
    }

    return true;
}

In any lengthy code (like your column sort) you would need to find places where to sprinkle this PumpAppMessages to keep program responsive.

Of course you don't want to call this all the time and you might want to make sure you only call it after certain number of milliseconds (250 in my example below):

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;
}

where m_last_pump_message_time is initialized with ::GetTickCount() .

To show progress bar and block UI, you would need to write a progress dialog class which calls your lengthy function once it's created. You would instantiate this dialog before the function and block UI with DoModal call.

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

void CMyGUIWnd::LengthyCall()
{
    // Long process with lots of PumpMessages calls to keep UI alive
}
  1. The second approach is a bit more work, but makes the UI more responsive because it doesn't depend on how often message pumping is done.

You would again use the progress dialog class which takes the lengthy function pointer and executes it in worker thread. Once the thread is finished it should post a message to dialog. In response to this message the dialog would close itself unblocking UI.

For actual progress reporting in both cases your LengthyCall should take a pointer to the progress dialog so that it could update proper controls. In the second approach you would need to add sync objects like CCriticalSection when setting any progress variables but you would not modify any controls directly. Instead, you would setup a timer and check and update progress controls in regular intervals based on provided variables.

Hope this helps.

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

CreateDialog returns immediately, the thread exits after ShowWindow , and so the mode-less dialog closes because the thread is finished.

This is different than DialogBox , DialogBoxIndirect which have their own message loop, and do not return until the dialog is closed by the user or another message.

The usage for CreateDialog , CreateDialogIndirectParam ... is as follows:

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

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


In this case you can just borrow a window from GUI thread. Example:

 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); } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM