MFC TN014--Custom Controls and Other Topics

Created: March 25, 1992

Abstract

This technical note describes the custom control support in MFC. It also discusses self-drawn controls and the CBitmapButton class.

The Microsoft Foundation Class (MFC) library provides a full-featured set of C++ object classes for the MicrosoftÒ WindowsÔ graphical environment. It includes classes that directly support application development for Windows as well as general-purpose classes for collections, files, persistent storage, exceptions, diagnostics, memory management, strings, and time. Each MFC technical note describes a feature of MFC using code fragments and examples.

Introduction

This technical note describes the custom control support in MFC, how self-drawn controls are supported, and the interface and use of the CBitmapButton class.

Dynamic subclassing is also described, as well as general advice on ownership of CWnd objects versus HWNDs.

The MFC sample application CTRLTEST illustrates many of these features. Please refer to the source code for CTRLTEST in the Sample Code, MFC Samples area on this disc.

Custom Control Interface

Owner-Drawn Controls/Menus

The MicrosoftÒ WindowsÔ graphical environment provides support for "owner-drawn" for controls and menus. These are Windows-based messages sent to a parent window of a control or menu that allow you to customize the visual appearance and behavior of the control or menu.

MFC directly supports owner-drawn with the message map entries:

CWnd::OnDrawItem

CWnd::OnMeasureItem

CWnd::OnCompareItem

CWnd::OnDeleteItem

You can override these in your CWnd derived class (usually a dialog or main frame window) to implement the owner-drawn behavior, just as it is done in the C API.

This approach does not lead to reusable code. If you have two similar controls in two different dialogs, you must implement the custom control behavior in two places. The MFC-supported self-drawn control architecture solves this problem.

Self-Drawn Controls/Menus

MFC provides a default implementation (in CWnd) for the standard owner-drawn messages. This default implementation will decode the owner-drawn parameters and delegate the owner-drawn messages to the controls or menu. This is called "self-drawn" because the drawing (/measuring/comparing) code is in the class of the control or menu, not in the owner window.

This allows you to build reusable control classes that display using owner-drawn semantics, but the code for drawing the control is in the control class and not the owner. This is an object-oriented approach to custom control programming.

For self-drawn buttons:

CButton:DrawItem(LPDRAWITEMSTRUCT);

// draw this button

For self-drawn menus:

CMenu:MeasureItem(LPMEASUREITEMSTRUCT);

// measure the size of an item in this menu

CMenu:DrawItem(LPDRAWITEMSTRUCT);

// draw an item in this menu

For self-drawn list boxes:

CListBox:MeasureItem(LPMEASUREITEMSTRUCT);

// measure the size of an item in this list box

CListBox:DrawItem(LPDRAWITEMSTRUCT);

// draw an item in this list box

CListBox:CompareItem(LPCOMPAREITEMSTRUCT);

// compare two items in this list box if LBS_SORT

CListBox:DeleteItem(LPDELETEITEMSTRUCT);

// delete an item from this list box

For self-drawn combo boxes:

CComboBox:MeasureItem(LPMEASUREITEMSTRUCT);

// measure the size of an item in this combo box

CComboBox:DrawItem(LPDRAWITEMSTRUCT);

// draw an item in this combo box

CComboBox:CompareItem(LPCOMPAREITEMSTRUCT);

// compare two items in this combo box if CBS_SORT

CComboBox:DeleteItem(LPDELETEITEMSTRUCT);

// delete an item from this combo box

For details on the owner-drawn structures DRAWITEMSTRUCT, MEASUREITEMSTRUCT, COMPAREITEMSTRUCT, and DELETEITEMSTRUCT, please refer to the MFC documentation for CWnd::OnDrawItem, OnMeasureItem, OnCompareItem, and OnDeleteItem, respectively.

You do not have to examine the CtlType or CtlID fields of these structures (MFC does that for you).

For self-drawn menus, you must override both MeasureItem and DrawItem member functions.

