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

带合并单元格和冻结行/列功能的 CGridCtrl

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (29投票s)

2010 年 7 月 5 日

CPOL

7分钟阅读

viewsIcon

93301

downloadIcon

5776

为 Chris Maunder 的 CGridCtrl 添加 XL 风格的单元格合并以及冻结窗格(冻结行/列)功能。

screenshot.JPG

引言

本文的灵感来源于 Chris Maunder 及其团队在 CGridCtrl 类方面所做的出色工作。本文将尝试描述如何通过对现有 CGridCtrlCGridCellBase 类进行一些添加和修改来实现单元格合并和冻结窗格功能。

感兴趣的读者可以查看以下链接,获取一个更完整的网格版本,该版本具有平滑滚动和迷你网格单元格功能:带有平滑滚动和复合单元格的 Grid Control Re-dux

背景

在 GUI 控件开发的初期,我发现自己编写一个所有者绘制/自定义控件非常困难。当我接触到 CGridCtrl 类时,我逐渐理解了 GUI 控件开发的不同方面。我个人认为,CGridCtrl 类中引入的设计概念和架构远远超出了普通的 GUI 开发,对于任何类型的面向对象软件开发都很有用。在我看来,这是 OOP 概念最出色的实现之一。CGridCtrl 似乎拥有一切,直到需要为这个出色的控件添加单元格合并和窗格冻结功能。因此,通过对现有代码进行一些调整,再加上几行代码,我成功地实现了我的目标。

如何添加单元格合并和冻结窗格功能

实现合并单元格的主要思想是确保只有左上角的合并单元格对用户可见。同时,在绘制和编辑时,请确保您将一个矩形传递给该单元格,该矩形等于合并单元格范围内所有单元格的总矩形。

实现冻结行和冻结列功能的主要技巧是,让冻结的行和列在滚动时表现得像固定行和列,但在其他情况下,让它们像普通单元格(即非固定单元格)一样被绘制或编辑。

以下代码片段添加到头文件 GridCtrl.h 中,用于单元格合并:

//////////////////////////////////////////////////////////
// Attributes
//////////////////////////////////////////////////////////
public:

//////////////////Merge Cell related additions ///////////

    INT_PTR MergeCells(CCellRange& mergedCellRange);
    void SplitCells(INT_PTR nMergeID);

    BOOL IsMergedCell(int row, int col, CCellRange& mergedCellRange);
    BOOL GetMergedCellRect(int row, int col, CRect& rect);
    BOOL GetMergedCellRect(CCellRange& mergedCell, CRect& rect);
    BOOL GetTopLeftMergedCell(int& row, 
         int& col, CRect& mergeRect);
    BOOL GetBottomRightMergedCell(int& row, 
         int& col, CRect& mergeRect);
    virtual BOOL IsFocused(CGridCellBase& cell, int nRow, int nCol);
    virtual BOOL IsSelected(CGridCellBase& cell, int nRow, int nCol);

    BOOL    m_bDrawingMergedCell;
    INT_PTR    m_nCurrentMergeID;

    static CRect rectNull;        
    static CCellID cellNull;

    // Declare an array to contain the merge cell ranges
    CArray<ccellrange,> m_arMergedCells;

我认为合并单元格相关的 API 名称已经足够清楚地表明了这些 API 的功能。如果需要,您可以查看 GridCtrl.cpp 文件以了解上述 API 的具体实现。以下代码片段添加到头文件 GridCtrl.h 中,用于单元格冻结:

///////////Freezed Cells related additions ///////////////////////////////

// holds the freeze row and column count
int m_nFreezedRows, m_nFreezedCols;

// holds whether to exclude the freezed rows
// and columns while drag selection is taking place
BOOL m_bExcludeFreezedRowsFromSelection;
BOOL m_bExcludeFreezedColsFromSelection;

BOOL SetFreezedRowCount(int nFreezedRows)
{
    BOOL bRet = FALSE;
    if( (nFreezedRows >= 0) && 
        ((nFreezedRows + m_nFixedRows) <= m_nRows) )
    {
        m_nFreezedRows = nFreezedRows;
        ResetScrollBars();
        Refresh();
        bRet = TRUE;
    }

    return bRet;
    
}
    
BOOL SetFreezedColumnCount(int nFreezedCols)
{
    BOOL bRet = FALSE;
    if( (nFreezedCols >= 0) && 
        ((nFreezedCols + m_nFixedCols) <= m_nCols) )
    {
        m_nFreezedCols = nFreezedCols;
        ResetScrollBars();
        Refresh();
        bRet = TRUE;
    }

    return bRet;
}

