Resize columns to avoid horizontal scroll

Resize columns to avoid horizontal scroll

 
Avoid horizontal scroll and effectivelly use whole horizontal control width: resize all columns except one to their content and this one to all remaining space.
 

Introduction

There are number of articles on CodeProject related to avoiding inappropriate horisontal scrolling in list controls ([1][2]). This article provides one more way to do this. This way hovers situation when you have one column needed to resize to all available space and (optionally) other columns which have known small width and need to be sized to content. Implementation prevents horisontal scroll appearing or flickering and properly reacts on vertical scrolling appearing.

See at screenshot below, even if text appears partially hidden - in this case it is smaller problem than invisibility of last column in case of horisontal scroll use (especially if LVS_EX_LABELTIP list style used). Example source code contains few other appropriate usage ways.

Image 1

Using the code

Add CListColumnAutoSize member to your window class implementation:

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

  CListColumnAutoSize columns_resize_;
};

Subclass your list control at window initialization, for example in WM_INITDIALOG handler:

C++
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;
}

Well, in most cases that is all what need to do. Class resizes columns automatically. Automatic update can be turned off when it need. This is usefull for example when you add/remove/change big number of items at once, in this case auto updating can give huge overhead so better turn it off before batch operation and turn on after it. Functions which help to do this:

C++
// 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();

Source code containt one more class CListColumnAutoSizeEx which implements same columns resizing mechanism (i.e. resize to available space) but for several columns instead one. One usage difference here - at setting variable width column need also set percentage of available space which it should use. Example:

C++
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

Now we'll see how it works.

Background

Implementation consist of two parts: preventing header resize by the user and actual columns resizing in response to list content change or list control resize.

Prevent header resize 

Header can be resized by several ways. First is press Ctrl and + key. This causes all list columns resize to their content (ignoring header text width). Prevents by filtering appropriate WM_KEYDOWN message:

C++
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));
}

Note that implementation uses 'cracked' message map defined in WTL header <atlcrack.h>

Other ways to resize header is drag header's divider or double click on it (which causes resizing to content for column at left of clicked divider, also ignoring text width of column header). This prevented by filtering HDN_BEGINTRACK and HDN_DIVIDERDBLCLICK notifications from header control:

C++
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()

But there is a moment. Since Vista header control have style HDS_NOSIZING which does exactly what we need. Better to use native features when it is possible so notification filtering implemented by this way:

C++
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)
}

For Vista and above also need ensure that header have HDS_NOSIZING style. This done in PostInit() function which calls after windows subclassing or creation:

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

The last thing need to do here is prevent cursor changing when it is over divider. For Vista and above this is already done by HDS_NOSIZING style. For XP need manually handle WM_SETCURSOR message sended to header control. To handle header control messages in list control implementation used CContainedWindow class, more info about which can be found this great article: [3]. So, to catch mouse messages sended to header control we do next things:

 
// 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
}

Columns resizing

Columns width should be updated when control size changed and when list content changed. First done by processing WM_SIZE message:

C++
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;
}

Updating column width on content change have 'lazy' implementation - update goes after any message which may change content. Class does not track actual change because this looks too error-prone, especially for highly customized list controls. So for any message which may change content (LVM_INSERTITEMLVM_SETITEMTEXTA etc) there emits next function:

C++
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;
}

For list with small numbers of items there is no reasons to do anything more specific and optimal. For lists with thousands elements maybe need something more specific. In this case there are two ways. First is set auto update off by calling EnableAutoUpdate(false) and manually update columns at appropriate time using UpdateColumnsWidth() function. Second is implement own class delivered from CListColumnAutoSizeImplBase and override UpdateColumnsWidth() function there.

Updating of columns with fixed width done using header control ability resize column to content, but with small hack:

C++
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));
}

Instead of custom width calculation this approach should definitely work in any cases. To use different algorithm can be implemented child class of CListColumnAutoSizeImplBase with overriden UpdateColumnsWidth() function.

And finally about updating variable width column:

 
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);
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

posted @ 2022-12-13 13:59  小风风的博客  阅读(16)  评论(0编辑  收藏  举报