IntroductionThere 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 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 ClassTo 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 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
Deriving from 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
Use ClassWizard to add a WM_MOUSEMOVE and WM_TIMER message handlers mapped to
ClassWizard will add the following code to your new button class: Collapse | Copy Code 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 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 Collapse | Copy Code 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
Use the ClassWizard to add a Collapse | Copy 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 This function is called by 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 Use ClassWizard to override Collapse | Copy Code void CMyButton::PreSubclassWindow() { CButton::PreSubclassWindow(); ModifyStyle(0, BS_OWNERDRAW); // make the button owner drawn } Congratulations - you now have a The SubclassUsing DDX to subclass a window at creation timeIn this example I'm working with a dialog on which I've placed a button control:
We let the normal dialog creation routines create the dialog with the control, and
use the
The ClassWizard generates a Subclassing a window using a class not recognised by the ClassWizardIf 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, Subclassing an existing windowUsing 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 For example, suppose we have a dialog containing a button with ID 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. Collapse | Copy Code CMyButton m_btnMyButton; Then call in your dialog's Collapse | Copy Code 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 Collapse | Copy Code 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.
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. ConclusionSubclassing 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. History26 Oct 2001 - added info in SubclassWindow and SubclassDlgItem |
|