分类:
2007-08-04 16:47:13
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, . 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 .
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.
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.
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);
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(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.
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.
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:
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:
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.
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:
Each of these steps is followed in the CHeaderCtrlDemoDlg::InitHeaderCtrl
initialization function.
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.
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(pWnd); ASSERT(pHeader); // Set the CImageList pHeader->SetImageList(&m_cImageList);
With the ImageList attached, the CHeaderCtrl
images can now be assigned.
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:
CHeaderCtrl::SetItem(...)
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 ).
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:
CHeaderCtrl
in the CListCtrl
is not given an explicit resource ID;
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:
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.
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);
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.
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_
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:
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_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. //}}AFX_MSG_MAP 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.
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