Introduction
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
released.
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 CButton
.
Then I added handlers for OnLButtonDown
, OnLButtonUp
, and OnTimer
.
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, sent
:
protected:
UINT sent;
I changed the OnLButtonDown
handler to read as follows:
void CAutoRepeatButton::OnLButtonDown(UINT nFlags, CPoint point)
{
SetTimer(IDT_TIMER, INITIAL_DELAY, NULL);
sent = 0;
CButton::OnLButtonDown(nFlags, point);
}
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.
In the OnTimer
handler I added the indicated code:
void CAutoRepeatButton::OnTimer(UINT nIDEvent)
{
if( (GetState() & BST_PUSHED) == 0)
return;
SetTimer(IDT_TIMER, REPEAT_DELAY, NULL);
GetParent()->SendMessage(WM_COMMAND,
MAKELONG(GetDlgCtrlID(), BN_CLICKED),
(LPARAM)m_hWnd);
sent++;
CButton::OnTimer(nIDEvent);
}
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 BST_PUSHED
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 SetTimer
operations on the same timer; the timer is just restarted with the new
interval). I generate a BN_CLICKED
notification to the parent. The SendMessage
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 OnLButtonUp
by
using the 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)
{
KillTimer(IDT_TIMER);
if(GetCapture() != NULL)
{
ReleaseCapture();
if(sent == 0 && (GetState() & BST_PUSHED) != 0)
GetParent()->SendMessage(WM_COMMAND,
MAKELONG(GetDlgCtrlID(), BN_CLICKED),
(LPARAM)m_hWnd);
}
CButton::OnLButtonUp(nFlags, point);
}
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.
First, an 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 GetCapture()
!= 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
the default 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 INITIAL_DELAY
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 newcomer@flounder.com
with questions or comments about this web site.
Copyright � 2001 All Rights Reserved