subclassing art

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. 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. ClassWizard will add the following code to your new button class: Collapse 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 Collapse 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. Use the ClassWizard to add a DrawItem override and add in the following code Collapse 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 Collapse 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: 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. 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. Collapse CMyButton m_btnMyButton; Then call in your dialog's OnInitDialog (or whereever is appropriate) call Collapse 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 Collapse 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.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值