优化列表控件中的水平滚动问题

在开发具有列表控件的应用程序时,经常遇到需要优化水平滚动的问题。本文将介绍一种方法,通过自动调整列宽来避免水平滚动的出现或闪烁,同时确保在垂直滚动出现时也能正确响应。

列表控件中,经常需要一个或多个列能够根据内容自动调整宽度,以适应可用空间。这种做法可以防止水平滚动条的出现,提高用户体验。

实现方法

为了实现这一功能,可以创建一个类,比如 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_BEGINTRACKHDN_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_INSERTITEMLVM_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); }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485