Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CListCtrl and Cell Navigation

0.00/5 (No votes)
29 Aug 2008 1  
An example of how to implement subitem focus in an MFC list control

Introduction

Microsoft's CListCtrl has support for displaying data in a grid, but it will not support subitem keyboard navigation out of the box.

This article will demonstrate the following:

  • How to listen for events that tell what cell currently has focus.
  • How to display what cell currently has focus, while maintaining the Windows XP/Vista look.

screenshot_vista.png

screenshot_xp.png

Background

There are lots of advanced grid controls that extend the CListCtrl so it can perform subitem navigation. But, because these grid controls can be very complex, it can be difficult to see how they do it.

The article SubItem Selection in List Control demonstrates how to use custom drawing to show cell focus, but it doesn't support selecting multiple rows, and it doesn't support Windows Vista.

How to Implement Subitem Navigation in CListCtrl

There are several issues one has to take care of:

  • Have to react to the keyboard events for left and right arrow-keys, and update the focused cell accordingly.
    • CListCtrl already supports the up and down arrow-keys, so no need to handle those.
  • Have to react to events from the left mouse button, and update the focused cell accordingly.
  • When updating which cell has focus, one must be aware of the column display order in CHeaderCtrl.
  • When updating which cell has focus, one must check whether to scroll to display the whole cell.
  • Have to modify the CListCtrl drawing to make the focused cell visible to the user.
  • Extend the keyboard search navigation to also support searching in the focused column.

CListCtrl already handles row selection, so we have to ensure that this feature stays intact when implementing subitem focus.

Handle Keyboard Events

Usually, when handling keyboard events in CListCtrl, one would react to the LVN_KEYDOWN event, which is generated by the CListCtrl itself. But, if the CListCtrl has a horizontal scrollbar (many columns), then the right- and left-arrow key-events will cause the CListCtrl to scroll. Therefore, we have to intercept the keyboard event before the CListCtrl reacts to it, and this can be done with ON_WM_KEYDOWN().

BEGIN_MESSAGE_MAP(CListCtrl_CellNav, CListCtrl)
    ON_WM_KEYDOWN()        // OnKeydown
END_MESSAGE_MAP()

void CListCtrl_CellNav::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    // Catch event before the parent listctrl gets it to avoid extra scrolling
    //    - OBS! This can also prevent the key-events to reach LVN_KEYDOWN handlers
    switch(nChar)
    {
        case VK_RIGHT:    MoveFocusCell(true);    return;    // Do not allow scroll
        case VK_LEFT:    MoveFocusCell(false);    return;    // Do not allow scroll
    }
    CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}

The MoveFocusCell() function is explained in detail later in this article.

Handle Mouse Events

When the user clicks on a subitem using the left mouse-button, we have to give it focus. After having found out what subitem was clicked, we pass the event to the CListCtrl so it can handle the normal row-selection.

BEGIN_MESSAGE_MAP(CListCtrl_CellNav, CListCtrl)
    ON_WM_LBUTTONDOWN()    // OnLButtonDown()
END_MESSAGE_MAP()

void CListCtrl_CellNav::OnLButtonDown(UINT nFlags, CPoint point)
{
    // Find out what subitem was clicked
    LVHITTESTINFO hitinfo = {0};
    hitinfo.flags = nFlags;
    hitinfo.pt = point;
    SubItemHitTest(&hitinfo);

    // Update the focused cell before calling CListCtrl::OnLButtonDown()
    // as it might cause a row-repaint
    m_FocusCell = hitinfo.iSubItem;
    CListCtrl::OnLButtonDown(nFlags, point);

    // CListCtrl::OnLButtonDown() doesn't always cause a row-repaint,
    // call our own method to ensure the row is repainted
    UpdateFocusCell(hitinfo.iSubItem);
}

// Force redraw of focus row, so the focus cell becomes visible
void CListCtrl_CellNav::UpdateFocusCell(int nCol)
{
    m_FocusCell = nCol;    // Update focus cell before starting re-draw
    int nFocusRow = GetNextItem(-1, LVNI_FOCUSED);
    if (nFocusRow >= 0)
    {
        CRect itemRect;
        VERIFY( GetItemRect(nFocusRow, itemRect, LVIR_BOUNDS) );
        InvalidateRect(itemRect);
        UpdateWindow();
    }
}

