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.

screenshot.png

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 equivilent 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:

// Grab the window handle for our list view
m_list = GetDlgItem(IDC_LIST);

// Set some styles for the list view control.  We want checkboxes and full row select.
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:

// Add some columns to the list view control
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);

// Set column widths
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 it's 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:

// Here's where we can add the checkbox to the column header
// First, we need to snag the header control and give it the
// HDS_CHECKBOXES style since the list view doesn't do this for us
HWND header = ListView_GetHeader(m_list);
DWORD dwHeaderStyle = ::GetWindowLong(header, GWL_STYLE);
dwHeaderStyle |= HDS_CHECKBOXES;
::SetWindowLong(header, GWL_STYLE, dwHeaderStyle);

// Store the ID of the header control so we can handle its notification by ID
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:

// Now, we can update the format for the first header item,
// which corresponds to the first column
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 chck 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 /*idCtrl*/, LPNMHDR pnmh, BOOL& /*bHandled*/) {
	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) {
	// Loop through all of our items.  If any of them are
	// unchecked, we'll want to uncheck the header checkbox.
	BOOL fChecked = TRUE;
	for (int nItem = 0; nItem < ListView_GetItemCount(m_list); nItem++) {
		if (!ListView_GetCheckState(m_list, nItem)) {
			fChecked = FALSE;
			break;
		}
	}

	// We need to get the current format of the header
	// and set or remove the HDF_CHECKED flag
	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 /*idCtrl*/, LPNMHDR pnmh, BOOL& /*bHandled*/) {
	LPNMLISTVIEW pnmlv = (LPNMLISTVIEW)pnmh;

	if (pnmlv->uChanged & LVIF_STATE) {
		SetHeaderCheckbox();
	}
	return 0;
}

History

  • 2011-02-12 Initial release.
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"