Click here to Skip to main content
Click here to Skip to main content

Automatic Application Wait Cursor

By , 16 Mar 2005
 

Sample Screen Shot from Demo

Introduction

Making your WinForm applications User Friendly is important. One aspect of a good user experience is informing your user that your application is unresponsive during short periods of work. This article introduces an effective and simple way of adding application wide WaitCursors using one line of start-up code.

Background

Recently, I have been working on a WinForms project at home that occasionally performs some short (less than 5 seconds) running tasks. I wanted the user to be informed that during that short period the UI is unresponsive, so I chose to use the Cursors.WaitCursor to indicate this. This article shows how I came to my final WaitCursor library implementation which I believe is a completely re-usable library.

Using the code

From a developer's point of view, using the WaitCursor library could not get any simpler. Add a reference to the WaitCursor assembly and then add the following line to your application start-up code:

ApplicationWaitCursor.Cursor = Cursors.WaitCursor;

That’s it!

You can of course use any Cursor you like, you can use one of the predefined Cursors or you can create a new cursor and use that instead. You can also fine tune the amount of work time that will elapse before the Cursor is shown:

ApplicationWaitCursor.Delay = 
             new TimeSpan(0, 0, 0, 1, 0);  // Delay of 1 second

How the library came about

During development of a recent WinForms project at home, I had decided to use the cursor to indicate short running tasks to the user, like so:

private void DoShortRunningTask()
{
    Cursor.Current = Cursors.WaitCursor; 

    .. do some work .. 

    Cursor.Current = Cursors.Default; 
}

Now, before you say "Where’s the exception handling code?", I am trying to illustrate how I eventually came to my final cut of the WaitCursor library.

The above code works, or, at least it works most of the time. I, of course, found that without any exception handling I could end up with the WaitCursor on permanently, so I quickly came up with:

private void DoShortRunningTask()
{
  Cursor.Current = Cursors.WaitCursor;
  try
  {
    .. do some work ..
  }
  finally
  {
    Cursor.Current = Cursors.Default;
  }
}

Now that’s better, I've guaranteed that the Cursor will always be returned to the Default cursor even if an exception occurs. However, I found it arduous to wrap that code around every short running task that I was developing.

Then I remembered how I used to use C++ stack based destructors to perform tear down work upon exiting of a method:

void DoShortRunningTask()
{
StWaitCursor cursor = new StWaitCursor();

  .. do some work .. 
// Implicitly called ~StWaitCursor returns the Cursor to Default


}

But I couldn't use C# destructors the same way since you can't guarantee when a C# destructor is called since the Garbage Collector thread is responsible for that.

Instead, C# uses a different language feature, the using statement which implicitly calls the Dispose method of objects that implement IDisposable. Although (I find it's) not quite as easy to use as the C++ destructor, you can achieve the same result with the using statement:

private void DoShortRunningTask()
{
    using (new StWaitCursor())
    { 
        .. do some work .. 
    } 
}

This code, of course, requires the StWaitCursor class:

public class StWaitCursor : IDisposable
{
  public StWaitCursor()
  {
    Cursor.Current = Cursors.WaitCursor;
  }
  public void Dispose()
  {
    Cursor.Current = Cursors.Default;
  }
}

There, nice and simple. I found, however, that if my short running task was too short then the user sees a quick flickering of the Cursor to and from the WaitCursor, quite annoying. So I decided I needed a way of turning the WaitCursor on after a predefined amount of time during a task.

After quite a few iterations and refactorings, I came up with what I called the StDelayedCallback class (see source code) which is a generic class that once instantiated will wait for a specified amount of time before calling the Start method of an IDelayedCallbackHandler, and if Start was called that it was guaranteed to call the Finish method of the same interface. Once I had this, I could easily implement the interface such that the WaitCursor was turned on when Start was called and returned the Default cursor when Finish was called.

Some things I found

During development of the WaitCursor library, I discovered that I could not set Cursor.Current on any thread other than the GUI thread. This is where the Win32 AttachThreadInput method can be used to get around this problem effectively. So I developed the StThreadAttachedDelayedCallback class which wraps up the call to the AttachThreadInput.

So eventually I ended up with a generic StDelayedCallback class, a ThreadInput attached version of it called StThreadAttachedDelayedCallback and a specific Cursor implementation called StWaitCursor. Incidentally, I used the prefix St since I had originally intended on using these classes in a similar way to the C++ Stack based classes I had used in the past. So up until now, I had intended on using the following:

