Introduction
There doesn't appear to be much, if any, example code of how to place a checkbox in the column header of a list view control. This can be used to add "check/uncheck all" functionality if you're using LVS_EX_CHECKBOXES
in your list view window style to display a check box next to each item.
Unfortunately, the method used here only works with Windows Vista/Server 2008 and later. It appears to fail gracefully (no checkbox is displayed) when run on XP, but extensive testing has not been done.
Background
The list view control does not expose a way to add a checkbox to a column heading directly. It creates a Header control to display the column headings. A handle to this control can be obtained with the ListView_GetHeader()
macro (or equivalent message) with which you can then manipulate.
Using the code
The sample project included was generated with VS2010 using the WTL AppWizard. Neither ATL nor WTL are needed for this technique to work. The code relevant to this method has actually been written using the SDK macros. You can of course use ATL/WTL helpers to make life easier.
The sample app is simply a modal dialog application with a list view control.
We'll throw some initialization code in our OnInitDialog
method. First, we'll save the HWND
of the list view control to a member variable for easy access later. Then, we add some styles to the control so it will display checkboxes and select the full row when clicked:
m_list = GetDlgItem(IDC_LIST);
ListView_SetExtendedListViewStyle(m_list,
LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT);
Next, we'll actually create the columns. The first column will hold only the checkbox:
LVCOLUMN lvc = {0};
ListView_InsertColumn(m_list, 0, &lvc);
lvc.mask = LVCF_TEXT;
lvc.iSubItem++;
lvc.pszText = _T("First Name");
ListView_InsertColumn(m_list, 1, &lvc);
lvc.iSubItem++;
lvc.pszText = _T("Last Name");
ListView_InsertColumn(m_list, 2, &lvc);
lvc.iSubItem++;
lvc.pszText = _T("Company");
ListView_InsertColumn(m_list, 3, &lvc);
ListView_SetColumnWidth(m_list, 0, LVSCW_AUTOSIZE_USEHEADER);
ListView_SetColumnWidth(m_list, 1, LVSCW_AUTOSIZE_USEHEADER);
ListView_SetColumnWidth(m_list, 2, LVSCW_AUTOSIZE_USEHEADER);
ListView_SetColumnWidth(m_list, 3, LVSCW_AUTOSIZE_USEHEADER);
And here's where the magic starts. First, we obtain the HWND
to the header control used by the list view. Then, we can modify its window style to add the HDS_CHECKBOXES
style which will allow us to display a checkbox in the header. If we don't do this, the control will not render a checkbox. We also store the control ID for later use by our message handler so we can detect when someone clicks the checkbox:
HWND header = ListView_GetHeader(m_list);
DWORD dwHeaderStyle = ::GetWindowLong(header, GWL_STYLE);
dwHeaderStyle |= HDS_CHECKBOXES;
::SetWindowLong(header, GWL_STYLE, dwHeaderStyle);
m_HeaderId = ::GetDlgCtrlID(header);
Finally, we ask the header control to populate an HDITEM
struct with the format for the first column in our list view. We then apply the HDF_CHECKBOX
format flag. We also apply the HDF_FIXEDWIDTH
flag which prevents users from resizing the column. This is also a Vista and later flag:
HDITEM hdi = { 0 };
hdi.mask = HDI_FORMAT;
Header_GetItem(header, 0, &hdi);
hdi.fmt |= HDF_CHECKBOX | HDF_FIXEDWIDTH;
Header_SetItem(header, 0, &hdi);
Okay, that will get the checkbox to display in the header. If we don't handle any of the notifications, the default behavior is to select and unselect the items in the list view when clicking the checkbox. This probably isn't what you want, so we'll make it actually check and uncheck the items. First, let's set up a couple notification mappings:
BEGIN_MSG_MAP(CMainDlg)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)
COMMAND_ID_HANDLER(IDOK, OnOK)
COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
NOTIFY_HANDLER(m_HeaderId, HDN_ITEMSTATEICONCLICK, OnHeaderItemStateIconClick)
NOTIFY_HANDLER(IDC_LIST, LVN_ITEMCHANGED, OnListItemChanged)
END_MSG_MAP()
The header control will send a HDN_ITEMSTATEICONCLICK
notification when the user clicks the checkbox. We handle this in our OnHeaderItemStateIconClick
method. Basically, we check to see if the provided HDITEM
contains information about the state of our checkbox. If it does, we call our CheckAllItems()
function to check the checkboxes of all of the items in the list view. Then, we call SetHeaderCheckbox()
which sets the state of the checkbox in the header:
LRESULT OnHeaderItemStateIconClick(int ,
LPNMHDR pnmh, BOOL& ) {
LPNMHEADER pnmHeader = (LPNMHEADER)pnmh;
if (pnmHeader->pitem->mask & HDI_FORMAT &&
pnmHeader->pitem->fmt & HDF_CHECKBOX) {
CheckAllItems(!(pnmHeader->pitem->fmt & HDF_CHECKED));
SetHeaderCheckbox();
return 1;
}
return 0;
}
void CheckAllItems(BOOL fChecked) {
for (int nItem = 0; nItem < ListView_GetItemCount(m_list); nItem++) {
ListView_SetCheckState(m_list, nItem, fChecked);
}
}
void SetHeaderCheckbox(void) {
BOOL fChecked = TRUE;
for (int nItem = 0; nItem < ListView_GetItemCount(m_list); nItem++) {
if (!ListView_GetCheckState(m_list, nItem)) {
fChecked = FALSE;
break;
}
}
HWND header = ListView_GetHeader(m_list);
HDITEM hdi = { 0 };
hdi.mask = HDI_FORMAT;
Header_GetItem(header, 0, &hdi);
if (fChecked) {
hdi.fmt |= HDF_CHECKED;
} else {
hdi.fmt &= ~HDF_CHECKED;
}
Header_SetItem(header, 0, &hdi);
}
Now, we handle when the user checks one of the items in the list. We want the header checkbox to check itself if the user manually checks all of the items. We do this by just calling the SetHeaderCheckbox()
method:
LRESULT OnListItemChanged(int , LPNMHDR pnmh, BOOL& ) {
LPNMLISTVIEW pnmlv = (LPNMLISTVIEW)pnmh;
if (pnmlv->uChanged & LVIF_STATE) {
SetHeaderCheckbox();
}
return 0;
}
History
- 2011-02-12: Initial release.