Suppress Flickering Scrollbars in Autosizing CListCtrl

Introduction
LVSCW_AUTOSIZE_USEHEADER
value simplifies adjustment of column widths when listview
is in details view. There are several articles that already have covered the use of this value (e.g. "Autosize ListCtrl Header"). However, resizing such control may create annoying artifacts and I could not find any resource that deals with them consistently. This article proposes a solution how to avoid them. The recipe is implemented through CLastColumnAutoResizingListCtrl
class derived from CListCtrl
control.
Background
It is often useful to adjust the width of the last column in the listview
control so that it extends to the right end of the control. Logical approach would be to evaluate the width of the last column by subtracting all other column widths from list control's client rectangle width. To simplify this, a special value LVSCW_AUTOSIZE_USEHEADER
(equal to -2
) has been introduced for column width. If this value is used with the last column, its width will be automatically calculated to fill the remaining width of the control. For other columns, this value will fit column width to its content.
However, if control is resized, last column width will keep its initial width. This is quite logical if we understand that LVSCW_AUTOSIZE_USEHEADER
is just a hint for layout procedure to calculate column width and is not stored permanently. Therefore, to assure that column width is adjusted on each resize, it is necessary to set LVSCW_AUTOSIZE_USEHEADER
flag after each resize event. One approach would be to handle this in the parent dialog resizing procedure but then this must be applied for each list control instance separately. Better approach is to subclass CListCtrl
and reapply LVSCW_AUTOSIZE_USEHEADER
value inside overridden WM_SIZE
message handler (it is assumed that the reader is familiar with how to add message handlers and only function bodies will be listed in the article):
void CLastColumnAutoResizingListCtrl::OnSize(UINT nType, int cx, int cy)
{
CListCtrl::OnSize(nType, cx, cy);
// apply only when control is not minimized
if (cx > 0)
SetColumnWidth(GetLastColumnIndex(), LVSCW_AUTOSIZE_USEHEADER);
}
in which GetLastColumnIndex
helper function is called:
void CLastColumnAutoResizingListCtrl::GetLastColumnIndex()
{
return GetHeaderCtrl()->GetItemCount() - 1;
}
This way, each control instance takes care of its own column adjustment.
Nevertheless, while narrowing the control, horizontal scrollbar flickers as shown in the animated screenshot below. This can be pretty annoying if there are several list controls being resized simultaneously or if control's height is such that both horizontal and vertical scrollbars flicker simultaneously.
Moreover, scrollbars may remain visible although there is no need for them. To prevent this, OnSize
function must include statements that postpone window update procedure till the moment all columns have been evaluated:
void CLastColumnAutoResizingListCtrl::OnSize(UINT nType, int cx, int cy)
{
CListCtrl::OnSize(nType, cx, cy);
// apply only when control is not minimized
if (cx > 0) {
SetRedraw(FALSE);
SetColumnWidth(GetLastColumnIndex(), LVSCW_AUTOSIZE_USEHEADER);
SetRedraw(TRUE);
Invalidate();
UpdateWindow();
}
}
However, flickering scrollbar problem still persists. Obviously, client area is not always calculated correctly during control resize.
Avoiding Flickering Scrollbars
When LVSCW_AUTOSIZE_USEHEADER
value is applied, last column width is evaluated so that it fills the right-hand side of the control. During control resizing, column will retain that width until LVSCW_AUTOSIZE_USEHEADER
value is reapplied and therefore flickering horizontal scrollbar appears as control is being narrowed. To suppress scrollbar when it is not needed actually, last column width must be adjusted before control is repainted. This can be done by overriding WM_WINDOWPOSCHANGING
message handler. Function takes a pointer to WINDOWPOS
structure as parameter which among others contains new width of the control after it will be resized. So, it is sufficient to evaluate the width change and correct the last column accordingly:
void CLastColumnAutoResizingListCtrl::OnWindowPosChanging(WINDOWPOS* lpwndpos)
{
CListCtrl::OnWindowPosChanging(lpwndpos);
// override only if control is resized
if ((lpwndpos->flags & SWP_NOSIZE) != 0)
return;
// get current size of the control
RECT rect;
GetWindowRect(&rect);
// calculate control width change
int deltaX = lpwndpos->cx - (rect.right - rect.left);
// if control is narrowed, correct the width of the last column
// to prevent horizontal scroll bar to appear
if (deltaX < 0) {
int lastColumnIndex = GetLastColumnIndex();
int columnWidth = GetColumnWidth(lastColumnIndex);
SetColumnWidth(lastColumnIndex, columnWidth + deltaX);
}
}
Blank Row(s) Issue
Unfortunately, with the above implementation, derived list control exhibits a bizarre feature: if list is scrolled down (with vertical scrollbar visible) and then being enlarged until vertical scrollbar is hidden, the first item is regularly not moved to the top of control but one or more blank rows will appear at the top of the list. This situation is shown in the screenshots below.

