在开发具有列表控件的应用程序时,经常遇到需要优化水平滚动的问题。本文将介绍一种方法,通过自动调整列宽来避免水平滚动的出现或闪烁,同时确保在垂直滚动出现时也能正确响应。
在列表控件中,经常需要一个或多个列能够根据内容自动调整宽度,以适应可用空间。这种做法可以防止水平滚动条的出现,提高用户体验。
为了实现这一功能,可以创建一个类,比如 CListColumnAutoSize
,用于自动调整列宽。以下是实现步骤:
首先,在窗口类实现中添加 CListColumnAutoSize
成员变量:
class CMainDlg : public CDialogImpl {
// ...
CListColumnAutoSize columns_resize_;
};
在窗口初始化时,比如在 WM_INITDIALOG
处理器中,子类化列表控件:
BOOL CMainDlg::OnInitDialog(CWindow wndFocus, LPARAM lInitParam) {
// ...
columns_resize_.SubclassWindow(GetDlgItem(IDC_MYLIST));
// 可选地设置需要调整宽度的列的索引,默认是第一列
columns_resize_.SetVariableWidthColumn(1);
return TRUE;
}
通常情况下,类会自动调整列宽。但是,如果需要,可以关闭自动更新功能。例如,在一次性添加、删除或更改大量项目时,关闭自动更新可以减少开销。以下是相关函数:
// 开启/关闭列宽自动更新
void EnableAutoUpdate(bool enable);
// 返回当前是否启用了自动更新
bool IsAutoUpdateEnabled() const;
// 手动更新列宽,如果自动更新不适合或没有覆盖所有应该执行的情况
void UpdateColumnsWidth();
源代码中还包含另一个类 CListColumnAutoSizeEx
,它实现了相同的列宽调整机制,但可以应用于多个列。使用时,需要设置可变宽度列的可用空间百分比。示例如下:
CListColumnAutoSizeEx list;
// ...
list.AddVariableWidthColumn(1, 0.4);
list.AddVariableWidthColumn(2, 0.6);
// 现在,第1列调整到40%的空闲空间,第2列调整到60%,第0列和其他列调整到内容宽度
标题栏可以通过多种方式调整大小。首先,按下 Ctrl
+ +
键会导致所有列调整到内容宽度(忽略标题文本宽度)。可以通过过滤相应的 WM_KEYDOWN
消息来阻止这种行为:
BEGIN_MSG_MAP_EX(CListColumnAutoSizeImplBase)
// ...
MSG_WM_KEYDOWN(OnKeyDown)
// ...
END_MSG_MAP()
void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
// 如果按下了CTRL + +,则将消息标记为已处理,并且不会传递给控件的DefWindowProc()函数
SetMsgHandled(VK_ADD == nChar && 0 != ::GetKeyState(VK_CONTROL));
}
其他调整标题栏的方式包括拖动标题栏分隔符或双击它(这会导致点击分隔符左侧的列调整到内容宽度,同时忽略列标题的文本宽度)。可以通过过滤 HDN_BEGINTRACK
和 HDN_DIVIDERDBLCLICK
通知来阻止这种行为:
BEGIN_MSG_MAP_EX(CListColumnAutoSizeImplBase)
// ...
// 标题栏向其父控件发送通知,父控件是类
NOTIFY_CODE_HANDLER_EX(HDN_BEGINTRACK, OnHeaderBeginTrack)
NOTIFY_CODE_HANDLER_EX(HDN_DIVIDERDBLCLICK, OnHeaderDividerDblclick)
// ...
END_MSG_MAP()
对于Vista及以上版本,HDS_NOSIZING
样式已经足够。对于XP,需要手动处理发送到标题栏控件的 WM_SETCURSOR
消息。
当控件大小改变或列表内容改变时,应更新列宽。首先通过处理 WM_SIZE
消息来实现:
LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam) {
T* pT = static_cast(this);
if (pT->IsAutoUpdate() && SIZE_MINIMIZED != wParam) {
// 只需要更新可变宽度的列
pT->UpdateVariableWidthColumns();
}
SetMsgHandled(FALSE);
return 0;
}
内容改变时更新列宽的实现是“懒加载”的——更新会在任何可能改变内容的消息之后进行。类不会跟踪实际的变化,因为这看起来太容易出错了,尤其是对于高度定制的列表控件。所以对于任何可能改变内容的消息(如 LVM_INSERTITEM
、LVM_SETITEMTEXTA
等),会调用以下函数:
LRESULT OnItemChange(UINT uMsg, WPARAM wParam, LPARAM lParam) {
// 应用这个操作
LRESULT lr = DefWindowProc(uMsg, wParam, lParam);
T* pT = static_cast(this);
// 如果自动更新打开
if (pT->IsAutoUpdate()) {
// 更新所有列的宽度
pT->UpdateColumnsWidth();
}
return lr;
}
固定宽度列的更新使用标题栏控件调整列宽到内容的能力,但有一个小hack:
void UpdateFixedWidthColumns() {
// 最简单的方法是让系统来调整。但在LVSCW_AUTOSIZE_USEHEADER的情况下,它会将最后一列调整到所有剩余空间。解决方法是在末尾添加一个假列
int count = GetHeader().GetItemCount();
ATLVERIFY(count == InsertColumn(count, _T("")));
T* pT = static_cast(this);
for (int i = 0; i < count; i++) {
if (!pT->IsVariableWidthColumn(i)) {
// 这里列肯定不是最后一列,所以它不会将内容调整到剩余空间
SetColumnWidth(i, LVSCW_AUTOSIZE_USEHEADER);
}
}
ATLVERIFY(DeleteColumn(count));
}
void UpdateVariableWidthColumns() {
// 获取完整的可用宽度
RECT rect = {0};
GetClientRect(▭);
// 从中减去固定列的宽度
T* pT = static_cast(this);
int count = GetHeader().GetItemCount();
for (int i = 0; i < count; i++) {
if (!pT->IsVariableWidthColumn(i)) {
rect.right -= GetColumnWidth(i);
}
}
// 将剩余宽度应用到可变宽度列
SetColumnWidth(variable_width_column_, rect.right - rect.left);
}