We also need to handle the right-click mouse event ON_WM_RBUTTONDOWN(), but it is pretty much identical to how the left-click mouse event is handled, which can be seen in the source code.

Handle Column Display Order

Microsoft extended the CListCtrl with the ability to change the order of columns by using drag 'n' drop (LVS_EX_HEADERDRAGDROP). When a column is dragged to a new position, it keeps its column-ID, but the display-order in the CHeaderCtrl is changed.

We have to use the display-order to find the following column, and this job is handled by MoveFocusCell().

void CListCtrl_CellNav::MoveFocusCell(bool right)
{
    if (GetItemCount()<=0)
    {
        m_FocusCell = -1;    // Entire row selected
        return;
    }

    if (m_FocusCell == -1)
    {
        // Entire row already selected
        if (right)
        {
            // Change to the first column in the current order
            m_FocusCell = GetHeaderCtrl()->OrderToIndex(0);
        }
    }
    else
    {
        // Convert focus-cell to order index
        int nOrderIndex = -1;
        for(int i = 0; i < GetHeaderCtrl()->GetItemCount(); ++i)
        {
            int nCol = GetHeaderCtrl()->OrderToIndex(i);
            if (nCol == m_FocusCell)
            {
                nOrderIndex = i;
                break;
            }
        }

        // Move to the following column
        if (right)
            nOrderIndex++;
        else
            nOrderIndex--;

        // Convert order-index to focus cell
        if (nOrderIndex >= 0
         && nOrderIndex < GetHeaderCtrl()->GetItemCount())
        {
            m_FocusCell = GetHeaderCtrl()->OrderToIndex(nOrderIndex);
        }
        else if (!right)
            m_FocusCell = -1;    // Entire row selection
    }

    // Ensure the column is visible
    if (m_FocusCell >= 0)
    {
        VERIFY( EnsureColumnVisible(m_FocusCell, false) );
    }

    // Ensure the row is repainted, so the focused cell is visible
    UpdateFocusCell(m_FocusCell);
}

Ensure that the Column is Visible

The CListCtrl can contain multiple columns, and when using the arrow-keys to navigate, we have to ensure that the focused column is visible for the user. This is done by scrolling to the focused column, and I have pretty much "stolen" the code from Daniel Frey's article: Ensure (partial) visibility of a column.

BOOL CListCtrl_CellNav::EnsureColumnVisible(int nCol, bool bPartialOK)
{
    if (nCol < 0 || nCol >= GetHeaderCtrl()->GetItemCount())
        return FALSE;

    CRect rcHeader;
    if (GetHeaderCtrl()->GetItemRect(nCol, rcHeader)==FALSE)
        return FALSE;

    CRect rcClient;
    GetClientRect(&rcClient);

    int nOffset = GetScrollPos(SB_HORZ);

    if(bPartialOK)
    {
        if((rcHeader.left - nOffset < rcClient.right)
         && (rcHeader.right - nOffset > 0) )
        {
            return TRUE;
        }
    }

    int nScrollX = 0;

    if((rcHeader.Width() > rcClient.Width()) || (rcHeader.left - nOffset < 0))
    {
        nScrollX = rcHeader.left - nOffset;
    }
    else if(rcHeader.right - nOffset > rcClient.right)
    {
        nScrollX = rcHeader.right - nOffset - rcClient.right;
    }

    if(nScrollX != 0)
    {
        CSize size(nScrollX, 0);
        if (Scroll(size)==FALSE)
            return FALSE;
    }

    return TRUE;

}

Handle Custom Drawing of Focused Subitem

The CListCtrl handles the drawing of items and selection all by itself. If holding down the CTRL key and using the arrow-keys to navigate, then we will see that the CListCtrl just uses a focus-rectangle to display the focused row.

