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:
public:
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;
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:
int m_nFreezedRows, m_nFreezedCols;
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;
}
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:
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:
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:
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
:
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:
if (next != m_idCurrentCell)
{
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)
{
next.col--;
}
}
break;
}
case VK_RIGHT:
{
if(GetBottomRightMergedCell(nNextRow, nNextCol, rectNull))
{
next.col = nNextCol;
if(bMerged)
{
next.col++;
}
}
break;
}
case VK_UP:
{
if(GetTopLeftMergedCell(nNextRow, nNextCol, rectNull))
{
next.row = nNextRow;
if(bMerged)
{
next.row--;
}
}
break;
}
case VK_DOWN:
{
if(GetBottomRightMergedCell(nNextRow, nNextCol, rectNull))
{
next.row = nNextRow;
if(bMerged)
{
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:
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:
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.
void CGridCtrl::OnDraw(CDC* pDC)
{
if (!m_bAllowDraw)
return;
CRect clipRect;
if (pDC->GetClipBox(&clipRect) == ERROR)
return;
EraseBkgnd(pDC);
#ifdef _DEBUG
LARGE_INTEGER iStartCount;
QueryPerformanceCounter(&iStartCount);
#endif
CRect rc;
GetClientRect(rc);
CRect rect;
int row, col;
CGridCellBase* pCell;
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);
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);
}
}
}
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;
if (rect.top > clipRect.bottom)
break; if (rect.bottom < clipRect.top)
continue;
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; if (rect.right < clipRect.left)
continue;
pCell = GetCell(row, col);
if (pCell)
{
pCell->SetCoords(row,col);
pCell->Draw(pDC, row, col, rect, FALSE);
}
}
}
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;
if (rect.top > clipRect.bottom)
break; if (rect.bottom < clipRect.top)
continue;
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; if (rect.right < clipRect.left)
continue;
pCell = GetCell(row, col);
if (pCell)
{
pCell->SetCoords(row,col);
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);
}
}
}
}
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;
if (rect.top > clipRect.bottom)
break; if (rect.bottom < clipRect.top)
continue;
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; if (rect.right < clipRect.left)
continue;
pCell = GetCell(row, col);
if (pCell)
{
pCell->SetCoords(row,col);
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);
if (m_nGridLines == GVL_BOTH || m_nGridLines == GVL_VERT)
{
int x = GetFixedColumnWidth();
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, nFixedRowHeightExcludingFreezedRows);
pDC->LineTo(x-1, VisRect.bottom);
}
}
if (m_nGridLines == GVL_BOTH || m_nGridLines == GVL_HORZ)
{
int y = GetFixedRowHeight();
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(nFixedColumnWidthExcludingFreezedColumns, y-1);
pDC->LineTo(VisRect.right, y-1);
}
}
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;
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);
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:
BOOL CGridCtrl::RedrawCell(int nRow, int nCol, CDC* pDC )
{
BOOL bResult = TRUE;
BOOL bMustReleaseDC = FALSE;
if (!m_bAllowDraw || !IsCellVisible(nRow, nCol))
return FALSE;
CRect rect;
if (!GetCellRect(nRow, nCol, rect))
return FALSE;
BOOL bIsMergeCell = GetTopLeftMergedCell(nRow, nCol, rect);
if (!pDC)
{
pDC = GetDC();
if (pDC)
bMustReleaseDC = TRUE;
}
if (pDC)
{
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);
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);
if (bMustReleaseDC)
ReleaseDC(pDC);
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):
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);
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;
}
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:
void CGridCtrl::SelectCells(CCellID currentCell,
BOOL bForceRedraw ,
BOOL bSelectCells )
{
if (!m_bEnableSelection)
return;
int row = currentCell.row;
int col = currentCell.col;
if (row < GetFixedRowCount(m_bExcludeFreezedRowsFromSelection) ||
col < GetFixedColumnCount(m_bExcludeFreezedColsFromSelection))
if (row < GetFixedRowCount() || col < GetFixedColumnCount())
{
return;
}
if (!IsValid(currentCell))
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.
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:
static int g_nLastMergeID = -1;
void CGridCtrlTestDlg::OnBnClickedButtonMerge()
{
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()
{
m_grid.SplitCells(g_nLastMergeID);
m_grid.Refresh();
}
For cell freezing, you can consider this sample:
void CGridCtrlTestDlg::OnBnClickedButtonFreeze()
{
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:
- A cell cannot be edited if a tooltip is shown.
InplaceEditCtrl
is not multiline even if a cell can contain multiline text.- 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
- 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.
- Chris Maunder: Thanks to Chris for this excellent control, which made me drastically improve on GUI control developing and architecture designing.
- Jacquese Raphanel: A little known legend, who can recreate MS Office from scratch by himself alone.
History
- Article completed on 5th of July, 2010.