Resize columns to avoid horizontal scroll
Resize columns to avoid horizontal scroll
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.
Using the code
Add CListColumnAutoSize
member to your window class implementation:
class CMainDlg: public CDialogImpl<CMainDlg> {
//...
CListColumnAutoSize columns_resize_;
};
Subclass your list control at window initialization, for example in WM_INITDIALOG
handler:
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:
// 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:
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:
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:
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:
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:
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:
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_INSERTITEM
, LVM_SETITEMTEXTA
etc) there emits next function:
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:
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)