Suppress Flickering Scrollbars in Autosizing CListCtrl






4.87/5 (27 votes)
How to avoid flickering scrollbars that appear when resizing CListCtrl which has a last column with the LVSCW_AUTOSIZE_USEHEADER value set.
Introduction
The LVSCW_AUTOSIZE_USEHEADER
value simplifies the adjustment of column widths when a 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 a control may create annoying artifacts and I could not find any resource that deals with this consistently. This article proposes a solution to avoid them. The recipe is implemented through the CLastColumnAutoResizingListCtrl
class derived from the CListCtrl
control.
Background
It is often useful to adjust the width of the last column in a listview control so that it extends to the right end of the control. The logical approach would be to evaluate the width of the last column by subtracting all other column widths from the list control's client rectangle width. To simplify this, a special value LVSCW_AUTOSIZE_USEHEADER
(equal to -2
) has been introduced for the column width. If this value is used with the last column, its width will automatically be calculated to fill the remaining width of the control. For other columns, this value will fit the column width to its content.
However, if the control is resized, the last column width will keep its initial width. This is quite logical if we understand that LVSCW_AUTOSIZE_USEHEADER
is just a hint for a layout procedure to calculate the column width and is not stored permanently. Therefore, to assure that the column width is adjusted on each resize, it is necessary to set the 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. A better approach is to subclass CListCtrl
and reapply the LVSCW_AUTOSIZE_USEHEADER
value inside the 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 the GetLastColumnIndex
helper function is called:
int CLastColumnAutoResizingListCtrl::GetLastColumnIndex()
{
return GetHeaderCtrl()->GetItemCount() - 1;
}
This way, each control instance takes care of its own column adjustment.
Nevertheless, while narrowing the control, the 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 the control's height is such that both the horizontal and vertical scrollbars flicker simultaneously.
Moreover, scrollbars may remain visible although there is no need for them. To prevent this, the OnSize
function must include statements that postpone the window update procedure till 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, the flickering scrollbar problem still persists. Obviously, the client area is not always calculated correctly during control resize.
Avoiding Flickering Scrollbars
When the LVSCW_AUTOSIZE_USEHEADER
value is applied, the last column width is evaluated so that it fills the right-hand side of the control. During control resizing, the column will retain that width until the LVSCW_AUTOSIZE_USEHEADER
value is reapplied and therefore the flickering horizontal scrollbar appears as the control is being narrowed. To suppress the scrollbar when it is not needed actually, the last column width must be adjusted before the control is repainted. This can be done by overriding the WM_WINDOWPOSCHANGING
message handler. The function takes a pointer to the WINDOWPOS
structure as a parameter, which among others contains the 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, the derived list control exhibits a bizarre feature: if the list is scrolled down (with the vertical scrollbar visible) and then enlarged until the vertical scrollbar is hidden, the first item is regularly not moved to the top of the 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, the layout is not evaluated correctly. To circumvent this problem, the OnWindowPosChanging
function member must include code that checks if the visible scrollbar will become hidden. In such a case, the EnsureVisible
member function is called to ensure that the first item will scroll to the top of the client area:
kipvoid 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 scrollbar to appear
if (deltaX < 0) {
int lastColumnIndex = GetLastColumnIndex();
int columnWidth = GetColumnWidth(lastColumnIndex);
SetColumnWidth(lastColumnIndex, columnWidth + deltaX);
}
// calculate control height change
int deltaY = lpwndpos->cy - (rect.bottom - rect.top);
// if area decreases, skip further processing
if (deltaX <= 0 && deltaY <= 0)
return;
// is vertical scrollbar visible?
if (IsScrollBarVisible(WS_VSCROLL)) {
// height required for all items to be visible
int allItemsHeight = GetAllItemsHeight();
// row (i.e item) width
int rowWidth = GetRowWidth();
// calculate new client height and width after resize
RECT clientRect;
GetClientRect(&clientRect);
int newClientHeight = clientRect.bottom - GetHeaderHeight() + deltaY;
// is horizontal scrollbar is visible?
if (IsScrollBarVisible(WS_HSCROLL)) {
int newClientWidth = clientRect.right + deltaX;
int hScrollBarHeight = GetSystemMetrics(SM_CYHSCROLL);
int vScrollBarWidth = GetSystemMetrics(SM_CXVSCROLL);
// if both scrollbars will be hidden then correct
// new height of client area
if ((newClientHeight + hScrollBarHeight >= allItemsHeight) &&
(newClientWidth + vScrollBarWidth >= rowWidth))
newClientHeight += hScrollBarHeight;
// more code to come here! (see next section)
}
// ensure the first item is moved to the top
if (newClientHeight >= allItemsHeight)
EnsureVisible(0, FALSE);
}
}
In the code above, several helper functions have been used:
int CLastColumnAutoResizingListCtrl::GetAllItemsHeight()
{
if (GetItemCount() == 0)
return 0;
RECT itemRectLast;
GetItemRect(GetItemCount() - 1, &itemRectLast, LVIR_BOUNDS);
RECT itemRectFirst;
GetItemRect(0, &itemRectFirst, LVIR_BOUNDS);
return itemRectLast.bottom - itemRectFirst.top;
}
int CLastColumnAutoResizingListCtrl::GetRowWidth()
{
if (GetItemCount() == 0)
return 0;
RECT rect;
GetItemRect(0, &rect, LVIR_BOUNDS);
return rect.right - rect.left;
}
int CLastColumnAutoResizingListCtrl::GetHeaderHeight()
{
RECT headerRect;
GetHeaderCtrl()->GetWindowRect(&headerRect);
return headerRect.bottom - headerRect.top;
}
bool CLastColumnAutoResizingListCtrl::IsScrollBarVisible(DWORD scrollBar)
{
return (GetWindowLong(m_hWnd, GWL_STYLE) & scrollBar) != 0;
}
Similarly, blank leading row may appear when scrollbars are visible and items are deleted from the list. Therefore, correction procedure has been extracted into PrepareForScrollbarsHiding()
method which is invoked both from the above described OnWindowPosChanging()
method and from OnLvnDeleteitem()
method that handles LVN_DELETEITEM
notifications.
Column Offset Issue
When expanding a list control which has view offset horizontally, the moment the horizontal scroll bar is hidden, a blank vertical stripe may appear at the left end of the control as seen on the rightmost image below:
It is obvious that the view must be scrolled in order to align the first column to the left border. This can be done by inserting the following statements into the OnWindowPosChanging
function:
void CLastColumnAutoResizingListCtrl::OnWindowPosChanging(WINDOWPOS* lpwndpos)
{
// same as above
// following statements should replace
// "more code to come here" comment
// if vertical scrollbar is going to be hidden then
// correct new width of client area
if (newClientHeight >= allItemsHeight)
newClientWidth += vScrollBarWidth;
// horizontal scrollbar is going to be hidden...
if (newClientWidth >= rowWidth) {
// ...so scroll the view to the left to avoid
// blank column at the left end
SendMessage(WM_HSCROLL, LOWORD(SB_LEFT), NULL);
// ensure that bottom item remains visible
if (IsItemVisible(GetItemCount() - 1))
PostMessage(WM_VSCROLL, LOWORD(SB_BOTTOM), NULL);
}
// same as above (final if condition)
// ...
}
The last if
condition makes sure that the bottom item remains visible when the horizontal scroll bar is hidden. If this condition is omitted, the situation depicted below on the right (the last item not scrolled into the view) would occur.
Header Resizing
The list control should also adjust the last column width when any column header is resized. The user can resize columns by:
- dragging a divider between the column headers
- double-clicking on a column header divider, or
- simultaneously pressing the
<Ctrl>
and+
key on the numeric keypad
Each operation will be handled separately.
Dragging a Divider
If the user drags a divider between two column headers to the right, a horizontal scrollbar will appear because the rightmost column is pushed outside the client area. This triggers the WM_SIZE
message, calling our overridden OnSize
function that reevaluates the column width and the scrollbar will be hidden (if it is not needed). Evidently, this produces a flickering scrollbar. In order to avoid this flickering, the last column must be narrowed before the control is repainted. On the other side, if the user drags a divider to the left, the rightmost column must be widened or it will depart from the right border of the control.
In order to cope with this problem, three notifications that are invoked during the header divider dragging must be processed:
HDN_BEGINTRACK
when the user begins dragging a divider- a sequence of
HDN_ITEMCHANGING
notifications while the divider is being dragged, and finally HDN_ENDTRACKING
when the user releases the divider
When the user begins dragging a divider, the current width of the column resized is stored and a flag is set indicating that the 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 the last column from being resized since its width is evaluated automatically by the control. Hence, for that column, the function sets *pResult
to 1. m_oldColumnWidth
and m_trackingHeaderDivider
are class data members of int
and BOOL
types, respectively.
The flag indicating the drag procedure is reset in the HND_ENDTRACKING
notification handler:
void CLastColumnAutoResizingListCtrl::OnHdnEndtrack(NMHDR *pNMHDR, LRESULT *pResult) {
m_trackingHeaderDivider = FALSE;
*pResult = 0;
}
The last column header resizing is done inside the 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 the dragging process, this function is actually called in pairs. Initially, it is called for the column that is being resized. In that invocation, the 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 the list control in Report view has the 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 the HDN_TRACK
notification because the control is not updated as long as the user doesn't release the divider. At that moment, the 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 the divider has been released, the 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 the corresponding column width so that its content fits exactly. Again, the last column width must be readjusted. To accomplish this, the 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 a numeric keypad adjusts all the column widths to their content. For our list control, the last column must be excluded. To achieve this, the WM_KEYDOWN
message handler is implemented, inside which the 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 the LVSCW_AUTOSIZE_USEHEADER
value to each of them.
Cursor Appearance
The above implementations (hopefully) solve all functional aspects of our list control. There still remains a visual aspect: although it is not possible to resize the last column manually, the cursor will change its shape above the rightmost divider that is just by the right edge of the control. To deal with this problem, the list control's header will be replaced with CNonExtendableHeaderCtrl
(subclassed from CHeaderCtrl
) which will prevent a cursor change (an approach similar to the one presented in "Prevent column resizing (2)" by Charles Herman). The WM_NCHITTEST
message handler checks if the cursor is above the resize area and sets the 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;
}
The WM_SETCURSOR
message handler is responsible for preventing a cursor change by 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 type CNonExtendableHeaderCtrl
is added to the list control definition and is attached to the list control inside the overridden PreSubClass
function member:
void CLastColumnAutoResizingListCtrl::PreSubclassWindow()
{
m_header.SubclassDlgItem(0, this);
CListCtrl::PreSubclassWindow();
}
Using the Code
Simply include the header and source files for the CLastColumnAutoResizingListCtrl
and CNonExtendableHeaderCtrl
classes into your code. Then replace the declarations of CListCtrl
instances in your project with CLastColumnAutoSizingListCtrl
. The control will retain its functionality even if the header is hidden (LVS_NOCOLUMNHEADER
style) so it can be substituted with a list box when a single column is used.
Please note that the code was written assuming the control is in Report view (i.e., the LVS_REPORT
style set) and no corresponding check has been included in the code. Also, if new items are added or current items changed after the control is displayed, you must not forget to re-apply the LVSCW_AUTOSIZE_USEHEADER
value to the last column.
The demo project contains two list controls that are resized with the parent window. The control on the left side is an ordinary CListCtrl
for which LVSCW_AUTOSIZE_USEHEADER
is set whenever the parent dialog is resized. On the right-hand side is the CLastColumnAutoResizingListCtrl
control.
History
- 20th February, 2011: Initial version.
- 22nd February, 2011: Fixed the bug outlined by Hans Dietrich.
- 1st March, 2011: The
OnWindowPosChanging
function has been modified, "Blank Column Issue" section added. - 19th November, 2013: Blank row issue fixed for the case when items are deleted from the list.