Apparently, at the moment the scrollbar has to be hidden, layout is not evaluated correctly. To circumvent this problem, OnWindowPosChanging
function member must include code that checks if visible scrollbar will become hidden. In such case, EnsureVisible
member function is called to ensure that the first item will scroll to the top of the client area:
void CLastColumnAutoResizingListCtrl::OnWindowPosChanging(WINDOWPOS* lpwndpos)
{
CListCtrl::OnWindowPosChanging(lpwndpos);
// override only if control is resized
if ((lpwndpos->flags & SWP_NOSIZE) != 0)
return;
// get current size of the control
RECT rect;
GetWindowRect(&rect);
// calculate control width change
int deltaX = lpwndpos->cx - (rect.right - rect.left);
// if control is narrowed, correct the width of the last column
// to prevent horizontal scroll bar to appear
if (deltaX < 0) {
int lastColumnIndex = GetLastColumnIndex();
int columnWidth = GetColumnWidth(lastColumnIndex);
SetColumnWidth(lastColumnIndex, columnWidth + deltaX);
}
// is vertical scrollbar visible?
else if ((GetWindowLong(GetSafeHwnd(), GWL_STYLE) & WS_VSCROLL) == WS_VSCROLL) {
// vertical scroll bar may become hidden either if height of control is increased
// or if width is increased (in case both scrollbars are visible)
int deltaY = lpwndpos->cy - (rect.bottom - rect.top);
if (deltaX > 0 || deltaY > 0) {
// calculate the client height required for all items to be visible
RECT viewRect;
GetViewRect(&viewRect);
int allItemsHeight = viewRect.bottom - viewRect.top;
// calculate new client rectangle height after resize
RECT clientRect;
GetClientRect(&clientRect);
RECT headerRect;
GetHeaderCtrl()->GetWindowRect(&headerRect);
int newClientHeight = clientRect.bottom -
(headerRect.bottom - headerRect.top) + deltaY;
// if horizontal scrollbar is visible...
if ((GetWindowLong(GetSafeHwnd(), GWL_STYLE) & WS_HSCROLL) == WS_HSCROLL) {
int scrollBarHeight = GetSystemMetrics(SM_CYHSCROLL);
// ...and is going to be hidden after resize,
// then correct new height of client area
if (newClientHeight + scrollBarHeight >= allItemsHeight)
newClientHeight += scrollBarHeight;
}
// ensure the first item is moved to the top
if (allItemsHeight <= newClientHeight)
EnsureVisible(0, FALSE);
}
}
}
After WM_WINDOWPOSCHANGING
handler has been added, OnSize
function can be reverted to the initial version (without SetRedraw
fuction calls) since OnWindowPosChanging
adjusts columns before OnSize
is encountered.
Header Resizing
List control should also adjust last column width when any column header is resized. User can resize columns by:
- dragging a divider between column headers
- double-clicking on a column header divider, or
- simultaneously pressing
<Ctrl>
and+
key on numeric keypad
Each operation will be handled separately.
Dragging a Divider
If user drags a divider between two column headers to the right, horizontal scrollbar will appear because rightmost column is pushed outside client area. This triggers WM_SIZE
message, calling our overridden OnSize
function that reevaluates column width and scrollbar will be hidden (if it is not needed). Evidently, this produces flickering scrollbar. In order to avoid this flickering, the last column must be narrowed before the control is repainted. On the other side, if user drags a divider to the left, the rightmost column must be widened or it will depart from right border of control.
In order to cope with this problem, three notifications that are invoked during header divider dragging must be processed:
HDN_BEGINTRACK
when user begins dragging a divider- a sequence of
HDN_ITEMCHANGING
notifications while divider is being dragged, and finally HDN_ENDTRACKING
in the moment user releases divider
When user begins dragging a divider, the current width of the column resized is stored and a flag set indicating that drag procedure is going on:
void CLastColumnAutoResizingListCtrl::OnHdnBegintrack(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
if ((phdr->pitem) != 0 && (phdr->pitem->mask & HDI_WIDTH) != 0) {
// prevent resizing the last column
if (phdr->iItem == GetLastColumnIndex()) {
*pResult = 1;
return;
}
// save current width of the column being resized
m_oldColumnWidth = phdr->pitem->cxy;
m_trackingHeaderDivider = TRUE;
}
*pResult = 0;
}
Note that we must prevent last column to be resized since its width is evaluated automatically by the control. Hence, for that column function sets *pResult
to 1
. m_oldColumnWidth
and m_trackingHeaderDivider
are class data members of int
and BOOL
type, respectively.
Flag indicating drag procedure is reset in HND_ENDTRACKING
notification handler:
void CLastColumnAutoResizingListCtrl::OnHdnEndtrack(NMHDR *pNMHDR, LRESULT *pResult) {
m_trackingHeaderDivider = FALSE;
*pResult = 0;
}
Last column header resizing is done inside HDN_ITEMCHANGING
notification handler:
void CLastColumnAutoResizingListCtrl::OnHdnItemchanging(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
if ((phdr->pitem) != 0 && (phdr->pitem->mask & HDI_WIDTH) != 0 &&
m_trackingHeaderDivider) {
int lastColumnIndex = GetLastColumnIndex();
// if resizing any column except the last one...
if (phdr->iItem < lastColumnIndex) {
SetRedraw(FALSE);
int newWidth = phdr->pitem->cxy;
// if column is being widened, correct width of the last column
// to avoid flickering horizontal scrollbar
if (newWidth > m_oldColumnWidth) {
int lastColumnWidth = GetColumnWidth(lastColumnIndex) -
newWidth + m_oldColumnWidth;
SetColumnWidth(lastColumnIndex, lastColumnWidth);
}
// if column is narrowed, set LVSCW_AUTOSIZE_USEHEADER for the last column
else
SetColumnWidth(lastColumnIndex, LVSCW_AUTOSIZE_USEHEADER);
// store new width of the column
m_oldColumnWidth = newWidth;
}
else {
// all columns have been resized, so redraw the control
SetRedraw(TRUE);
Invalidate();
UpdateWindow();
}
}
*pResult = 0;
}
During dragging process, this function is actually called in pairs. Initially, it is called for the column that is being resized. In that invocation, function sets the width of the last column so it will be called again for the last column.
HDS_FULLDRAG and HDN_TRACK Issue
Since version 4.70 of ComCtl32.dll
, the header of list control in report view has HDS_FULLDRAG
style applied by default so that column content is displayed while being dragged. Notification of column width change is sent by HDN_ITEMCHANGING
messages. But if HDS_FULLDRAG
is not set, instead of HDN_ITEMCHANGING
a sequence of HDN_TRACK
messages is generated.
Actually, we do not need to handle HDN_TRACK
notification because control is not updated as long as user doesn't release divider. In that moment, list control needs to repaint the column and HDN_ITEMCHANGING
notification, followed by HDN_ITEMCHANGED
is sent. A handler for HDN_ITEMCHANGED
notification will be used to deal with headers that have HDS_FULLDRAG
reset. Since this function is called after divider has been released m_trackingHeaderDivider
data member will be reset and is used as a filter:
void CLastColumnAutoResizingListCtrl::OnHdnItemchanged(NMHDR *pNMHDR, LRESULT *pResult) {
LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
if ((phdr->pitem) != 0 && (phdr->pitem->mask & HDI_WIDTH) != 0 &&
m_trackingHeaderDivider == FALSE) {
int lastColumnIndex = GetLastColumnIndex();
// if any column except the last one was resized
if (phdr->iItem < lastColumnIndex) {
SetRedraw(FALSE);
SetColumnWidth(lastColumnIndex, LVSCW_AUTOSIZE_USEHEADER);
SetRedraw(TRUE);
Invalidate();
UpdateWindow();
}
}
*pResult = 0;
}
Double-Clicking a Divider
Double clicking a divider will adjust corresponding column width so that its content fits exactly. Again, last column width must be readjusted. To accomplish this, HDN_DIVIDERDBLCLICK
notification handler has to be added:
void CLastColumnAutoResizingListCtrl::OnHdnDividerdblclick
(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMHEADER phdr = reinterpret_cast<LPNMHEADER>(pNMHDR);
int lastColumnIndex = GetLastColumnIndex();
// prevent double-click resizing for the last column
if (phdr->iItem < lastColumnIndex) {
SetRedraw(FALSE);
SetColumnWidth(phdr->iItem, LVSCW_AUTOSIZE_USEHEADER);
SetColumnWidth(lastColumnIndex, LVSCW_AUTOSIZE_USEHEADER);
SetRedraw(TRUE);
Invalidate();
UpdateWindow();
}
*pResult = 0;
}
Ctrl And Add Key Combination
Pressing Ctrl
and +
key on numeric keypad adjusts all column widths to their content. For our list control, the last column must be excluded. To achieve this, WM_KEYDOWN
message handler is implemented, inside which base class implementation is overridden for the key combination:
void CLastColumnAutoResizingListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// handle CTRL + Add to adjust all column widths
if (nChar == VK_ADD && ::GetKeyState(VK_CONTROL) != 0) {
SetRedraw(FALSE);
for (int i = 0; i <= GetLastColumnIndex(); ++i)
SetColumnWidth(i, LVSCW_AUTOSIZE_USEHEADER);
SetRedraw(TRUE);
Invalidate();
UpdateWindow();
return;
}
CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}
As the reader may notice, it simply passes through all columns, applying LVSCW_AUTOSIZE_USEHEADER
value to each of them.
Cursor Appearance
The above implementations (hopefully) solve all functional aspects of our list control. Still remains a visual aspect: although it is not possible to resize last column manually, the cursor will change its shape above the rightmost divider that is just by the right edge of control. To deal with this problem, list control's header will be replaced with CNonExtendableHeaderCtrl
(subclassed from CHeaderCtrl
) which will prevent cursor change (approach similar to one presented in "Prevent column resizing (2)" by Charles Herman). WM_NCHITTEST
message handler checks if cursor is above resize area and sets m_headerResizeDisabled
data member (of BOOL
type) accordingly:
LRESULT CNonExtendableHeaderCtrl::OnNcHitTest(CPoint point)
{
POINT clientPoint = point;
ScreenToClient(&clientPoint);
m_headerResizeDisabled = IsOnLastColumnDivider(clientPoint);
return CHeaderCtrl::OnNcHitTest(point);
}
IsOnLastColumnDivider
is a helper function:
BOOL CNonExtendableHeaderCtrl::IsOnLastColumnDivider(const CPoint& point)
{
// width of the area above header divider in which cursor
// changes its shape to double-pointing east-west arrow
int dragWidth = GetSystemMetrics(SM_CXCURSOR);
// last column's header rectangle
RECT rect;
GetItemRect(GetItemCount() - 1, &rect);
return point.x > rect.right - dragWidth / 2;
}
WM_SETCURSOR
message handler is responsible for preventing cursor change checking the m_headerResizeDisabled
flag:
BOOL CNonExtendableHeaderCtrl::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
if (m_headerResizeDisabled)
return TRUE;
return CHeaderCtrl::OnSetCursor(pWnd, nHitTest, message);
}
Finally, we must substitute the original CHeaderCtrl
in our list control with CNonExtendableHeaderCtrl
. A data member m_header
of CNonExtendableHeaderCtrl
type is added to list control definition and is attached to list control inside overridden PreSubClass
function member:
void CLastColumnAutoResizingListCtrl::PreSubclassWindow()
{
m_header.SubclassDlgItem(0, this);
CListCtrl::PreSubclassWindow();
}
Using the Code
Simply include header and source files for CLastColumnAutoResizingListCtrl
and CNonExtendableHeaderCtrl
classes into your code. Then replace declarations of CListCtrl
instances in your project with CLastColumnAutoSizingListCtrl
.
Please note that code is written assuming the control is in report view (i.e. LVS_REPORT
style set) and no corresponding check has been included into code. Also, if new items are added or current items changed after the control is displayed, you must not forget to re-apply LVSCW_AUTOSIZE_USEHEADER
value to the last column.
The demo project contains two list controls that are resized with parent window. Control on the left side is an ordinary CListCtrl
for which LVSCW_AUTOSIZE_USEHEADER
is set whenever parent dialog is resized. On the right-hand side is the CLastColumnAutoResizingListCtrl
control.
History
- 20th February, 2011: Initial version
发表评论
Hj1Bjy Spot on with this write-up, I seriously think this web site needs much more attention. I all probably be returning to see more, thanks for the advice!
P3F9Da you ave an excellent weblog right here! would you wish to make some invite posts on my weblog?
EUhCaR Think about it I remember saying I want to screw
sciatic neuroendocrine cassan manualizing chemoprophylaxis nobel cerebrate
Great info. I love all the posts, I really enjoyed
Poor news - Syria's 'mutilation mystery' increases...