Click here to Skip to main content
Click here to Skip to main content

Suppress Flickering Scrollbars in Autosizing CListCtrl

, 19 Nov 2013
Rate this:
Please Sign up or sign in to vote.
How to avoid flickering scrollbars that appear when resizing CListCtrl which has a last column with the LVSCW_AUTOSIZE_USEHEADER value set.

Animated control sample

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.

Screenshot of flickering scrollbar

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.

Screenshot with blank row at the top

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:

Blank column

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.

Last item not scrolled in

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.

License

This article, along with any associated source code and files, is licensed under The zlib/libpng License

About the Author

Julijan Sribar
Software Developer (Senior) Syntellect
Croatia Croatia
Graduated at the Faculty of Electrical Engineering and Computing, University of Zagreb (Croatia) and received M.Sc. degree in electronics. For several years he was research and lecturing assistant in the fields of solid state electronics and electronic circuits, published several scientific and professional papers, as well as a book "Physics of Semiconductor Devices - Solved Problems with Theory" (in Croatian).
During that work he gained interest in C++ programming language and have co-written "C++ Demystified" (in Croatian), 1st edition published in 1997, 2nd in 2001, 3rd in 2010.
After book publication, completely switched to software development, programming mostly in C++ and in C#.
Follow on   LinkedIn

Comments and Discussions

 
GeneralGreat work! PinmentorHans Dietrich21-Feb-11 17:47 
AnswerRe: Great work! PinmemberJulijan Sribar22-Feb-11 11:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140709.1 | Last Updated 19 Nov 2013
Article Copyright 2011 by Julijan Sribar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid