[Previous] [Next]

Advanced Control Programming

One of the benefits of programming controls the MFC way is the ease with which you can modify a control's behavior by deriving classes of your own from the MFC control classes. It's easy, for example, to create an edit control that accepts only numbers or a list box that displays pictures instead of text. You can also build reusable, self-contained control classes that respond to their own notification messages.

The remainder of this chapter is about techniques you can use to shape the controls to make them work the way you want them to work by combining the best features of C++ and MFC.

Numeric Edit Controls

The MFC control classes are useful in their own right because they provide an object-oriented interface to the built-in control types. But their utility is enhanced by the fact that you can use them as base classes for control classes of your own. By adding new message handlers to a derived class or overriding message handlers acquired through inheritance, you can modify certain aspects of the control's behavior while leaving other aspects unchanged.

A perfect example of a derived control class is a numeric edit control. A normal edit control accepts a wide range of characters, including numbers, letters of the alphabet, and punctuation symbols. A numeric edit control accepts only numbers. It's perfect for entering phone numbers, serial numbers, IP addresses, and other numeric data.

Creating a numeric edit control is no big deal in an MFC application because the basic features of an edit control are defined in CEdit. Thanks to C++ inheritance and MFC message mapping, you can derive a control class from CEdit and supply custom message handlers to change the way the control responds to user input. The following CNumEdit class models an edit control that accepts numbers but rejects all other characters:

class CNumEdit : public CEdit
{
protected:
    afx_msg void OnChar (UINT nChar, UINT nRepCnt, UINT nFlags);
    DECLARE_MESSAGE_MAP ()
};

BEGIN_MESSAGE_MAP (CNumEdit, CEdit)
    ON_WM_CHAR ()
END_MESSAGE_MAP ()