For self-drawn list boxes and combo boxes, you must override MeasureItem and DrawItem. You must specify the OWNERDRAWVARIABLE style in the dialog template (LBS_OWNERDRAWVARIABLE and CBS_OWNERDRAWVARIABLE, respectively). The OWNERDRAWFIXED style will not work with self-drawn items because the fixed item height is determined before self-drawn controls are attached to the list box (the Windows version 3.1 member functions CListBox::SetItemHeight and CComboBox::SetItemHeight can be used to get around this limitation).

For self-drawn list boxes and combo boxes with the SORT style (LBS_SORT and CBS_SORT, respectively), you must override the CompareItem member function.

Examples of Self-Drawn Controls/Menus:

The CTRLTEST sample application provides samples of a self-drawn menu (showing colors) and a self-drawn list box (also showing colors).

The most typical example of a self-drawn button is a bitmap button (a button that shows one, two, or three bitmap images for the different states). This is so common we have provided a class in MFC that directly provides this functionality (see "CBitmapButton" below).

CBitmapButton

The CBitmapButton class is derived from CButton and provides a self-drawn implementation of a pushbutton. This button uses bitmaps instead of text for the face of the button.

Creating a bitmap button

Here are the steps to create a bitmap button and use it in a dialog. This just shows one way of using this class. There are more options available using the CBitmapButton class.

1.Create the bitmaps.

Create one, two, or three bitmaps using IMAGEDIT.EXE. Normally you should create all three. The first bitmap is for the "up" button state, the second bitmap is for the "down" button state, and the third bitmap is for the "focused" button state, which is used when the input focus is on the button (usually the same as "up" but with a heavier border). All bitmaps should be the same size, but there is no restriction on that size.

For each button, pick an image name for that button (as many as seven characters, for example, MYIMAGE). Save the bitmaps to three separate files with filenames ending in U, D, and F, all with the .BMP extension (for example, MYIMAGEU.BMP, MYIMAGED.BMP, and MYIMAGEF.BMP).

2.Place the bitmap button in a dialog.

Using DLGEDIT to edit your dialog, add an owner-drawn button wherever you want to have a bitmap button (add a pushbutton and set the Owner-Draw style). Set the text to the image name (for example, MYIMAGE) and define a symbol for that button (for example, IDC_MYIMAGE). The size of the button you draw on the dialog does not matter, because the default CBitmapButton autoload routine will resize it to the exact size of the bitmap.

3.Add the bitmaps to your .RC file:

Each of the bitmaps should be included in the RC file with the same name as the file, for example:

MYIMAGEU: bitmap MYIMAGEU.BMP

MYIMAGED: bitmap MYIMAGED.BMP

MYIMAGEF: bitmap MYIMAGEF.BMP

Note the choice of U, D, and F is not arbitrary; the AutoLoad function relies on these particular names.

4.Alias the dialog control with the C++ CBitmapButton object.

Create a C++ dialog class (usually derived from CModalDialog). For each bitmap button in the dialog, have a CBitmapButton member object (the name of the member is not important). In the OnInitDialog routine for your dialog, call AutoLoad for each bitmap button (passing the control ID for the button and the dialog pointer). The AutoLoad member function will load in the bitmaps (based on the image name we set up earlier). AutoLoad will also attach the dialog control to the C++ CBitmapButton object using SubclassDlgItem (SubclassDlgItem is a very general control/window aliasing mechanism that is described in detail below).

class CMyDlg :

{

CBitmapButton m_mybtn;

...

};

BOOL CMyDlg::OnInitDialog()

{

VERIFY(m_mybtn.AutoLoad(IDC_MYIMAGE, this));

// verify we have loaded the image into the

// button with control ID of IDC_MYIMAGE

...

}

Examples of CBitmapButtons

See CTRLTEST and SPEAKN for examples of bitmap buttons.

Member functions of CBitmapButton

CBitmapButton Two constructors are provided, one with no parameters that does nothing, and another with three parameters that loads the bitmaps for you.
LoadBitmaps Loads the bitmaps from the named resource. The first bitmap is required, the other two are optional.
SizeToContent Resizes the button to the size of the first bitmap.
AutoLoad Does everything:

Loads in the bitmaps based on the text of the button + suffixes U, D, and F

Resizes the button to content

Dynamically subclasses the button object


Dynamic Subclassing

Subclassing is the Windows-based term for replacing the WndProc of a window with a different WndProc, and calling the old WndProc for default (super class) functionality.

This should not be confused with C++ class derivation (C++ terminology uses the words "base" and "derived," while the object model for Windows uses "super" and "sub"). C++ derivation with MFC and Windows-based subclassing are very similar in functionality—except that C++ does not support a feature similar to dynamic subclassing.

The CWnd class provides the connection between a C++ object (derived from CWnd) and a Windows-based window object (also known as an HWND).

There are three common ways these are related:

CWnd creates the HWND. The behavior can be modified in a derived class. This is a case of "class derivation" and is done by creating a class derived from CWnd and created with calls to Create.

CWnd gets attached to an existing HWND. The behavior of the existing window is not modified. This is a case of "delegation" and is made possible by calling Attach to alias an existing HWND to a CWnd C++ object.

CWnd gets attached to an existing HWND, and you can modify the behavior in a derived class. This is called dynamic subclassing because we are changing the behavior (and hence the class) of a Windows-based object at run time.

This last case is done with the member functions CWnd::SubclassWindow and SubclassDlgItem.

Both routines attach a CWnd object to an existing HWND. SubclassWindow takes the HWND directly, and SubclassDlgItem is a helper that takes a control ID and the parent window (usually a dialog). SubclassDlgItem is designed for attaching C++ objects to dialog controls created from a dialog template.

Please refer to the CTRLTEST sample for several examples of when to use SubclassWindow and SubclassDlgItem.

Cleanup: CWnd::PostNCDestroy

The following is an important topic. If you follow the guidelines set out below, you will have very few problems with cleanup problems (either in forgetting to delete/free C++ memory, forgetting to free up system resources like HWNDs, or freeing objects too many times).

There are typically four types of CWnd derived objects:

Child windows/controls (derived from CWnd)

Main frame windows (MDI and SDI included, derived from CFrameWnd)

Modeless dialogs (derived from CDialog)

Modal dialogs (derived from CModalDialog)

Here are the recommended ownership rules:

Child windows/controls should be embedded as members in their parent window/dialog class. They will automatically get cleaned up when the parent window/dialog object gets destroyed.

Main frame windows should be allocated with "new." The standard application startup will do this, for example:

m_pMainWnd = new CMyMainWindow;

Main frame windows should not be embedded as members in other classes/objects.

Main frame windows should not be deleted with the "delete" operator but should use DeleteWindow instead.

Modeless dialogs usually follow the same rules as main frame windows—but you must override PostNcDestroy yourself to call "delete this".

Modal dialogs should be embedded on the frame (that is, auto variables) and get cleaned up when the routine using the dialog returns.

When destroying a window in Windows, the last Windows-based message sent to the window is WM_NCDESTROY. The default CWnd handler for that message (CWnd::OnNcDestroy) will detach the HWND from the C++ object and call the virtual function PostNcDestroy.

The default PostNcDestroy implementation does nothing. The CFrameWnd implementation will delete the C++ object.

When to override PostNcDestroy:

If you have a CFrameWnd derived class embedded in another class or statically allocated

If you have a modeless dialog you want to automatically clean up

For example, if you want to have a frame window class that can be allocated on the stack frame, as a static member, or embedded in another object, you would derive your own class as usual. You would, in addition, override the virtual member function PostNcDestroy as follows:

class CMyFrameWnd : public CFrameWnd

// frame window class that can be allocated on the stack or

// in static memory.

{

// standard function overrides, constructors, message handlers

protected:

virtual void PostNcDestroy();

};

void CMyFrameWnd::PostNcDestroy()

{

// do nothing

}

The owner of the object is responsible for calling DestroyWindow and making sure the object is properly cleaned up. This is demonstrated in the CTRLTEST sample application.