可以显示和隐藏列的 CListCtrl






4.95/5 (19投票s)
在 MFC 列表控件中实现列选择器的示例
引言
Microsoft 的 CListCtrl
支持以网格形式显示数据,但需要借助其他功能来处理列的选择。
本文将演示以下内容
- 如何隐藏列而不删除它。
- 如何恢复列的宽度和位置,以便在重新显示列时使用。
演示应用程序允许我们在右键单击列标题时显示/隐藏列。
背景
有很多高级网格控件可以扩展 CListCtrl
,因此可以在运行时更改列的配置。但是,由于这些网格控件可能非常复杂,因此很难理解它们是如何实现的。
本文是系列文章的一部分,最后一篇文章 CGridListCtrlEx 汇总了所有文章的细节。
如何在 CListCtrl 中实现列选择器
通常,有两种方法可以在 CListCtrl
中动态隐藏和显示列:
- 维护两个列列表。一个列表包含显示的列,另一个列表包含所有可用的列。当需要显示一列时,我们需要提供所有必要的信息将其插入到显示的列表中。
- 将隐藏列的宽度更改为零像素,这样它们就不会显示。当需要显示一列时,只需将其调整回其原始大小即可。
第二种方法是本文所述的方法,它会引入一些我们需要注意的问题:
- 显示和隐藏列,同时保留原始宽度和位置。
- 阻止用户调整隐藏列的宽度。
- 阻止用户拖动隐藏列。
- 插入或删除列时,我们需要更新列的可见状态。
- 列配置的持久化必须包括每列的显示/隐藏状态(本文不包含此部分)。
显示和隐藏列
通过将宽度设置为零来隐藏列的解决方案看起来很简单,但还有一些额外的工作要做:
- 必须维护一个辅助列表,该列表指示哪些列可见,用于防止用户操作隐藏的列。
- 隐藏列时,必须将其移到
CHeaderCtrl
显示顺序列表的开头,这样隐藏的列就不会干扰可见的列(例如,防止调整可见列的宽度,因为隐藏的列在中间)。 - 显示列时,我们必须恢复其在
CHeaderCtrl
显示顺序列表中的位置,同时恢复其宽度。
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)
{
// Restore the position of the column
int nCurIndex = -1;
for(int i = 0; i < nColCount ; ++i)
{
if (pOrderArray[i]==nCol)
nCurIndex = i;
else
if (nCurIndex!=-1)
{
// We want to move it to the original position,
// and after the last hidden column
if ( (i <= columnState.m_OrgPosition)
|| !IsColumnVisible(pOrderArray[i])
)
{
pOrderArray[nCurIndex] = pOrderArray[i];
pOrderArray[i] = nCol;
nCurIndex = i;
}
}
}
}
else
{
// Move the column to the front of the display order list
int nCurIndex(-1);
for(int i = nColCount-1; i >=0 ; --i)
{
if (pOrderArray[i]==nCol)
{
// Backup the current position of the column
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)
{
// Restore the column width
columnState.m_Visible = true;
VERIFY( SetColumnWidth(nCol, columnState.m_OrgWidth) );
}
else
{
// Backup the column width
int orgWidth = GetColumnWidth(nCol);
VERIFY( SetColumnWidth(nCol, 0) );
columnState.m_Visible = false;
columnState.m_OrgWidth = orgWidth;
}
SetRedraw(TRUE);
Invalidate(FALSE);
return TRUE;
}
阻止调整隐藏列的宽度
我们必须阻止对隐藏列的调整宽度事件。这可以通过截获 CHeaderCtrl
的调整宽度事件 (HDN_BEGINTRACK
) 来实现。我们还希望阻止任何错误的隐藏列宽度调整 (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)
{
// Check that column is allowed to be resized
NMHEADER* pNMH = (NMHEADER*)pNMHDR;
int nCol = (int)pNMH->iItem;
if (!IsColumnVisible(nCol))
{
*pResult = TRUE; // Block resize
return TRUE; // Block event
}
return FALSE;
}
LRESULT CListCtrl_Column_Picker::OnSetColumnWidth(WPARAM wParam, LPARAM lParam)
{
// Check that column is allowed to be resized
int nCol = (int)wParam;
if (!IsColumnVisible(nCol))
{
return FALSE;
}
// Let the CListCtrl handle the event
return DefWindowProc(LVM_SETCOLUMNWIDTH, wParam, lParam);
}
这并不能处理用户双击列分隔区域导致列根据其内容调整宽度的这种情况。处理调整宽度事件 (LVM_SETCOLUMNWIDTH
) 并不能阻止这种调整。必须处理消息 HDN_DIVIDERDBLCLICK
来阻止这种调整。
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; // Don't let parent handle the event
}
还有一个特殊的快捷键,我们可以在任何 CListCtrl
中按下该快捷键,它会使所有列根据最宽的 字符串
进行调整(Ctrl+数字加号)。人们可能会认为处理调整宽度事件 (LVM_SETCOLUMNWIDTH
) 会禁用此快捷键,但遗憾的是,必须显式处理此快捷键。
BEGIN_MESSAGE_MAP(CListCtrl_Column_Picker, CListCtrl)
ON_WM_KEYDOWN() // OnKeydown
END_MESSAGE_MAP()
void CListCtrl_Column_Picker::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
switch(nChar)
{
case VK_ADD: // CTRL + NumPlus (Auto size all columns)
{
if (GetKeyState(VK_CONTROL) < 0)
{
// Special handling to avoid showing "hidden" columns
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);
}
}
阻止拖动隐藏列
如果宽度为零,我们就无法拖动列,但我们仍然必须确保在拖动其他列时,它们不会被拖动到隐藏列之间。这可以通过截获 CHeaderCtrl
的结束拖动事件 (HDN_ENDDRAG
) 来实现。
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)
{
// Correct iOrder so it is just after the last hidden column
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;
}
插入和删除列
我们必须保持列可见状态列表与实际显示的列列表同步。这可以通过提供自定义的插入/删除列方法来实现,该方法也会更新列状态列表。另一种方法是监视列插入/删除时发出的事件。
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)
{
// Let the CListCtrl handle the event
LRESULT lRet = DefWindowProc(LVM_DELETECOLUMN, wParam, lParam);
if (lRet == FALSE)
return FALSE;
// Book keeping of columns
DeleteColumnState((int)wParam);
return lRet;
}
LRESULT CListCtrl_Column_Picker::OnInsertColumn(WPARAM wParam, LPARAM lParam)
{
// Let the CListCtrl handle the event
LRESULT lRet = DefWindowProc(LVM_INSERTCOLUMN, wParam, lParam);
if (lRet == -1)
return -1;
int nCol = (int)lRet;
// Book keeping of columns
if (GetColumnStateCount() < GetHeaderCtrl()->GetItemCount())
InsertColumnState((int)nCol, true); // Insert as visible
return lRet;
}
Using the Code
提供的源代码包含一个简单的 CListCtrl
实现,它实现了上述用于显示和隐藏列的解决方案 (CListCtrl_Column_Picker
)。
关注点
演示应用程序展示了 CListCtrl
中的一个奇怪现象。当隐藏的列不是标签列(第一列)时,标签列的边距会略有变化。其他列不会出现这种情况,所以标签列再次破坏了完美的画面。我们可以考虑将标签列默认设置为隐藏(并添加一个启用它的块),从而避免标签列的一些小怪癖。
演示应用程序还展示了使用这种显示/隐藏列方式带来的一些视觉上的怪癖。如果隐藏一列,然后将鼠标移到第一个列标题的开头,鼠标图标会发生变化,就像我们应该能够调整该列的宽度一样。
历史
- 2008-08-23
- 文章初稿
- 2008-08-25
- 修复了源代码中的一个错误,该错误导致原始位置无法正确恢复(可能将可见列放置在隐藏列之间)。
- 更新了文章,以确保将隐藏的列保留在
CHeaderCtrl
显示顺序列表的开头。
- 2008-09-04
- 修复了一个错误,该错误会导致在双击标题分隔符时隐藏的列可以被调整宽度。