Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

CGridCtrl with Merge Cell and Freeze Row/Col Capability

4.83/5 (29 votes)
18 Aug 2010CPOL7 min read 91.9K   5.7K  
Add XL style merge cell as well as Freeze Pane (freeze row/col) functionality to Chris Maunder's CGridCtrl.

screenshot.JPG

Introduction

This article is inspired by the amazing work of Chris Maunder and co. with relation to the CGridCtrl class. The article will try to describe, how merge cell and freeze pane functionality can be achieved by making some additions and modifications to the existing CGridCtrl and CGridCellBase classes.

Those who are interested can look up the following link for a more complete version of the grid where it has smooth scrolling and mini-grid cell features: Grid Control Re-dux with Smooth Scroll and Composite Cell.

Background

In the initial stages of GUI control development, I found it pretty overwhelming to write an owner draw/custom control by myself. Once I came across the CGridCtrl class, gradually I understood different aspects of GUI control development. Personally, I think the design concepts and the architecture introduced in the CGridCtrl class extends far beyond just normal GUI development, and is useful for any kind of object oriented software development. In my view, this is one of the most superb implementation of OOP concepts. It seemed CGridCtrl had everything, until came the requirement of adding Cell Merging and Pane Freezing capabilities to this excellent control. So, by making a few adjustments to the already existing code, plus adding a few line of code, I was able to achieve my target.

How Merge Cell and Freeze Pane Functionalities are Added

The main idea behind implementing merged cells is to make sure that only your top left merged cell is visible to the user. Also make sure, during drawing and editing, you are passing this cell a rectangle that equals to the total rect of all the cells that fall within the merged cell range.

The main trick to achieve the freeze row and freeze column functionality is to make the freeze rows and columns behave like fixed rows and columns when it comes to scrolling, but let them be drawn or edited like normal cells (i.e., not fixed cells) otherwise.

The following bit of code is added in the header GridCtrl.h for cell merging:

C++
//////////////////////////////////////////////////////////
// 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;

I think the merge cell related API names are evident enough to suggest what the functions of these APIs are. If required, you can look up in the GridCtrl.cpp file to see the exact implementation of the above APIs. The following bit of code is added in the header GridCtrl.h for cell freezing:

C++
///////////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; }

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

You need to make a merge cell related modification in the GetCellFromPt API:

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

Now you have to modify the fixed cells related getters, as follows, for cell freezing:

C++
/////////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;

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

In the above four methods, the idea is quite clear, whether to consider the frozen row/columns as fixed row and columns. By sending TRUE or FALSE to the arguments for these methods properly at the proper time, most of the task is achieved related to frozen rows and columns.

These were all the necessary changes for merge cells and freeze cells in the header.

Let's have a look at some of the changes in the CPP now to give a little bit of idea of how to achieve the merge and freeze functionalities.

The following were added in the constructor:

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

m_bDrawingMergedCell = FALSE;
m_nCurrentMergeID = -1;

For accurately doing drag selection with frozen cells, the following code was placed in the OnTimer event just after GetClientRect:

C++
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);

What the m_bExcludeFreezedRowsFromSelection and m_bExcludeFreezedColsFromSelection member variables do is keep track of whether a frozen cell is selectable while dragging the mouse. Because, as long as horizontal or vertical scrolling occurs because of mouse dragging, the frozen cells should not be selectable.

Next, in the OnKeyDown event handler, all occurrences of m_nFixedRows/GetFixedRowCount are replaced with GetFixedRowCount(m_bExcludeFreezedRowsFromSelection), and all occurences of m_nFixedCols/GetFixedColumnCount are replaced with GetFixedColumnCount(m_bExcludeFreezedColsFromSelection).

In the same event handler, the if(next != m_idCurrentCell) block was changed as follows for cell merging:

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

The scrolling needed to be adjusted a little too. In the OnHScroll event handler, the following code related to SB_PAGERIGHT and SB_PAGELEFT was modified:

C++
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;

And similarly for OnVScroll, the code was changed to:

C++
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;

Now comes the most important method, OnDraw(), which is the main drawing function of CGridCtrl. You don't need to make a whole lot of changes here for cell merging and freezing. In fact, the code change is minimal because of the excellent organization and design of the code in this method. What you may consider a major addition here is the code related to merge cells. Here is a sneak peek (actually more than that) at the changed OnDraw, and the changes made are pointed out by the //LUC comment on relevant lines.

C++
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

}

Don't get confused with the m_bShowHorzNonGridArea member, which I introduced just to eliminate the horizontal gray area to make the grid look good if the number of columns are less. In some cases, this will make up for the slight problem related to (horizontal) scrolling which Chris himself pointed out (remember his too much of gray area comment?). Since OnDraw has changed, RedrawCell needs to be changed a wee bit as well:

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

And also, some slight adjustments in the SetSelectedRange for cell merging which you can see below (again pointed out by the // LUC comment):

C++
// 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;
        }
    }
}

The code should be placed just on top of the if (bSelectCells) block. And also, there is a slight change in the SelectCells API:

C++
// 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);
}

Now, the rest of the changes in the CPP are just minor changes, and that is, calling the GetFixedRow/ColCount or GetFixedRowHeight() or GetFixedColumnWidth() properly for cell freezing functionalities, as well as GetTopLeftMergedCell for merge cell functionalities. The methods containing the rest of the changes are: GetCellFromPt(...) GetTopleftNonFixedCell(...) GetVisibleNonFixedCellRange(...) GetVisibleFixedCellRange(...) GetCellOrigin(...) GetFixedRowHeight(...) GetFixedColumnWidth(...) EnsureVisible(...) IsCellVisible(...) InvalidateCellRect(...) OnMouseMove(...) OnLButtonDblClk(...) OnLButtonDown(...) OnLButtonUp(...) OnEditCell(...).

If you browse through the source code, just search for the tag/comment // LUC, and you will find the minor modifications done in these functions, and it is not very hard to figure out what is going on.

Changes made for the CGridCellBasee class

Another important change made is in the GridCellBase.cpp where instead of directly calling the IsFocused(...)/IsSelected(...) API of the cell, we are calling GetGrid()->IsFocused(...) / GetGrid()->IsSelected(...). This is done to achieve the merge cell drawing effect, and is quite evident from the definition of the IsFocused and IsSelected API of the grid.

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

You can see from the code, during merge cell drawing, these APIs are trying to figure out if any of the cells which fall in the merge cell range are focused or selected. If that is the case, then the merge cell will be drawn as selected or focused.

Using the Code

Now that you have quite successfully added the merging and freezing functionalities, let's have a look at how to use them. For cell merging and splitting, here is a sample code:

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

For cell freezing, you can consider this sample:

C++
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 );
}

Points of Interest

I found some very, very minor issues (which can be easily addressed) in the original grid control source:

  1. A cell cannot be edited if a tooltip is shown.
  2. InplaceEditCtrl is not multiline even if a cell can contain multiline text.
  3. If a cell is too small, InplaceEditCtrl is almost invisible.

These problems are so minor, I guess nobody bothered to fix those. Another thing to note is, I added an extra functionality of removing the horizontal gray area which can be switched on and off by setting m_bShowHorzNonGridArea to TRUE and FALSE, respectively. You can see the OnDraw to see how this is achieved.

Acknowledgements

  1. Late Great Paul DiLascia: For his amazing articles, which proved writings on programming can be an art form and gave me inspiration to work on this subject.
  2. Chris Maunder: Thanks to Chris for this excellent control, which made me drastically improve on GUI control developing and architecture designing.
  3. Jacquese Raphanel: A little known legend, who can recreate MS Office from scratch by himself alone.

History

  • Article completed on 5th of July, 2010.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)