void CNumEdit::OnChar (UINT nChar, UINT nRepCnt, UINT nFlags)
{
    if (((nChar >= _T (`0')) && (nChar <= _T (`9'))) ¦¦
        (nChar == VK_BACK))
        CEdit::OnChar (nChar, nRepCnt, nFlags);
}

How does CNumEdit work? When an edit control has the input focus and a character key is pressed, the control receives a WM_CHAR message. By deriving a new class from CEdit, mapping WM_CHAR messages to the derived class's OnChar handler, and designing OnChar so that it passes WM_CHAR messages to the base class if and only if the character encoded in the message is a number, you create an edit control that rejects nonnumeric characters. VK_BACK is included in the list of acceptable character codes so that the Backspace key won't cease to function. It's not necessary to test for other editing keys such as Home and Del because they, unlike the Backspace key, don't generate WM_CHAR messages.

Owner-Draw List Boxes

By default, items in a list box consist of strings of text. Should you need a list box that displays graphical images instead of text, you can create an owner-draw list box—one whose contents are drawn by your application, not by Windows—by following two simple steps.

  1. Derive a new list box class from CListBox, and override CListBox::MeasureItem and CListBox::DrawItem. Also override PreCreateWindow, and make sure that either LBS_OWNERDRAWFIXED or LBS_OWNERDRAWVARIABLE is included in the list box style.

  2. Instantiate the derived class, and use Create or CreateEx to create the list box.

Functionally, owner-draw list boxes are similar to owner-draw menus. When an item in an owner-draw list box needs to be drawn (or redrawn), Windows sends the list box's parent a WM_DRAWITEM message with a pointer to a DRAWITEMSTRUCT structure containing a device context handle, a 0-based index identifying the item to be drawn, and other information. Before the first WM_DRAWITEM message arrives, the list box's parent receives one or more WM_MEASUREITEM messages requesting the height of the list box's items. If the list box style is LBS_OWNERDRAWFIXED, WM_MEASUREITEM is sent just once. For LBS_OWNERDRAWVARIABLE list boxes, a WM_MEASUREITEM message is sent for each item. MFC calls the list box object's virtual DrawItem function when the parent receives a WM_DRAWITEM message and MeasureItem when it receives a WM_MEASUREITEM message. Therefore, you don't have to modify the parent window class or worry about message maps and message handlers; just override DrawItem and MeasureItem in the list box class, and your list box can do its own drawing without any help from its parent.

CListBox supports two other owner-draw overridables in addition to DrawItem and MeasureItem. The first is CompareItem. If an owner-draw list box is created with the style LBS_SORT and items are added to it with AddString, CListBox::CompareItem must be overridden with a version that compares two arbitrary items packaged in COMPAREITEMSTRUCT structures. The overridden function must return -1 if item 1 comes before item 2, 0 if the items are lexically equal, or 1 if item 1 comes after item 2. Owner-draw list boxes are seldom created with the style LBS_SORT because nontextual data typically has no inherent order. (How would you sort a list of colors, for example?) And if you don't use LBS_SORT, you don't have to write a CompareItem function. If you don't implement CompareItem in a derived owner-draw list box class, it's prudent to override PreCreateWindow and make sure the list box style doesn't include LBS_SORT.

The final owner-draw list box overridable is DeleteItem. It's called when an item is deleted with DeleteString, when the list box's contents are erased with ResetContent, and when a list box containing one or more items is destroyed. DeleteItem is called once per item, and it receives a pointer to a DELETEITEMSTRUCT structure containing information about the item. If a list box uses per-item resources (for example, bitmaps) that need to be freed when an item is removed or the list box is destroyed, override DeleteItem and use it to free those resources.

The following COwnerDrawListBox class is a nearly complete C++ implementation of an LBS_OWNERDRAWFIXED-style owner-draw list box:

class COwnerDrawListBox : public CListBox
{
public:
    virtual BOOL PreCreateWindow (CREATESTRUCT&);
    virtual void MeasureItem (LPMEASUREITEMSTRUCT);
    virtual void DrawItem (LPDRAWITEMSTRUCT);
};

BOOL COwnerDrawListBox::PreCreateWindow (CREATESTRUCT& cs)
{
    if (!CListBox::PreCreateWindow (cs))
        return FALSE;

    cs.style &= ~(LBS_OWNERDRAWVARIABLE ¦ LBS_SORT);
    cs.style ¦= LBS_OWNERDRAWFIXED;
    return TRUE;
}

void COwnerDrawListBox::MeasureItem (LPMEASUREITEMSTRUCT lpmis)
{
    lpmis->itemHeight = 32;    // Item height in pixels
}

void COwnerDrawListBox::DrawItem (LPDRAWITEMSTRUCT lpdis)
 {
    CDC dc;
    dc.Attach (lpdis->hDC);
    CRect rect = lpdis->rcItem;
    UINT nIndex = lpdis->itemID;

    CBrush* pBrush = new CBrush (::GetSysColor ((lpdis->itemState &
        ODS_SELECTED) ? COLOR_HIGHLIGHT : COLOR_WINDOW));
    dc.FillRect (rect, pBrush);
    delete pBrush;
    if (lpdis->itemState & ODS_FOCUS)
        dc.DrawFocusRect (rect);
    if (nIndex != (UINT) -1) {
        // Draw the item.
    }
    dc.Detach ();
}

Before you use COwnerDrawListBox in an application of your own, change the 32 in COwnerDrawListBox::MeasureItem to the desired item height in pixels and replace the comment "Draw the item" in COwnerDrawListBox::DrawItem with code that draws the item whose index is nIndex. Use the dc device context object to do the drawing and restrict your output to the rectangle specified by rect, and the list box should function superbly. (Be sure to preserve the state of the device context so that it's the same going out as it was coming in.) COwnerDrawListBox's implementation of DrawItem paints the item's background with the system color COLOR_HIGHLIGHT if the item is selected (if the lpdis->itemState's ODS_SELECTED bit is set) or COLOR_WINDOW if it isn't, and it draws a focus rectangle if the item has the input focus (if the lpdis->itemState's ODS_FOCUS bit is set). All you have to do is draw the item itself. The PreCreateWindow override ensures that LBS_OWNERDRAWFIXED is set and that LBS_OWNERDRAWVARIABLE isn't. It also clears the LBS_SORT bit to prevent calls to CompareItem.

A final feature needed to transform COwnerDrawListBox into a complete class is an AddItem function that can be called to add a nontextual item to the list box. For a list box that displays bitmaps, for example, AddItem might look like this:

int COwnerDrawListBox::AddItem (HBITMAP hBitmap)
{
    int nIndex = AddString (_T (""));
    if ((nIndex != LB_ERR) && (nIndex != LB_ERRSPACE))
        SetItemData (nIndex, (DWORD) hBitmap);
    return nIndex;
}

In this example, AddItem uses SetItemData to associate a bitmap handle with a list box index. For a given item, the list box's DrawItem function can retrieve the bitmap handle with GetItemData and draw the bitmap. Bitmaps are resources that must be deleted when they're no longer needed. You can either leave it to the list box's parent to delete the bitmaps or override CListBox::DeleteItem and let the list box delete them itself. The choice is up to you.

The IconView application shown in Figure 7-8 uses an owner-draw list box class named CIconListBox to displays icons. CIconListBox overrides the PreCreateWindow, MeasureItem, and DrawItem functions it inherits from CListBox and adds two functions of its own. AddIcon adds an icon to the list box, and ProjectImage "projects" an icon onto a display surface, shrinking or expanding the image as needed to fit a specified rectangle. IconView's source code is shown in Figure 7-9.

The only form of input that IconView accepts is drag-and-drop. To try it out, grab an EXE, DLL, or ICO file with the left mouse button, drag it to the IconView window, and release the mouse button. Any icons contained in the file will be displayed in the list box, and an enlarged image of the first icon will be displayed in the Detail window. To get a close-up view of any of the other icons in the file, just click the icon or cursor through the list with the up and down arrow keys.

Figure 7-8. IconView showing the icons contained in Pifmgr.dll.

IconView uses MFC's handy CDC::DrawIcon function to draw icons into the list box. The core code is found in CIconListBox::DrawItem:

if (nIndex != (UINT) -1)
    dc.DrawIcon (rect.left + 4, rect.top + 2,
        (HICON) GetItemData (nIndex));

Icon handles are stored with SetItemData and retrieved with GetItemData. The call to DrawIcon is skipped if nIndex—the index of the currently selected list box item—is -1. That's important, because DrawItem is called with a list box index of -1 when an empty list box receives the input focus. DrawItem's job in that case is to draw a focus rectangle around the nonexistent item 0. You shouldn't assume that DrawItem will always be called with a valid item index.

CMainWindow's OnPaint handler does nothing more than construct a paint device context and call the list box's ProjectImage function to draw a blown-up version of the currently selected icon in the window's client area. ProjectImage uses the CDC functions BitBlt and StretchBlt to project the image. This code probably won't make a lot of sense to you right now, but its meaning will be crystal clear once you've read about bitmaps in Chapter 15.

The drag-and-drop mechanism that IconView uses is a primitive form of drag-and-drop that was introduced in Windows 3.1. Briefly, the call to DragAcceptFiles in CMainWindow::OnCreate registers CMainWindow as a drop target. Once registered, the window receives a WM_DROPFILES message whenever a file is dragged from the shell and dropped on top of it. CMainWindow::OnDropFiles responds to WM_DROPFILES messages by using the ::DragQueryFile API function to retrieve the name of the file that was dropped. It then uses ::ExtractIcon to extract icons from the file and CIconListBox::AddIcon to add the icons to the list box.

In Chapter 19, you'll learn about a richer form of drag-and-drop called OLE drag-and-drop. "Old" drag-and-drop is still supported in 32-bit Windows, but it's not nearly as flexible as OLE drag-and-drop. That's why I haven't gone into more detail about it. Once you see OLE drag-and-drop in action, I think you'll agree that time spent understanding Windows 3.1-style drag-and-drop is time better spent elsewhere.

Figure 7-9. The IconView application.

IconView.h

class CMyApp : public CWinApp
{
public:
    virtual BOOL InitInstance ();
};

class CIconListBox : public CListBox
{
public:
    virtual BOOL PreCreateWindow (CREATESTRUCT& cs);
    virtual void MeasureItem (LPMEASUREITEMSTRUCT lpmis);
    virtual void DrawItem (LPDRAWITEMSTRUCT lpdis);
    int AddIcon (HICON hIcon);
    void ProjectImage (CDC* pDC, LPRECT pRect, COLORREF clrBackColor);
};

class CMainWindow : public CWnd
{
protected:
    int m_cxChar;
    int m_cyChar;

    CFont m_font;
    CRect m_rcImage;

    CButton m_wndGroupBox;
    CIconListBox m_wndIconListBox;
    CStatic m_wndLabel;

public:
    CMainWindow ();

protected:
    virtual void PostNcDestroy ();

    afx_msg int OnCreate (LPCREATESTRUCT lpcs);
    afx_msg void OnPaint ();
    afx_msg void OnSetFocus (CWnd* pWnd);
    afx_msg void OnDropFiles (HDROP hDropInfo);
    afx_msg void OnSelChange ();

    DECLARE_MESSAGE_MAP ()
};

IconView.cpp

#include <afxwin.h>
#include "IconView.h"

#define IDC_LISTBOX 100

CMyApp myApp;

/////////////////////////////////////////////////////////////////////////
// CMyApp member functions

BOOL CMyApp::InitInstance ()
{
    m_pMainWnd = new CMainWindow;
    m_pMainWnd->ShowWindow (m_nCmdShow);
    m_pMainWnd->UpdateWindow ();
    return TRUE;
}

/////////////////////////////////////////////////////////////////////////
// CMainWindow message map and member functions

BEGIN_MESSAGE_MAP (CMainWindow, CWnd)
    ON_WM_CREATE ()
    ON_WM_PAINT ()
    ON_WM_SETFOCUS ()
    ON_WM_DROPFILES ()
    ON_LBN_SELCHANGE (IDC_LISTBOX, OnSelChange)
END_MESSAGE_MAP ()

CMainWindow::CMainWindow ()
{
    CString strWndClass = AfxRegisterWndClass (
        0,
        myApp.LoadStandardCursor (IDC_ARROW),
        (HBRUSH) (COLOR_3DFACE + 1),
        myApp.LoadStandardIcon (IDI_WINLOGO)
    );
	CreateEx (0, strWndClass, _T ("IconView"),
        WS_OVERLAPPED ¦ WS_SYSMENU ¦ WS_CAPTION ¦ WS_MINIMIZEBOX,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL, NULL);

    CRect rect (0, 0, m_cxChar * 84, m_cyChar * 21);
    CalcWindowRect (&rect);

    SetWindowPos (NULL, 0, 0, rect.Width (), rect.Height (),
        SWP_NOZORDER ¦ SWP_NOMOVE ¦ SWP_NOREDRAW);
}

int CMainWindow::OnCreate (LPCREATESTRUCT lpcs)
{
    if (CWnd::OnCreate (lpcs) == -1)
        return -1;

    m_font.CreatePointFont (80, _T ("MS Sans Serif"));

    CClientDC dc (this);
    CFont* pOldFont = dc.SelectObject (&m_font);
    TEXTMETRIC tm;
    dc.GetTextMetrics (&tm);
    m_cxChar = tm.tmAveCharWidth;
    m_cyChar = tm.tmHeight + tm.tmExternalLeading;
    dc.SelectObject (pOldFont);

    m_rcImage.SetRect (m_cxChar * 4, m_cyChar * 3, m_cxChar * 46,
        m_cyChar * 19);

    m_wndGroupBox.Create (_T ("Detail"),  WS_CHILD ¦ WS_VISIBLE ¦ BS_GROUPBOX,
        CRect (m_cxChar * 2, m_cyChar, m_cxChar * 48, m_cyChar * 20),
        this, (UINT) -1);

    m_wndLabel.Create (_T ("Icons"), WS_CHILD ¦ WS_VISIBLE ¦ SS_LEFT,
        CRect (m_cxChar * 50, m_cyChar, m_cxChar * 82, m_cyChar * 2),
        this);

    m_wndIconListBox.Create (WS_CHILD ¦ WS_VISIBLE ¦ WS_VSCROLL ¦
        WS_BORDER ¦ LBS_NOTIFY ¦ LBS_NOINTEGRALHEIGHT,
        CRect (m_cxChar * 50, m_cyChar * 2, m_cxChar * 82, m_cyChar * 20),
        this, IDC_LISTBOX);

    m_wndGroupBox.SetFont (&m_font);
    m_wndLabel.SetFont (&m_font);
    DragAcceptFiles ();
    return 0;
}

void CMainWindow::PostNcDestroy ()
{
    delete this;
}

void CMainWindow::OnPaint ()
{
    CPaintDC dc (this);
    m_wndIconListBox.ProjectImage (&dc, m_rcImage,
        ::GetSysColor (COLOR_3DFACE));
}

void CMainWindow::OnSetFocus (CWnd* pWnd)
{
    m_wndIconListBox.SetFocus ();
}

void CMainWindow::OnDropFiles (HDROP hDropInfo)
{
    //
    // Find out how many files were dropped.
    //
    int nCount = ::DragQueryFile (hDropInfo, (UINT) -1, NULL, 0);

    if (nCount == 1) { // One file at a time, please
        m_wndIconListBox.ResetContent ();
        //
        // Extract the file's icons and add them to the list box.
        //
        char szFile[MAX_PATH];
        ::DragQueryFile (hDropInfo, 0, szFile, sizeof (szFile));
        int nIcons = (int) ::ExtractIcon (NULL, szFile, (UINT) -1);

        if (nIcons) {
            HICON hIcon;
            for (int i=0; i<nIcons; i++) {
                hIcon = ::ExtractIcon (AfxGetInstanceHandle (),
                    szFile, i);
                m_wndIconListBox.AddIcon (hIcon);
            }
        }

        //
        // Put the file name in the main window's title bar.
        //
        CString strWndTitle = szFile;
        strWndTitle += _T (" - IconView");
        SetWindowText (strWndTitle);
		//
        // Select item number 0.
        //
        CClientDC dc (this);
        m_wndIconListBox.SetCurSel (0);
        m_wndIconListBox.ProjectImage (&dc, m_rcImage,
            ::GetSysColor (COLOR_3DFACE));
    }
    ::DragFinish (hDropInfo);
}

void CMainWindow::OnSelChange ()
{
    CClientDC dc (this);
    m_wndIconListBox.ProjectImage (&dc, m_rcImage,
        ::GetSysColor (COLOR_3DFACE));
}

/////////////////////////////////////////////////////////////////////////
// CIconListBox member functions

BOOL CIconListBox::PreCreateWindow (CREATESTRUCT& cs)
{
    if (!CListBox::PreCreateWindow (cs))
        return FALSE;

    cs.dwExStyle ¦= WS_EX_CLIENTEDGE;
    cs.style &= ~(LBS_OWNERDRAWVARIABLE ¦ LBS_SORT);
    cs.style ¦= LBS_OWNERDRAWFIXED;
    return TRUE;
}

void CIconListBox::MeasureItem (LPMEASUREITEMSTRUCT lpmis)
{
    lpmis->itemHeight = 36;
}

void CIconListBox::DrawItem (LPDRAWITEMSTRUCT lpdis)
{
    CDC dc;
    dc.Attach (lpdis->hDC);
    CRect rect = lpdis->rcItem;
    int nIndex = lpdis->itemID;

    CBrush* pBrush = new CBrush;
    pBrush->CreateSolidBrush (::GetSysColor ((lpdis->itemState &
        ODS_SELECTED) ? COLOR_HIGHLIGHT : COLOR_WINDOW));
    dc.FillRect (rect, pBrush);
    delete pBrush;

    if (lpdis->itemState & ODS_FOCUS)
        dc.DrawFocusRect (rect);

    if (nIndex != (UINT) -1)
        dc.DrawIcon (rect.left + 4, rect.top + 2,
            (HICON) GetItemData (nIndex));

    dc.Detach ();
}

int CIconListBox::AddIcon (HICON hIcon)
{
    int nIndex = AddString (_T (""));
    if ((nIndex != LB_ERR) && (nIndex != LB_ERRSPACE))
        SetItemData (nIndex, (DWORD) hIcon);
    return nIndex;
}

void CIconListBox::ProjectImage (CDC* pDC, LPRECT pRect,
    COLORREF clrBackColor)
{
    CDC dcMem;
    dcMem.CreateCompatibleDC (pDC);

    CBitmap bitmap;
    bitmap.CreateCompatibleBitmap (pDC, 32, 32);
    CBitmap* pOldBitmap = dcMem.SelectObject (&bitmap);

    CBrush* pBrush = new CBrush (clrBackColor);
    dcMem.FillRect (CRect (0, 0, 32, 32), pBrush);
    delete pBrush;

    int nIndex = GetCurSel ();
    if (nIndex != LB_ERR)
        dcMem.DrawIcon (0, 0, (HICON) GetItemData (nIndex));

    pDC->StretchBlt (pRect->left, pRect->top, pRect->right - pRect->left,
        pRect->bottom - pRect->top, &dcMem, 0, 0, 32, 32, SRCCOPY);

    dcMem.SelectObject (pOldBitmap);
}

Graphical Push Buttons

MFC includes three derived control classes of its own: CCheckListBox, CDragListBox, and CBitmapButton. CCheckListBox turns a normal list box into a "check" list box—a list box with a check box by each item and added functions such as GetCheck and SetCheck for getting and setting check box states. CDragListBox creates a list box that supports its own primitive form of drag-and-drop. CBitmapButton encapsulates owner-draw push buttons that display pictures instead of text. It supplies its own DrawItem handler that draws a push button in response to WM_DRAWITEM messages. All you have to do is create the button and supply four bitmaps representing the button in various states.

CBitmapButton was a boon back in the days of 16-bit Windows because it simplified the task of creating graphical push buttons. Today, however, owner-draw push buttons are rarely used. Two button styles that were first introduced in Windows 95—BS_BITMAP and BS_ICON—make graphical push buttons a breeze by taking a single image and creating a push button from it. A BS_BITMAP-style push button (henceforth, a bitmap push button) displays a bitmap on the face of a push button. A BS_ICON-style push button (an icon push button) displays an icon. Most developers prefer icon push buttons because icons, unlike bitmaps, can have transparent pixels. Transparent pixels are great for displaying nonrectangular images on button faces because they decouple the image's background color from the button color.

Creating an icon push button is a two-step process:

  1. Create a push button whose style includes a BS_ICON flag.

  2. Call the button's SetIcon function, and pass in an icon handle.

The following example creates an icon push button from an icon whose resource ID is IDI_OK:

m_wndIconButton.Create (_T (""), WS_CHILD ¦ WS_VISIBLE ¦ BS_ICON,
    rect, this, IDC_BUTTON);
m_wndIconButton.SetIcon (AfxGetApp ()->LoadIcon (IDI_OK));

The icon is drawn in the center of the button unless you alter its alignment by applying one or more of the following button styles:

Button Style Description
BS_LEFT Aligns the icon image with the left edge of the button face
BS_RIGHT Aligns the icon image with the right edge of the button face
BS_TOP Aligns the icon image with the top edge of the button face
BS_BOTTOM Aligns the icon image with the bottom edge of the button face
BS_CENTER Centers the icon image horizontally
BS_VCENTER Centers the icon image vertically

Chapter 8's Phone application uses icon push buttons to represent the OK and Cancel buttons in a dialog box.

The procedure for creating a bitmap button is almost the same as the one for creating an icon button. Just change BS_ICON to BS_BITMAP and SetIcon to SetBitmap and you're set. Of course, you'll have to replace the call to LoadIcon with code that loads a bitmap, too. You'll learn how that's done in Chapter 15.

One problem to watch out for when you're using icon push buttons is what happens when the button becomes disabled. Windows generates a disabled button image from the button's icon, but the results aren't always what you'd expect. In general, the simpler the image, the better. Unfilled figures render better when disabled than filled figures.

Customizing a Control's Colors

The most glaring deficiency in the Windows control architecture is that there's no obvious way to change a control's colors. You can change a control's font with SetFont, but there is no equivalent function for changing a control's colors.

MFC supports two mechanisms for changing a control's colors. Both rely on the fact that before a control paints itself, it sends its parent a message containing the handle of the device context used to do the painting. The parent can call CDC::SetTextColor and CDC::SetBkColor on that device context to alter the attributes of any text drawn by the control. It can also alter the control's background color by returning a brush handle (HBRUSH).

The message that a control sends to its parent prior to painting varies with the control type. For example, a list box sends a WM_CTLCOLORLISTBOX message; a static control sends a WM_CTLCOLORSTATIC message. In any event, the message's wParam holds the device context handle, and lParam holds the control's window handle. If a window processes a static control's WM_CTLCOLORSTATIC messages by setting the device context's text color to red and background color to white and returning a brush handle for a blue brush, the control text will be red, the gaps in and between characters will be white, and the control background—everything inside the control's borders not covered by text—will be blue.

MFC's ON_WM_CTLCOLOR message-map macro directs WM_CTLCOLOR messages of all types to a handler named OnCtlColor. OnCtlColor is prototyped as follows:

afx_msg HBRUSH OnCtlColor (CDC* pDC, CWnd* pWnd, UINT nCtlColor)

pDC is a pointer to the control's device context, pWnd is a CWnd pointer that identifies the control itself, and nCtlColor identifies the type of WM_CTLCOLOR message that prompted the call. Here are the possible values for nCtlColor.

nCtlColor Control Type or Window Type
CTLCOLOR_BTN Push button. Processing this message has no effect on a button's appearance.
CTLCOLOR_DLG Dialog box.
CTLCOLOR_EDIT Edit control and the edit control part of a combo box.
CTLCOLOR_LISTBOX List box and the list box part of a combo box.
CTLCOLOR_MSGBOX Message box.
CTLCOLOR_SCROLLBAR Scroll bar.
CTLCOLOR_STATIC Static control, check box, radio button, group box, read-only or disabled edit control, and the edit control in a disabled combo box.

Five nCtlColor values pertain to controls, and two—CTLCOLOR_DLG and CTLCOLOR_MSGBOX—apply to dialog boxes and message boxes. (That's right: You can control the color of dialog boxes and message boxes by processing WM_CTLCOLOR messages.) Static controls aren't the only controls that send WM_CTLCOLORSTATIC messages. You'd think that a radio button would send a WM_CTLCOLORBTN message, but in fact it sends a WM_CTLCOLORSTATIC message in 32-bit Windows.

One way, then, to change a control's colors is to implement OnCtlColor in the parent window class. The following OnCtlColor implementation changes the color of a static text control named m_wndText to white-on-red in a frame window:

HBRUSH CMainWindow::OnCtlColor (CDC* pDC, CWnd* pWnd,
    UINT nCtlColor)
{
    if (m_wndText.m_hWnd == pWnd->m_hWnd) {
        pDC->SetTextColor (RGB (255, 255, 255));
        pDC->SetBkColor (RGB (255, 0, 0));
        return (HBRUSH) m_brRedBrush;
    }
    CFrameWnd::OnCtlColor (pDC, pWnd, nCtlColor);
}

m_brRedBrush is a CMainWindow data member whose type is CBrush. It is initialized as follows:

m_brRedBrush.CreateSolidBrush (RGB (255, 0, 0));

Note that this implementation of OnCtlColor compares the window handle of the control whose color it wishes to change with the window handle of the control that generated the message. If the two are not equal, the message is forwarded to the base class. If this check were not performed, OnCtlColor would affect all the controls in CMainWindow, not just m_wndText.

That's one way to change a control's color. The problem with this technique is that it's up to the parent to do the changing. What happens if you want to derive a control class of your own and include in it a SetColor function for modifying the control's color?

A derived control class can set its own colors by using MFC's ON_WM_CTLCOLOR_REFLECT macro to pass WM_CTLCOLOR messages that aren't handled by the control's parent back to the control. Here's the code for a CStatic-like control that paints itself white-on-red:

class CColorStatic : public CStatic
{
public:
    CColorStatic ();

protected:
    CBrush m_brRedBrush;
    afx_msg HBRUSH CtlColor (CDC* pDC, UINT nCtlColor);
    DECLARE_MESSAGE_MAP ()
};

BEGIN_MESSAGE_MAP (CColorStatic, CStatic)
    ON_WM_CTLCOLOR_REFLECT ()
END_MESSAGE_MAP ()

CColorStatic::CColorStatic ()
{
    m_brRedBrush.CreateSolidBrush (RGB (255, 0, 0));
}

HBRUSH CColorStatic::CtlColor (CDC* pDC, UINT nCtlColor)
{
    pDC->SetTextColor (RGB (255, 255, 255));
    pDC->SetBkColor (RGB (255, 0, 0));
    return (HBRUSH) m_brRedBrush;
}

CtlColor is similar to OnCtlColor, but it doesn't receive the pWnd parameter that OnCtlColor does. It doesn't need to because the control to which the message applies is implicit in the call.

The ColorText application shown in Figure 7-10 uses a static text control whose colors are configurable. CColorStatic implements the control. This version of CColorStatic is more versatile than the one in the previous paragraph because rather than use hardcoded colors, it includes member functions named SetTextColor and SetBkColor that can be used to change its colors. When ColorText's Red, Green, or Blue radio button is clicked, the control's text color changes. The button click activates a handler that calls the control's SetTextColor function. (See Figure 7-11.) ColorText doesn't use the control's SetBkColor function, but I included the function anyway for completeness. SetBkColor controls the fill color drawn behind the text. CColorStatic'sdefault colors are black (foreground) and the system color COLOR_3DFACE (background), but a simple function call is sufficient to change either one.

Figure 7-10. The ColorText window.

Figure 7-11. The ColorText application.

ColorText.h

#define IDC_RED         100
#define IDC_GREEN       101
#define IDC_BLUE        102

class CColorStatic : public CStatic
{
protected:
    COLORREF m_clrText;
    COLORREF m_clrBack;
    CBrush m_brBkgnd;

public:
    CColorStatic ();
    void SetTextColor (COLORREF clrText);
    void SetBkColor (COLORREF clrBack);

protected:
    afx_msg HBRUSH CtlColor (CDC* pDC, UINT nCtlColor);
    DECLARE_MESSAGE_MAP ()
};

class CMyApp : public CWinApp
{
public:
    virtual BOOL InitInstance ();
};

class CMainWindow : public CFrameWnd
{
protected:
    int m_cxChar;
    int m_cyChar;
    CFont m_font;

    CColorStatic m_wndText;
    CButton m_wndRadioButtonRed;
    CButton m_wndRadioButtonGreen;
    CButton m_wndRadioButtonBlue;
    CButton m_wndGroupBox1;
    CButton m_wndGroupBox2;

public:
    CMainWindow ();

protected:
    afx_msg int OnCreate (LPCREATESTRUCT lpcs);
    afx_msg void OnRedButtonClicked ();
    afx_msg void OnGreenButtonClicked ();
    afx_msg void OnBlueButtonClicked ();

    DECLARE_MESSAGE_MAP ()
};

ColorText.cpp

#include <afxwin.h>
#include "ColorText.h"

CMyApp myApp;

/////////////////////////////////////////////////////////////////////////
// CMyApp member functions

BOOL CMyApp::InitInstance ()
{
    m_pMainWnd = new CMainWindow;
    m_pMainWnd->ShowWindow (m_nCmdShow);
    m_pMainWnd->UpdateWindow ();
    return TRUE;
}

/////////////////////////////////////////////////////////////////////////
// CMainWindow message map and member functions

BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd)
    ON_WM_CREATE ()
    ON_BN_CLICKED (IDC_RED, OnRedButtonClicked)
    ON_BN_CLICKED (IDC_GREEN, OnGreenButtonClicked)
    ON_BN_CLICKED (IDC_BLUE, OnBlueButtonClicked)
END_MESSAGE_MAP ()

CMainWindow::CMainWindow ()
{
    CString strWndClass = AfxRegisterWndClass (
        0,
        myApp.LoadStandardCursor (IDC_ARROW),
        (HBRUSH) (COLOR_3DFACE + 1),
        myApp.LoadStandardIcon (IDI_WINLOGO)
    );

    Create (strWndClass, _T ("ColorText"));
}

int CMainWindow::OnCreate (LPCREATESTRUCT lpcs)
{
    if (CFrameWnd::OnCreate (lpcs) == -1)
        return -1;

    m_font.CreatePointFont (80, _T ("MS Sans Serif"));

    CClientDC dc (this);
    CFont* pOldFont = dc.SelectObject (&m_font);
    TEXTMETRIC tm;
    dc.GetTextMetrics (&tm);
    m_cxChar = tm.tmAveCharWidth;
    m_cyChar = tm.tmHeight + tm.tmExternalLeading;
    dc.SelectObject (pOldFont);

    m_wndGroupBox1.Create (_T ("Sample text"), WS_CHILD ¦ WS_VISIBLE ¦
        BS_GROUPBOX, CRect (m_cxChar * 2, m_cyChar, m_cxChar * 62,
        m_cyChar * 8), this, UINT (-1));

    m_wndText.Create (_T ("Click a button to change my color"),
        WS_CHILD ¦ WS_VISIBLE ¦ SS_CENTER, CRect (m_cxChar * 4,
        m_cyChar * 4, m_cxChar * 60, m_cyChar * 6), this);

    m_wndGroupBox2.Create (_T ("Color"), WS_CHILD ¦ WS_VISIBLE ¦
        BS_GROUPBOX, CRect (m_cxChar * 64, m_cyChar, m_cxChar * 80,
        m_cyChar * 8), this, UINT (-1));

    m_wndRadioButtonRed.Create (_T ("Red"), WS_CHILD ¦ WS_VISIBLE ¦
        WS_GROUP ¦ BS_AUTORADIOBUTTON, CRect (m_cxChar * 66, m_cyChar * 3,
        m_cxChar * 78, m_cyChar * 4), this, IDC_RED);

    m_wndRadioButtonGreen.Create (_T ("Green"), WS_CHILD ¦ WS_VISIBLE ¦
        BS_AUTORADIOBUTTON, CRect (m_cxChar * 66, (m_cyChar * 9) / 2,
        m_cxChar * 78, (m_cyChar * 11) / 2), this, IDC_GREEN);

    m_wndRadioButtonBlue.Create (_T ("Blue"), WS_CHILD ¦ WS_VISIBLE ¦
        BS_AUTORADIOBUTTON, CRect (m_cxChar * 66, m_cyChar * 6,
        m_cxChar * 78, m_cyChar * 7), this, IDC_BLUE);

    m_wndRadioButtonRed.SetCheck (1);
    m_wndText.SetTextColor (RGB (255, 0, 0));

    m_wndGroupBox1.SetFont (&m_font, FALSE);
    m_wndGroupBox2.SetFont (&m_font, FALSE);
    m_wndRadioButtonRed.SetFont (&m_font, FALSE);
    m_wndRadioButtonGreen.SetFont (&m_font, FALSE);
    m_wndRadioButtonBlue.SetFont (&m_font, FALSE);
    return 0;
}

void CMainWindow::OnRedButtonClicked ()
{
    m_wndText.SetTextColor (RGB (255, 0, 0));
}

void CMainWindow::OnGreenButtonClicked ()
{
    m_wndText.SetTextColor (RGB (0, 255, 0));
}

void CMainWindow::OnBlueButtonClicked ()
{
    m_wndText.SetTextColor (RGB (0, 0, 255));
}

/////////////////////////////////////////////////////////////////////////
// CColorStatic message map and member functions

BEGIN_MESSAGE_MAP (CColorStatic, CStatic)
    ON_WM_CTLCOLOR_REFLECT ()
END_MESSAGE_MAP ()

CColorStatic::CColorStatic ()
{
    m_clrText = RGB (0, 0, 0);
    m_clrBack = ::GetSysColor (COLOR_3DFACE);
    m_brBkgnd.CreateSolidBrush (m_clrBack);
}

void CColorStatic::SetTextColor (COLORREF clrText)
{
    m_clrText = clrText;
    Invalidate ();
}

void CColorStatic::SetBkColor (COLORREF clrBack)
{
    m_clrBack = clrBack;
    m_brBkgnd.DeleteObject ();
    m_brBkgnd.CreateSolidBrush (clrBack);
    Invalidate ();
}

HBRUSH CColorStatic::CtlColor (CDC* pDC, UINT nCtlColor)
{
    pDC->SetTextColor (m_clrText);
    pDC->SetBkColor (m_clrBack);
    return (HBRUSH) m_brBkgnd;
}

Different controls respond to actions performed by OnCtlColor and CtlColor handlers in different ways. You've seen how static controls respond to CDC::SetTextColor and CDC::SetBkColor . For a scroll bar control, SetTextColor and SetBkColor do nothing, but the brush handle returned by a WM_CTLCOLORSCROLLBAR message handler sets the color of the scroll bar's shaft. For a list box, SetTextColor and SetBkColor affect unhighlighted list box items but have no effect on highlighted items, and the brush handle controls the color of the list box's background—anything on an empty or unhighlighted line that isn't painted over with text. For a push button, OnCtlColor and CtlColor have no effect whatsoever because Windows uses system colors to draw push button controls. If nCtlType contains the code CTLCOLOR_BTN, you might as well pass it on to the base class because nothing you do to the device context will affect how the control is drawn.

Message Reflection

ON_WM_CTLCOLOR_REFLECT is one of several message-map macros introduced in MFC 4.0 that permit notification messages to be reflected back to the controls that sent them. Message reflection is a powerful tool for building reusable control classes because it empowers derived control classes to implement their own behavior independent of their parents. Previous versions of MFC reflected certain messages back to the controls that sent them using a virtual CWnd function named OnChildNotify. Modern versions of MFC make the concept of message reflection generic so that a derived control class can map any message sent to its parent to a class member function. You saw an example of message reflection at work in the previous section when we derived a new class from CStatic and allowed it to handle its own WM_CTLCOLOR messages.

The following table contains a list of the message reflection macros MFC provides and short descriptions of what they do.

MFC Message Reflection Macros

Macro Description
ON_CONTROL_REFLECT Reflects notifications relayed through WM_COMMAND messages
ON_NOTIFY_REFLECT Reflects notifications relayed through WM_NOTIFY messages
ON_UPDATE_COMMAND_UI_REFLECT Reflects update notifications to toolbars, status bars, and other user interface objects
ON_WM_CTLCOLOR_REFLECT Reflects WM_CTLCOLOR messages
ON_WM_DRAWITEM_REFLECT Reflects WM_DRAWITEM messages sent by owner-draw controls
ON_WM_MEASUREITEM_REFLECT Reflects WM_MEASUREITEM messages sent by owner-draw controls
ON_WM_COMPAREITEM_REFLECT Reflects WM_COMPAREITEM messages sent by owner-draw controls
ON_WM_DELETEITEM_REFLECT Reflects WM_DELETEITEM messages sent by owner-draw controls
ON_WM_CHARTOITEM_REFLECT Reflects WM_CHARTOITEM messages sent by list boxes
ON_WM_VKEYTOITEM_REFLECT Reflects WM_VKEYTOITEM messages sent by list boxes
ON_WM_HSCROLL_REFLECT Reflects WM_HSCROLL messages sent by scroll bars
ON_WM_VSCROLL_REFLECT Reflects WM_VSCROLL messages sent by scroll bars
ON_WM_PARENTNOTIFY_REFLECT Reflects WM_PARENTNOTIFY messages

Suppose you want to write a list box class that responds to its own LBN_DBLCLK notifications by displaying a message box containing the text of the item that was double-clicked. In an SDK-style application, the list box's parent would have to process the notification message and pop up the message box. In an MFC application, the list box can handle the notification and display the message box itself. Here's a derived list box class that does just that:

class CMyListBox : public CListBox
{
protected:
    afx_msg void OnDoubleClick ();
    DECLARE_MESSAGE_MAP ()
};
BEGIN_MESSAGE_MAP (CMyListBox, CListBox)
    ON_CONTROL_REFLECT (LBN_DBLCLK, OnDoubleClick)
END_MESSAGE_MAP ()

void CMyListBox::OnDoubleClick ()
{
    CString string;
    int nIndex = GetCurSel ();
    GetText (nIndex, string);
    MessageBox (string);
}

The ON_CONTROL_REFLECT entry in the derived class's message map tells MFC to call CMyListBox::OnDoubleClick anytime the list box sends an LBN_DBLCLK notification to its parent. It's worth noting that the notification is reflected only if the parent doesn't process the notification itself—that is, if the parent's message map doesn't include an ON_LBN_DBLCLK entry for this list box. The parent receives precedence, which is consistent with the fact that Windows expects the parent to process any notifications in which it is interested.

The CHM file was converted to HTML by chm2web software.