With custom drawing, we can change the normal drawing of the row-focus rectangle. The trick is to remove the flag that tells the CListCtrl that it should draw the focus-rectangle (for the entire row), and instead provide our own implementation of how to draw the row focus rectangle (for the subitem in focus). We will soon realize that the focus rectangle is quite hard to see when moving within a selected row. This is solved by removing the flag that tells the CListCtrl that it should draw the selection background (for the subitem in focus).

BEGIN_MESSAGE_MAP(CListCtrl_CellNav, CListCtrl)
    ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, OnCustomDraw)
END_MESSAGE_MAP()

void CListCtrl_CellNav::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
{
    NMLVCUSTOMDRAW* pLVCD = (NMLVCUSTOMDRAW*)(pNMHDR);
    int nRow = (int)pLVCD->nmcd.dwItemSpec;
    int nRowItemData = (int)pLVCD->nmcd.lItemlParam;

    switch (pLVCD->nmcd.dwDrawStage)
    {
        case CDDS_PREPAINT:
            *pResult |= CDRF_NOTIFYITEMDRAW;
            break;
        // Before painting a row
        case CDDS_ITEMPREPAINT:
        {
            if (pLVCD->nmcd.uItemState & CDIS_FOCUS)
            {
                // If drawing focus row, then remove focus state
                // and request to draw it later
                if (GetNextItem(-1, LVNI_FOCUSED)==nRow)
                {
                    if (m_FocusCell >= 0)
                    {
                        // We want to draw a cell-focus-rectangle
                        // instead of row-focus-rectangle
                        pLVCD->nmcd.uItemState &= ~CDIS_FOCUS;
                        *pResult |= CDRF_NOTIFYPOSTPAINT;
                    }
                }
            }

            if (pLVCD->nmcd.uItemState & CDIS_SELECTED)
            {
                // Remove the selection color for the focus cell,
                // to make it easier to see focus
                if (m_FocusCell!=-1)
                    *pResult |= CDRF_NOTIFYSUBITEMDRAW;
            }
        } break;

        // Before painting a cell
        case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
        {
            // Remove the selection color for the focus cell,
            // to make it easier to see focus
            int nCol = pLVCD->iSubItem;
            if (pLVCD->nmcd.uItemState & CDIS_SELECTED
             && m_FocusCell==nCol
             && GetNextItem(-1, LVNI_FOCUSED)==nRow)
            {
                pLVCD->nmcd.uItemState &= ~CDIS_SELECTED;
            }
        } break;

        // After painting the entire row
        case CDDS_ITEMPOSTPAINT:
        {
            if (GetNextItem(-1, LVNI_FOCUSED)!=nRow)
                break;

            // Perform the drawing of the focus rectangle
            if (m_FocusCell >= 0)
            {
                // Draw the focus-rectangle for a single-cell
                CRect rcHighlight;
                CDC* pDC = CDC::FromHandle(pLVCD->nmcd.hdc);
                VERIFY( GetCellRect(nRow, m_FocusCell, rcHighlight) );
                pDC->DrawFocusRect(rcHighlight);
            }
        } break;
    }
}

// Used instead of GetSubItemRect(), which returns the entire
// row-rect for label-column (nCol==0)
BOOL CListCtrl_CellNav::GetCellRect(int nRow, int nCol, CRect& rect)
{
    // Find the top and bottom of the cell-rectangle
    CRect rowRect;
    if (GetItemRect(nRow, rowRect, LVIR_BOUNDS)==FALSE)
        return FALSE;

    // Find the left and right of the cell-rectangle using the CHeaderCtrl
    CRect colRect;
    if (GetHeaderCtrl()->GetItemRect(nCol, colRect)==FALSE)
        return FALSE;

    // Adjust for scrolling
    colRect.left -= GetScrollPos(SB_HORZ);
    colRect.right -= GetScrollPos(SB_HORZ);

    rect.left = colRect.left;
    rect.top = rowRect.top;
    rect.right = colRect.right;
    rect.bottom = rowRect.bottom;
    return TRUE;
}

The reason for using GetCellRect() is that GetSubItemRect() cannot return the proper focus-rectangle for the label-column (even when using LVIR_LABEL). The label column is the first column inserted in the CListCtrl, and it has some small differences compared to the following inserted columns.