// To avoid calling ResetScrollBars twice you can use SetFreezedFrame
BOOL SetFreezedFrame(int nFreezedRows, int nFreezedCols)
{
    BOOL bRet = FALSE;
    if( (nFreezedRows >= 0) && 
        ((nFreezedRows + m_nFixedRows) <= m_nRows) )
    {
        m_nFreezedRows = nFreezedRows;            
        bRet = TRUE;
    }
    if( (nFreezedCols >= 0) && 
        ((nFreezedCols + m_nFixedCols) <= m_nCols) )
    {
        m_nFreezedCols = nFreezedCols;
        bRet = TRUE;
    }
    else
    {
        bRet = FALSE;
    }

    ResetScrollBars();
        
    return bRet;            
}    

int  GetFreezedRowCount() const          { return m_nFreezedRows; }
int  GetFreezedColumnCount() const       { return m_nFreezedCols; }

////////////////////////////////////////////////////////////////////////////////

您需要在 GetCellFromPt API 中进行与合并单元格相关的修改:

///////Change for MergeCell///////////////////////////////////
CCellID GetCellFromPt(CPoint point, BOOL bAllowFixedCellCheck = TRUE,
 CCellID& cellOriginal = cellNull);

现在,您必须按如下方式修改与固定单元格相关的 getter,以实现单元格冻结:

/////////Freezed Cells related modifications//////////////////////////////////

int  GetFixedRowCount(BOOL bIncludeFreezedRows = FALSE) const
{ 
    return (bIncludeFreezedRows) ? (m_nFixedRows + m_nFreezedRows) : m_nFixedRows;
}

int  GetFixedColumnCount(BOOL bIncludeFreezedCols = FALSE) const            
{
    return (bIncludeFreezedCols) ? (m_nFixedCols + m_nFreezedCols) : m_nFixedCols; 
}    

int GetFixedRowHeight(BOOL bIncludeFreezedRows = FALSE) const;

int GetFixedColumnWidth(BOOL bIncludeFreezedCols = FALSE) const;

///////////////////////////////////////////////////////////////////////////////

在上述四个方法中,思想非常明确,即是否将冻结的行/列视为固定行和列。通过在适当的时间向这些方法的参数发送 TRUEFALSE,就可以完成与冻结行和列相关的大部分任务。

这些是头文件中合并单元格和冻结单元格的所有必要更改。

现在,我们来看一下 CPP 中的一些更改,以稍微了解如何实现合并和冻结功能。

以下内容已添加到构造函数中:

m_nFreezedRows = 0;
m_nFreezedCols = 0;
m_bExcludeFreezedRowsFromSelection = FALSE;
m_bExcludeFreezedColsFromSelection = FALSE;

m_bDrawingMergedCell = FALSE;
m_nCurrentMergeID = -1;

为了准确地使用冻结单元格进行拖动选择,以下代码在 OnTimer 事件中紧随 GetClientRect 之后:

CCellID cell = GetCellFromPt(origPt);

CCellID idTopLeft = GetTopleftNonFixedCell();
if(idTopLeft.row == GetFixedRowCount(TRUE))
{
    m_bExcludeFreezedRowsFromSelection = FALSE;
}
else if((cell.row > idTopLeft.row) || 
        (m_LeftClickDownCell.row >= idTopLeft.row))
{
    m_bExcludeFreezedRowsFromSelection = TRUE;

}    
if(idTopLeft.col == GetFixedColumnCount(TRUE))
{
    m_bExcludeFreezedColsFromSelection = FALSE;
}
else if((cell.col > idTopLeft.col) || 
        (m_LeftClickDownCell.col >= idTopLeft.col))
{
    m_bExcludeFreezedColsFromSelection = TRUE;
}

int nFixedRowHeight = GetFixedRowHeight(m_bExcludeFreezedRowsFromSelection);
int nFixedColWidth = GetFixedColumnWidth(m_bExcludeFreezedColsFromSelection);

m_bExcludeFreezedRowsFromSelectionm_bExcludeFreezedColsFromSelection 成员变量的作用是跟踪在拖动鼠标时冻结单元格是否可选。因为只要由于鼠标拖动而发生水平或垂直滚动,冻结单元格就不应该是可选的。

接下来,在 OnKeyDown 事件处理程序中,所有出现的 m_nFixedRows/GetFixedRowCount 都替换为 GetFixedRowCount(m_bExcludeFreezedRowsFromSelection),所有出现的 m_nFixedCols/GetFixedColumnCount 都替换为 GetFixedColumnCount(m_bExcludeFreezedColsFromSelection)

在同一个事件处理程序中,if(next != m_idCurrentCell) 块已更改如下,用于单元格合并:

