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

Grid Control Re-dux with Smooth Scroll and Composite Cell

4.95/5 (32 votes)
27 Aug 2010CPOL4 min read 122.8K   4.9K  
Chris Maunder's CGridCtrl had been modified to accomodate merged cells and freezed panes, now it has the cool feature of Horizontal Smooth Scrolling and Composite Cell (to show a minigrid inside a single cell)
gridctrl_redux.jpg

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:

  1. Horizontal smooth scrolling - which helps to get rid of the annoying (to some people) gray area from the Grid, and
  2. 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:

C++
CGridCtrl::GetTopleftNonFixedCell(...) :
C++
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 :

C++
//
// Definition of the CGridObj class
//
class CGridObj : public CObject
{
typedef std::vector<int> intlist;
// storage typedef for each row in the grid
typedef CTypedPtrArray<CObArray, CGridCellBase*> GRID_ROW;
public:
// Constructor
//
// owner : Specifies the existing CHotListCtrl which contains the CT_GRID cell 
// and the CGridObj
//
CGridObj(CGridCtrl& owner);
virtual ~CGridObj();

////////////////////////////////////////////////////////////////////////////////
// Attributes
////////////////////////////////////////////////////////////////////////////////
public:
// Gets the row count
int GetRowCount() const;

// Gets the column count
int GetColumnCount() const;

// Sets the row count
BOOL SetRowCount(int nRows);

// Sets the column count
BOOL SetColumnCount(int nCols);

// Deletes all items
BOOL DeleteAllItems(); 

// Checks if a given cell is in valid range
BOOL IsValid(int nRow, int nCol) const;

// Gets the row height
int GetRowHeight(int nRow) const;

// Sets the row height
BOOL SetRowHeight(int row, int height);

// Gets the column width
int GetColumnWidth(int nCol) const;

// Sets the column width
BOOL SetColumnWidth(int col, int width);

// Sets the default row height to use if row height is not specified
void SetDefaultRowHeight(int nHeight);

// Specifies the column which should be stretched if the grid is expanded
void SetStretchColIndex(int nCol);

// Gets pointer to a cell given row and column index
CGridCellBase* GetCell(int nRow, int nCol) const; 

// Sets a new type of cell given a row and column index
BOOL SetCell(int nRow, int nCol, CGridCellBase* pCell); 

// Sets a new type of cell given a row and column index removing the old cell 
// given runtime class of the cell
BOOL SetCellType(int nRow, int nCol, CRuntimeClass* pRuntimeClass);

// Sets cell text to a cell given row and column index
BOOL SetCellText(int nRow, int nCol, LPCTSTR pszText);

// Gets the cell text
LPCTSTR GetCellText(int nRow, int nCol); 

// Gets cell id given a point relative to the grid
CCellID GetCellFromPt(CPoint point);

// Gets cell origin of a cell
BOOL GetCellOrigin(int nRow, int nCol, LPPOINT p);

// Gets cell area given row and column index
BOOL GetCellRect(int nRow, int nCol, LPRECT pRect); 

// Gets cell text area given row and column index
BOOL GetTextRect(int nRow, int nCol, LPRECT pRect); 

// Specifies whether to show horizontal and vertical grid lines
void SetGridLines(BOOL bHorzGridLines, BOOL bVertGridLines, 
	COLORREF clrGridLines = RGB(192,192,192)); 

// Specifies whether the grid is in selected mode
void SetSelected(BOOL bSelected = TRUE);

// Determines if the grid is in selected mode
BOOL IsSelected(); 

// Specifies whether the grid is in focus mode
void SetFocus(BOOL bFocus = TRUE);

// Determines if the grid is in focus mode
BOOL IsFocused();

// Gets total sum of cell extents of the grid
//
// bAutoSizeing : This should be passed as TRUE if this API is called because 
// of due to the invoking of GridCtrl::AutoSize... routines.
// When this is TRUE, the GetGridExtent actually changes resizes the internal 
// cells to their extents (useful when user double clicks on a composite cell 
// containing the GridObj)
//
virtual CSize GetGridExtent(BOOL bAutoSizing, CDC* pDC);

////////////////////////////////////////////////////////////////////////////////
//Operations
////////////////////////////////////////////////////////////////////////////////

// Start editing of a cell in the grid given row and col index
BOOL Edit(int nRow, int nCol, CRect rect, CPoint point, UINT nID, UINT nChar);

// Ends the current editing in the grid
void EndEdit();

// Specifies the focus cell in the grid
CCellID SetFocusCell(CCellID cell); 

// Retrieves the focus cell in the grid
CCellID GetFocusCell();

////////////////////////////////////////////////////////////////////////////////
//virtual overrides
////////////////////////////////////////////////////////////////////////////////
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();

////////////////////////////////////////////////////////////////////////////////
//Drawing
////////////////////////////////////////////////////////////////////////////////

// Draws the content of the grid in a device context
virtual void Draw(CDC* pDC, CRect rcDraw); 

// Specifies a coordinate to use for cells in the grid
void SetCoords(int nRow, int nCol); 

protected:

////////////////////////////////////////////////////////////////////////////////
//Creation and clean up
////////////////////////////////////////////////////////////////////////////////
virtual CGridCellBase* CreateCell(int nRow, int nCol);
virtual void DestroyCell(int nRow, int nCol); 

////////////////////////////////////////////////////////////////////////////////
//Implementation
////////////////////////////////////////////////////////////////////////////////

CCellID GetTopleftNonFixedCell(BOOL bForceRecalculation = FALSE);

CCellRange GetVisibleNonFixedCellRange
	(LPRECT pRect = NULL, BOOL bForceRecalculation = FALSE);

// Cell details
int m_nRows, m_nCols;
CUIntArray m_arRowHeights, m_arColWidths;
intlist m_arRowOrder, m_arColOrder; 
CRuntimeClass* m_pRtcDefault; // determines kind of Grid Cell created by default

// Cell data
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; 

// for future use
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:

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

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

License

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