An AutoRepeat Button Class |
|
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) { /* release capture */ ReleaseCapture(); if(sent == 0 && (GetState() & BST_PUSHED) != 0) GetParent()->SendMessage(WM_COMMAND, MAKELONG(GetDlgCtrlID(), BN_CLICKED), (LPARAM)m_hWnd); } /* release capture */ //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 autorepeat button class and its testbed can be downloaded. You can incorporate this code into my Better Bitmap Button class as well, if you need a bitmap button that can autorepeat.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.