Click here to Skip to main content
15,889,651 members
Articles / Desktop Programming / MFC
Article

Adding mouse functionality to any control

Rate me:
Please Sign up or sign in to vote.
4.74/5 (14 votes)
19 Jan 20025 min read 84.9K   3.6K   44   2
An article on adding mouse based functionality to any CWnd derived control

Introduction

What is CMouseAction? CMouseAction is a serializable baseclass that allows any CWnd derived class to implement all manner of mouse movement actions. CMouseAction provides an implementation of mouse notifications, run-time resizing and moving, control transparency, control toolips, and automatic window placement. Automatic window placement is done through serialization of the window position when created or destroyed and then MoveWindow is used to properly place the control window on its parent. SetWindowName sets the window name used during serialization, so that each control window can easily be identified in a serialized file. The serialization requires you to call Serialize with the properly set CArchive. CMouseAction works for multiple classes by using #define to control its base class.

// In the mouseaction header you create define that look like this
//
// These base classes can easily be changed to any CWnd derived class
#if defined(MYBTN)
  #define BASECLASS CButton
#elif defined(MYSLID)
  #define BASECLASS CSliderCtrl
#elif defined(MYTEXT)
  #define BASECLASS CStatic
#else
  #define BASECLASS CWnd
#endif
/////////////////////////////////////////////////////////////////////////////
// CMouseAction window

class CMouseAction : public BASECLASS
// BASECLASS will then be replaced with the proper base class 
// when included in each file
{
}
In your derived control class header file you create a define that maps to the proper class in the Mouseaction header:
#define MYBTN
or else
#define MYSLID

When the code is compiled, CMyBtn will be derived from CMouseAction, and CMouseAction will be derived from CButton for this instance. CMySlider will be derived from CMouseAction, and CMouseAction will be derived from CSlider for this instance. CMyStatic will be derived from CMouseAction, and CMouseAction will be derived from CStatic for this instance. Even though they are all derived from the same class each, by using #defines we are able to derive each from its proper class. After processing notifications are then sent to the proper baseclass by using our define's and calling, for ex.

BASECLASS::OnMouseMove(nFlags,point);
would call the OnMouseMove function from the CButton, CStatic, CSliderCtrl, or CWnd class depending on the #define in the subclassed header file.

When the mouse is moved over the control, we tell windows that we want to be notified of any mouse movements by simply calling:

if (!m_bTracking)
    {
        TRACKMOUSEEVENT tme;
        tme.cbSize = sizeof(tme);
        tme.hwndTrack = m_hWnd;
        tme.dwFlags = TME_LEAVE|TME_HOVER;
        tme.dwHoverTime = 1;

        //Tell Windows that we want to process all mouse Hover and Leave Messages
        m_bTracking = _TrackMouseEvent(&tme); 
        m_point = point;
    }

from within the OnMouseMove function. This will then track the mouse and present us with two new functions that we need to implement.

//In the cpp message map call
ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave)
ON_MESSAGE(WM_MOUSEHOVER, OnMouseHover)
//In the header AFX_MSG section
afx_msg LRESULT OnMouseLeave(WPARAM wparam, LPARAM lparam);
afx_msg LRESULT OnMouseHover(WPARAM wparam, LPARAM lparam);

Within the OnMouse functions, we set BOOL m_bHover to true if the mouse is hovering over our control, and false otherwise. After setting m_bHover, we call Invalidate() so that our control will update its interface. From within the drawing code of our control, we then call IsHovering() to determine if we need to draw the control with the mouse hovering or not. We use IsHovering() in our drawing code, so that if the hover code is changed at all, or m_bHover changed, using IsHovering() will still determine if the mouse is hovering, and thus prevent future potential problems. If you wanted to add a third MouseDown state, you could easily add OnLButtonDown() to your control, set a boolean variable in the OnLButtonDown function, and Invalidate your control, thus providing three drawing states for the control, MouseHover, MouseOut\Leave, and MouseDown.

HBRUSH CMouseAction::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 
{
    HBRUSH hbr = NULL;
    
    if (m_bTranparent)
    {
        pDC->SetBkMode(TRANSPARENT);//Set transparent background
        LOGBRUSH brush;
        brush.lbColor = HOLLOW_BRUSH;
        brush.lbHatch = 0;
        brush.lbStyle = 0;
        hbr = CreateBrushIndirect(&brush);//Use a transparent brush for painting
    }
    else
        hbr = BASECLASS::OnCtlColor(pDC, pWnd, nCtlColor);//Else use default
    return hbr;
}

If SetTransparent(TRUE) is used, then the control is set to draw with a transparent background. The transparent background is achieved by creating a NULL brush for our control when OnCtlColor is called. If our control is not transparent then the base class OnCtlColor is called and returns the proper background drawing brush. This allows our control to be drawn transparently or with the default background from our base class that was previously set using the BASECLASS defines.

We also allow for custom tooltips for each control window. SetToolTipText is used to set the text that will be displayed in the ToolTip. When the mouse hovers over the window, the tooltip is then displayed. The tooltip is displayed by using the OnMouseHover function, and immediately after calling RedrawWindow to update our control, we the update the tooltip to display since the mouse is now hovering. We update the tooltip while the mouse hovers by using the following code:

