Как создать немодальный диалог из рабочего потока, когда основной поток GUI заблокирован? - PullRequest
0 голосов
/ 24 января 2019

Моя цель - написать класс (назовем его CProgressDlg), который можно использовать для отображения диалогового окна с индикатором выполнения, когда для завершения какой-либо операции в основном потоке пользовательского интерфейса требуется, скажем, 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;
}

Но когда я запускаю этот код, мое немодальное диалоговое окно показывается на долю секунды, а затем исчезает. Я понимаю, что я, вероятно, не сделал что-то, чтобы правильно отобразить это из рабочего потока.

Есть идеи, что мне не хватает?

Ответы [ 3 ]

0 голосов
/ 26 января 2019
CreateDialogIndirectParam(...)
ShowWindow(...)

CreateDialog возвращается немедленно, поток завершается после ShowWindow, и поэтому диалоговое окно без режима закрывается, поскольку поток завершается.

Это отличается от DialogBox, DialogBoxIndirect, которые имеют свой собственный цикл сообщений и не возвращаются, пока диалог не будет закрыт пользователем или другим сообщением.

Использование для CreateDialog, CreateDialogIndirectParam ... следующее:

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);
}
0 голосов
/ 02 февраля 2019

Как уже отмечали другие, вы не можете просто создавать окна из не GUI-потока. Даже если бы вы смогли, у вас все равно была бы проблема «зависания» основного потока, как вы сказали.

У вас есть 2 решения здесь: 1) Используйте технику прокачки сообщений 2) Переместите работу в поток и дождитесь графического интерфейса, покажет окно прогресса

К сожалению, оба решения требуют, чтобы вы имели дело с этим в каждом конкретном случае. Вам необходимо вручную определить все потенциально длинные операции в графическом интерфейсе, а затем изменить их код.

В обоих случаях мне нравится использовать модальное диалоговое окно для управления индикатором выполнения, потому что модальное диалоговое окно блокирует доступ к основному пользовательскому интерфейсу. Это предотвращает взаимодействие пользователя с другими функциями до тех пор, пока текущая функция не будет завершена.

  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().

Чтобы отобразить индикатор выполнения и заблокировать пользовательский интерфейс, вам нужно написать класс диалогового окна прогресса, который вызывает вашу длинную функцию после ее создания. Вы бы создали этот диалог перед функцией и заблокировали пользовательский интерфейс с помощью 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. Второй подход - немного больше работы, но делает пользовательский интерфейс более отзывчивым, поскольку он не зависит от того, как часто выполняется накачка сообщений.

Вы бы снова использовали класс диалога прогресса, который берет длинный указатель на функцию и выполняет его в рабочем потоке. Как только поток закончен, он должен отправить сообщение в диалог. В ответ на это сообщение диалоговое окно само закрывается, разблокируя пользовательский интерфейс.

Для получения отчетов о реальном прогрессе в обоих случаях ваш LengthyCall должен взять указатель на диалог прогресса, чтобы он мог обновить надлежащие элементы управления. Во втором подходе вам нужно будет добавить объекты синхронизации, такие как CCriticalSection, при установке любых переменных прогресса, но вы не будете изменять какие-либо элементы управления напрямую. Вместо этого вы должны установить таймер и регулярно проверять и обновлять элементы управления прогрессом на основе предоставленных переменных.

Надеюсь, это поможет.

0 голосов
/ 24 января 2019

Чтобы поток отображал окно, должен быть цикл сообщений, чтобы окно получало сообщения. Рабочие потоки обычно не имеют циклов сообщений, поэтому окно не может быть отображено. В противном случае вам необходимо периодически вызывать GetMessage () , что является плохой практикой, но в любом случае это сработает. После получения сообщения используйте TranslateMessage () и DispatchMessage ().

Также см. Рабочий поток не имеет цикла сообщений (MFC, windows). Можем ли мы сделать это для получения сообщений?

...