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

An AutoRepeat Button Class

By , 14 Apr 2002
 

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)
       { /* 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 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Joseph M. Newcomer
United States United States
Member
No Biography provided

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   
GeneralSimilar but slightly differentmemberRod Stone13 Nov '03 - 22:53 
I had the same the problems with auto repeat buttons and overcame them by hijacking the DrawItem() function. As long as the button state is ODS_SELECTED then the button is pressed and the timer can be started. Any other state means the button is not pressed and the timer can be stopped. There was no need to use LBUTTONDOWN/UP messages and the button also responds to the Space Bar if it has the focus.
 
My DrawItem() function contains the following..
 
if ( lpDrawItemStruct->itemState & ODS_SELECTED )
{
nStyle |= DFCS_PUSHED;
 
if ( m_nTimerID == 0 )
StartTimer ();
}
else if ( m_nTimerID != 0 )
StopTimer ();
 
My implementation can either use a timer proc callback function or post a user defined message to the parent to do the work.
 
With regard to using repeat buttons on toolbars there is a method of putting controls other that toolbar buttons on the bar. Its messy and I've had wierd difference with sizes between release and debug builds but it can be made to work. See some of the excellent articles on this site regarding toolbar manipulation. If anybody understands Microsofts toolbar implementation in MFC I'd love to hear from them!
 

 
Rod Stone
QuestionAutofire function ?sussNick Theo15 Jun '03 - 0:12 
This is a nice pice of code that I was looking for some time. Well done.
Although it is working OK while pressing the button, I would like to use this program to detect the Left mouse down button without clicking the autorepeat button in this application. For example if I am playing a game I would like to use the autorepeat function. The same way microsoft drivers work, universal for all the applications running in windows. If that is possible it would be great. I am just a beginner in C.
 
Many thanks. Smile | :)
 

AnswerRe: Autofire function ?memberJoseph M. Newcomer15 Jun '03 - 5:18 
I'm curious what "autorepeat" function of the left mouse button works "universal for all the applications running Windows". I've certainly not seen such a feature anywhere in Windows. There is an autorepeat for scrollbars, and spin controls, and that is carefully programmed into both of them; but I haven't seen the feature you claim exists "universally".
GeneralNormal button click doesn't workmemberpaulb5 Nov '02 - 15:54 
If you click and release the button before the timer fires then the button clicked message is not sent. Reason seems to be the call to ReleaseCapture below:
 

 
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 ReleaseCapture screws up the button state so that GetState() & BST_PUSHED is always 0. Easy fixed by moving it after the if statement:
 

void CAutoRepeatButton::OnLButtonUp(UINT nFlags, CPoint point) 
   {
    KillTimer(IDT_TIMER);
	
    if(GetCapture() != NULL)
       { /* release capture */
//	ReleaseCapture(); // not here
	if(sent == 0 && (GetState() & BST_PUSHED) != 0)
	   GetParent()->SendMessage(WM_COMMAND, MAKELONG(GetDlgCtrlID(), BN_CLICKED), (LPARAM)m_hWnd);
	ReleaseCapture(); // but here!
       } /* release capture */
	   
    
    //CButton::OnLButtonUp(nFlags, point);
   }
 
I'm surprised none has picked this up. Do you guys just blindly post praise for an article without even checking it? Makes the article ratings pretty useless.

GeneralRe: Normal button click doesn't workmemberDeuce10 Feb '03 - 19:21 
Thanks for the correction. Another addition is a handler for the double-click. If you press the button twice in quick succession, the second button press will never repeat. The double-click message is sent instead of the second LButtonDown. A handler for the doubleclick does the trick....
 
void CAutoRepeatButton::OnLButtonDblClk(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
SetTimer(IDT_TIMER, INITIAL_DELAY, NULL);
sent = 0;
 
CButton::OnLButtonDblClk(nFlags, point);
}

 
I haven't tried it (yet), but I wonder if overriding the BM_SETSTATE message would be a better place to insert the auto-repeat behavior. This would/may allow for the 'proper' response to the spacebar.
 

Generalthanks!memberlucy10 Jun '02 - 6:45 
very useful class and nice drawn face when the button is pressed.
GeneralKeyboard is not workingmemberDavide Calabro15 Apr '02 - 21:35 
Sorry, but pressing the button with the space bar doesn't generate the auto-repeat effect Frown | :(
 
Cheers,
 
SoftechSoftware
Davide Calabro'
davide_calabro@yahoo.com
http://www.softechsoftware.it
GeneralThank youmemberNish [BusterBoy]15 Apr '02 - 18:49 
Thanks again Joe, for your 2nd cool article for today.
 
Nish
 

The rumours that I am an AI bot are absolutely false. These rumours have been propogated by *them* to focus all the attention on to me, while *their* bots take over the planet. Thank y%%%% Divide by zero. Cannot proceed. Abort(y/y)?

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 15 Apr 2002
Article Copyright 2002 by Joseph M. Newcomer
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid