Click here to Skip to main content
15,881,709 members
Articles / Desktop Programming / MFC
Article

Create your own controls - the art of subclassing

Rate me:
Please Sign up or sign in to vote.
4.85/5 (104 votes)
25 Oct 2001CPOL 1M   6.2K   377   120
An introduction to subclassing the Windows common controls using MFC
  • Download source files - 18 Kb
  • Introduction

    There are many windows common controls that you as a programmer can use to provide an interface to an application. Everything from lists to buttons to progress controls are available. Even so, there will often come a time when the standard selection of controls - diverse as they are - are just not enough. Welcome to the gentle art of subclassing controls.

    Subclassing a window control is not the same as subclassing a C++ class. Subclassing a control means you replace some or all of the message handlers of a window with your own. You effectively hijack the control and make it behave the way you want, not the way Windows wants. This allows you to take a control that is almost, but not quite, what you want, and make it perfect. There are two types of subclassing: instance subclassing and global subclassing. Instance subclassing is when you subclass a single instance of a window. Global subclassing subclasses all windows of a particular type with your own version. We'll only discuss single instance here.

    It's important to remember the distinction between an object derived from CWnd and the window itself (a HWND). You C++ CWnd-derived object contains a member variable that points to a HWND, and contains functions that the HWND message pump calls when processing messages (eg WM_PAINT, WM_MOUSEMOVE). When you subclass a window with your C++ object, you are attaching that HWND to your C++ object and setting that objects callback functions as the one the message pump for that HWND will invoke.

    Subclassing is easy. First you create a class that will handle all the windows messages you are interested in, and then you physically subclass an exising window and make it behave the way your new class dictates. The window becomes possessed, in a way. For this example we'll subclass a button control and make it do things it never knew it was capable of.

    A New Class

    To subclass a control we need to create a new class that handles all the windows messages we are interested in. Since we are lazy it's best to minimise the number of messages you actually have to deal with, and the best way of doing this is by deriving your class from the control class you are subclassing. In our case CButton.

    Lets assume we want to do something bizarre like make the button glow bright yellow everytime the mouse moves over it. Stranger things have been done. First thing we do is use ClassWizard to create a new class derived from CButton called CMyButton.

    Adding a new class

    Deriving from CButton within the MFC framework has a lot of advantages, with the biggest one being we don't actually have to add a single line of code for our class to be a fully functioning windows control. If we wished we could move onto the next step and subclass a button control with our new class and we would have a perfectly functioning, though somewhat boring, button control. This is becuase MFC implements default handlers for all it's messages, so we can simply pick the ones we are interested in, and ignore the others.

    However for this example we have loftier plans for our control - making it bright yellow.

    To check if the mouse is over the control we will set a variable m_bOverControl to TRUE when the mouse enters the control, and then check periodically (using a timer) to keep track of when the mouse leaves the control. Unfortunately for us there is no OnMouseEnter and OnMouseLeave function that can be used across platforms, so we have to make do with using OnMouseMove. If, on a timer tick, we find the mouse is no longer in the control we turn off the timer and redraw the control.

    Use ClassWizard to add a WM_MOUSEMOVE and WM_TIMER message handlers mapped to OnMouseMove and OnTimer respectively.

    Adding message handlers

    ClassWizard will add the following code to your new button class:

    BEGIN_MESSAGE_MAP(CMyButton, CButton)
        //{{AFX_MSG_MAP(CMyButton)
        ON_WM_MOUSEMOVE()
        ON_WM_TIMER()
        //}}AFX_MSG_MAP
    END_MESSAGE_MAP()
    
    /////////////////////////////////////////////////////////////////////////////
    // CMyButton message handlers
    
    void CMyButton::OnMouseMove(UINT nFlags, CPoint point) 
    {
        // TODO: Add your message handler code here and/or call default
    	
        CButton::OnMouseMove(nFlags, point);
    }
    
    void CMyButton::OnTimer(UINT nIDEvent) 
    {
        // TODO: Add your message handler code here and/or call default
    	
        CButton::OnTimer(nIDEvent);
    }

    The message map entries (in the BEGIN_MESSAGE_MAP section) map the windows message to the function. ON_WM_MOUSEMOVE maps WM_MOUSEMOVE to your OnMouseMove function, and ON_WM_TIMER maps WM_TIMER to OnTimer. These macros are defined in the MFC source, but they are not required reading. For this excercise simply have faith that they do their job.

    Assuming we have declared two variables m_bOverControl and m_nTimerID of type BOOL and UINT respectively, and initialised them in the constructor, our message handlers will be as follows

    void CMyButton::OnMouseMove(UINT nFlags, CPoint point) 
    {
        if (!m_bOverControl)                    // Cursor has just moved over control
        {
            TRACE0("Entering control\n");
    
            m_bOverControl = TRUE;              // Set flag telling us the mouse is in
            Invalidate();                       // Force a redraw
    
            SetTimer(m_nTimerID, 100, NULL);    // Keep checking back every 1/10 sec
        }
    	
        CButton::OnMouseMove(nFlags, point);    // drop through to default handler
    }
    
    void CMyButton::OnTimer(UINT nIDEvent) 
    {
        // Where is the mouse?
        CPoint p(GetMessagePos());
        ScreenToClient(&p);
    
        // Get the bounds of the control (just the client area)
        CRect rect;
        GetClientRect(rect);
    
        // Check the mouse is inside the control
        if (!rect.PtInRect(p))
        {
            TRACE0("Leaving control\n");
    
            // if not then stop looking...
            m_bOverControl = FALSE;
            KillTimer(m_nTimerID);
    
            // ...and redraw the control
            Invalidate();
        }
    	
        // drop through to default handler
        CButton::OnTimer(nIDEvent);
    }

    The final piece of our new class is drawing, and for this we don't handle a message, but rather override the CWnd::DrawItem virtual function. This function is only called for owner-drawn controls, and does not have a default implementation that can be called (it ASSERT's if you try). This function is designed to be overriden and used by derived classes only.

    Adding DrawItem override

    Use the ClassWizard to add a DrawItem override and add in the following code

    void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) 
    {
        CDC* pDC   = CDC::FromHandle(lpDrawItemStruct->hDC);
        CRect rect = lpDrawItemStruct->rcItem;
        UINT state = lpDrawItemStruct->itemState;
    
        CString strText;
        GetWindowText(strText);
    
        // draw the control edges (DrawFrameControl is handy!)
        if (state & ODS_SELECTED)
            pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH | DFCS_PUSHED);
        else
            pDC->DrawFrameControl(rect, DFC_BUTTON, DFCS_BUTTONPUSH);
    
        // Deflate the drawing rect by the size of the button's edges
        rect.DeflateRect( CSize(GetSystemMetrics(SM_CXEDGE), GetSystemMetrics(SM_CYEDGE)));
        
        // Fill the interior color if necessary
        if (m_bOverControl)
            pDC->FillSolidRect(rect, RGB(255, 255, 0)); // yellow
    
        // Draw the text
        if (!strText.IsEmpty())
        {
            CSize Extent = pDC->GetTextExtent(strText);
            CPoint pt( rect.CenterPoint().x - Extent.cx/2, 
            rect.CenterPoint().y - Extent.cy/2 );
    
            if (state & ODS_SELECTED) 
                pt.Offset(1,1);
    
            int nMode = pDC->SetBkMode(TRANSPARENT);
    
            if (state & ODS_DISABLED)
                pDC->DrawState(pt, Extent, strText, DSS_DISABLED, TRUE, 0, (HBRUSH)NULL);
            else
                pDC->TextOut(pt.x, pt.y, strText);
    
            pDC->SetBkMode(nMode);
        }
    }

    Everything is now in place - but there is one last step. The DrawItem function requires that the control be owner drawn. This can be achieved in the dialog resource editor by checking the appropriate box - but a far nicer way is to have the class itself set the style of the window it is subclassing automatically in order to make the class a true "drop-in" replacement for CButton. To do this we override a final function: PreSubclassWindow.

    This function is called by SubclassWindow, which in turn is called by either CWnd::Create or DDX_Control, meaning that if you created an instance of you new class either dynamically or by using a dialog template, PreSubclassWindow will still be called. PreSubclassWindow will be called after the window you are subclassing has been created, but before it becomes visible after you subclass it. In other words - a perfect time to perform initialisation that requires the window to be present.

    An important note here: if you create a control using a dialog resource, then your subclassed control will not see the WM_CREATE message, hance we cannot use OnCreate for our initialisation, since it won't be called in all cases.

    Use ClassWizard to override PreSubclassWindow and add the following code

    void CMyButton::PreSubclassWindow() 
    {
        CButton::PreSubclassWindow();
    
        ModifyStyle(0, BS_OWNERDRAW);	// make the button owner drawn
    }

    Congratulations - you now have a Cbutton derived class!

    The Subclass

    Using DDX to subclass a window at creation time

    In this example I'm working with a dialog on which I've placed a button control:

    a button

    We let the normal dialog creation routines create the dialog with the control, and use the DDX_... routines to subclass the control with our new class. To do this simply use ClassWizard to add a member variable to you dialog class attached to your button control (in my case it's ID is IDC_BUTTON1), and choose the variable as a Control type, with class name CMyButton.

    subclassing the control

    The ClassWizard generates a DDX_Control call in your dialog's DoDataExchange function. DDX_Control calls SubclassWindow which causes the button to use the CMyButton message handlers instead of the usual CButton handlers. The button has been hijacked and will behave from now on the way we want it to.

    Subclassing a window using a class not recognised by the ClassWizard

    If you have added a window class to your project and want to subclass a window with an object of this new class' type, but the ClassWizard isn't offering you that new object's type as an option, then you may need to rebuild the class wizard file.

    Make a backup of your projects .clw file, delete the original file, then go into Visual Studio and hit Ctrl+W. You will then be prompted for which files you want to have included in the class scan. Ensure that the new class files are included!

    Your new class should now be available as an option. If not, then you can always use the classwizard to subclass you control as a generic control (say, CButton) and then go into the header file manually and change this to the class that you want (eg CMyButton).

    Subclassing an existing window

    Using DDX is simple, but doesn't help us if we need to subclass a control that already exists. For instance, say you want to subclass the Edit control in a combobox. You need to have the combobox (and hence it's child edit window) already created before you can subclass the edit window.

    In this case you make use of the handy SubclassDlgItem or SubclassWindow functions. These two functions allow you to dynamically subclass a window - in other words, attach an object of your new window class type to an existing window.

    For example, suppose we have a dialog containing a button with ID IDC_BUTTON1. That button has already been created and we want to associate that button with an object of type CMyButton so that the button behaves in the manner we want.

    To do this we need to have an object of our new type already created. A member variable of your dialog or view class is perfect.

    CMyButton m_btnMyButton;

    Then call in your dialog's OnInitDialog (or whereever is appropriate) call

    m_btnMyButton.SubclassDlgItem(IDC_BUTTON1, this);

    Alternatively suppose you already have a pointer to a window you wish to subclass, or you are working within a CView or other CWnd derived class where the controls are created dynamically or you dont't wish to use SubclassDlgItem. Simply call

    CWnd* pWnd = GetDlgItem(IDC_BUTTON1); // or use some other method to get
                                          // a pointer to the window you wish
                                          // to subclass
    ASSERT( pWnd && pWnd->GetSafeHwnd() );
    m_btnMyButton.SubclassWindow(pWnd->GetSafeHwnd());

     

    The button drawing is very simple and does not take into account button styles such as flat buttons, or justified text, but scope is there for you to do whatever you wish. If you compile and run the accompanying code you'll see a simple button that turns bright yellow when the mouse passes over it.

    The finished product

    Notice that we only really overrode the drawing functionality, and intercepted the mouse movement functions (but passed these on to the default handler). This means that the control is still, deep down, a button. Add a button click handler to your dialog class and you'll see it will still get called.

    Conclusion

    Subclassing is not hard - you just need to choose the class you wish to subclass carefully, and be aware of what messages you need to handle. Read up on the control you are subclassing - learn about the messages it handles and also the virtual member functions of its implementation class. Once you've hooked into a control and taken over it's inner workings the sky's the limit.

    History

    26 Oct 2001 - added info in SubclassWindow and SubclassDlgItem

    License

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


    Written By
    Founder CodeProject
    Canada Canada
    Chris Maunder is the co-founder of CodeProject and ContentLab.com, and has been a prominent figure in the software development community for nearly 30 years. Hailing from Australia, Chris has a background in Mathematics, Astrophysics, Environmental Engineering and Defence Research. His programming endeavours span everything from FORTRAN on Super Computers, C++/MFC on Windows, through to to high-load .NET web applications and Python AI applications on everything from macOS to a Raspberry Pi. Chris is a full-stack developer who is as comfortable with SQL as he is with CSS.

    In the late 1990s, he and his business partner David Cunningham recognized the need for a platform that would facilitate knowledge-sharing among developers, leading to the establishment of CodeProject.com in 1999. Chris's expertise in programming and his passion for fostering a collaborative environment have played a pivotal role in the success of CodeProject.com. Over the years, the website has grown into a vibrant community where programmers worldwide can connect, exchange ideas, and find solutions to coding challenges. Chris is a prolific contributor to the developer community through his articles and tutorials, and his latest passion project, CodeProject.AI.

    In addition to his work with CodeProject.com, Chris co-founded ContentLab and DeveloperMedia, two projects focussed on helping companies make their Software Projects a success. Chris's roles included Product Development, Content Creation, Client Satisfaction and Systems Automation.

    Comments and Discussions

     
    GeneralGreat article - changing default buttons in property sheets Pin
    michael thomas12-Aug-03 17:01
    michael thomas12-Aug-03 17:01 
    QuestionHas this been tried this with a CStatic? Pin
    ScottLeff20-Jun-03 15:31
    ScottLeff20-Jun-03 15:31 
    AnswerRe: Has this been tried this with a CStatic? Pin
    dsoykurum12-Aug-03 10:37
    dsoykurum12-Aug-03 10:37 
    GeneralRe: Has this been tried this with a CStatic? Pin
    mark.zhong21-Nov-03 5:00
    mark.zhong21-Nov-03 5:00 
    Generalthanx....A LOT!!!!! Pin
    namaskaaram31-Oct-04 23:30
    namaskaaram31-Oct-04 23:30 
    Answeru can also do it ain an another way... Pin
    namaskaaram4-Nov-04 22:50
    namaskaaram4-Nov-04 22:50 
    GeneralExcellent articlr - thanks! Pin
    dregan13-Jun-03 12:53
    dregan13-Jun-03 12:53 
    GeneralCreateIndirect not creating custom control Pin
    IOUATP7-Jun-03 4:47
    IOUATP7-Jun-03 4:47 
    GeneralA problem subclassing CEdit Pin
    Alton Williams24-May-03 4:12
    Alton Williams24-May-03 4:12 
    GeneralPb with single click Pin
    situ14-May-03 7:30
    situ14-May-03 7:30 
    GeneralBN_CLICKED Pin
    lucy2-Apr-03 5:48
    lucy2-Apr-03 5:48 
    GeneralRe: BN_CLICKED Pin
    Wes Aday2-Apr-03 6:31
    professionalWes Aday2-Apr-03 6:31 
    GeneralRe: BN_CLICKED Pin
    rkhuang4-Dec-03 5:35
    rkhuang4-Dec-03 5:35 
    GeneralAbout parent Wnd Pin
    tigerlc1-Oct-02 23:55
    tigerlc1-Oct-02 23:55 
    GeneralCheck box Pin
    Knowthyself18-Sep-02 22:25
    sussKnowthyself18-Sep-02 22:25 
    QuestionAppication to ASP.NET? Pin
    ian_sreyes30-Aug-02 19:09
    ian_sreyes30-Aug-02 19:09 
    AnswerRe: Appication to ASP.NET? Pin
    TigerNinja_13-Sep-02 8:54
    TigerNinja_13-Sep-02 8:54 
    GeneralDiff between subclassing and attaching Pin
    srinivas vaithianathan2-Jul-02 8:31
    srinivas vaithianathan2-Jul-02 8:31 
    GeneralSubclassing CReBar Pin
    Sephrenia21-Mar-02 22:49
    Sephrenia21-Mar-02 22:49 
    GeneralSubclassing a CEdit control Pin
    18-Mar-02 11:36
    suss18-Mar-02 11:36 
    GeneralBig Doubt in Win 32 API - Hooks Pin
    5-Mar-02 19:35
    suss5-Mar-02 19:35 
    GeneralCan't get it to work. Pin
    Lazerhead6913-Feb-02 12:35
    Lazerhead6913-Feb-02 12:35 
    GeneralRe: Can't get it to work. Pin
    Christian Graus13-Feb-02 12:41
    protectorChristian Graus13-Feb-02 12:41 
    GeneralRe: Can't get it to work. Pin
    20-Feb-02 10:19
    suss20-Feb-02 10:19 
    QuestionWhat about subclassing buttons created in a VB form? Pin
    30-Jan-02 23:55
    suss30-Jan-02 23:55 

    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.