<转>Developing Custom Draw Controls in Visual C++
原谅链接:http://msdn.microsoft.com/en-us/library/ms364048(v=vs.80).aspx
Visual Studio 2005
Tom Archer
Program Manager, Microsoft
January 2006
Applies to:
Win32 API
Microsoft Foundation Classes
Visual C++ 2005
Summary: Tom Archer presents the custom draw technique of developing custom controls to give your application a unique look and feel. (9 printed pages)
Download the associated code sample, CustomDraw.exe.
Contents
Just How Different Do You Want to Be?
Taking Ownership of Drawing
The Three Steps to Implementing Custom Draw
Example: Creating a List-view Control Custom Draw Control
Conclusion
Acknowledgements
References
I still remember to this day a conversation I had back in 1995 when I managed a development team at Peachtree Software regarding how much time Visual C++ and MFC were going to save us and, therefore, how much quicker we were going to get to market with our accounting system. It went something like this:
Me: The Visual Studio wizards will enable us to generate the framework for an application in seconds. We basically get all the user interface for free. Menus, status bar, a complete document/view architecture to separate data and presentation, toolbars, and so on. They even have things like file-open, print, and print-preview built right in!
Marketing: Sounds great. So how long will it take you guys to code everything?
Me: Considering we're getting all the UI for free and only need to plug in the accounting stuff, we could be done in 6 to 9 months. And best of all, the application will look just like a Microsoft Office application!
Marketing: Huh?
Me: Exactly. We get the subliminal benefit of our application looking like one of Microsoft's. This is especially important in that if we look like an Office product, it'll be much easier to get the Windows 95 logo on our boxes.
Marketing: We can't go to the marketplace saying, "Buy our product because it looks like other products." All accounting products have the same basic functionality. The only way we can differentiate ours is with the user interface. We're going to hire graphical artists that will design a completely customized user interface and then your team will code that. How long will that take?
Me: Without seeing the exact controls they design it's tough to say, but it'll probably double our work—at least.
Marketing: Then, you'd better get started.
Two years later, Peachtree Software shipped the first product that it had designed and created from scratch and I'm proud to have been a major contributor to that effort. Over the past 10 years, I've led the development of several well-known products from IBM, AT&T, and VeriSign that run on millions of PCs and telephones worldwide and during that time, I've always remembered that lesson: No matter how good your application is internally, if it doesn't stand out in a crowd and grab the user's attention, then it's not going to sell.
Therefore, as my first article for MSDN, I thought I'd focus on one of my favorite topics—and a technique we used quite often at Peachtree to develop those fancy UI widgets the marketing team wanted—developing custom draw controls.
Just How Different Do You Want to Be?
After you've decided that you want to develop controls outside the normal range of the custom controls that Windows gives you for free, you then have to decide just how different your controls will be—both in terms of functionality and appearance. For example, let's say you're creating a speedometer-like control. Since there's nothing like that in the common controls library (ComCtrl32.dll), you're completely on your own in terms of writing all the code required for the control's functionality, drawing, default end-user interaction, and any messaging that needs to happen between the control and the control's parent window.
The other end of the spectrum encompasses scenarios where you simply want to tweak the functionality of a common control. For example, let's say that you want to create a masked-edit control that allows only specific characters to be accepted. If you're using MFC, this typically involves deriving a class from an MFC-provided class that encapsulates one of the common controls (typically CEdit in the case of a masked-edit control), overriding the necessary virtual functions (or handling specific messages), and then injecting your own custom code.
This article's focus falls somewhere in between—where a common control gives you most of the functionality you want, but the control's appearance isn't quite what you want. For example, a list-view control provides a means of displaying lists of data in a number of view styles—small icon, large icon, list, and details (report). However, what if you want a grid control? While the common controls library doesn't specifically contain a grid, the list-view control comes close in that it represents data in rows and columns and has an associated header control. Therefore, many people create their grid controls by first starting with a standard list-view control and then overriding its rendering, or drawing, of the control and its items.
Taking Ownership of Drawing
Even when "only" doing the painting, there are at least four options available to you—all with distinct advantages and disadvantages:
- Handling WM_PAINT
- Owner Draw
- Custom Draw
- Handling WM_CTLCOLOR
Handling WM_PAINT
The most extreme choice is to implement a WM_PAINT handler and do all the painting yourself. This means that your code will need to do even the most mundane chore associated with rendering the control—creating the appropriate device context(s), determining the size and location of the control, drawing the control, and so on. It is very rare that you'll need this level of control over the drawing process.
Owner Draw
Another method of controlling the drawing of a control is through owner-draw. In fact, you may have heard developers refer to owner-draw controls, as this is the most common technique used to develop custom controls. The main reason for this technique's popularity lies in how much Windows helps you. When its time for your control to be rendered, Windows has already created and filled in the device context, determined the size and location of the control, and even passes you information to let you know just what needs to be drawn. For list controls—such as the listbox and list-view—Windows will call your drawing code for each item in the list, which means that you only have to draw that item, without worrying about the remainder of the control. Note that owner-draw will work for most controls. However, it doesn't work for edit controls; and with regards to the list control, it works only for report-view style.
Custom-draw
This is probably the least understood technique of drawing your own controls. In fact, many very knowledgeable developers still confuse the terms owner-draw and custom-draw. The first thing to know about custom control is that it is only for specific common controls: header, list-view, rebar, toolbar, tooltip, trackbar, and tree-view. In addition, while owner-draw will only allow painting for the report-view style of the list-view control, custom-draw enables you to handle painting for all view styles of the list-view control. Another huge advantage of using custom draw is that you can pick and choose exactly what you want to paint. This is done by Windows sending your code a message during each stage of the control's drawing. This way, you can determine—at each stage—whether you want to do all the painting yourself, augment the default painting, or allow Windows to perform all the painting for that stage. (As custom-draw is the topic of this article, you'll see how this works shortly.)
Handling WM_CTLCOLOR
This is probably the easiest way to have a hand in determining how a control is rendered. As the message name indicates, the WM_CTLCOLOR message is sent when a control is to be painted and it enables your code to determine the brush to use. Typically, this technique is used if you simply want to change the color of the control and doesn't provide much functionality beyond that. In addition, this message is not sent for the common controls introduced with Internet Explorer—list-view, tree-view, rebar and so on—and works only with the standard controls (edit, listbox, and so on.)
The Three Steps to Implementing Custom Draw
Now that you've seen the various options available to you regarding drawing a control—including the benefits of using custom draw—let's take a look at the three main steps needed to implement a custom-draw control.
- Implement an NM_CUSTOMDRAW message handler.
- Specify the desired drawing stages to handle.
- Filter for specific drawing stages (where you also introduce your own control-specific drawing code).
Implement an NM_CUSTOMDRAW Message Handler
When a common control needs to be drawn, MFC reflects the control's custom-draw notification message (originally sent to the control's parent) to the control in the form of anNM_CUSTOMDRAW message. Here is an example of an NM_CUSTOMDRAW handler.
void CMyCustomDrawControl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR); ... }
As you can see, the NM_CUSTOMDRAW handler is passed a pointer to a structure of type NMHDR. However, that value isn't of much use in that form as the NMHDR structure contains only three members—hwndFrom, idFrom, and code.
Therefore, you'll generally need to cast this structure pointer to something a little more informative—LPNMCUSTOMDRAW. The LPNMCUSTOMDRAW points toNMCUSTOMDRAW, which contains members such as dwDrawStage, dwItemSpec and uItemState—all of which are needed to determine the current drawing stage and what exactly is being drawn (e.g., the control itself, or one of the control's items or subitems).
It's worth mentioning at this point that you can also cast the NMHDR pointer to a structure that is specific to the type of control being drawn. Table 1 shows a list of controls and their associated custom-draw structure type names.
Table 1: Controls and their associated custom draw structures
Control
Structure (defined in commctrl.h)
Rebar, trackbar, and header
NMCUSTOMDRAW
List-view
NMLVCUSTOMDRAW
Toolbar
NMTBCUSTOMDRAW
Tooltip
NMTTCUSTOMDRAW
Tree-view
NMTVCUSTOMDRAW
Specify the Desired Drawing Stages to Handle
As I mentioned earlier, there are "stages" of drawing a control. Specifically, you can think of the drawing process a series of stages where the control notifies its parent that something needs to be drawn. In fact, the control will even send a notification before and after the drawing of the control and its items to give the programmer even more control over this process.
In all cases, the single NM_CUSTOMDRAW handler is called for each stage of drawing. However, keeping in mind that custom draw allows you to combine default control drawing with your own drawing, you need to specify which stages of drawing you will handle. This is done by setting the second parameter to the NM_CUSTOMDRAW handler (pResult). In fact, if you never set this value, then after the function is called with the initial stage of CDDS_PREPAINT, your function will not be called again!
Technically, there are only two stages where specifying the desired drawing stages (CDDS_PREPAINT and CDDS_ITEMPREPAINT) impacts what notification messages are sent. However, it is common to simply specify at the end of the handler the drawing stages that your code will handle. Table 2 lists the values that are used to specify the desired drawing stages that your code is interested in.
Table 2: Custom-draw Return Flags
Custom Draw Return Flag Value
Meaning
CDRF_DEFAULT
Indicates that the control is to draw itself. This value—which should not be combined with any other value—is the default value.
CDRF_SKIPDEFAULT
Used to specify that the control is not to do any drawing at all.
CDRF_NEWFONT
Used if your code changes the font of an item/subitem being drawn.
CDRF_NOTIFYPOSTPAINT
Results in notification messages being sent after the control or each item/subitem is drawn.
CDRF_NOTIFYITEMDRAW
Indicates that an item (or subitem) is about to be drawn. Note that the underlying value for this is the same as CDRF_NOTIFYSUBITEMDRAW.
CDRF_NOTIFYSUBITEMDRAW
Indicates that a subitem (or item) is about to be drawn. Note that the underlying value for this is the same as CDRF_NOTIFYITEMDRAW.
CDRF_NOTIFYPOSTERASE
Used if your code needs to be notified after the control has been erased.
Here's an example where the code is specifying that the NM_CUSTOMDRAW handler should be called when the control's items (CDRF_NOTIFYITEMDRAW) and subitems (CDRF_NOTIFYSUBITEMDRAW) are being drawn as well as when the drawing is completed (CDRF_NOTIFYPOSTPAINT).
void CListCtrlWithCustomDraw::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR); ... *pResult = 0; // Initialize value *pResult |= CDRF_NOTIFYITEMDRAW; *pResult |= CDRF_NOTIFYSUBITEMDRAW; *pResult |= CDRF_NOTIFYPOSTPAINT; }
Filter for Specific Drawing Stages
Once you've specified which stages you care about, you need to handle those stages. As there is only one message sent for each stage of the drawing process, it is customary to implement a switch statement to determine the exact drawing stage. The different drawing stages are defined by the following flags:
CDDS_PREPAINT
CDDS_ITEM
CDDS_ITEMPREPAINT
CDDS_ITEMPOSTPAINT
CDDS_ITEMPREERASE
CDDS_ITEMPOSTERASE
CDDS_SUBITEM
CDDS_POSTPAINT
CDDS_PREERASE
CDDS_POSTERASE
Here's an example of an NM_CUSTOMDRAW handler for a CListCtrl-derived class where you can see how the code determines the current drawing stage:
void CMyCustomDrawControl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR); switch(pNMCD->dwDrawStage) { case CDDS_PREPAINT: ... break; case CDDS_ITEMPREPAINT: ... break; case CDDS_ITEMPREPAINT | CDDS_SUBITEM: ... break; ... } *pResult = 0; }
Note that in order to determine the stage of drawing for a subitem—such as with a list-view control—you must use the bitwise or operator with two values: one being eitherCDDS_ITEMPREPAINT or CDDS_ITEMPOSTPAINT and the other being CDDS_SUBITEM.
To illustrate that, let's say you want to do some processing before a list-view item is painted. You would code your switch statement to handle CDDS_ITEMPREPAINT.
case CDDS_ITEMPREPAINT: ... break;
However, if it's the prepaint stage of the subitem you care about, you would do the following:
case CDDS_ITEMPREPAINT | CDDS_SUBITEM: ... break;
Example: Creating a List-view Control Custom-Draw Control
As mentioned earlier, custom draw enables you to completely take over all aspects of drawing the control and its items or perform only a small amount of application-specific drawing and let the control do the rest. As the focus of this article is more on the technique of custom draw rather than advanced drawing techniques, we'll put together a simple example where a list-view control is custom drawn such that the item text is shown in different colors in alternating cells creating a patchwork-like look.
- Create a Visual C++ 2005 dialog-based project named ListCtrlColor.
- From the Class View, select the Project menu option and click Add Class to invoke the Add Class dialog box.
- Select MFC from the list of categories and then MFC Class from the list of templates.
- Click the Add button to invoke the MFC Class Wizard dialog box.
- For the Class name, type in the value CListCtrlWithCustomDraw and select a Base class of CListCtrl.
- Click the Finish button to generate the class's header and implementation files.
- From the Class View, right-click the CListCtrlWithCustomDraw class and select the Properties context menu option.
- When the Properties window displays, click the Messages button at the top to display a two-column list of messages for which you can implement handlers.
- Click the NM_CUSTOMDRAW entry from the messages list, then drop down the combobox arrow of the second column and select the value <Add> OnNMCustomdraw.
- Now for the drawing code. Here, we'll simply handle the item and subitem prepaint stages and specify the text and background colors to use based on the current row (item) and column (subitem). To do that, modify the OnNMCustomdraw function as follows:
void CListCtrlWithCustomDraw::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLVCUSTOMDRAW lpLVCustomDraw = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR); switch(lpLVCustomDraw->nmcd.dwDrawStage) { case CDDS_ITEMPREPAINT: case CDDS_ITEMPREPAINT | CDDS_SUBITEM: if (0 == ((lpLVCustomDraw->nmcd.dwItemSpec + lpLVCustomDraw->iSubItem) % 2)) { lpLVCustomDraw->clrText = RGB(255,255,255); // white text lpLVCustomDraw->clrTextBk = RGB(0,0,0); // black background } else { lpLVCustomDraw->clrText = CLR_DEFAULT; lpLVCustomDraw->clrTextBk = CLR_DEFAULT; } break; default: break; } *pResult = 0; *pResult |= CDRF_NOTIFYPOSTPAINT; *pResult |= CDRF_NOTIFYITEMDRAW; *pResult |= CDRF_NOTIFYSUBITEMDRAW; }
Now let's test the new control. To do that, all you'll need to do is place a list-view control on a dialog and subclass it with your CListCtrlWithCustomDraw class. Here are the steps for accomplishing that.
- From the Resource view, open the application's main dialog box (IDD_LISTCTRLCOLOR_DIALOG).
- From the Toolbox, drag and drop a List Control onto the dialog box.
- Right-click the list control and select the Properties context menu option.
- Set the View property to Report.
- Right-click the control and select the Add Variable context menu option.
- When the Add Member Variable Wizard dialog box appears, specify a Variable name of m_lstBooks and click the Finish button.
- At this point, you have a CListCtrl-derived class (m_lstBooks) that is subclassing the list-view control on the dialog box. However, m_lstBooks needs to be derived from your newly created CListCtrlWithCustomDraw so that your drawing code is called. Therefore, open the dialog's header file (ListCtrlColorDlg.h) and change them_lstBooks to be of type CListCtrlWithCustomDraw.
- Just before the beginning of the CListCtrlColorDlg class, add the following include directive.
#include "ListCtrlWithCustomDraw.h"
- Add the following code to the dialog's OnInitDialog member function so that we have a few list-view rows to see.
// Insert the columns m_lstBooks.InsertColumn(0, _T("Author")); m_lstBooks.InsertColumn(1, _T("Book")); // Define the data static struct { TCHAR m_szAuthor[50]; TCHAR m_szTitle[100]; } BOOK_INFO[] = { _T("Tom Archer"), _T("Visual C++.NET Bible"), _T("Tom Archer"), _T("Extending MFC with the .NET Framework"), _T("Brian Johnson"), _T("XBox 360 For Dummies") }; // Insert the data int idx; for (int i = 0; i < sizeof BOOK_INFO / sizeof BOOK_INFO[0]; i++) { idx = m_lstBooks.InsertItem(i, BOOK_INFO[i].m_szAuthor); m_lstBooks.SetItemText(i, 1, BOOK_INFO[i].m_szTitle); }
- Now, build and run the application. Figure 1 shows an example of how your application should appear.
Figure 1. Custom draw example application
Conclusion
When Windows was first introduced as the "next generation" of operating system for developing applications, one of the main arguments for the new graphical user interface was consistency. The crux of that argument was that applications would have a common look and feel: consistent menu items, common controls, and so on. This sense of commonality probably lasted until the second company wanted to design their application. Put simply, companies don't survive by delivering applications that look like every other application.
One of the ways to create a unique and memorable user interface is by designing and developing custom controls for your application. Hopefully with what you've learned from this article you now know one very powerful technique that should enable you to further distinguish your application from those of your competitors.
Acknowledgements
I would like to thank fellow Microsoft Program Manager Andrew Whitechapel, with whom I have co-authored two books (Inside C#, Second Edition and Visual C++.NET Bible). Over the years I have learned a great deal from Andrew's work, including much of what I write about in this article.