Introduction
Microsoft's CListCtrl
has support for displaying data in a grid, but requires a helping hand for handling column selection.
This article will demonstrate the following:
- How to make a column hidden without deleting it.
- How to restore the width and position of the column when showing the column again.
The demo application allows us to show / hide columns when right-clicking the column headers.
Background
There are lots of advanced grid controls that extend the CListCtrl
, so it is possible to change the column configuration at runtime. But, because these grid controls can be very complex, it can be difficult to see how they do it.
This article is part of a series, where the final article CGridListCtrlEx combines the details of all the articles.
How To Implement a Column Picker in CListCtrl
Usually, there are two approaches for implementing the ability to dynamically hide and show columns in a CListCtrl
:
- Maintaining two lists of columns. A list of columns that are displayed, and a list of all available columns. When having to show a column, we will have to provide all the necessary details to insert the column in the displayed list.
- Change the width of hidden columns to zero pixels, so they are not shown. When having to show a column, we just have to resize it back to its original size.
The second approach is the one described in this article, and it introduces some issues we have to take care of:
- Show and hide columns while preserving the original width and position.
- Prevent the user from being able to resize columns that are hidden.
- Prevent the user from being able to drag columns that are hidden.
- When columns are inserted or deleted, we have to update the visible state of the columns.
- Persistence of the column configuration has to include the show / hide state of each column (not part of this article).
Show and Hide Columns
The solution of hiding a column by changing the width to zero seems easy, but there is also some extra work:
- Have to maintain a secondary list which tells what columns are visible, which is used to prevent the user from messing with the hidden columns.
- When hiding a column, it must be moved to the beginning of the
CHeaderCtrl
display order list, so the hidden columns will not interfere with the visible columns (E.g. prevent resize of visible columns, because hidden columns are in between).
- When showing a column, we must restore its position in the
CHeaderCtrl
display order list, and at the same time restore its width.
BOOL CListCtrl_Column_Picker::ShowColumn(int nCol, bool bShow)
{
SetRedraw(FALSE);
ColumnState& columnState = GetColumnState(nCol);
int nColCount = GetHeaderCtrl()->GetItemCount();
int* pOrderArray = new int[nColCount];
VERIFY( GetColumnOrderArray(pOrderArray, nColCount) );
if (bShow)
{
int nCurIndex = -1;
for(int i = 0; i < nColCount ; ++i)
{
if (pOrderArray[i]==nCol)
nCurIndex = i;
else
if (nCurIndex!=-1)
{
if ( (i <= columnState.m_OrgPosition)
|| !IsColumnVisible(pOrderArray[i])
)
{
pOrderArray[nCurIndex] = pOrderArray[i];
pOrderArray[i] = nCol;
nCurIndex = i;
}
}
}
}
else
{
int nCurIndex(-1);
for(int i = nColCount-1; i >=0 ; --i)
{
if (pOrderArray[i]==nCol)
{
columnState.m_OrgPosition = i;
nCurIndex = i;
}
else
if (nCurIndex!=-1)
{
pOrderArray[nCurIndex] = pOrderArray[i];
pOrderArray[i] = nCol;
nCurIndex = i;
}
}
}
VERIFY( SetColumnOrderArray(nColCount, pOrderArray) );
delete [] pOrderArray;
if (bShow)
{
columnState.m_Visible = true;
VERIFY( SetColumnWidth(nCol, columnState.m_OrgWidth) );
}
else
{
int orgWidth = GetColumnWidth(nCol);
VERIFY( SetColumnWidth(nCol, 0) );
columnState.m_Visible = false;
columnState.m_OrgWidth = orgWidth;
}
SetRedraw(TRUE);
Invalidate(FALSE);
return TRUE;
}
Prevent Resizing of Hidden Columns
We have to block the resize events for the hidden columns. This is done by intercepting the resize event (HDN_BEGINTRACK
) for the CHeaderCtrl
. We also want to block any mistaken resizing of the hidden columns (LVM_SETCOLUMNWIDTH
).
BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
ON_MESSAGE(LVM_SETCOLUMNWIDTH, OnSetColumnWidth)
ON_NOTIFY_EX(HDN_BEGINTRACKA, 0, OnHeaderBeginResize)
ON_NOTIFY_EX(HDN_BEGINTRACKW, 0, OnHeaderBeginResize)
END_MESSAGE_MAP()
BOOL CListCtrl_Column_Picker::OnHeaderBeginResize(UINT, NMHDR* pNMHDR, LRESULT* pResult)
{
NMHEADER* pNMH = (NMHEADER*)pNMHDR;
int nCol = (int)pNMH->iItem;
if (!IsColumnVisible(nCol))
{
*pResult = TRUE;
return TRUE;
}
return FALSE;
}
LRESULT CListCtrl_Column_Picker::OnSetColumnWidth(WPARAM wParam, LPARAM lParam)
{
int nCol = (int)wParam;
if (!IsColumnVisible(nCol))
{
return FALSE;
}
return DefWindowProc(LVM_SETCOLUMNWIDTH, wParam, lParam);
}
This doesn't handle the situation where one can double-click the divider area of the column, which causes the column to resize according to its entire content. Handling the resize event (LVM_SETCOLUMNWIDTH
) doesn't prevent the resizing. One has to handle the messsage HDN_DIVIDERDBLCLICK
to block this resizing:
BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
ON_NOTIFY_EX(HDN_DIVIDERDBLCLICKA, 0, OnHeaderDividerDblClick)
ON_NOTIFY_EX(HDN_DIVIDERDBLCLICKW, 0, OnHeaderDividerDblClick)
END_MESSAGE_MAP()
BOOL CListCtrl_Column_Picker::OnHeaderDividerDblClick(UINT, NMHDR* pNMHDR,
LRESULT* pResult)
{
NMHEADER* pNMH = (NMHEADER*)pNMHDR;
SetColumnWidthAuto(pNMH->iItem);
return TRUE;
}
There is also a special shortcut key we can press in any CListCtrl
, which causes all columns to resize according to the widest string
(CTRL+Numeric-plus). One would think the handling of the resize event (LVM_SETCOLUMNWIDTH
) will take off this shortcut, but sadly enough, this shortcut has to be handled explicitly.
BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
ON_WM_KEYDOWN()
END_MESSAGE_MAP()
void CListCtrl_Column_Picker::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
switch(nChar)
{
case VK_ADD:
{
if (GetKeyState(VK_CONTROL) < 0)
{
SetColumnWidthAuto(-1);
return;
}
} break;
}
CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}
BOOL CListCtrl_Column_Picker::SetColumnWidthAuto(int nCol, bool includeHeader)
{
if (nCol == -1)
{
for(int i = 0; i < GetHeaderCtrl()->GetItemCount() ; ++i)
{
SetColumnWidthAuto(i, includeHeader);
}
return TRUE;
}
else
{
if (includeHeader)
return SetColumnWidth(nCol, LVSCW_AUTOSIZE_USEHEADER);
else
return SetColumnWidth(nCol, LVSCW_AUTOSIZE);
}
}
Prevent the Drag of Hidden Columns
We cannot drag a column if the width is zero, but we still have to ensure that when other columns are dragged, they are not dragged in between the hidden ones. This is done by intercepting the end drag event (HDN_ENDDRAG
) for the CHeaderCtrl
.
BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
ON_NOTIFY_EX(HDN_ENDDRAG, 0, OnHeaderEndDrag)
END_MESSAGE_MAP()
BOOL CListCtrl_Column_Picker::OnHeaderEndDrag(UINT, NMHDR* pNMHDR, LRESULT* pResult)
{
NMHEADER* pNMH = (NMHEADER*)pNMHDR;
if (pNMH->pitem->mask & HDI_ORDER)
{
int nColCount = GetHeaderCtrl()->GetItemCount();
int* pOrderArray = new int[nColCount];
VERIFY( GetColumnOrderArray(pOrderArray, nColCount) );
for(int i = 0; i < nColCount ; ++i)
{
if (IsColumnVisible(pOrderArray[i]))
{
pNMH->pitem->iOrder = max(pNMH->pitem->iOrder,i);
break;
}
}
delete [] pOrderArray;
}
return FALSE;
}
Inserting and Deleting Columns
We have to keep the list of column-visible-state synchronized with the actual list of displayed columns. This could be done by providing a custom method for inserting / deleting columns, which also updates the column state list. Another approach is to monitor for the events issued when a column is inserted / deleted.
BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
ON_MESSAGE(LVM_DELETECOLUMN, OnDeleteColumn)
ON_MESSAGE(LVM_INSERTCOLUMN, OnInsertColumn)
END_MESSAGE_MAP()
LRESULT CListCtrl_Column_Picker::OnDeleteColumn(WPARAM wParam, LPARAM lParam)
{
LRESULT lRet = DefWindowProc(LVM_DELETECOLUMN, wParam, lParam);
if (lRet == FALSE)
return FALSE;
DeleteColumnState((int)wParam);
return lRet;
}
LRESULT CListCtrl_Column_Picker::OnInsertColumn(WPARAM wParam, LPARAM lParam)
{
LRESULT lRet = DefWindowProc(LVM_INSERTCOLUMN, wParam, lParam);
if (lRet == -1)
return -1;
int nCol = (int)lRet;
if (GetColumnStateCount() < GetHeaderCtrl()->GetItemCount())
InsertColumnState((int)nCol, true);
return lRet;
}
Using the Code
The source code provided includes a simple implementation of a CListCtrl
, which implements the above solution for showing and hiding columns (CListCtrl_Column_Picker
).
Points of Interest
The demo application demonstrates a little oddity in the CListCtrl
. When hiding a different column than the label column (first column), the label-column will change its margins a little. This does not happen with other columns, so once again, the label-column ruins the perfect picture. We could consider creating the label column as hidden by default (and add a block for enabling it) thus avoiding the small quirks of the label column.
The demo application also shows a little visual quirk introduced by using this way of showing / hiding columns. If hiding a column, then move the mouse to the start of the first column-header, so the mouse icon will change as we should be able to resize the column.
History
- 2008-08-23
- First release of the article
- 2008-08-25
- Fixed a bug in the source code where it wouldn't restore the original position correctly (could place the visible column between hidden columns)
- Updated the article about making sure to keep the hidden columns at the beginning of the
CHeaderCtrl
display order list
- 2008-09-04
- Fixed a bug where a hidden column could be resized, when double clicking the header divider.