简体   繁体   中英

IProgressDialog questions: Do I misunderstand its multithreading, as I seem to need a message pump? Can I avoid opening another dialog before it ends?

I'm trying to use IProgressDialog in PROGDLG_MODAL mode and I'm hitting two snags.

First, from what I gather from MSDN and the block comment in the <shlobj.h> header file describing IProgressDialog, you are supposed to use it directly from the thread you are doing work and that the IProgressDialog will do its UI stuff from another thread; that is,

while (there_is_still_work_to_do()) {
    if (pd->HasUserCancelled())
        break;
    do_more_work();
    completed++;
    pd->SetProgress64(completed, total);
}

is sufficient to get a responsive progress dialog up and running.

However, in practice, I still need to pump messages in do_more_work() ; if I don't, the progress dialog won't show up until after the loop ends! So am I misunderstanding MSDN when it says "The object then handles updating on a background thread"? Okay so I do need to pump messages, but now I'm still confused by what MSDN says insofar as: which methods of IProgressDialog can I run from my worker thread? Knowing this, I can restructure my code properly.

A more dire issue, however, is that StopProgressDialog() doesn't tear down the window immediately. In fact, neither does Release() ! Under non-modal situations, it'll appear that the progress dialog lingers on screen for some more time. This particular issue has been addressed before; I'll get to that. This time, however, I'm using the progress dialog as a UI-modal dialog. If I happen to call another dialog box function, such as MessageBox() , immediately after releasing the IProgressDialog, then we wind up with two modal dialogs with the same owner window: the progress dialog dismisses itself and re-enables the main window while the message box is still running.

The non-modal case was solved by others by merely hiding the progress dialog after calling StopProgressDialog() . While this does hide the window, it doesn't do anything about modality. Based on the question asker's hypothesis at the end of the linked question, I also tried posting and sending WM_NULL s after StopProgressDialog() . That also didn't work.

Last, I tried to set up a WinEvents hook, waiting for the window to be destroyed and firing an event object when it is. This does work; the MessageBox() will not happen until the progress dialog is destroyed. However, this does not work perfectly: the main window doesn't become active again immediately, and the MessageBox() doesn't even show up in the background until I click the main window's taskbar icon.

(Even then, because I can't pass an LPARAM to my WinEvents hook, if I wanted to handle multiple IProgressDialogs across different threads I would need to have a global list of window handles to watch for (and their associated event objects) which is synchronized. Fortunately I don't need this for my purposes. And furthermore, all of the above goes under the assumption that CLSID_ProgressDialog is also an IOleWindow; if that ever changes, then...)

Am I doing something wrong with this WinEvents way? I suspect I'm not calling MsgWaitForMultipleObjectsEx() correctly, but if it turns out that IProgressDialog doesn't implement modality properly, then I guess I'm out of luck :S

The sample program below does all of the above. I've tested it with Visual Studio 2013 on Windows 7 64-bit (building as 64-bit).

// 14 december 2015
#define _UNICODE
#define UNICODE
#define STRICT
#define STRICT_TYPED_ITEMIDS
// get Windows version right; right now Windows XP
#define WINVER 0x0501
#define _WIN32_WINNT 0x0501
#define _WIN32_WINDOWS 0x0501       /* according to Microsoft's winperf.h */
#define _WIN32_IE 0x0600            /* according to Microsoft's sdkddkver.h */
#define NTDDI_VERSION 0x05010000    /* according to Microsoft's sdkddkver.h */
#include <windows.h>
#include <shlobj.h>
#include <shlwapi.h>

void chk(HRESULT hr) { if (hr != S_OK) DebugBreak(); }

void doWork(bool pumpMessages)
{
    UINT_PTR timer;
    MSG msg;

    if (!pumpMessages) {
        Sleep(2000);
        return;
    }
    timer = SetTimer(NULL, 20, 2000, NULL);
    while (GetMessageW(&msg, NULL, 0, 0)) {
        if (msg.message == WM_TIMER && msg.hwnd == NULL)
            break;
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
    KillTimer(NULL, timer);
}

HWINEVENTHOOK hook;
HWND dialogWindow;
HANDLE dialogEvent;
void CALLBACK onDialogClosed(HWINEVENTHOOK hWinEventHook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD dwEventThread, DWORD dwmsEventTime)
{
    if (hwnd == dialogWindow)
        SetEvent(dialogEvent);
}
void waitEvent(void)
{
    MSG msg;
    DWORD ret;

    for (;;) {
        ret = MsgWaitForMultipleObjectsEx(1, &dialogEvent,
            INFINITE, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
        if (ret == WAIT_OBJECT_0)   // event
            break;
        if (GetMessage(&msg, NULL, 0, 0) == 0)
            break;
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
}

void rundialogs(HWND parent, bool pumpMessages, bool tryHide, int tryHideWhat, bool abort)
{
    IProgressDialog *pd;
    IOleWindow *olewin;
    HWND hwnd;
    DWORD process;
    DWORD thread;

    chk(CoCreateInstance(CLSID_ProgressDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pd)));
    chk(pd->SetTitle(L"Test"));
    chk(pd->StartProgressDialog(parent, NULL,
        PROGDLG_NORMAL | PROGDLG_MODAL | PROGDLG_AUTOTIME | PROGDLG_NOMINIMIZE,
        NULL));

    doWork(pumpMessages);

    chk(pd->Timer(PDTIMER_RESET, NULL));
    for (ULONGLONG i = 0; i < 10; i++) {
        if (pd->HasUserCancelled())
            break;

        doWork(pumpMessages);
        if (i == 5 && abort)
            break;

        chk(pd->SetProgress64(i + 1, 10));
    }

    chk(pd->QueryInterface(IID_PPV_ARGS(&olewin)));
    chk(olewin->GetWindow(&hwnd));
    olewin->Release();

    // set up event hoook before stopping the progress dialog
    // this way it won't get sdestroyed before the hook is set up
    if (tryHide && tryHideWhat == 3) {
        thread = GetWindowThreadProcessId(hwnd, &process);
        dialogWindow = hwnd;
        dialogEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
        ResetEvent(dialogEvent);
        hook = SetWinEventHook(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY,
            NULL, onDialogClosed,
            process, thread,
            WINEVENT_OUTOFCONTEXT);
    }

    chk(pd->StopProgressDialog());
    if (tryHide)
        switch (tryHideWhat) {
        case 0:     // hide
            ShowWindow(hwnd, SW_HIDE);
            break;
        case 1:     // send WM_NULL
            SendMessageW(hwnd, WM_NULL, 0, 0);
            break;
        case 2:     // post WM_NULL
            PostMessageW(hwnd, WM_NULL, 0, 0);
            break;
        case 3:     // winevents
            waitEvent();
            UnhookWinEvent(hook);
            break;
        }
    pd->Release();

    MessageBoxW(parent,
        L"This should be MODAL to the main window!\n"
        L"But you should see that in reality the main window\n"
        L"is still enabled!",
        L"mainwin",
        MB_OK | MB_ICONINFORMATION);
}

HWND button;
HWND checkbox;
HWND checkbox2;
HWND combobox;
HWND checkbox3;

bool ischecked(HWND hwnd) { return SendMessageW(hwnd, BM_GETCHECK, 0, 0) == BST_CHECKED; }

static LRESULT CALLBACK wndproc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (uMsg == WM_COMMAND && lParam == (LPARAM) button)
        rundialogs(hwnd,
            ischecked(checkbox),
            ischecked(checkbox2),
            (int) SendMessageW(combobox, CB_GETCURSEL, 0, 0),
            ischecked(checkbox3));
    if (uMsg == WM_CLOSE)
        PostQuitMessage(0);
    return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSW wc;
    HWND mainwin;
    MSG msg;

    CoInitialize(NULL);

    ZeroMemory(&wc, sizeof (WNDCLASSW));
    wc.lpszClassName = L"mainwin";
    wc.lpfnWndProc = wndproc;
    wc.hInstance = hInstance;
    wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
    RegisterClassW(&wc);

    mainwin = CreateWindowExW(0,
        L"mainwin", L"mainwin",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        200, 220,
        NULL, NULL, hInstance, NULL);

    button = CreateWindowExW(0,
        L"button", L"Click Me",
        BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE,
        10, 10, 150, 100,
        mainwin, (HMENU) 100, hInstance, NULL);

    checkbox = CreateWindowExW(0,
        L"button", L"Pump Messages",
        BS_CHECKBOX | BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE,
        10, 110, 150, 20,
        mainwin, (HMENU) 101, hInstance, NULL);
    checkbox2 = CreateWindowExW(0,
        L"button", L"Try",
        BS_CHECKBOX | BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE,
        10, 130, 50, 20,
        mainwin, (HMENU) 101, hInstance, NULL);
    combobox = CreateWindowExW(0,
        L"combobox", L"",
        CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE,
        60, 130, 100, 100,
        mainwin, (HMENU) 102, hInstance, NULL);
    SendMessageW(combobox, CB_ADDSTRING, 0, (LPARAM) L"Hide");
    SendMessageW(combobox, CB_ADDSTRING, 0, (LPARAM) L"Send");
    SendMessageW(combobox, CB_ADDSTRING, 0, (LPARAM) L"Post");
    SendMessageW(combobox, CB_ADDSTRING, 0, (LPARAM) L"WinEvents");
    SendMessageW(combobox, CB_SETCURSEL, 0, 0);
    checkbox3 = CreateWindowExW(0,
        L"button", L"Abort Early",
        BS_CHECKBOX | BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE,
        10, 150, 150, 20,
        mainwin, (HMENU) 103, hInstance, NULL);

    ShowWindow(mainwin, nCmdShow);
    UpdateWindow(mainwin);

    while (GetMessageW(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    CoUninitialize();
    return 0;
}

UPDATE Huh, upon further examination it appears that when I stop the progress dialog without showing the messagebox, the owner window does NOT get focus back either! I'm guessing the progress dialog simply does not handle modality correctly; if I remove the PROGDLG_MODAL flag everything works fine. Oh well :/ I'm going to have to either fake the modality or switch to something else.

I might just wind up showing my message box over the progress dialog itself and hope that a future version of Windows doesn't take away IOleWindow. Unless there is a better way? Or unless doing modality manually and keeping the progress dialog modeless after the call to StopProgressDialog() is good enough (an error will only be reported after StopProgressDialog() to begin with) but then again...

I am not sure if this is a "clean" solution, but since you have a window handle to that progress dialog, why don't you just kill it before calling MessageBox() ?

This worked for me (in your test app):

::SendMessage(hwnd, WM_DESTROY, 0, 0);
MessageBoxW(parent,
    L"This should be MODAL to the main window!\n"
    L"But you should see that in reality the main window\n"
    L"is still enabled!",
    L"mainwin",
    MB_OK | MB_ICONINFORMATION);

For whatever reason, the key to get smooth animation from the start to the very and end of the operation is to "pump timer messages" (I believe that this is what happens).

In Delphi, it can be done like this, as part of the IsCancelled method:

function TProgressDialog.IsCancelled: boolean;
begin
  self.PumpTimerMessages;
  Result := FDlgIntf.HasUserCancelled;
end;


procedure TProgressDialog.PumpTimerMessages;
var
  Msg: TMsg;
  i: integer;
begin
  for i := 1 to 32 do begin
    while Windows.PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do begin
      Windows.TranslateMessage(Msg);
      Windows.DispatchMessage(Msg);
    end;
  end;
end;

I observerd no messages being ever retrieved by PeekMessage, but nevertheless, it makes the progress dialog to update much more consistent and to display the text lines and the marquee mode properly (which was very erratic/random before this measure).

Partial answer

[cf. UPDATE ] I use a child as the dialog parent, rather than the actual parent and the modality works well with this setting.

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