Introduction
It's recommended practice to change the mouse cursor to
an hourglass wait cursor to inform the user that the program is
working on something. This is easy enough to do when the programmer
knows in advance that something is going to take a while.
However, there are often circumstances where you can't predict
how long something is going to take, so it's hard to know whether
to display the hourglass or not. Ideally, you'd like the cursor
to change to an hourglass automatically, as soon as your task had
run for more than some time limit, e.g. 1/10 of a second. I needed
to do this for a project of mine so I searched through my books and
the internet, but couldn't find any applicable code. The closest thing
I could find was an
article explaining how to do it in Java. It didn't seem
like it should be too tough to write a Windows/C++ version, but it
ended up taking me longer than I expected to get it working
properly so I thought I'd share the results.
Implementation
The basic idea I had was to create a secondary
thread that would act as a "timer". The message loop
would keep resetting the timer so it normally wouldn't run out.
However, if a task took longer, then the timer would run out and the
secondary thread would change the cursor to an hourglass. It didn't' take
long to set this up, but mysteriously, it didn't work. A little debugging
reassured me that my code was working properly, but that the SetCursor
call in
the secondary thread wasn't working. I guessed that SetCursor
didn't work from
secondary threads, but a search of the MSDN documentation and the internet didn't
find anything about this. Finally, I posted a question to
comp.os.ms-windows.programmer.win32 and went home. The first few responses
I got didn't really help, but the third response turned out to be the key.
The message referred me to an article by Petter Hesselberg in the Nov. 2001
Windows Developers Journal - "The Right Time and Place for the Wait Cursor".
I couldn't find the article, but the
source-code
was online and the key turned out to be using AttachThreadInput
. Once I added this
to my code, things started to work.
I actually thought I was finished until I discovered that
when I pulled down a menu (or brought up a context menu) the cursor
changed to an hourglass. The problem was that while the menu was
displayed, my message loop wasn't running and so the timer wasn't getting
reset. After a certain amount of head scratching I came up with the idea
of using a GetMessage
hook to reset the timer, since I figured the menu must
still be calling GetMessage
(or PeekMessage
). Sure enough this solved the menu
problem. (And probably some related issues like modal dialogs.)
Again I thought I was finished, but I found one last glitch.
Just before a tool-tip appeared, the cursor flashed to an hourglass
and back. I guess tool-tips don't call GetMessage
or PeekMessage
while
they wait. I fixed this by simply making my timer longer than the tool-tip timer.
My last task was to extract the code, which I'd mixed
in with my message loop, into something more reusable. I ended up
packaging it into a C++ class. To use the class you simply have to create
an instance of it inside your message loop. Something like:
#include "awcursor.h"
...
while (GetMessage(&msg, NULL, 0, 0))
{
AutoWaitCursor awc;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
The AutoWaitCursor
constructor "starts" the
timer, and the destructor (called automatically at the end of the loop)
restores the cursor if necessary. The constructor also looks after creating
the thread and the hook the first time around.
If you look at the code, you may notice that the only concession I've made
to multi-threading is declaring several of the variables volatile. I don't make
any attempt to synchronize the threads or prevent them from accessing variables
at the same time. This was a deliberate choice, because I wanted to make
the code as fast as possible in order to not add overhead to the
message loop. How do I justify this? First, the variables are
plain integers, and therefore reading and writing them is atomic
anyway. Second, if in some rare case, a synchronization problem occurs,
the worst that can happen is the cursor might be wrong momentarily.
This is a small price to pay to keep the code simple and fast.
In practice I haven't seen any problems.
For simplicity, I've included the definition/initialization of
the static class members in the header file. This isn't the best
setup since it means you can't include this header in more than one
source file. Ideally, you'd put the definitions in a separate .cpp file.
However, you normally only have one message loop in your program anyway,
so this setup seems acceptable.
I hope you find the code and the explanation useful. Good luck!