DeleteToolTip();//Remove old tooltip
// Create a new Tooltip with new Button Size and Location
SetToolTipText(m_tooltext);
if (m_ToolTip != NULL)
    if (::IsWindow(m_ToolTip->m_hWnd))
         //Display ToolTip
m_ToolTip->Update();

We add runtime resizing and moving to control by implementing a CRectTracker along with four functions. First we use SetMoveable() to TRUE, to allow our control to be moved and resized. We use IsMoveable in our functions and drawing code to determine if the control is allowed to be moved or not by the user at runtime. If our control is allowed to be moved and resized at runtime, then we use the OnRButtonDown to allow the user to resize and move the control. Since it is common place for most controls to perform specific tasks when the left button is down, I have decided to use the OnLButtonDown to allow subclassed controls to perform their specific functions, while reserving the OnRButtonDown to perform the resizing and moving of the control. In addition to using OnRButtonDown,

OnMove
is also implemented to correct any potential issues when a transparent window is moved or resized over a window that uses a bitmap or image as its background. When the right mouse button is down, the user is allowed to move the control. When the CTRL key is down and the Right Button is clicked on our control, then the user is allowed to resize the window instead. If our control is already in resize or move mode, and the right button is clicked again, then the action is cancelled. If the left button is clicked while we are resizing or moving the control, then the control window is updated to the new size and location. See OnRButtonDown below:

void CMouseAction::OnRButtonDown( UINT nFlags, CPoint point )
{
    // Are we allowed to resize the window?
    if ((IsMoveable() == TRUE) && (m_bResizing == FALSE)) 
    {
        m_bResizing = TRUE;
        m_point = point;
        CRect rect;
        BOOL bSuccess = FALSE;
        GetClientRect(rect);//Tell the tracker where we are
        m_track = new CRectTracker(&rect, CRectTracker::dottedLine | 
                                CRectTracker::resizeInside |
                                CRectTracker::hatchedBorder);
        if (nFlags & MK_CONTROL) // If Ctrl + Right-Click then Resize object
        {
            GetWindowRect(rect);
            GetParent()->ScreenToClient(&rect);

            //Let user resize window
            bSuccess = m_track->TrackRubberBand(GetParent(),rect.TopLeft());
            m_track->m_rect.NormalizeRect();
        }
        else // If not Ctrl + Right-Click, then Move Object
        {
            bSuccess = m_track->Track(GetParent(), point);//Let user move window
        }
        if (bSuccess)
        {
            rect = LPRECT(m_track->m_rect);
            //ClientToScreen(&rect);
            //GetParent()->ScreenToClient(&rect);
            //SetWindowPos(&wndTopMost,rect.left,rect.top,
                           rect.Width(),rect.Height(),SWP_SHOWWINDOW);

            //Move Window to our new position
            MoveWindow(rect.left,rect.top,
                       rect.Width(),rect.Height(),TRUE);
        }
        delete m_track;
        m_track = NULL;
        rect = NULL;
        m_bResizing = FALSE;
    }
    BASECLASS::OnRButtonDown(nFlags,point);
}

To avoid issues with transparent controls over bitmapped windows, we use the following to hide the window, move it, and then show it and redraw it.

void CMouseAction::OnMove(int x, int y) 
{
    BASECLASS::OnMove(x, y);
    
    // This code is so that when a transparent control is moved
    // and the dialog or app window behind the transparent control
    // is showing a bitmap, this forces the parent to redraw
    // before we redraw so that the bitmap is shown properly
    // and also eliminates any window overlapping that may occur with
    // using a Transparent Window on top of a Bitmap...
    // If you are not using a transparent window, you shouldn't need this...
    
    ShowWindow(SW_HIDE);// Hide Window
    CRect rect;
    GetWindowRect(&rect);
    GetParent()->ScreenToClient(&rect);
    GetParent()->InvalidateRect(&rect);//Tell Parent to redraw the rect
    ShowWindow(SW_SHOW);//Now redraw us so that Control displays correctly
}

This class helps add a lot of mouse functionality to any control window, and still allows us to subclass our control window from the proper class using the set defines. Hopefully this is useful to someone... Please remember that when deriving a control from this class you must change the class it is derived from in all areas, this includes the following:

class CText : public CMouseAction
....

//This is very important or else the mouse actions will not work properly
BEGIN_MESSAGE_MAP(CText, CMouseAction) 
    //{{AFX_MSG_MAP(CText)
        // NOTE - the ClassWizard will add and remove mapping macros here.
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

void CText::OnLButtonDown(...)
{
    CMouseAction::OnLButtonDown(...); //Don't forget this either
}

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


Written By
Web Developer
United States United States
Programming using MFC and ATL for almost 12 years now. Currently studying Operating System implementation as well as Image processing. Previously worked on DSP and the use of FFT for audio application. Programmed using ADO, ODBC, ATL, COM, MFC for shell interfacing, databasing tasks, Internet items, and customization programs.

Comments and Discussions

 
GeneralWhy not use C++ templates Pin
Anonymous28-Apr-05 13:04
Anonymous28-Apr-05 13:04 
GeneralRe: Why not use C++ templates Pin
Fred Ackers29-Mar-06 4:29
Fred Ackers29-Mar-06 4:29 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.