I have excluded the details about handling the extended style LVS_EX_GRIDLINES, but this can be seen in the source code. This is because, when CListCtrl draws grid lines, we have to ensure that the focus rectangle is not placed on top of the grid-lines.

Extending Keyboard Search to Subitems

Microsoft Windows Explorer allows one to skip to the first row matching the characters entered by the keyboard. This makes row navigation a lot easier, and this feature is also supported by the CListCtrl, but only for the label column. We want to handle the keyboard characters events, but we will use ON_WM_CHAR() instead of ON_WM_KEYDOWN(), because we only want letters and digits and not all of the keyboard characters.

BEGIN_MESSAGE_MAP(CListCtrl_CellNav, CListCtrl)
    ON_WM_CHAR()        // OnChar
END_MESSAGE_MAP()

void CListCtrl_CellNav::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    // No input within 2 seconds, resets the search
    if (m_LastSearchTime.GetCurrentTime() >= (m_LastSearchTime+2)
     && m_LastSearchString.GetLength()>0)
        m_LastSearchString = "";

    // Changing cells, resets the search
    if (m_LastSearchCell!=m_FocusCell)
        m_LastSearchString = "";

    // Changing rows, resets the search
    if (m_LastSearchRow!=GetNextItem(-1, LVNI_FOCUSED))
        m_LastSearchString = "";

    m_LastSearchCell = m_FocusCell;
    m_LastSearchTime = m_LastSearchTime.GetCurrentTime();

    if ( m_LastSearchString.GetLength()==1
      && m_LastSearchString.GetAt(0)==nChar)
    {
        // When the same first character is entered again,
        // then just repeat the search
    }
    else
        m_LastSearchString.AppendChar(nChar);

    int nRow = GetNextItem(-1, LVNI_FOCUSED);
    if (nRow < 0)
        nRow = 0;
    int nCol = m_FocusCell;
    if (nCol < 0)
        nCol = GetHeaderCtrl()->OrderToIndex(0);
    int nRowCount = GetItemCount();

    // Perform the search loop twice
    //    - First search from current position down to bottom
    //    - Then search from top to current position
    for(int j = 0; j < 2; ++j)
    {
        for(int i = nRow + 1; i < nRowCount; ++i)
        {
            CString cellText = GetItemText(i, nCol);
            if (cellText.GetLength()>=m_LastSearchString.GetLength())
            {
                cellText.Truncate(m_LastSearchString.GetLength());
                if (cellText.CompareNoCase(m_LastSearchString)==0)
                {
                    // De-select all other rows
                    SetItemState(-1, 0, LVIS_SELECTED);
                    // Select row found
                    SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
                    // Focus row found
                    SetItemState(i, LVIS_FOCUSED, LVIS_FOCUSED);
                    // Scroll to row found
                    EnsureVisible(i, FALSE);
                    m_LastSearchRow = i;
                    return;
                }
            }
        }
        nRowCount = nRow;
        nRow = -1;
    }
}

We have on purpose prevented the event to reach the CListCtrl::OnChar() method, because it would attempt to search in the label-column.

Points of Interest

If using Windows XP, then one will see the following drawing flaws with the label-column (first column), because of its special margin. When dragging the label-column in the middle, then the selection marking of the entire row, will have a gap where the background shows through.

If running the application in classic style without Windows XP themes or Windows Vista themes, then the left border of the focus rectangle will remain visible on the previous row when moving focus to another row. One can easily fix this bug by moving the left border of the focus rectangle a few pixels, but this doesn't solve the issue above. Instead one should consider hiding the label column.

If using Windows Vista, then the trick of removing the selection marking for a single cell doesn't have any effect. Luckily enough, the focus rectangle is easy to see on Vista, even within a selected row.

Using the Code

The source code provided includes a simple implementation of a CListCtrl which supports cell navigation (CListCtrl_CellNav).

History

  • 2008-08-22 - First release of the article
  • 2008-08-23 - Added handling of the right-click mouse event
  • 2008-08-29 - Added keyboard searching in subitems

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here