user541686
user541686

Reputation: 210765

What's the correct way to tentatively switch to a wait cursor in Win32? (Or: How to trigger WM_SETCURSOR properly?)

Here's the problem I'm having:

You would think I could just do both of the above, calling SetCursor and modifying the behavior of WM_SETCURSOR simultaneously so that I get the cursor to change until some point in the future.

But I can't just do that. Why? Because SetCursor is application-wide, yet a random window has no idea (nor should it) what the correct cursor is across the entire application. It would need to perform the proper hit-tests to send a WM_SETCURSOR to the window that is actually under the cursor, and it's unclear what the right way to do that is.

What's the right way to switch cursors and back in Win32? Every example I see is trivial and ignores these problems.

Upvotes: 1

Views: 2009

Answers (2)

user541686
user541686

Reputation: 210765

I think I found a solution that kind of works reasonably. It's not perfect (I do still see some rough edges), but I think it's okay for me so far and behaves more reasonably than the lazy approach given all the trade-offs.

Your mileage may vary depending on the complexity of your app.

Here's what I'd suggest doing to someone in a similar situation:

  1. Define an app-wide message, e.g. WM_UPDATECURSOR = WM_APP + 0.

  2. Have your main (GUI) thread process WM_UPDATECURSOR by triggering WM_SETCURSOR:

    a. Use GetCursorPos and WindowFromPoint to get the thread under the cursor.

    b. Use GetWindowThreadProcessId to check the process and thread IDs.

    c. If it's in a different process, stop.

    d. If it's in a different thread from your process, PostThreadMessage(thread_id, WM_UPDATECURSOR) to it, then stop. (This is highly unusual and generally poor practice; I'm only mentioning it for completeness.)

    e. If it's for your own thread, then use WM_NCHITTEST, and if it succeeds, figure out (as explained further below) what the correct cursor should be, if anything. If anything, set it with SetCursor; if not, SendMessage(WM_SETCURSOR).

  3. Keep a list of counters corresponding to different cursors that you consider "stackable". Earlier elements are lower priority. Anything that isn't in the list will not be overridden or considered later.

  4. Whenever you want to change the cursor on any thread, increment or decrement its corresponding counter, atomically if needed. (It is assumed this cursor is in the list of stackable cursors.)
    Then trigger a cursor update in the same manner as for WM_UPDATECURSOR above.

  5. To figure out if a cursor has been stacked at any point, go through the list in reverse, and find the cursor corresponding to the first positive counter. If there is none, then return NULL; there is no stacked cursor.

  6. In your main window/dialog's WM_SETCURSOR handler, evaluate the cursor from the global list as explained in the previous step, and use SetCursor to set it. However, return FALSE so that child windows (like Edit controls) still override it if they want to.

I'll update this if I figure out more issues, but I think overall it behaves decently.

Upvotes: 0

Daniel Sęk
Daniel Sęk

Reputation: 2779

1_ Implementing busy cursor is easiest when it is combined with disabled input. First disable input EnableWindow( hwnd, FALSE ); and then set busy cursor SetCursor( LoadCursor( 0, IDC_WAIT ) );. Now you can do some operation (ideally not longer than 5 seconds). After that enable input EnableWindow( hwnd, TRUE );. When your operation takes longer than 5 seconds, window will be "ghosted", so it would lose busy cursor on title bar and sizing borders.

2_ If window needs to accept input when displaying busy cursor, you have to handle WM_SETCURSOR message not only in window procedure of your top level window, but also for all its children (simple STATIC controls are an exception). This requires subclassing (SetWindowSubclass) of these children, which can be rather though task unless you can use some advanced framework.

In subclassing window procedure, just set busy cursor and return TRUE. Do not call DefWindowProc or DefSubclassProc for WM_SETCURSOR case.

switch ( message )
{
case WM_SETCURSOR:
    SetCursor( MyCursor );
    return TRUE;
...
}

Subclassing can be done on each child creation or at later time, by enumerating them.

Interestingly it works even for menu and combo boxes with popup lists.

3_ Another option would be to hide cursor ShowCursor( FALSE ); and display instead semitransparent window tracking mouse cursor position with some click through capability. Personally I would start with window displayed just few pixels above or below current cursor position.

Maybe progress bar on status bar or simple animation (hourglass?) on main window would be easier.

Upvotes: 1

Related Questions