65.9K
CodeProject 正在变化。 阅读更多。
Home

调整列大小以避免水平滚动

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2013年9月3日

CPOL

4分钟阅读

viewsIcon

34307

downloadIcon

953

避免水平滚动并有效利用整个水平控件宽度:将除一列外的所有列调整为其内容大小,并将这一列调整为剩余的所有空间。

引言

CodeProject 上有很多文章讨论如何在列表控件中避免不必要的水平滚动([1][2])。本文提供了一种新的方法。这种方法适用于需要一列调整为所有可用空间,以及(可选)其他几列宽度已知且较小,需要根据内容调整大小的情况。该实现可防止出现或闪烁水平滚动条,并能正确响应垂直滚动条的出现。

请参见下面的屏幕截图,即使文本部分隐藏 - 在这种情况下,它比水平滚动导致最后一列不可见的问题要小得多(尤其是在使用LVS_EX_LABELTIP 列表样式时)。示例源代码包含一些其他的用法。

使用代码

CListColumnAutoSize 成员添加到您的窗口类实现中

class CMainDlg: public CDialogImpl<CMainDlg> {
  //...

  CListColumnAutoSize columns_resize_;
};

在窗口初始化时为您的列表控件创建子类,例如在WM_INITDIALOG 处理程序中

BOOL CMainDlg::OnInitDialog(CWindow wndFocus, LPARAM lInitParam) {
  //...

  columns_resize_.SubclassWindow(GetDlgItem(IDC_MYLIST));
  // Optionally set index of column to resize. By default it is first column
  columns_resize_.SetVariableWidthColumn(1);
 
  return TRUE;
}

好了,在大多数情况下,这就是需要做的全部工作。类会自动调整列大小。当需要时,可以关闭自动更新功能。例如,当您一次添加/删除/更改大量项目时,这非常有用,在这种情况下,自动更新会带来巨大的开销,因此最好在批处理操作之前关闭它,并在之后打开它。以下函数可以帮助您做到这一点

  // Turn columns width automatic update on / off
  void EnableAutoUpdate(bool enable);

  // Returns true if automatic update currently is on
  bool IsAutoUpdateEnabled() const;

  // Manually update columns width if auto updating does not suit you or
  // does not cover all cases when it should be performed
  void UpdateColumnsWidth();

源代码包含另一个类CListColumnAutoSizeEx,它实现了相同的列大小调整机制(即调整为可用空间),但适用于多列而不是一列。这里的一个用法区别在于:在设置可变宽度列时,还需要设置它应该使用的可用空间的百分比。示例

  CListColumnAutoSizeEx list;
  //...
  list.AddVariableWidthColumn(1, 0.4);
  list.AddVariableWidthColumn(2, 0.6);
  // Now column #1 resizes to 40% of free space, column #2 to 60%, column #0 and
  // others - to content

现在我们将看看它是如何工作的。

背景

实现包括两个部分:阻止用户调整标头大小以及响应列表内容更改或列表控件大小调整而实际调整列大小。

阻止标头调整大小

标头可以通过多种方式调整大小。首先是按住Ctrl+ 键。这会导致所有列表列调整为其内容大小(忽略标头文本宽度)。通过过滤相应的WM_KEYDOWN 消息来阻止。

BEGIN_MSG_MAP_EX(CListColumnAutoSizeImplBase)
  //...
  MSG_WM_KEYDOWN(OnKeyDown)
  //...
END_MSG_MAP()
 
void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
  // If CTRL + Add was pressed then message sets as handled and does not pass to control's
  // DefWindowProc() function
  SetMsgHandled(VK_ADD == nChar && 0 != ::GetKeyState(VK_CONTROL));
}

请注意,该实现使用在 WTL 头文件<atlcrack.h> 中定义的“破解”消息映射。

其他调整标头大小的方法是拖动标头的分隔符或双击它(这会导致左侧分隔符的列调整为内容大小,也忽略列标头的文本宽度)。通过过滤标头控件的HDN_BEGINTRACKHDN_DIVIDERDBLCLICK 通知来阻止。

BEGIN_MSG_MAP_EX(CListColumnAutoSizeImplBase)
  //...
  // Header sends notifications to its parent which is our class
  NOTIFY_CODE_HANDLER_EX(HDN_BEGINTRACK, OnHeaderBeginTrack)
  NOTIFY_CODE_HANDLER_EX(HDN_DIVIDERDBLCLICK, OnHeaderDividerDblclick)
  //...
END_MSG_MAP()

但是有一个问题。从 Vista 开始,标头控件具有HDS_NOSIZING 样式,这正是我们需要的。最好在可能的情况下使用原生功能,因此通知过滤以此方式实现。

LRESULT OnHeaderBeginTrack(LPNMHDR pnmh) {
  // For Vista and above message stays unhandled, return value in this case ignored
  SetMsgHandled(!WTL::RunTimeHelper::IsVista());
  return TRUE; // prevent tracking
}
 
LRESULT OnHeaderDividerDblclick(LPNMHDR pnmh) {
  SetMsgHandled(!WTL::RunTimeHelper::IsVista());
  return 0; // prevent reaction (header resizing to content)
}

对于 Vista 及更高版本,还需要确保标头具有HDS_NOSIZING 样式。这在PostInit() 函数中完成,该函数在窗口子类化或创建后调用。

