Animated control sample

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.

Screenshot of flickering scrollbar

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.

Screenshot with blank row at the top

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
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架