Introduction
I am in need of an autorepeat button. A button that when the user presses and holds it down will fire multiple BN_CLICKED
notifications. I did a search and found Joseph M. Newcomer's article "An AutoRepeat Button Class". While the code presented in the article worked exactly as advertised, it had one major flaw. It did not work when the user pressed the space bar, it only worked with the left mouse button. The comments attached to the article provided some ideas on how to fix it, but none were ideal. I took the ideas presented in the article and some of the comments and came up with the code I am sharing here.
Using the Code
The best way to get the button control to do what I needed it to do was to subclass the CButton
control and override its OnLButtonDown
, OnLButtonUp
, OnKeyDown
, and OnKeyUp
message handlers. I added four bool
member variables that act as flags to control what has happened and what has to happen next when the control is used. The first two variables are KeyPress
and MousePress
that control whether it is the mouse button or the space bar that started the timer. The next one is TimerActive
that prevents the timer from being restarted when the key press auto-repeats. The last one is MessageSent
that controls whether a BN_CLICK
message should be sent when the button is released.
void CAutoRepeatButton::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if (!MousePress) {
if (VK_SPACE == nChar && !TimerActive)
{
SetTimer(TIMERID, InitialTime, NULL);
TimerActive = true;
KeyPress = true;
}
CButton::OnKeyDown(nChar, nRepCnt, nFlags);
}
}
void CAutoRepeatButton::OnLButtonDown(UINT nFlags, CPoint point)
{
if (!KeyPress) {
if (!TimerActive)
{
SetTimer(TIMERID, InitialTime, NULL);
TimerActive = true;
MousePress = true;
}
CButton::OnLButtonDown(nFlags, point);
}
}
When the button is pressed with either the left mouse button or the space bar, we first check that the button is not already activated by the other one. Then we check if the timer is not already started and start it with the initial delay time if it was not. Finally, place a call to the base class handler to do the default handler which captures the mouse and draws the button as depressed.
void CAutoRepeatButton::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == TIMERID)
{
if (BST_PUSHED == (BST_PUSHED & GetState()))
{
if (!MessageSent)
{
SetTimer(TIMERID, RepeatTime, NULL);
MessageSent = true;
}
Parent->SendMessage(WM_COMMAND, MAKELPARAM(GetDlgCtrlID(), BN_CLICKED), (WPARAM)m_hWnd);
}
}
else
{
CButton::OnTimer(nIDEvent);
}
}
When the timer fires, we first check the state of the button. If the mouse was used to start the timer and then it is moved off of the button, the button will no longer be pushed so we will not want the BN_CLICKED
messages to be sent. But we do want them to continue if the mouse moves back onto the button. After the initial time delay, the timer is reset to the repeat time and a BN_CLICKED
message is sent.
void CAutoRepeatButton::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if (VK_SPACE == nChar && KeyPress)
{
KillTimer(TIMERID);
if (MessageSent)
{
ReleaseCapture(); SetState(0); }
else
{
CButton::OnKeyUp(nChar, nRepCnt, nFlags);
}
TimerActive = false;
KeyPress = false;
MessageSent = false;
}
}
void CAutoRepeatButton::OnLButtonUp(UINT nFlags, CPoint point)
{
if (MousePress)
{
KillTimer(TIMERID);
if (MessageSent)
{
ReleaseCapture();
SetState(0);
}
else
{
CButton::OnLButtonUp(nFlags, point);
}
TimerActive = false;
MousePress = false;
MessageSent = false;
}
}
When the button is released, the timer is stopped. And then, depending on whether the button was held down long enough to generate a BN_CLICKED
message, we either simply release the mouse capture and draw the button as not pressed or we call the base class handler to fire off a BN_CLICKED
message. Then the flags are reset for the next time the button is pressed.
void CAutoRepeatButton::OnLButtonDblClk(UINT nFlags, CPoint point)
{
OnLButtonDown(nFlags, point);
}
The last thing to handle is if the user double clicks and then holds the button down. I simply call OnLButtonDown
to treat the double click as a second single click. If this is not done, then the timer is not started.
void CAutoRepeatButton::SetTimes(UINT Initial, UINT Repeat)
{
InitialTime = Initial;
RepeatTime = Repeat;
}
Finally, as a convenience function, I added the ability to set the initial and repeating delay time interval.
History
- April 13, 2017 - Published on CodeProject