0

I know how to create a working ProgressBar with PBS_MARQUEE style, but I am having trouble implementing it in a situation where I want the marquee animation as long as some long_operation() runs, without having to call SendMessage(hPB, PBM_STEPIT, 0, 0); continuously from long_operation() to advance the animation.

Here is one of my failed attempts:

INT_PTR CALLBACK ProgressDlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch(message) {
        case WM_INITDIALOG:
        {
            HWND hProgressBar = GetDlgItem(hWnd, IDC_PROGRESS1);                
            LONG_PTR style_flags = GetWindowLongPtr(hProgressBar, GWL_STYLE);
            SetWindowLongPtr(hProgressBar, GWL_STYLE, style_flags | PBS_MARQUEE);

            SendMessage(hProgressBar, (UINT)PBM_SETMARQUEE, (WPARAM)1, (LPARAM)NULL);

            break;
        }
    }

    return FALSE;
}

void long_operation() {
    for(int i = 0; i < 9; ++i) {
        for(int j = 0; j < 99999999; ++j)
        ;
        Beep(5000, 100);
    }
}

void do_operation() {
    HWND hDlg = CreateDialog(Dll_globals::g_hInst,
                             MAKEINTRESOURCE(IDD_DIALOG4),  // assume this contains a ProgressBar ctl
                             Dll_globals::g_hWndMain, ProgressDlgProc);
    if(hDlg) {
        ShowWindow(hDlg, SW_SHOW);
        UpdateWindow(hDlg); 
    }

    long_operation();
}

What I get with the code above is a marquee progress bar without any animation while beeping continues, and then a normal animated marquee when it stops.

As far as I understand, since long_operation() blocks the thread, message queues are blocked as well, and the default 30ms update message is not sent to/received by the ProgressBar control.

I feel there must be an intuitive way to do this, but I can't figure it out.

What it way to go about this?

Kemal
  • 849
  • 5
  • 21
  • Not so sure if std::thread has ever been accused of being intuitive. But that is what it takes to keep your UI thread capable of updating the bar. An hourglass cursor instead of a progress bar is pretty intuitive. – Hans Passant Sep 28 '17 at 07:52
  • 2
    Offload `long_operation` onto a worker thread. This can be done in a number of ways, e.g. by using `std::thread`, `std::async`, or Windows' native thread implementation (`CreateThread`/`_beginthreadex`). Note in particular, that calling `SendMessage` from `long_operation` only implements a partial solution. It still prevents dispatching of other messages, causing the dialog to appear hung, when a user is trying to interact with it, for example. – IInspectable Sep 28 '17 at 07:52
  • What's happening is that you are doing work in the UI thread, which renders the UI unresponsive. Solve that problem by doing the work in a background thread. – David Heffernan Sep 28 '17 at 08:03
  • @IInspectable : I know this relates more to multi-threading than the OP, but lets say I want to make sure `long_operation()` is completed in `do_operation()`, and then destroy the dialog. I would have to do something like: `std::thread t1{long_operation}; t1.join();`, right? But that blocks the thread again. So, how can I do that? Detach the thread, devise a way to make sure `long_operation()` is completed from where its results are needed, and then destroy the ProgressBar dialog from there? – Kemal Sep 28 '17 at 08:18
  • 1
    You could just fire and forget the thread, and immediately give control back to the dialog manager to dispatching messages. Once the thread is done, it can post a custom message (`WM_APP + x`) to the dialog window, or send it, in case it needs to transfer data back. – IInspectable Sep 28 '17 at 08:36
  • @IInspectable In my experience `SendMessage()` from a thread, especially when used to signal the end of the thread, often leads to deadlocks (for instance, if OP would call `t1.join()` in the handler of that message). I would rather do a `PostMessage()` from the thread, then `t1.join();` can be safely called from the handler and one can [use a `std::future` to get the return data from the thread](https://stackoverflow.com/a/7687050/7571258). – zett42 Sep 28 '17 at 10:36
  • You should call SetWindowPos after calling SetWindowLongPtr – Asesh Sep 28 '17 at 11:27
  • 2
    @Asesh: This only applies to changing certain styles (like the border). It does not apply here. – IInspectable Sep 28 '17 at 11:47
  • @zett42: That's an easy problem to solve: Make sure that `t1` is not accessible from the message handler. This could be done by `detach()`-ing the thread, and letting the `t1` instance go out of scope. With nothing to `join()`, there's no way to deadlock. – IInspectable Sep 28 '17 at 11:50

1 Answers1

0

If You prefer to stay with one thread, You need to periodically pump messages so window could be moved or refreshed if uncovered.

// Process all queued messages. Handle keyboard dialog nawigation
// for dialog_hwnd. For more then one modeless dialog modify code by
// calling IsDialogMessage for each dialog.
void PumpMesages( HWND dialog_hwnd ) {
    MSG msg;
    while ( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) != 0 ) {
        // Handling modeless dialog nawigation.
        if ( dialog_hwnd && IsDialogMessage( dialog_hwnd ) )
            continue;

        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }
}

void long_operation( HWND dialog_hwnd, HWND progress_hwnd ) {
    for(int i = 0; i < 9; ++i) {
        for(int j = 0; j < 99999999; ++j)
        ;

        // I'm not shure if this is needed.
        // I have always used propper progress bar (PBM_SETPOS).
        SendMessage(progress_hwnd, PBM_STEPIT, 0, 0);
        // Periodically process messages.
        PumpMessages( dialog_hwnd );

        Beep(5000, 100);
    }
}

void do_operation() {
    HWND hDlg = CreateDialog(Dll_globals::g_hInst,
                             MAKEINTRESOURCE(IDD_DIALOG4),  // assume this contains a ProgressBar ctl
                             Dll_globals::g_hWndMain, ProgressDlgProc);
    if(hDlg==0)
        return;

    ShowWindow(hDlg, SW_SHOW);

    // Disable input processing in main window if there is possibility
    // of recursion. For example, user select Open file, we begin
    // loading and inside PumpMessages, user can again select Open
    // file so we will end with two progress dialogs and nested
    // message loops.
    EnableWindow(Dll_globals::g_hWndMain,FALSE);

      long_operation( hDlg, GetDlgItem( hDlg, progress_bar_id_here ) );

    // Remember to enable input processing in main window.
    EnableWindow(Dll_globals::g_hWndMain,TRUE);
}

This can be enhanced by adding Abort button, handling WM_COMMAND in ProgressDlgProc, setting some flag (Dll_globals::g_Abort?) and leaving long_operation early.

Going multithread, You have to establish protocol for starting work (this depends on which API You select), and signaling completion (this could be done by PostMessage with custom message or even WM_COMMAND with adequate control id). Even in this scenario beware of potential problems when user starts same operation again before completion of earlier call.

Daniel Sęk
  • 2,504
  • 1
  • 8
  • 17
  • This proposed solution breaks keyboard navigation in the dialog. Keyboard navigation is implemented by the [IsDialogMessage](https://msdn.microsoft.com/en-us/library/windows/desktop/ms645498.aspx) API call. – IInspectable Sep 29 '17 at 06:48
  • Yes you are right, we are using modeless dialog so IsDialogMessage is needed to handle kayboard navigation. Forgot about it. Updated code. – Daniel Sęk Sep 29 '17 at 08:09
  • Problem with this solution is that it may create lags when the user interacts with the UI. `long_operation` may not be able to call `PumpMessages()` in short enough intervalls. Even if it is, if `PumpMessages()` is called from an inner loop, it may hamper performance. – zett42 Sep 29 '17 at 09:25