void PostInit() {
  //...
  if (WTL::RunTimeHelper::IsVista()) {
    GetHeader().ModifyStyle(0, HDS_NOSIZING);
  }
  //...
}

这里需要做的最后一件事是阻止光标在位于分隔符上方时发生变化。对于 Vista 及更高版本,这已经由HDS_NOSIZING 样式完成。对于 XP,需要手动处理发送到**标头控件**的WM_SETCURSOR 消息。为了在列表控件中处理标头控件消息,实现使用了CContainedWindow 类,有关此类的更多信息可以在这篇优秀的文章中找到:[3]。因此,为了捕获发送到标头控件的鼠标消息,我们执行以下操作

// At first add alternative message map with WM_SETCURSOR handler
BEGIN_MSG_MAP_EX(CListColumnAutoSizeImplBase)
  //...
ALT_MSG_MAP(T::kHeaderMsgMapId) // header control message map
  MSG_WM_SETCURSOR(OnHeaderSetCursor)
END_MSG_MAP()
 
// Next add CContainedWindow variable for header, we use its specialized version to be
// able to call header control functions without any type casts
ATL::CContainedWindowT<WTL::CHeaderCtrl> header_;
 
// CContainedWindow needs CMessageMap-based class where to pass messages (first arg) and
// map id in this message map (second arg)
CListColumnAutoSizeImplBase(): header_(this, T::kHeaderMsgMapId), ...
 
// Subclass header control in class initialization function
void PostInit() {
  //...
  if (WTL::RunTimeHelper::IsVista()) {
    GetHeader().ModifyStyle(0, HDS_NOSIZING);
  }
  else {
    ATLVERIFY(header_.SubclassWindow(GetHeader()));
  }
}
 
// And finally process cursor message
BOOL OnHeaderSetCursor(ATL::CWindow wnd, UINT nHitTest, UINT message) {
  return TRUE; // prevent cursor change over dividers
}

列大小调整

当控件大小更改以及列表内容更改时,应更新列宽。首先通过处理WM_SIZE 消息来完成。

LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam) {
  T* pT = static_cast<T*>(this);
  if (pT->IsAutoUpdate() && SIZE_MINIMIZED != wParam) {
    // Need update only columns with variable width
    pT->UpdateVariableWidthColumns();
  }
  SetMsgHandled(FALSE);
  return 0;
}

内容更改时更新列宽具有“延迟”实现 - 更新在任何可能更改内容的消息之后进行。类不跟踪实际更改,因为这看起来很容易出错,特别是对于高度定制的列表控件。因此,对于任何可能更改内容的消息(LVM_INSERTITEMLVM_SETITEMTEXTA 等),都会发出以下函数

LRESULT OnItemChange(UINT uMsg, WPARAM wParam, LPARAM lParam) {
  // Apply this action
  LRESULT lr = DefWindowProc(uMsg, wParam, lParam);
  T* pT = static_cast<T*>(this);
  // If auto update turned on
  if (pT->IsAutoUpdate()) {
    // Update widths for all columns
    pT->UpdateColumnsWidth();
  }
  return lr;
}

对于项目数量较少的列表,没有理由执行更具体和更优化的操作。对于包含数千个元素的列表,可能需要更具体的操作。在这种情况下,有两种方法。第一种是通过调用EnableAutoUpdate(false) 关闭自动更新,并在适当的时候使用UpdateColumnsWidth() 函数手动更新列。第二种是实现从CListColumnAutoSizeImplBase 派生的自己的类,并在其中重写UpdateColumnsWidth() 函数。

使用标头控件能够将列调整为内容大小来更新具有固定宽度的列,但需要一个小技巧。

void UpdateFixedWidthColumns() {
  // The easiest way to not screw it up is left resizing to the system. But in
  // case of LVSCW_AUTOSIZE_USEHEADER it resizes last column to all remaining
  // space. Workaround - made column not last by adding fake column to the end
  int count = GetHeader().GetItemCount();
  ATLVERIFY(count == InsertColumn(count, _T("")));
  T* pT = static_cast<T*>(this);
  // Loop for all columns except added
  for (int i = 0; i < count; i ++) {
    if (!pT->IsVariableWidthColumn(i)) {
      // Column here definitely not last so it will not resize content to remaining space
      SetColumnWidth(i, LVSCW_AUTOSIZE_USEHEADER);
    }
  }
  ATLVERIFY(DeleteColumn(count));
}

这种方法应该肯定能在任何情况下都能工作,而不是自定义宽度计算。若要使用不同的算法,可以实现CListColumnAutoSizeImplBase 的子类,并重写UpdateColumnsWidth() 函数。

最后是关于更新可变宽度列。

void UpdateVariableWidthColumns() {
  // Get full available width
  RECT rect = {0};
  GetClientRect(&rect);
  // Substract from it widhts of fixed columns
  T* pT = static_cast<T*>(this);
  int count = GetHeader().GetItemCount();
  for (int i = 0; i < count; i ++) {
    if (!pT->IsVariableWidthColumn(i)) {
      rect.right -= GetColumnWidth(i);
    }
  }
  // And apply remaining width to our variable width column
  SetColumnWidth(variable_width_column_, rect.right - rect.left);
}
© . All rights reserved.