<!-- Download Links -->
<!-- Add the rest of your HTML here -->
In the past, I have done custom buttons that auto-repeat "from
scratch", that is, I start with a raw
CWnd and build on top of it.
Recently, there have been a number of questions in the microsoft.public.vc.mfc
newsgroup inquiring after auto-repeat buttons. My first answer was to say
"Just add a timer to a subclassed button", but after mulling it over
for a few days, I realized there were some problems with this.
I don't claim this is an elegant solution, but it does work, and it saves
having to reinvent all of the button drawing, styles, etc.
The basic problem is that most auto-repeat buttons have the characteristic
that they send a message to the parent when the left button is clicked, and then
repeat messages as long as it is held down.
A regular button doesn't work this way. It sends a message to the parent when
the left button is released. This means that if you just set a timer, you
will get one event for each timer tick, and one at the end when the button is
OK, I cheated. Big Time. Amazingly, I seem to have gotten away with it! So
I'll observe that this technique is not without risk. THIS ARTICLE HAS NO
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING THOSE OF MERCHANTABILITY OR FITNESS
FOR A PARTICULAR TASK [if I had a lawyer, he'd make me say something like this,
in all caps].
First I used ClassWizard to create a
CAutoRepeatButton subclass of
Then I added handlers for
I defined two constants (you could make variables and use these as initial
settings if you want programmability) for the initial delay and the repeat
delay. Also, a constant for the timer ID.
#define INITIAL_DELAY 500
#define REPEAT_DELAY 200
#define IDT_TIMER 1
Then I wrote the handlers.
In the header file for the class, I added a new member variable,
I changed the
OnLButtonDown handler to read as follows:
void CAutoRepeatButton::OnLButtonDown(UINT nFlags, CPoint point)
SetTimer(IDT_TIMER, INITIAL_DELAY, NULL);
sent = 0;
I start a timer, based on the initial delay, and zero the clicks-sent
counter. I'll discuss the need for it a bit later.
OnTimer handler I added the indicated code:
void CAutoRepeatButton::OnTimer(UINT nIDEvent)
if( (GetState() & BST_PUSHED) == 0)
SetTimer(IDT_TIMER, REPEAT_DELAY, NULL);
What is going on here? Well, for one thing, if you drag the mouse out of the
button, it pops back up again. This is standard button behavior. So I looked
around for the state that indicates this, and discovered that it is the
state. So I check the state to see if that bit is set. If it is not, I do not
want to generate a
BN_CLICKED event. I decided that I also did not want
to change the timer interval. So I just return directly if the button state is
not "pushed". If the button state is pushed, I reset the
timer to the shorter interval (it is always permissible to do multiple
operations on the same timer; the timer is just restarted with the new
interval). I generate a
BN_CLICKED notification to the parent. The
just creates the same
WM_COMMAND message as the button would have
generated on an
OnLButtonUp event. I then increment a counter of the
number of items I have sent.
This all worked fine, except that if I did a normal
CButton superclass event, I always got an extra click. This is
easy to see if you set the time constants to something large, like 1000 and
1000. You see the counter click into "27", release the button, and it
reads "28". This Is Not Good.
So I added the following code to the
OnLButtonUp handler, and removed
the call to the superclass.
void CAutoRepeatButton::OnLButtonUp(UINT nFlags, CPoint point)
if(GetCapture() != NULL)
if(sent == 0 && (GetState() & BST_PUSHED) != 0)
The obvious thing to do is to kill the timer. That's easy. But what I have to
do is "fake out" the normal button behavior. So the first thing I did
was to comment out the call on the superclass. Now, I knew that this would cause
serious malfunction; in fact, if you were to do only this, you would find that
the button never released capture. So I cheated, and forced the capture release
myself. What surprised me was that the button actually pops back up and redraws
properly, something that I was sure would not work and would force me to
actually hand-code the whole thing. I got away with it, but I'm not comfortable
with the idea.
Here's how it works.
OnLButtonUp event isn't interesting if I just clicked the
mouse down somewhere on the dialog, dragged it into the button area, and
released it. So I only want to do this if I actually had capture. Hence the
!= NULL test. If I don't have capture, I don't need to do anything
because the button wasn't active. If I had capture, I now release it, doing what
OnLButtonUp handler does. Now, if the user released the
button before the
INITIAL_DELAY interval, nothing has been sent, so I
want to send something so a single fast click will actually be seen. Hence the
use of the
sent counter (it could have been a
BOOL as well, but I
decided to count rather than just mark as being sent. This is a gratuitous
choice). But suppose the user clicked in the button, and within the
time dragged the mouse out of the button and then released it? So I added
in the test for the
BST_PUSHED state, and only send a message to the
parent if both conditions, nothing has already been sent and the button is
actually pushed, are both true.
The views expressed in these essays are those of the
author, and in no way represent, nor are they endorsed by, Microsoft.
Send mail to email@example.com
with questions or comments about this web site.
Copyright © 2001 All Rights Reserved