65.9K
CodeProject 正在变化。 阅读更多。
Home

CListCtrl 和单元格导航

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (10投票s)

2008年8月21日

CPOL

5分钟阅读

viewsIcon

121614

downloadIcon

4337

实现 MFC 列表控件中子项焦点的示例

引言

Microsoft 的 CListCtrl 支持在网格中显示数据,但它本身并不支持子项键盘导航。

本文将演示以下内容

  • 如何监听事件以了解当前哪个单元格具有焦点。
  • 如何在保持 Windows XP/Vista 外观的同时,显示当前哪个单元格具有焦点。

screenshot_vista.png

screenshot_xp.png

背景

有许多高级网格控件可以扩展 CListCtrl 以实现子项导航。但是,由于这些网格控件可能非常复杂,因此很难理解它们是如何实现的。

文章 列表控件中的子项选择 演示了如何使用自定义绘制来显示单元格焦点,但它不支持选择多行,也不支持 Windows Vista。

如何在 CListCtrl 中实现子项导航

有几个问题需要注意

  • 必须对左、右箭头键的键盘事件做出响应,并相应地更新焦点单元格。
    • CListCtrl 已支持上下箭头键,因此无需处理它们。
  • 必须对左键鼠标事件做出响应,并相应地更新焦点单元格。
  • 在更新具有焦点的单元格时,必须注意 CHeaderCtrl 中的列显示顺序。
  • 在更新具有焦点的单元格时,必须检查是否需要滚动以显示整个单元格。
  • 必须修改 CListCtrl 的绘制,使焦点单元格对用户可见。
  • 将键盘搜索导航扩展到支持在焦点列中进行搜索。

CListCtrl 已处理行选择,因此在实现子项焦点时,我们必须确保此功能保持不变。

处理键盘事件

通常,在 CListCtrl 中处理键盘事件时,我们会响应 CListCtrl 本身生成的 LVN_KEYDOWN 事件。但是,如果 CListCtrl 有水平滚动条(很多列),那么左右箭头键事件会导致 CListCtrl 滚动。因此,我们必须在 CListCtrl 响应之前拦截键盘事件,这可以通过 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);
}

MoveFocusCell() 函数将在本文稍后详细介绍。

处理鼠标事件

当用户使用左键单击子项时,我们必须为其设置焦点。在找出哪个子项被单击后,我们将事件传递给 CListCtrl,以便它可以处理正常的行选择。

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();
    }
}

我们还需要处理右键单击鼠标事件 ON_WM_RBUTTONDOWN(),但它与左键单击鼠标事件的处理方式几乎相同,可以在源代码中看到。

处理列显示顺序

Microsoft 扩展了 CListCtrl 的功能,使其能够通过拖放(LVS_EX_HEADERDRAGDROP)来更改列的顺序。当一列被拖到新的位置时,它会保留其列 ID,但 CHeaderCtrl 中的显示顺序会改变。

我们必须使用显示顺序来查找下一列,这项工作由 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);
}

确保列可见

CListCtrl 可以包含多列,在使用箭头键导航时,我们必须确保焦点列对用户可见。这通过滚动到焦点列来实现,我几乎是“窃取”了 Daniel Frey 的文章中的代码:确保列(部分)可见

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;

}

处理焦点子项的自定义绘制

CListCtrl 会自行处理项目和选择的绘制。如果按住 CTRL 键并使用箭头键导航,我们会看到 CListCtrl 只使用一个焦点矩形来显示焦点行。

通过自定义绘制,我们可以改变行焦点矩形的正常绘制。技巧是移除告诉 CListCtrl 它应该绘制焦点矩形(针对整行)的标志,而是提供我们自己如何绘制行焦点矩形(针对焦点子项)的实现。我们很快就会发现,当在选择的行内移动时,焦点矩形很难看到。这可以通过移除告诉 CListCtrl 它应该绘制选择背景(针对焦点子项)的标志来解决。

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;
}

使用 GetCellRect() 的原因在于 GetSubItemRect() 无法为标签列(即使使用 LVIR_LABEL)返回正确的焦点矩形。标签列是插入到 CListCtrl 中的第一列,与后面插入的列相比,它有一些细微的差别。

我已排除处理扩展样式 LVS_EX_GRIDLINES 的详细信息,但可以在源代码中找到。这是因为,当 CListCtrl 绘制网格线时,我们必须确保焦点矩形不会绘制在网格线上方。

将键盘搜索扩展到子项

Microsoft Windows 资源管理器允许用户跳转到键盘输入字符所匹配的第一行。这使得行导航更加容易,并且 CListCtrl 也支持此功能,但仅限于标签列。我们希望处理键盘字符事件,但我们将使用 ON_WM_CHAR() 而不是 ON_WM_KEYDOWN(),因为我们只需要字母和数字,而不是所有键盘字符。

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;
    }
}

我们特意阻止了该事件到达 CListCtrl::OnChar() 方法,因为它会尝试在标签列中进行搜索。

关注点

如果使用 Windows XP,则由于标签列(第一列)的特殊边距,会看到以下绘制缺陷。当在中间拖动标签列时,整个行的选择标记将出现一个间隙,背景会透过。

如果在没有 Windows XP 主题或 Windows Vista 主题的经典样式下运行应用程序,那么当焦点移到另一行时,焦点矩形的左边框将保留在前一行上可见。可以通过将焦点矩形的左边框移动几个像素来轻松修复此错误,但这并不能解决上述问题。相反,应该考虑隐藏标签列。

如果使用 Windows Vista,则移除单个单元格选择标记的技巧没有效果。幸运的是,即使在选择的行内,焦点矩形在 Vista 上也很容易看到。

Using the Code

提供的源代码包含一个支持单元格导航的简单 CListCtrl 实现(CListCtrl_CellNav)。

历史

  • 2008-08-22 - 文章首次发布
  • 2008-08-23 - 添加了右键单击鼠标事件的处理
  • 2008-08-29 - 添加了子项中的键盘搜索
CListCtrl 和单元格导航 - CodeProject - 代码之家
© . All rights reserved.