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));
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:
void EnableAutoUpdate(bool enable);
bool IsAutoUpdateEnabled() const;
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 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) {
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)
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) {
SetMsgHandled(!WTL::RunTimeHelper::IsVista());
return TRUE; }
LRESULT OnHeaderDividerDblclick(LPNMHDR pnmh) {
SetMsgHandled(!WTL::RunTimeHelper::IsVista());
return 0; }
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:
BEGIN_MSG_MAP_EX(CListColumnAutoSizeImplBase)
ALT_MSG_MAP(T::kHeaderMsgMapId) MSG_WM_SETCURSOR(OnHeaderSetCursor)
END_MSG_MAP()
ATL::CContainedWindowT<WTL::CHeaderCtrl> header_;
CListColumnAutoSizeImplBase(): header_(this, T::kHeaderMsgMapId), ...
void PostInit() {
if (WTL::RunTimeHelper::IsVista()) {
GetHeader().ModifyStyle(0, HDS_NOSIZING);
}
else {
ATLVERIFY(header_.SubclassWindow(GetHeader()));
}
}
BOOL OnHeaderSetCursor(ATL::CWindow wnd, UINT nHitTest, UINT message) {
return TRUE; }
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) {
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) {
LRESULT lr = DefWindowProc(uMsg, wParam, lParam);
T* pT = static_cast<T*>(this);
if (pT->IsAutoUpdate()) {
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() {
int count = GetHeader().GetItemCount();
ATLVERIFY(count == InsertColumn(count, _T("")));
T* pT = static_cast<T*>(this);
for (int i = 0; i < count; i ++) {
if (!pT->IsVariableWidthColumn(i)) {
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() {
RECT rect = {0};
GetClientRect(&rect);
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);
}
}
SetColumnWidth(variable_width_column_, rect.right - rect.left);
}