if (next != m_idCurrentCell)
{
    // LUC

    int nNextRow = next.row;
    int nNextCol = next.col;

    int nCurRow = m_idCurrentCell.row;
    int nCurCol = m_idCurrentCell.col;

    BOOL bMerged = GetTopLeftMergedCell(nCurRow, nCurCol, rectNull);

    switch(nChar)
    {
        case VK_LEFT:
        {
            if(GetTopLeftMergedCell(nNextRow, nNextCol, rectNull))
            {                    
                next.col = nNextCol;
                if(bMerged)
                {
                    // if already in a merged cell make sure
                    // the next column is not the leftmost
                    // column of the merged cell
                    next.col--;    
                }
            }
            break;
        }

        case VK_RIGHT:
        {
            if(GetBottomRightMergedCell(nNextRow, nNextCol, rectNull))
            {
                next.col = nNextCol;
                if(bMerged)
                {
                    // if already in a merged cell make sure the next
                    // column is not the rightmost column of the merged cell
                    next.col++;    
                }
            }
            break;
        }

        case VK_UP:
        {
            if(GetTopLeftMergedCell(nNextRow, nNextCol, rectNull))
            {
                next.row = nNextRow;
                if(bMerged)
                {
                    // if already in a merged cell make sure
                    // the next row is not the topmost row
                    // of the merged cell
                    next.row--;    
                }
            }
            break;
        }

    case VK_DOWN:
    {
        if(GetBottomRightMergedCell(nNextRow, nNextCol, rectNull))
        {
            next.row = nNextRow;
            if(bMerged)
            {
                // if already in a merged cell make sure
                // the next row is not the bottommost row
                // of the merged cell
                next.row++;    
            }
        }
        break;
    }
}

滚动也需要稍微调整。在 OnHScroll 事件处理程序中,与 SB_PAGERIGHTSB_PAGELEFT 相关的以下代码已修改:

case SB_PAGERIGHT:
    if (scrollPos < m_nHScrollMax)
    {
        
        rect.left = GetFixedColumnWidth(TRUE);
        int offset = rect.Width();
        int pos = min(m_nHScrollMax, scrollPos + offset);
        SetScrollPos32(SB_HORZ, pos);
        
        rect.left = GetFixedColumnWidth(FALSE);
        InvalidateRect(rect);
    }
    break;
    
case SB_PAGELEFT:
    if (scrollPos > 0)
    {
        
        rect.left = GetFixedColumnWidth(TRUE);
        int offset = -rect.Width();
        int pos = __max(0, scrollPos + offset);
        SetScrollPos32(SB_HORZ, pos);
        
        rect.left = GetFixedColumnWidth(FALSE);
        InvalidateRect(rect);
    }
    break;

同样,对于 OnVScroll,代码更改为:

case SB_PAGEDOWN:
    if (scrollPos < m_nVScrollMax)
    {
        
        rect.top = GetFixedRowHeight(TRUE);
        scrollPos = min(m_nVScrollMax, scrollPos + rect.Height());
        SetScrollPos32(SB_VERT, scrollPos);
        
        rect.top = GetFixedRowHeight(FALSE);
        InvalidateRect(rect);
    }
    break;
    
case SB_PAGEUP:
    if (scrollPos > 0)
    {
        
        rect.top = GetFixedRowHeight(TRUE);
        int offset = -rect.Height();
        int pos = __max(0, scrollPos + offset);
        SetScrollPos32(SB_VERT, pos);
        
        rect.top = GetFixedRowHeight(FALSE);
        InvalidateRect(rect);
    }
    break;

现在,最重要的方法是 OnDraw(),它是 CGridCtrl 的主要绘图函数。您不需要在此处为单元格合并和冻结进行大量更改。事实上,由于此方法中代码的出色组织和设计,代码更改是最小的。您可能认为这里的一个主要添加是与合并单元格相关的代码。以下是更改后的 OnDraw 的一个预览(实际上不止于此),所做的更改由相关行上的 //LUC 注释指出。

void CGridCtrl::OnDraw(CDC* pDC)
{
    if (!m_bAllowDraw)
        return;

    CRect clipRect;
    if (pDC->GetClipBox(&clipRect) == ERROR)
        return;

    EraseBkgnd(pDC); // OnEraseBkgnd does nothing, so erase bkgnd here.
    // This necessary since we may be using a Memory DC.

#ifdef _DEBUG
    LARGE_INTEGER iStartCount;
    QueryPerformanceCounter(&iStartCount);
#endif

    CRect rc;
    GetClientRect(rc);

    CRect rect;
    int row, col;
    CGridCellBase* pCell;

    // LUC
    int nFixedRowHeight = GetFixedRowHeight(TRUE);
    int nFixedColWidth  = GetFixedColumnWidth(TRUE);

    CCellID idTopLeft = GetTopleftNonFixedCell();
    int minVisibleRow = idTopLeft.row,
        minVisibleCol = idTopLeft.col;

    CRect VisRect;
    CCellRange VisCellRange = GetVisibleNonFixedCellRange(VisRect);
    int maxVisibleRow = VisCellRange.GetMaxRow(),
        maxVisibleCol = VisCellRange.GetMaxCol();

    if (GetVirtualMode())
        SendCacheHintToParent(VisCellRange);

    // draw top-left cells 0..m_nFixedRows-1, 0..m_nFixedCols-1
    rect.bottom = -1;
    int nFixedRows = m_nFixedRows + m_nFreezedRows;
    int nFixedCols = m_nFixedCols + m_nFreezedCols;
    for (row = 0; row < nFixedRows; row++)
    {
        if (GetRowHeight(row) <= 0) continue;

        rect.top = rect.bottom+1;
        rect.bottom = rect.top + GetRowHeight(row)-1;
        rect.right = -1;

        for (col = 0; col < nFixedCols; col++)
        {
            if (GetColumnWidth(col) <= 0) continue;

            rect.left = rect.right+1;
            rect.right = rect.left + GetColumnWidth(col)-1;

            pCell = GetCell(row, col);
            if (pCell)
            {
                pCell->SetCoords(row,col);
                pCell->Draw(pDC, row, col, rect, FALSE);
            }
        }
    }

    // draw fixed column cells:  m_nFixedRows..n, 0..m_nFixedCols-1
    rect.bottom = nFixedRowHeight-1;
    for (row = minVisibleRow; row <= maxVisibleRow; row++)
    {
        if (GetRowHeight(row) <= 0) continue;

        rect.top = rect.bottom+1;
        rect.bottom = rect.top + GetRowHeight(row)-1;

        // rect.bottom = bottom pixel of previous row
        if (rect.top > clipRect.bottom)
            break;                // Gone past cliprect
        if (rect.bottom < clipRect.top)
            continue;             // Reached cliprect yet?

        rect.right = -1;
        for (col = 0; col < nFixedCols; col++)
        {
            if (GetColumnWidth(col) <= 0) continue;

            rect.left = rect.right+1;
            rect.right = rect.left + GetColumnWidth(col)-1;

            if (rect.left > clipRect.right)
                break;            // gone past cliprect
            if (rect.right < clipRect.left)
                continue;         // Reached cliprect yet?

            pCell = GetCell(row, col);
            if (pCell)
            {
                pCell->SetCoords(row,col);
                pCell->Draw(pDC, row, col, rect, FALSE);
            }
        }
    }

    // draw fixed row cells  0..m_nFixedRows, m_nFixedCols..n
    rect.bottom = -1;
    for (row = 0; row < nFixedRows; row++)
    {
        if (GetRowHeight(row) <= 0) continue;

        rect.top = rect.bottom+1;
        rect.bottom = rect.top + GetRowHeight(row)-1;

        // rect.bottom = bottom pixel of previous row
        if (rect.top > clipRect.bottom)
            break;                // Gone past cliprect
        if (rect.bottom < clipRect.top)
            continue;             // Reached cliprect yet?

        rect.right = nFixedColWidth-1;
        for (col = minVisibleCol; col <= maxVisibleCol; col++)
        {
            if (GetColumnWidth(col) <= 0) continue;

            rect.left = rect.right+1;
            rect.right = rect.left + GetColumnWidth(col)-1;

            if (rect.left > clipRect.right)
                break;        // gone past cliprect
            if (rect.right < clipRect.left)
                continue;     // Reached cliprect yet?

            pCell = GetCell(row, col);
            if (pCell)
            {
                pCell->SetCoords(row,col);
                // LUC
                if(!m_bShowHorzNonGridArea && (col == m_nCols - 1))
                {
                    pCell->Draw(pDC, row, col, rect, FALSE);

                    if(rect.right < rc.right)
                    {
                        CRect rcFill(rect.right + 1, rect.top, rc.right - 2, rect.bottom);
                        
                        CGridCell cell;
                        cell.SetGrid(this);

                        DWORD dwState = pCell->GetState() & ~(GVIS_SELECTED | GVIS_FOCUSED);
                        cell.SetState(dwState);

                        int nSortColumn = GetSortColumn();
                        m_nSortColumn = -1;

                        cell.Draw(pDC, row, col, rcFill, TRUE);
                        
                        if(!(pCell->GetState() & GVIS_FIXED))
                        {
                            rcFill.right++;
                            rcFill.bottom++;
                            pDC->Draw3dRect(rcFill, GetTextBkColor(), m_crGridLineColour);
                        }

                        m_nSortColumn = nSortColumn;
                    }
                }
                else
                {
                    pCell->Draw(pDC, row, col, rect, FALSE);
                }
            }
        }
    }

    // draw rest of non-fixed cells
    rect.bottom = nFixedRowHeight-1;
    for (row = minVisibleRow; row <= maxVisibleRow; row++)
    {
        if (GetRowHeight(row) <= 0) continue;

        rect.top = rect.bottom+1;
        rect.bottom = rect.top + GetRowHeight(row)-1;

        // rect.bottom = bottom pixel of previous row
        if (rect.top > clipRect.bottom)
            break;                // Gone past cliprect
        if (rect.bottom < clipRect.top)
            continue;             // Reached cliprect yet?

        rect.right = nFixedColWidth-1;
        for (col = minVisibleCol; col <= maxVisibleCol; col++)
        {
            if (GetColumnWidth(col) <= 0) continue;

            rect.left = rect.right+1;
            rect.right = rect.left + GetColumnWidth(col)-1;

            if (rect.left > clipRect.right)
                break;        // gone past cliprect
            if (rect.right < clipRect.left)
                continue;     // Reached cliprect yet?

            pCell = GetCell(row, col);
            // TRACE(_T("Cell %d,%d type: %s\n"), row,
            // col, pCell->GetRuntimeClass()->m_lpszClassName);
            if (pCell)
            {
                pCell->SetCoords(row,col);
                // LUC
                if(!m_bShowHorzNonGridArea && (col == m_nCols - 1))
                {
                    if(rect.right < rc.right)
                    {
                        pCell->Draw(pDC, row, col, rect, FALSE);

                        CRect rcFill(rect.right + 1, rect.top, 
                                     rc.right - 1, rect.bottom);
                        pDC->FillSolidRect(rcFill, GetTextBkColor());

                        rcFill.right++;
                        rcFill.bottom++;
                        pDC->Draw3dRect(rcFill, 
                          GetTextBkColor(), m_crGridLineColour);
                    }

                }
                else
                {
                    pCell->Draw(pDC, row, col, rect, FALSE);
                }
            }            
        }
    }    

    CPen pen;
    pen.CreatePen(PS_SOLID, 0, m_crGridLineColour);
    pDC->SelectObject(&pen);

    // draw vertical lines (drawn at ends of cells)
    if (m_nGridLines == GVL_BOTH || m_nGridLines == GVL_VERT)
    {
        // LUC
        //int x = nFixedColWidth;
        int x = GetFixedColumnWidth(); 
        
        // LUC
        //for (col = minVisibleCol; col < maxVisibleCol; col++)
        int nFixedRowHeightExcludingFreezedRows = GetFixedRowHeight();
        for (col = m_nFixedCols; col <= maxVisibleCol; col++)
        {
            if (GetColumnWidth(col) <= 0) continue;

            if(col == (m_nFixedCols + m_nFreezedCols))
            {
                col = minVisibleCol;
            }

            x += GetColumnWidth(col);
            //pDC->MoveTo(x-1, nFixedRowHeight);
            pDC->MoveTo(x-1, nFixedRowHeightExcludingFreezedRows);
            pDC->LineTo(x-1, VisRect.bottom);
        }
    }

    // draw horizontal lines (drawn at bottom of each cell)
    if (m_nGridLines == GVL_BOTH || m_nGridLines == GVL_HORZ)
    {
        // LUC
        //int y = nFixedRowHeight;
        int y = GetFixedRowHeight();
        //for (row = minVisibleRow; row <= maxVisibleRow; row++)
        int nFixedColumnWidthExcludingFreezedColumns = GetFixedColumnWidth();
        for (row = m_nFixedRows; row <= maxVisibleRow; row++)
        {
            if (GetRowHeight(row) <= 0) continue;
            
            if(row == (m_nFixedRows + m_nFreezedRows))
            {
                row = minVisibleRow;
            }

            y += GetRowHeight(row);
            //pDC->MoveTo(nFixedColWidth, y-1);
            pDC->MoveTo(nFixedColumnWidthExcludingFreezedColumns, y-1);
            // LUC
            pDC->LineTo(VisRect.right,  y-1);
        }
    }

    // LUC : Merge Cell
    m_bDrawingMergedCell = TRUE;
    INT_PTR size = m_arMergedCells.GetSize();
    if(size > 0)
    {    
        CRect rcMergeRect;
        for(INT_PTR i = 0; i < size; i++)
        {
            m_nCurrentMergeID = i;
            if(GetMergedCellRect(m_arMergedCells[i], rcMergeRect))
            {
                rcMergeRect.right--;
                rcMergeRect.bottom--;
                
                pCell = GetCell(m_arMergedCells[i].GetMinRow(), 
                                m_arMergedCells[i].GetMinCol());
                if (pCell)
                {
                    pCell->Draw(pDC, m_arMergedCells[i].GetMinRow(), 
                          m_arMergedCells[i].GetMinCol(), rcMergeRect, TRUE);
                }
            }
        }
    }
    m_bDrawingMergedCell = FALSE;
    m_nCurrentMergeID = -1;

    // LUC: 
    // Finally we can draw a line for the Freezed Frame
    ////
    pen.DeleteObject();
    pen.CreatePen(PS_SOLID, 0, RGB(0, 0, 255));
    pDC->SelectObject(&pen);
    if(m_nFreezedRows > 0)
    {
        pDC->MoveTo(0, nFixedRowHeight);
        pDC->LineTo(rc.right, nFixedRowHeight);
    }
    if(m_nFreezedCols > 0)
    {
        pDC->MoveTo(nFixedColWidth, 0);
        pDC->LineTo(nFixedColWidth, rc.bottom);
    }

    pDC->SelectStockObject(NULL_PEN);

    // Let parent know it can discard it's data if it needs to.
    if (GetVirtualMode())
       SendCacheHintToParent(CCellRange(-1,-1,-1,-1));

#ifdef _DEBUG
    LARGE_INTEGER iEndCount;
    QueryPerformanceCounter(&iEndCount);
    TRACE1("Draw counter ticks: %d\n", 
           iEndCount.LowPart-iStartCount.LowPart);
#endif

}

不要对 m_bShowHorzNonGridArea 成员感到困惑,我引入它只是为了在列数较少时消除水平灰色区域,使网格看起来更好。在某些情况下,这将弥补 Chris 自己指出的与(水平)滚动相关的轻微问题(还记得他关于太多灰色区域的评论吗?)。由于 OnDraw 已更改,RedrawCell 也需要进行一些微调:

BOOL CGridCtrl::RedrawCell(int nRow, int nCol, CDC* pDC /* = NULL */)
{    
    BOOL bResult = TRUE;
    BOOL bMustReleaseDC = FALSE;

    if (!m_bAllowDraw || !IsCellVisible(nRow, nCol))
        return FALSE;

    CRect rect;    
    if (!GetCellRect(nRow, nCol, rect))
        return FALSE;
    
    // LUC    
    BOOL bIsMergeCell = GetTopLeftMergedCell(nRow, nCol, rect);

    if (!pDC)
    {
        pDC = GetDC();
        if (pDC)
            bMustReleaseDC = TRUE;
    }

    if (pDC)
    {
        // Redraw cells directly
        if (nRow < m_nFixedRows || nCol < m_nFixedCols)
        {
            CGridCellBase* pCell = GetCell(nRow, nCol);
            if (pCell)
                bResult = pCell->Draw(pDC, nRow, nCol, rect, TRUE);
        }
        else
        {
            CGridCellBase* pCell = GetCell(nRow, nCol);
            if (pCell)
                bResult = pCell->Draw(pDC, nRow, nCol, rect, TRUE);

            // Since we have erased the background,
            // we will need to redraw the gridlines
            CPen pen;
            pen.CreatePen(PS_SOLID, 0, m_crGridLineColour);

            CPen* pOldPen = (CPen*) pDC->SelectObject(&pen);
            if (m_nGridLines == GVL_BOTH || m_nGridLines == GVL_HORZ)
            {
                pDC->MoveTo(rect.left,    rect.bottom);
                pDC->LineTo(rect.right + 1, rect.bottom);
            }
            if (m_nGridLines == GVL_BOTH || m_nGridLines == GVL_VERT)
            {
                pDC->MoveTo(rect.right, rect.top);
                pDC->LineTo(rect.right, rect.bottom + 1);
            }
            pDC->SelectObject(pOldPen);
        }
    } else
        InvalidateRect(rect, TRUE);
        // Could not get a DC - invalidate it anyway
    // and hope that OnPaint manages to get one

    if (bMustReleaseDC)
        ReleaseDC(pDC);

    // LUC : if this is a merge cell then we have to make sure
    // there are no drawing problem becoz of direct redraw of cell
    // specially becoz of the freeze pane lines
    if(bIsMergeCell)
    {
        InvalidateRect(rect, TRUE);
    }
    
    return bResult;
}

此外,在 SetSelectedRange 中也进行了一些细微的调整,用于单元格合并,您可以在下面看到(再次由 // LUC 注释指出):

// EFW - Bug fix - Don't allow selection of fixed rows
// LUC    
int Left= (m_AllowSelectRowInFixedCol ? 0 : 
    GetFixedColumnCount(m_bExcludeFreezedColsFromSelection));

if(nMinRow >= 0 && nMinRow < 
        GetFixedRowCount(m_bExcludeFreezedRowsFromSelection))
    nMinRow = GetFixedRowCount(m_bExcludeFreezedRowsFromSelection);
if(nMaxRow >= 0 && nMaxRow < 
        GetFixedRowCount(m_bExcludeFreezedRowsFromSelection))
    nMaxRow = GetFixedRowCount(m_bExcludeFreezedRowsFromSelection);
if(nMinCol >= 0 && nMinCol < Left)
    nMinCol = GetFixedColumnCount(m_bExcludeFreezedColsFromSelection);
if(nMaxCol >= 0 && nMaxCol < Left)
    nMaxCol = GetFixedColumnCount(m_bExcludeFreezedColsFromSelection);

// LUC
for(int row = nMinRow; row <= nMaxRow; row++)
{
    for(int col = nMinCol; col <= nMaxCol; col++)
    {                
        int nMergedMinRow = row, nMergedMinCol = col;
        if(GetTopLeftMergedCell(nMergedMinRow, nMergedMinCol, rectNull))
        {    
            if(nMinRow > nMergedMinRow)
            {
                nMinRow = nMergedMinRow;
            }
            if(nMinCol > nMergedMinCol)
            {
                nMinCol = nMergedMinCol;
            }
        }
        int nMergedMaxRow = row, nMergedMaxCol = col;
        
        if(GetBottomRightMergedCell(nMergedMaxRow, nMergedMaxCol, rectNull))
        {
            if(nMaxRow < nMergedMaxRow)
            {
                nMaxRow = nMergedMaxRow;
            }
            if(nMaxCol < nMergedMaxCol)
            {
                nMaxCol = nMergedMaxCol;
            }
        
            // let's try to make it a bit efficient
            row = nMergedMaxRow;
            col = nMergedMaxCol;
        }
    }
}

代码应放置在 if (bSelectCells) 块的正上方。此外,SelectCells API 中也有一点小改动:

// selects cells
void CGridCtrl::SelectCells(CCellID currentCell, 
                            BOOL bForceRedraw /*=FALSE*/, 
                            BOOL bSelectCells /*=TRUE*/)
{
    if (!m_bEnableSelection)
        return;

    int row = currentCell.row;
    int col = currentCell.col;
    // LUC
    if (row < GetFixedRowCount(m_bExcludeFreezedRowsFromSelection) || 
          col < GetFixedColumnCount(m_bExcludeFreezedColsFromSelection))
        if (row < GetFixedRowCount() || col < GetFixedColumnCount())
    {
        return;
    }
    if (!IsValid(currentCell))
        return;

    // Prevent unnecessary redraws
    //if (currentCell == m_LeftClickDownCell)  return;
    //else if (currentCell == m_idCurrentCell) return;

    SetSelectedRange(min(m_SelectionStartCell.row, row),
                     min(m_SelectionStartCell.col, col),
                     __max(m_SelectionStartCell.row, row),
                     __max(m_SelectionStartCell.col, col),
                     bForceRedraw, bSelectCells);
}

现在,CPP 中其余的更改都只是次要更改,即正确调用 GetFixedRow/ColCountGetFixedRowHeight()GetFixedColumnWidth() 以实现单元格冻结功能,以及调用 GetTopLeftMergedCell 以实现合并单元格功能。包含其余更改的方法有:GetCellFromPt(...) GetTopleftNonFixedCell(...) GetVisibleNonFixedCellRange(...) GetVisibleFixedCellRange(...) GetCellOrigin(...) GetFixedRowHeight(...) GetFixedColumnWidth(...) EnsureVisible(...) IsCellVisible(...) InvalidateCellRect(...) OnMouseMove(...) OnLButtonDblClk(...) OnLButtonDown(...) OnLButtonUp(...) OnEditCell(...)

如果您浏览源代码,只需搜索标签/注释 // LUC,您就会发现这些函数中进行的细微修改,而且不难弄清楚发生了什么。

CGridCellBasee 类的更改

另一个重要的更改是在 GridCellBase.cpp 中,我们不再直接调用单元格的 IsFocused(...)/IsSelected(...) API,而是调用 GetGrid()->IsFocused(...) / GetGrid()->IsSelected(...)。这样做是为了实现合并单元格的绘图效果,从网格的 IsFocusedIsSelected API 的定义中可以很明显地看出这一点。

BOOL CGridCtrl::IsFocused(CGridCellBase& cell, int nRow, int nCol)
{
    BOOL bRet = cell.IsFocused();
    if(!bRet && m_bDrawingMergedCell)
    {
        CCellRange& mergedCell = m_arMergedCells[m_nCurrentMergeID];
        for(int row = mergedCell.GetMinRow(); 
            row <= mergedCell.GetMaxRow(); row++)
        {
            for(int col = mergedCell.GetMinCol(); 
                col <= mergedCell.GetMaxCol(); col++)
            {
                CGridCellBase* pCell = GetCell(row, col);
                if(pCell != NULL)
                {
                    if(pCell->IsFocused())
                    {
                        bRet = TRUE;
                    }
                }
            }
        }                
    }

    return bRet;
}

BOOL CGridCtrl::IsSelected(CGridCellBase& cell, int nRow, int nCol)
{
    BOOL bRet = cell.IsSelected();
    if(!bRet && m_bDrawingMergedCell)
    {
        CCellRange& mergedCell = m_arMergedCells[m_nCurrentMergeID];
        for(int row = mergedCell.GetMinRow(); 
            row <= mergedCell.GetMaxRow(); row++)
        {
            for(int col = mergedCell.GetMinCol(); 
                col <= mergedCell.GetMaxCol(); col++)
            {
                CGridCellBase* pCell = GetCell(row, col);
                if(pCell != NULL)
                {
                    if(pCell->IsSelected())
                    {
                        bRet = TRUE;
                    }
                }
            }
        }                
    }

    return bRet;
}

从代码中可以看出,在合并单元格绘图期间,这些 API 试图找出合并单元格范围内的任何单元格是否被聚焦或选中。如果是这种情况,那么合并单元格将被绘制为选中或聚焦状态。

Using the Code

既然您已经成功添加了合并和冻结功能,那么我们来看看如何使用它们。对于单元格合并和拆分,这里有一个示例代码:

static int g_nLastMergeID = -1;
void CGridCtrlTestDlg::OnBnClickedButtonMerge()
{
    // TODO: Add your control notification handler code here
    
    CString str;
    TCHAR* endptr = NULL;
    int nRadix = 10;

    m_editMinRow.GetWindowText(str);    
    long nMinRow = _tcstol((LPCTSTR)str, &endptr, nRadix);
    m_editMaxRow.GetWindowText(str);    
    long nMaxRow = _tcstol((LPCTSTR)str, &endptr, nRadix);
    m_editMinCol.GetWindowText(str);    
    long nMinCol = _tcstol((LPCTSTR)str, &endptr, nRadix);
    m_editMaxCol.GetWindowText(str);    
    long nMaxCol = _tcstol((LPCTSTR)str, &endptr, nRadix);
    
    g_nLastMergeID = m_grid.MergeCells(CCellRange(nMinRow, 
                            nMinCol, nMaxRow, nMaxCol));
    m_grid.Refresh();
}

void CGridCtrlTestDlg::OnBnClickedButtonUnmerge()
{
    // TODO: Add your control notification handler code here
    m_grid.SplitCells(g_nLastMergeID);
    m_grid.Refresh();
}

对于单元格冻结,您可以考虑这个示例:

void CGridCtrlTestDlg::OnBnClickedButtonFreeze()
{
    // TODO: Add your control notification handler code here
    CString str;
    TCHAR* endptr = NULL;
    int nRadix = 10;

    m_editFreezedRows.GetWindowText(str);    
    long nFreezedRowCount  = _tcstol((LPCTSTR)str, &endptr, nRadix);
    m_grid.SetFreezedRowCount(nFreezedRowCount);

    m_editFreezedCols.GetWindowText(str);
    long nFreezedColCount  = _tcstol((LPCTSTR)str, &endptr, nRadix);
    m_grid.SetFreezedColumnCount(nFreezedColCount );
}

关注点

我在原始网格控件源代码中发现了一些非常非常小的问题(可以轻松解决):

  1. 如果显示了工具提示,则无法编辑单元格。
  2. 即使单元格可以包含多行文本,InplaceEditCtrl 也不是多行。
  3. 如果单元格太小,InplaceEditCtrl 几乎不可见。

这些问题太小了,我猜没有人费心去修复它们。另一点需要注意的是,我添加了一个额外的功能,即移除水平灰色区域,可以通过将 m_bShowHorzNonGridArea 分别设置为 TRUEFALSE 来开启和关闭。您可以查看 OnDraw 以了解如何实现此功能。

致谢

  1. 已故的保罗·迪拉西亚:感谢他那些精彩的文章,这些文章证明了编程写作可以成为一种艺术形式,并给了我研究这个主题的灵感。
  2. 克里斯·莫德:感谢克里斯的这个出色控件,它使我在 GUI 控件开发和架构设计方面取得了巨大的进步。
  3. 雅克·拉斐内尔:一位鲜为人知的传奇人物,他可以独自从头开始重现 MS Office。

历史

  • 文章于2010年7月5日完成。
© . All rights reserved.