Introduction
Earlier, I had uploaded a source code to CodeProject which added merge cell and freeze pane functionality to Chris Maunder's CGridCtrl
. This time, two new useful features have been added along with merge cell and freeze pane. The features are:
- Horizontal smooth scrolling - which helps to get rid of the annoying (to some people) gray area from the
Grid
, and - A provision to show a mini-grid inside a cell through a new cell type called
CGridCellComposite
Those who are familiar with my other CGridCtrl
related article should treat this one as the complete package.
Background
Ever since I came across this control in CodeProject, I have been somewhat obsessed by it. The concepts used in this control was useful in a project I was working with. In that project, there were requirements for merge cell, freeze pane, smooth horizontal scroll and mini grid cell in a list control. After I got over with the project, I turned my attention to CGridCtrl
and the concepts used in my project for merge cell, freeze pane, smooth scroll and mini grid cell - were the ones I tried to incorporate into CGridCtrl
. Because of the fabulous design and architecture of this control, I was easily able to integrate those concepts here.
How to Achieve Smooth Scrolling
Achieving the smooth scrolling is a little tricky and difficult. The way I did it was to introduce a variable m_nScrollX
and this variable keeps the value of how much the first visible top left cell is shifted from the actual position because of implementing Microsoft XL style scrolling where you only scroll by column to column. In other words, this variable holds the error margin introduced due to scrolling column by column. Then this error margin is used in the cell rectangle calculation and in OnDraw
to give the smooth scrolling effect.
This is how the value of m_nScrollX
is calculated inside:
CGridCtrl::GetTopleftNonFixedCell(...) :
while (nRight < nHorzScroll && m_idTopLeftCell.col < (GetColumnCount()-1))
{
m_nScrollX = nRight - nHorzScroll;
nRight += GetColumnWidth(m_idTopLeftCell.col++);
}
You can see that it is just taking the difference between the right of the cell and the horizontal scroll position, which actually is the error margin introduced.
You can search in the uploaded source code with this variable m_nScrollX
to find out the places where it needed to be used to adjust for the smooth scrolling.
The Composite (Mini Grid) Cell
The composite cell or the mini grid cell is a new cell type named CGridCellComposite
which can be used to show multiple cells inside a single cell. This is sometimes useful if you want to show button type cell (button type cell is not in this source code, but I developed a cell such as this for my project) beside an edit box. Even in some applications, you would see a cell can contain a grid itself. So the composite cell shows a way to achieve that effect of showing a mini-grid inside a cell of the grid.
In order to develop this kind of cell, I came up with a CGridObj
which actually contains the very basic functions and data structures of CGridCtrl
independent of window handles and other window related stuffs.
Following is a look at the basic structure of the CGridObj
:
class CGridObj : public CObject
{
typedef std::vector<int> intlist;
typedef CTypedPtrArray<CObArray, CGridCellBase*> GRID_ROW;
public:
CGridObj(CGridCtrl& owner);
virtual ~CGridObj();
public:
int GetRowCount() const;
int GetColumnCount() const;
BOOL SetRowCount(int nRows);
BOOL SetColumnCount(int nCols);
BOOL DeleteAllItems();
BOOL IsValid(int nRow, int nCol) const;
int GetRowHeight(int nRow) const;
BOOL SetRowHeight(int row, int height);
int GetColumnWidth(int nCol) const;
BOOL SetColumnWidth(int col, int width);
void SetDefaultRowHeight(int nHeight);
void SetStretchColIndex(int nCol);
CGridCellBase* GetCell(int nRow, int nCol) const;
BOOL SetCell(int nRow, int nCol, CGridCellBase* pCell);
BOOL SetCellType(int nRow, int nCol, CRuntimeClass* pRuntimeClass);
BOOL SetCellText(int nRow, int nCol, LPCTSTR pszText);
LPCTSTR GetCellText(int nRow, int nCol);
CCellID GetCellFromPt(CPoint point);
BOOL GetCellOrigin(int nRow, int nCol, LPPOINT p);
BOOL GetCellRect(int nRow, int nCol, LPRECT pRect);
BOOL GetTextRect(int nRow, int nCol, LPRECT pRect);
void SetGridLines(BOOL bHorzGridLines, BOOL bVertGridLines,
COLORREF clrGridLines = RGB(192,192,192));
void SetSelected(BOOL bSelected = TRUE);
BOOL IsSelected();
void SetFocus(BOOL bFocus = TRUE);
BOOL IsFocused();
virtual CSize GetGridExtent(BOOL bAutoSizing, CDC* pDC);
BOOL Edit(int nRow, int nCol, CRect rect, CPoint point, UINT nID, UINT nChar);
void EndEdit();
CCellID SetFocusCell(CCellID cell);
CCellID GetFocusCell();
virtual void OnEndEdit(int nRow, int nCol);
virtual void OnClickDown(CRect cellRect, CPoint PointCellRelative);
virtual void OnClick(CRect cellRect, CPoint PointCellRelative);
virtual void OnDblClick(CRect cellRect, CPoint PointCellRelative);
virtual BOOL OnSetCursor();
virtual void Draw(CDC* pDC, CRect rcDraw);
void SetCoords(int nRow, int nCol);
protected:
virtual CGridCellBase* CreateCell(int nRow, int nCol);
virtual void DestroyCell(int nRow, int nCol);
CCellID GetTopleftNonFixedCell(BOOL bForceRecalculation = FALSE);
CCellRange GetVisibleNonFixedCellRange
(LPRECT pRect = NULL, BOOL bForceRecalculation = FALSE);
int m_nRows, m_nCols;
CUIntArray m_arRowHeights, m_arColWidths;
intlist m_arRowOrder, m_arColOrder;
CRuntimeClass* m_pRtcDefault;
CTypedPtrArray<CObArray, GRID_ROW* > m_RowData;
CGridCtrl& m_Grid;
UINT m_nGridLines;
COLORREF m_clrGridLines;
CCellID m_idTopLeftCell;
CCellID m_idCurrentCell;
CRect m_rcDraw;
CPoint m_ptGridOrigin;
CCellID m_idCoords;
BOOL m_bSelected;
BOOL m_bFocus;
int m_nDefaultRowHeight;
int m_nStretchColIndex;
int m_nVertScroll;
int m_nHorzScroll;
};
Obviously, you can see it's sort of a replica of the CGridCtrl
itself except the window related features such as handling of WM_LBUTTONDOWN
, WM_LBUTTONUP
, etc. which belongs to a window. The CGridObj
is almost a stripped down raw version of CGridCtrl
to be precise.
The CGridCellComposite
cell holds a pointer to this CGridObj
and the user has to populate the CGridObj
just like he'd populate the normal CGridCtrl
with cells.
Using the Code
Following is an example of how you would populate the CGridObj
inside the composite cell:
void CGridCtrlDemoDlg::OnCellComposite()
{
CGridObj* pGrid = new CGridObj(m_Grid);
if(pGrid != NULL)
{
OnCellNormal();
if (!m_Grid.SetCellType(1,1, RUNTIME_CLASS(CGridCellComposite)))
return;
CGridCellComposite* pCell =
(CGridCellComposite*) m_Grid.GetCell(1,1);
m_Grid.SetColumnWidth(1, 150);
pGrid->SetRowCount(1);
pGrid->SetColumnCount(2);
pGrid->SetColumnWidth(0, 60);
pGrid->SetColumnWidth(1, 120);
pGrid->SetCellType(0, 0, RUNTIME_CLASS(CGridCellCombo));
pGrid->SetCellType(0, 1, RUNTIME_CLASS(CGridURLCell));
pGrid->SetCellText(0, 1, _T("www.codeproject.com"));
CStringArray options;
options.Add(_T("Code Project"));
options.Add(_T("Google"));
options.Add(_T("Enosis"));
CGridCellCombo *pComboCell = (CGridCellCombo*) pGrid->GetCell(0,0);
pComboCell->SetOptions(options);
pComboCell->SetStyle(CBS_DROPDOWN);
pComboCell->SetText(_T("Code Project"));
CCompositeCellInfo info;
info.m_pGrid = pGrid;
pCell->SetCellInfo(info);
}
}
Remember that the CGridObj
created dynamically will be automatically released by the Composite Cell on it's destruction. So DO NOT DELETE the CGridObj
created with new CGridObj(...)
. The CGridObj
cannot be created without a valid CGridCtrl
instance passed in the constructor. The CGridCtrl
passed in the constructor notifies the CGridObj
about whom it belongs to (i.e. which CGridCtrl
is the owner of the CGridObj
).
Here is an example of how to handle the editing of a composite cell:
void CGridCtrlDemoDlg::OnGridEndEdit(NMHDR *pNotifyStruct, LRESULT* pResult)
{
NM_GRIDVIEW* pItem = (NM_GRIDVIEW*) pNotifyStruct;
Trace(_T("End Edit on row %d, col %d\n"), pItem->iRow, pItem->iColumn);
*pResult = (m_bRejectEditChanges)? -1 : 0;
if(m_bRejectEditChanges)
return;
CGridCellComposite* pCell = DYNAMIC_DOWNCAST(CGridCellComposite,
m_Grid.GetCell(pItem->iRow,
pItem->iColumn));
if(pCell != NULL)
{
CCellID cellID = pCell->GetGridObj()->GetFocusCell();
if(cellID.col == 0)
{
CString str = pCell->GetGridObj()->GetCellText(0, 0);
if(!str.CompareNoCase(_T("Code Project")))
{
pCell->GetGridObj()->SetCellText(0, 1, _T("www.codeproject.com"));
}
else if(!str.CompareNoCase(_T("Google")))
{
pCell->GetGridObj()->SetCellText(0, 1, _T("www.google.com"));
}
else if(!str.CompareNoCase(_T("Enosis")))
{
pCell->GetGridObj()->SetCellText(0, 1, _T("www.enosisbd.com"));
}
}
}
}
The notable thing to watch here is the use of GetFocusCell()
API of the grid object to get the row and column index of the child cell inside the composite cell which has just been edited.
Points of Interest
You can enable/disable the smooth scrolling by calling the EnableSmoothScroll
API of the CGridCtrl
.
There is an SetStretchColIndex
API by which you can specify the index of the column which will be resized dynamically when the cell is resized so that its contents become visible if the cell is made larger in size or vice versa (contents becoming invisible if the cell is made smaller).
Acknowledgements
- Paul DiLascia: For his amazing contribution through his articles that helped me learn so much about MFC.
- Chris Maunder: For his excellent
CGridCtrl
which gave me an opportunity to work on a beautiful control. - Jacques Raphanel: An extraordinary programmer who helped me improve my MFC knowledge to apply in most cases of GUI development as well as other arenas of programming.
History
- 26th August, 2010 - Fixed some spelling errors, invalid tags, and code snippet format. JSOP.
- 18th August, 2010 - Article uploaded