Tuesday 22 February 2011

.net WaitCursor: how hard can it be to show an hourglass?


I've seen a couple of different ways of using the 'Wait' cursor (aka the 'Hourglass') and several forum posts discuss the problems people have when they haven't been able to work out how to use it properly. Hopefully this is a comprehensive discussion of this small but seemingly complicated topic.

When your program is doing something which stops users from accessing the UI, you should display a 'Wait' cursor. There are three different things you can do to get this (and usually none of them work the way you'd want on their own):
// Method #1
Control.Cursor = Cursors.WaitCursor

// Method #2
Cursor.Current = Cursors.WaitCursor

// Method #3
Control.UseWaitCursor = true;
For the first two, Cursors.Default can be used to return to the expected arrow after the operation has finished; UseWaitCursor should simply be set to false again.

Controls all have a Cursor property and this sets the cursor shape when the mouse pointer is over a control. This property is examined and acted upon only when a Windows message (WM_SETCURSOR) is sent to a window. This means that until the next time this message is sent (perhaps when the pointer is moved away from and back over the control in question), updating this property won't have any effect. To exacerbate the problem, if the UI thread is blocked by whatever operation the WaitCursor would be displayed for, any WM_SETCURSOR messages that are generated won't be processed until the operation has finished.

The other problem with using this alone is that it is a per-control setting: set it for a form and all the child controls on the form will still display the default cursor unless you update all of their Cursor properties as well.

The solution for this problem (and the suggested 'proper' way of displaying the WaitCursor) is to set the Form's UseWaitCursor property. This has the advantage of working for any given control and all its child controls, so set it for a Form and the whole UI for the form will display the WaitCursor when the pointer is over it, regardless of the control under the mouse. There is also an Application.UseWaitCursor which has the same effect across all the windows of a running application. However, this still suffers from the problem of needing a WM_SETCURSOR message before the cursor shape will change.

So what about the other option? Cursor.Current is a static member of the Cursor class and accesses the OS to change the current cursor immediately. This is great… until the next WM_SETCURSOR message is processed and it goes back to whatever the control underneath is supposed to display.

Problems with all these approaches are made less predictable with UI thread blocking too: Cursor.Current will affect a change for some time if UI messages aren't being processed, for example, and then might suddenly change back for no reason that is obvious as a message gets handled.

So the best approach looks like it is to set MyForm.UseWaitCursor to true and then set Cursor.Current. As well as putting any long-running activities in a separate thread, of course. Well, that does solve the problem, unless you want the relatively common ability to cancel a long-running activity and you want the default cursor shape (i.e. an arrow) over your cancel button.

If you look into the Control.UseWaitCursor setter in Reflector, you'll see that it sets its flag (a bit in the private 'state' field in Control) and then recurses into the UseWaitCursor setters in each of its child controls. You might think (i.e. I thought) that all that's then needed is to reverse the setting of this flag in the button in which you want to display a normal arrow and all would be well. Unfortunately, this doesn't work – you still get WaitCursor everywhere. So how can you do it?

Well, it turns out that if you use all of:

MyForm.UseWaitCursor = true;
CancelButton.UseWaitCursor = false;
CancelButton.Cursor = Cursors.Default;

Then you can get the desired behaviour. And of course, a Cursor.Current call would also be in order if you find the cursor shape isn't changing until the mouse is moved.

I don't know how this works: I would have thought that if Control.UseWaitCursor = true sets Control.Cursor to the WaitCursor (which appears to be the case) then setting it to false would have the opposite effect, but I found that CancelButton.Cursor was still set to WaitCursor even after the UseWaitCursor flag in the control (not the form) had been reset.

This solves the problem and is not overly arduous but if you can explain the behaviour, leave a comment and I'll update the article accordingly!

3 comments:

  1. I can't believe this worked for me...Tried all three methods separately but none of them worked. Found this post and the combination now works all of the time. Thanks so much for posting this!

    ReplyDelete
  2. Thank you so much for this post. I've been screaming at my program as it refuses to cooperate and show a wait cursor. If I hadn't found this post, I would still be wandering the web and trying to get a little icon to show. I can't believe that using all three at the same time would have made such a difference.

    Thank you once again for this!

    ReplyDelete