Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

An AutoRepeat Button Class

5.00/5 (3 votes)
12 Apr 2017CPOL3 min read 12.2K   498  
This is an alternative for An AutoRepeat Button Class

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 OnLButtonDownOnLButtonUpOnKeyDown, 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.

C++
void CAutoRepeatButton::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    if (!MousePress)    // Only if not already activated by the mouse
    {
        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)  // Only if not already activated with the space bar
    {
        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.

C++
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.

C++
void CAutoRepeatButton::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    if (VK_SPACE == nChar && KeyPress)
    {
        KillTimer(TIMERID);
        if (MessageSent)
        {
            ReleaseCapture();   // CButton::OnKeyDown captures the mouse
            SetState(0);        // Redraw button as not pushed
        }
        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.

C++
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.

C++
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)