private void DoShortRunningTask()
{
  using (new StWaitCursor(new TimeSpan(0, 0, 0, 0, 500))
  {
    .. do some work ..
  }
}

However, it still meant I had to be explicit about where I wanted my WaitCursor to show.

Then I had an attack of brilliance and came up with the ApplicationWaitCursor singleton class which neatly wraps the StWaitCursor such that whenever the application is working, or rather, whenever it’s not regularly calling OnApplicationIdle, then the StWaitCursor kicks in and shows the WaitCursor.

The only caveat I found was when I dragged a window around which blocks the OnApplicationIdle call. So, I also intercept the WM_NCLBUTTONDOWN message (which is sent at the beginning of the window dragging) to temporarily disable the StWaitCursor.

That's where I am up to with the current WaitCursor implementation and in brief how I got there. You can easily derive from the StDelayedCallback class to create similar effects. Perhaps you would like to show an Indefinite progress bar during your long running tasks, or indicate "Saving Changes..." in a StatusBar control:

private void SaveChanges()
{
  using (new StStatusBar(stbMain, "Saving Changes...")
  {
    .. do some work ..
  }
  // StStatusBar.IDispose could return the Text
  // of the StatusBar to its original value 
}

Note that the source code does not contain the StStatusBar class, I am just using it as an example of other ways you could use the StDelayedCallback class.

History

Version 1.0.0.0 (12th March 2005)

  • Initial version.

Version 1.0.1.0 (16th March 2005)

  • Updated to be CLS compliant (thanks to C a r l and Mathew Hall for pointing this out).

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication

About the Author

Steve Fillingham
Web Developer
Australia Australia
Member
I am a .NET Developer living in Melbourne, Australia. I am happily married with 2 gorgeous little girls, Georgia and Maddison.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralInteresting, but has a few little bugs...memberspam2@vbusers.com12 Aug '05 - 6:05 
Interesting article, but I found a few bugs with this.
 
Firstly the hour glass stays on when you show right click content menus (including built in content menus). This can be fixed by checking for the right button down + content menu messages in your message callback.
 
Secondly (depending on how busy the application is) the hour glass will sometimes stay on indefinitely (due to the order in which the application idle event fires).
 
Finally, it also continuously sets and unsets the cursor even when the application is idle - this seems unnecessary.
 
Anyway, still a good post. I have coded a revised version to this that takes a different approach and seems to work well (so far!). See below for the alternative:
 
http://www.vbusers.com/codecsharp/codeget.asp?ThreadID=58&PostID=1&NumReplies=0
 

Regards and thanks
 
andrew baker
www.vbusers.com
GeneralVery good job.memberHyperX11 Aug '05 - 12:35 
Nice features. Good coding practice. Well done overall.
 
Thank you,
 
HyperX.
 
Stress Comes From Making Things More Important Than They Are.
QuestionAm I missing anything?memberYaron Shkop6 Jun '05 - 1:36 
When you open a new thread (System.Thread) the cursor is overriden and returns to default.
I thought this solution should also overcome the problem of cursor being overriden on new threads... D'Oh! | :doh:
AnswerRe: Am I missing anything?memberSteve Fillingham6 Jun '05 - 4:30 
Not sure, can you send an example ?
 
Cheers
GeneralRe: Am I missing anything?memberYaron Sh14 Jun '05 - 0:15 
This is quite simple.
Guess we have some method that is called asynchronously and synchronously.
At synchronous way it works fine asynchronously it doesn't work. When you move the mouse you can see that the cursor is not consistent.
 
If you will locate two buttons on a form and bind them to the buttons events you will look what I'm talking about.
Button 1 click will open work on another thread in the process and in the mean time the cursor will not be consistent if you will move it over the form.
 
[STAThread]
static void Main()
{
//Set wait cursor with 150 ms delay
ApplicationWaitCursor.Cursor = Cursors.WaitCursor;
ApplicationWaitCursor.Delay = new TimeSpan(0, 0, 0, 0, 150);
 
Application.Run(new Form1());
}
 
//Async
private void button1_Click(object sender, System.EventArgs e)
{
SetCursor(Cursors.WaitCursor);
 
Thread workThread = new Thread(new ThreadStart(DoWork));
workThread.Start();
}
 
//Synchronous
private void button2_Click(object sender, System.EventArgs e)
{
SetCursor(Cursors.WaitCursor);
 
DoWork();
}
 
private void DoWork()
{
int len = 30;
 
for(int i=0; i
GeneralRe: Am I missing anything?memberSteve Fillingham14 Jun '05 - 4:07 
Yaron,
 
The idea of the Application WaitCursor is to show when the Main thread is busy. If you are performing work on other threads then the UI will be fully responsive and a wait cursor is probably not necessary.
 
Having said that and without having tried it, you should still be able to show the Wait Cursor on another thread using ..
 
using (new StWaitCursor())
{
DoWork();
}
 
You would generally perform short (<3 seconds) tasks on the Main thread and the Wait Cursor makes good sense. Any more than 3 or so seconds then you probably should use either a progress bar, or like you said use a seperate thread to perform the work in the background. If using a background thread you would probably want some way of informing the user of progress and perhaps the ability to cancel the operation aswell. This however is beyond what the Wait Cursor is intended for.
 
Cheers

GeneralA little bugmemberNikolyv24 May '05 - 21:25 
Hello Steve!
 
Great work!
BUT..
 
When I use your sample with a SandDock (http://www.divil.co.uk/net/)
I have a trouble when mouse pointer stop over a pinned window( like a Toolbox in VisualStudio).
Normally ( sithout your component) this window must be popped up. Your component disable this functionality.
 
Best regards
Nikoly

GeneralSeparate threadsussnamsaray21 Apr '05 - 8:10 
First, thank you for the impressive contribution.
Can you please post sample code how to use this
in a separate thread? I tried everything I could think of,
nothing works.
GeneralVB versionmembermarcrobitaille29 Mar '05 - 3:01 
Hello!
 
Is there a vb version of this?
Thank you
 
Marc R.
GeneralRe: VB versionmembermarcrobitaille30 Mar '05 - 4:18 
I have translate the code in vb format. If you want it, just send me an email. Bye
 
Marc Robitaille
QuestionWhen to expect an updated version?memberC a r l28 Mar '05 - 9:01 
Hi!
 
When can we expect an updated (and fixed) version of this? It is a very nice and useful component, but some clients are wondering why the hourglass shows up for nothing sometimes Smile | :)
 
Thanks!

AnswerRe: When to expect an updated version?memberSteve Fillingham29 Mar '05 - 0:27 
Crikey !
 
Well Carl, for my uses I dont need any fixes currently. I can see (from other posts) that under some scenarios the cursor will show unexpectantly but you have the same source code that I do and you are quite welcome to make enhancements and fixes to suite your needs and if you feel generous enough you can email me your changes so I can post updates (with contributors thanked), this is after all a community of sharing.
 
As for your clients, and not to be rude, but I am not getting anything from them nor do I have any responsibility to them, they are your clients and your responsibilities. The source I posted comes with no warranties, I am sure it has potentially saved your at least a few hours of tedious coding, so for you to make a few fixes here and there won't harm. Cool | :cool:
 
Cheers
GeneralRe: When to expect an updated version?memberC a r l29 Mar '05 - 5:05 
The fix proposed in the forum seem to work...
 
--EDIT: I just found major problems implementing the proposed fix. I'll keep you posted.
 
--EDIT 2: The proposed fixes seem to work, except when filtering WM_LBUTTONUP. When I filter it, everything that is done upon leftclicking a button (pretty much everything is started by a left click!), the application wait cursor doesn't show up. So here are the fixes that I added to ApplicationWaitCursor.cs:
 

private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_RBUTTONUP = 0x0205;
private const int WM_CONTEXTMENU = 0x007B;
private const int WM_SYSKEYUP = 0x0105;
private const int WM_SYSKEYDOWN = 0x0104;
 
bool IMessageFilter.PreFilterMessage(ref Message m)
{
if (m.Msg == WM_NCLBUTTONDOWN ||
m.Msg == WM_LBUTTONDOWN ||
m.Msg == WM_RBUTTONUP ||
m.Msg == WM_CONTEXTMENU ||
m.Msg == WM_SYSKEYUP ||
m.Msg == WM_SYSKEYDOWN)
_cursor.Enabled = false;

else
_cursor.Enabled = true;
 
// Always let the real Message through
return false;
}

GeneralProgressBarmemberDaniel Kamisnki23 Mar '05 - 1:23 
Hey I like your thinking!
How about putting a progressbar also...! Just a repeating bar from 1 to 10 or so!
 
Cheers,
Daniel
GeneralAnother IssuesussAnonymous18 Mar '05 - 12:35 
Another time the wait cursor is displayed when it shouldn't be is when a context menu is shown.
GeneralRe: Another Issuememberstankopl20 Mar '05 - 20:06 
I find out that you need to prefilter more messages in ApplicationWaitCursor.cs:
 
bool IMessageFilter.PreFilterMessage(ref Message m)
{
if (m.Msg == WM_NCLBUTTONDOWN ||
m.Msg == WM_LBUTTONDOWN ||
m.Msg == WM_RBUTTONUP ||
m.Msg == WM_CONTEXTMENU ||
m.Msg == WM_SYSKEYUP ||
m.Msg == WM_SYSKEYDOWN)
_cursor.Enabled = false;
...
 

Message defitions are:
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_RBUTTONUP = 0x0205;
private const int WM_CONTEXTMENU = 0x007B;
private const int WM_SYSKEYUP = 0x0105;
private const int WM_SYSKEYDOWN = 0x0104;
 

GeneralRe: Another Issuemembermaurost20 Mar '05 - 22:54 
This is ok for me, thanks stankopl
GeneralRe: Another IssuememberAngeliyo21 Mar '05 - 0:32 
Thanks, now it works... but i still have a problem.
 
Before your modification, when I got a Context Menu from a TextBox, ComboBox or any Windows Control it was busy. Now not, Thanks.
 
But when I show a ContexMenu still appears as busy. My ContexMenu is System.Windows.Forms.ContexMenu.. what messages i have to filter?
 
Sorry about my english.
GeneralRe: Another Issuememberstankopl21 Mar '05 - 1:32 
Tray to add
 
private const int WM_LBUTTONUP = 0x0202;
 
and
 
if (m.Msg == WM_NCLBUTTONDOWN ||
m.Msg == WM_LBUTTONDOWN ||
m.Msg == WM_RBUTTONUP ||
m.Msg == WM_CONTEXTMENU ||
m.Msg == WM_SYSKEYUP ||
m.Msg == WM_SYSKEYDOWN ||
m.Msg == WM_LBUTTONUP)
_cursor.Enabled = false;
 
I tested with
contextMenu1.Show(button4, new Point(1, 1));
and it worked fine.
 
I hope this helps!
Stanko Plohl

GeneralRe: Another IssuememberPlaceboZA22 Mar '05 - 0:07 
If you add WM_LBUTTONUP in there, the demo no longer works Wink | ;)
(Clicking the buttons with the left mouse button no longer causes the busy cursor)
GeneralRe: Another IssuememberAngeliyo22 Mar '05 - 21:59 
Thanks, but I show a context menu when I click right button on a listView (GlacialList 1.3 see at codoproject). I tried what you say, but It doesn´t work.
 
So I tried filter RBUTTONDOWN:
private const int WM_RBUTTONDOWN = 0x0204;
And It works for me!!Big Grin | :-D . Maybe depending of your program you´ll have to filter one message or others. This could be great if it can be possible at design time.
 
Thanks for all.
GeneralRe: Another IssuememberC a r l24 Mar '05 - 10:10 
I'm also facing the same issue... pretty weird.
 
I hope a fixed version can be posted soon!

Here's my code:
 
mnuPrint.Show(btnPrint, New System.Drawing.Point(btnPrint.Width, 0));
GeneralSmall issuememberEtienne Charland18 Mar '05 - 11:22 
Wow it's really a great peace of code! However I encountered an issue. If you have a ListView in detail view (and probably in other views too), click/drag to draw a selection rectangle. While the rectangle is displayed, the wait cursor gets displayed! Pretty much like when moving the form, which you fixed.
 
But great work! Smile | :)
GeneralRe: Small issuememberC a r l24 Mar '05 - 10:15 
Same problem here...
 

GeneralSome issuesmemberGeorgi Atanasov17 Mar '05 - 9:44 
Hello,
 
I just looked at your article
Nice coding, and my opinion(please don't get angry with itBig Grin | :-D ):
 
1. Using P/Invoke for GetCurrentThreadID is not necessary - use the following code instead:
 
_mainThreadId = AppDomain.GetCurrentThreadId();
 
2. Using the Application.Idle event can only slow down the performace of an application. At my work every millisecond is valuable.
 
3.Adding an IMessageFilter to an application message pump also slows down the performance. This way you have additional function call, which itself is not prefferable (see the MSDN documentation about message filters)
 
Generally a good programmer should know when to show the WaitCursor and hide it again.
 
Thanks,
Georgi

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 16 Mar 2005
Article Copyright 2005 by Steve Fillingham
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid