Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using the Header Control

0.00/5 (No votes)
12 May 2001 1  
An article describing how to use the header control

Introduction

This tutorial is designed to introduce you to the CHeaderCtrl class when used in the context of the CListCtrl. Although it is possible to use the CHeaderCtrl outside of the CListCtrl, the two are intimiately related in most applications. The topics covered include:

It also covers the more advanced issue of subclassing your own CHeaderCtrl in place of the standard CHeaderCtrl. This section addresses topics including:

This article assumes that you are relatively familiar with VC6++ and are able to navigate using both the Workspace window as well as ClassWizard. It also assumes that you are comfortable creating a dialog-based application, creating controls, and using ClassWizard to attach member variables to those controls. If you are not familiar with these topics, please consult Dr. Asad Altimeemy's article, A Beginners Guide to Dialog Base Applications - Part 1. The CHeaderCtrl demonstration project utilizes a dialog-based MFC application with a CListCtrl in Report mode. For an introductory tutorial addressing how to use a CListCtrl in a dialog-based application, see Using the List Control.

About CHeaderCtrl

Although the CHeaderCtrl is a relatively infrequently used Common Control class (compared to say, CButton or CEdit), it is most often used in tandem with the CListCtrl. When the CListCtrl is shown in Report mode (LVS_REPORT), the top portion of the ListView is taken over by a CHeaderCtrl to segregate the entries into items and sub-items. When the CListCtrl is displaying its contents in the other modes, the CHeaderCtrl is hidden. This is most easily seen in Explorer when the view is toggled between Folder Details and any of the other modes.

The fact that the CHeaderCtrl's visibility is managed by the CListCtrl will become important later when we address message handling. Whereas other Common Controls placed on CDialog or CForm objects have those Windows as their parents, the CHeaderCtrl's parent is actually the CListCtrl. This level of indirection causes problems for ClassWizard's default Message Map entries.

Retrieving a CHeaderCtrl Pointer

Given a CListCtrl in Report mode, there are two different ways to retrieve a pointer to the CHeaderCtrl. One is documented, while the other may be encountered in legacy code.

Supported Method

The easiest way to retrieve a CHeaderCtrl is to use the CListCtrl::GetHeaderCtrl() member function. The demonstration project uses this member function in the CHeaderCtrlDemoDlg::OnListHdr() function as follows:

// Get a CHeaderCtrl pointer

CHeaderCtrl *pHeader = m_cListCtrl.GetHeaderCtrl();
ASSERT(pHeader);

Undocumented Method

If memory serves me, the CListCtrl::GetHeaderCtrl() is a recent addition to the MFC library. If you are tasked with documenting or revising existing code, you may encounter an alternative way to retrieve a CHeaderCtrl pointer. This method relies on the Control ID that the CListCtrl assigns to the CHeaderCtrl. The easiest way to find this ID is to examine the CListCtrl with Spy++, which is truly invaluable when you are trying to determine how the Common Controls work and where they send their messages.

Examining the CListCtrl with Spy++ reveals that the Control ID assigned to the CHeaderCtrl is always 0. This implies an alternative method to retrieve the CHeaderCtrl is to use code such as that used in CHeaderCtrlDemoDlg::InitHeaderCtrl:

// Get a CHeaderCtrl pointer

CWnd *pWnd = m_cListCtrl.GetDlgItem(0);
ASSERT(pWnd);
CHeaderCtrl *pHeader = static_cast<CHeaderCtrl*>(pWnd); 
ASSERT(pHeader);

In either case, once the CListCtrl has been created we can access the CHeaderCtrl and begin to manipulate its contents and/or behavior.

Inserting Items

For most situations, inserting items into the CHeaderCtrl will be done via the CListCtrl::InsertColumn(...) function. This function ultimately uses the LVCOLUMN structure to insert items into the CHeaderCtrl child window. This overloaded function allows you to pass the 0-based column index and either a pointer to a LVCOLUMN structure itself, or simply the parameters for the new column. In the latter case the CListCtrl handles creating the LVCOLUMN structure and sending the appropriate message. The CHeaderCtrl demo project uses the following snippet in CHeaderCtrlDemoDlg::InitListCtrl() to create the columns:

// Create the columns

CRect rect;
m_cListCtrl.GetClientRect(&rect);
int nInterval = rect.Width()/5;
m_cListCtrl.InsertColumn(0, _T("Item"), LVCFMT_LEFT, nInterval*2);
m_cListCtrl.InsertColumn(1, _T("Type"), LVCFMT_LEFT, nInterval);
m_cListCtrl.InsertColumn(2, _T("Price"), LVCFMT_LEFT, rect.Width()-3*nInterval-16); 

The reason for the peculiar width assigned to column 3 will be discussed below.

Formatting Options

Items in the CHeaderCtrl can be aligned in three different ways: left, center, and right. If the CHeaderCtrl format is specified by calling the CListCtrl::InsertColumn(...) function, then the constants are:

  • LVCFMT_LEFT
  • LVCFMT_CENTER
  • LVCFMT_RIGHT

Alternatively, item formatting can be controlled by functions exposed by the CHeaderCtrl class itself. Functions such as CHeaderCtrl::InsertItem(...) and CHeaderCtrl::SetItem(...) the constants are defined as:

  • HDF_LEFT
  • HDF_CENTER
  • HDF_RIGHT

In reality, these constants are #define (d) to be equivalent values (see COMMCTRL.H), but they have distinct names. The CHeaderCtrl demo project uses the alignment options in CHeaderCtrlDemoDlg::InitHeaderCtrl(...) which is discussed below.

Using Images

Like many other Common Controls, the CHeaderCtrl supports images. However, unlike other Common Controls, when items are inserted into the control using the more common CListCtrl interfaces, there is no obvious way to attach the image list. Attaching an image list to the CHeaderCtrl is a three step process:

  1. Create the CImageList
  2. Attach the CImageList
  3. Set the item images

Each of these steps is followed in the CHeaderCtrlDemoDlg::InitHeaderCtrl initialization function.

1. Create the CImageList

CImageList creation is fairly standard. To create a CImageList populated with images stored in a bitmap resource, use code such as:

VERIFY(m_cImageList.Create(IDB_HEADER_CTRL, 16, 4, RGB(255, 0, 255)));

Please note that the m_cImageList member variable is a member of CHeaderCtrlDemoDlg, not a locally declared variable. The reason for this is that when the CImageList is attached to the CHeaderCtrl, the CHeaderCtrl does not make a copy of the object.. Instead, the caller is responsible for maintaining the validity of that address, since it is only a pointer that is passed when the CImageList is attached to the Common Control. Therefore, if a locally defined CImageList variable were used, the address would no longer be valid when the function goes out of scope. Making the m_cImageList a member of CHeaderCtrlDemoDlg avoids this problem.

2. Attach the CImageList

To attach the newly created CImageList to the CHeaderCtrl, we need only a pointer to the CHeaderCtrl to access the CHeaderCtrl::SetImageList(...) member function. Assigning the CImageList can therefore be accomplished with the following:

// Get a CHeaderCtrl pointer

CWnd *pWnd = m_cListCtrl.GetDlgItem(0); // Could also use m_cListCtrl.GetHeaderCtrl();

ASSERT(pWnd);
CHeaderCtrl *pHeader = static_cast<CHeaderCtrl*>(pWnd); 
ASSERT(pHeader);

//  Set the CImageList

pHeader->SetImageList(&m_cImageList);

With the ImageList attached, the CHeaderCtrl images can now be assigned.

3. Set the item images

Setting CHeaderCtrl images is done via the CHeaderCtrl::SetItem(...) member function. To my knowledge, there is no way to assign images to the CHeaderCtrl items using methods exposed by the CListCtrl. Therefore, if you wish to set the CHeaderCtrl images, you need to access the items that were inserted with CListCtrl methods and modify them using CHeaderCtrl methods. This can be done using the HDITEM structure and the CHeaderCtrl::SetItem(...) function. The HDITEM structure is similar to other Common Control item structures (LVITEM, TVITEM) and is used to both Set and Get information about an item. Like these other structures, the most important element in the structure is the HDITEM.mask variable. This UINT specifies either:

  • The fields that contain valid data; used for CHeaderCtrl::SetItem(...)
  • The fields whose data should be retrieved; used for CHeaderItem::GetItem(...)

For instance, the CHeaderCtrlDemo project uses the HDITEM structure to set the header image and alignment in the CHeaderCtrlDemoDlg::InitHeaderCtrl() member function:

// Iterate through the items and set the image

HDITEM hdi;
for (int i=0; i < pHeader->GetItemCount(); i++)
{
	pHeader->GetItem(i, &hdi);
	hdi.fmt |= g_uHDRStyles[i%3] | HDF_IMAGE;
	hdi.mask |= HDI_IMAGE | HDI_FORMAT;
	hdi.iImage =  i;
	pHeader->SetItem( i, &hdi);
}

In this loop, you first retrieve the contents of each item in the CHeaderCtrl. Then you also set the HDI_IMAGE and HDI_FORMAT flags in the retrieved structure, populate the necessary fields and re-assign the CHeaderCtrl item. Note that g_uHDRStyles is an array of UINTs that stores the three (3) different CHeaderCtrl text alignment constants (see above).

Finally, because we are using both an image and a HDF_RIGHT alignment parameter on the thirdmost column, we need to explicitly account for this difference when assigning column widths. This accounts for the rather unusual sizing calculation made above, to offset the width of the image which is "added" to the existing size of the column.

Handling Notification Messages

Handling CHeaderCtrl messages in a CListCtrl strains ClassWizard's magic powers. The problem is that ClassWizard considers the CHeaderCtrl embedded in a CListCtrl to be of the same "message depth" as any other Common Control (CButton or CEdit Control, for instance). In practice however, the CHeaderCtrl is actually a child of the CListCtrl in which it resides. While the CListCtrl's immediate parent is the CDialog in which it is instantiated, the CHeaderCtrl's immediate parent is the CListCtrl. This is also suggested by the following two points:

  • Unlike other Common Controls on a CDialog, the CHeaderCtrl in the CListCtrl is not given an explicit resource ID;
  • The existence of the CListCtrl::GetHeaderCtrl(...) method

This problem arises when you use ClassWizard to create CHeaderCtrl handlers in a CDialog derived class. To create a CHeaderCtrl message handler, you first right click the CListCtrl object in the Resource Editor and select ClassWizard from the popup menu. ClassWizard then enumerates all the messages for this CListCtrl, including those sent by the CHeaderCtrl window. These messages are handled through the ON_NOTIFY macro construction. The list looks something like this:

Attempting to implement a message handler for HDN_<X> messages using Class Wizard will then generate an incorrect Message Map entry. For instance, creating a message handler for the HDN_ENDDRAG message will create an entry such as: ON_NOTIFY(HDN_ENDDRAG, IDC_LIST_CTRL, OnEnddragListCtrl). Unfortunately, the IDC_LIST_CTRL is not responsible for notifying the parent of a HDN_ENDDRAG message. It is the embedded CHeaderCtrl which sends the message, so the ON_NOTIFY macro should use the ID of the CHeaderCtrl. Using Spy++, we can determine that the ID of the CHeaderCtrl is 0, so the ON_NOTIFY entry needs to be manually edited to the following: ON_NOTIFY(HDN_ENDDRAG, 0, OnEnddragListCtrl). This roundabout way connects the WM_NOTIFY messages of the CHeaderCtrl to the "second-level" parent of the CHeaderCtrl, which is often where message processing is handled.

Updating Items

Like the CListCtrl, items in the CHeaderCtrl can be updated at runtime. In the sample product, this is illustrated in CHeaderCtrlDemoDlg::OnSetHdr() which sets the column header text. After validating the input, the column header text is updated using the HDITEM structure. This structure is similar to the LVITEM and TVITEM structures that are used to Set and Get information from the CListCtrl and CTreeCtrls, respectively. The caller first specifies which HDITEM fields are valid (using the HDITEM.mask field) and then executes the Set method. In the sample project, this is done in the following snippet:

// Update this item

HDITEM hdi;
hdi.mask = HDI_TEXT;
hdi.pszText = (LPTSTR)(LPCTSTR)m_strColText;
pHeader->SetItem(m_nCol, &hdi);

Deriving a Custom CHeaderCtrl

In some cases it is useful to derive a custom CHeaderCtrl class that can be re-used in other projects; perhaps a CHeaderCtrl that can manage icons in the Header area denoting the current sort order. To derive a custom CHeaderCtrl class, invoke ClassWizard and create a custom class as in the following:

In the sample project, the classname for the custom CHeaderCtrl is CAdvHeaderCtrl. This class will be responsible for attaching images to the items in the column as well as dynamically setting the column image as the user changes its width.

Subclassing

The next problem is replacing the default CHeaderCtrl embedded in the CListCtrl with your custom CMyHeaderCtrl. This can be done by exposing an initialization function from your custom class (see CAdvHeaderCtrl::Init(...)) which can be called when the dialog or view is created; typically CDialog::OnInitDialog(), CWnd::PreCreateWindow() or CWnd::Create(). In the sample project, this is accomplished in the following snippet of CAdvHeaderCtrl::Init(CHeaderCtrl *pHeader):

ASSERT(pHeader && pHeader->GetSafeHwnd());
if (!SubclassWindow(pHeader->GetSafeHwnd()))
{
	OutputDebugString(_T("Unable to subclass existing header!\n"));
	return FALSE;
}

This replaces the existing WindowProc of the CHeaderCtrl with the WindowProc of the CAdvHeaderCtrl such that the custom class can respond to both conventional Window messages and HDN_<X> notifications.

Handling Reflected Notification Messages

The CAdvHeaderCtrl is a simple class whose only responsibility is to dynamically change an item image as the user changes the column width. It also overrides the default CHeaderCtrl style to enable Hot-Tracking: the CHeaderCtrl column text color changes when the user moves the mouse over its contents.

It accomplishes this through the handling of two reflected messages:

  • HDN_BEGINTRACK
  • HDN_ENDTRACK

However, this also proves to be the most difficult aspect of the CHeaderCtrlDemo project. Although I have not spent sufficient time to identify the exact problem, there appears to be an issue with ANSI versus UNICODE HDN_<X> notification messages. That is, the expected ANSI messages are not sent. MSFT KB article Q148533 discusses a similar problem regarding Common Controls: ANSI notification messages are not received when the controls are created in response to a WM_CREATE message handler in a CDialog or CFormView derived class. I suspect that something similar is afflicting the CAdvHeaderCtrl demo project. Therefore, I was forced to implement Message Map macros for both the ANSI and UNICODE versions of the reflected WM_NOTIFY messages. Without these entries, the reflected necessary message handlers were never called.

The Message Map therefore consists of four manually entered entries:

BEGIN_MESSAGE_MAP(CAdvHeaderCtrl, CHeaderCtrl) 
//{{AFX_MSG_MAP(CAdvHeaderCtrl) 

// NOTE - the ClassWizard will add and remove mapping macros here. 


ON_NOTIFY_REFLECT(HDN_ENDTRACKW, OnEndTrack) 
ON_NOTIFY_REFLECT(HDN_BEGINTRACKW, OnBeginTrack) 
ON_NOTIFY_REFLECT(HDN_ENDTRACKA, OnEndTrack) 
ON_NOTIFY_REFLECT(HDN_BEGINTRACKA, OnBeginTrack) 
END_MESSAGE_MAP() 

These entries seemed to do the trick, although I cannot guarantee that they will continue to do so in the future. Nevertheless, the OnBeginTrack and OnEndTrack are relatively simple functions that demonstrate the types of things that can be done in a self-contained CHeaderCtrl-derived class. Each function follows the standard prototype for WM_NOTIFY handlers: OnReflectedHandler(NMHDR * pNMHDR, LRESULT* pResult). OnBeginTrack queries the selected item for its image index, saves the index, and sets a new image index.

 NMHEADER *pHdr = (NMHEADER*)pNMHDR;
// Get the current image in the item and save the index

HDITEM hdi;
hdi.mask = HDI_IMAGE;
GetItem(pHdr->iItem, &hdi);
m_nSavedImage = hdi.iImage;
// Set the new image

SetImage(pHdr->iItem, 3);

CAdvHeaderCtrl::SetImage(nCol, nImage) simply set the image of nCol. OnEndTrack just restores the temporary assignment to the original value:

NMHEADER *pHdr = (NMHEADER*)pNMHDR;
if (-1 == m_nSavedImage)
   return;

SetImage(pHdr->iItem, m_nSavedImage);
m_nSavedImage = -1;

In this way, the behavior of the custom CHeaderCtrl can be managed by the class itself, rather than placing responsibility on the parent.

Summary

The CHeaderCtrl is a relatively underutilized (and thus underappreciated ?) MFC Common Control class. Particularly when working in database applications, a CHeaderCtrl is crucial to providing an effective user interface. It can also provide cues in response to user input. I hope that this tutorial has been a useful introduction to the CHeaderCtrl

History

13 May 2001 - Download now available

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here