Introduction
I needed a grid control for my latest application and decided to use Chris Maunder's excellent MFC Grid control. During development I made a few minor changes to the grid control so that it suited my need more exactly. I'll describe the changes later because it might be of interest to others, and anyway it's relevant to this article.
It soon became clear that I needed to add undo/redo support and since I was using MFC Document/View support and my documents would always be fairly small, Keith Rule's Simple and Easy Undo/Redo was ideal. I liked Alex Lemaresquier's suggestion in his comment attached to the article Small trick to enhance this brilliant code, so I incorporated his code.
So now I had a goody bag of great code written by other people (not the way I normally work, but this stuff is too good to ignore). Integrating the Undo/Redo was simply a matter of following Keith Rule's (and Alex Lemaresquier's) simple instructions. I had to choose a place to add a CheckPoint
call when the document had been modified; this turned out to be in my handler for the GVN_ENDLABELEDIT
notification. Everything worked straight away but I felt I needed a little more. To explain what and why, I should first describe the first change I made to the grid code.
Modification to the grid
I noticed that when I had selected multiple cells in the grid and pressed <Delete> only the contents of the single "current cell" in the grid were deleted. I felt that all the selected cells should be cleared. This was very simple to change. I just made the following modification to part of the code in CGridCtrl::OnKeyDown
:
...
if (nChar == VK_DELETE)
{
#if 1 // [pjp]
CCellRange Selection = GetSelectedCellRange();
if (IsValid(Selection))
ClearCells(Selection);
#else
ValidateAndModifyCellContents(m_idCurrentCell.row,
m_idCurrentCell.col, _T(""));
#endif
}
...
Problem to solve
This worked fine but there was a problem associated with undo/redo: because ClearCells
ends up generating a GVN_ENDLABELEDIT
notification for each cell whose content is deleted, I was modifying the document and making an entry in the undo list for each cell. That meant I could delete multiple cells with a single key press but only Undo the action one cell at a time! The same kind of problem happened when cutting and pasting multiple cells. What was needed was some kind of transaction system so that a multiple-cell deletion could be undone and redone as a single transaction. Implementing code like this in the grid itself would probably have meant lots of changes that would pollute the code, so I decided on a simpler solution that would keep the pseudo-transaction code entirely separate from the grid.
The solution
The only help I needed from the grid was to be told when each 'transaction' (multi-cell deletion/cut or paste operation) was beginning or ending. It would probably have been possible for the application to work this out by intercepting all relevant command and keydown messages going to the grid, but it would have been messy to say the least. I decided to ask the grid to be kind enough to send a few extra notification messages; it agreed, provided I wrote the code for it. I added a few lines to GridCtrl.h to define the new messages:
More modifications to the grid
#define GVN_BEGINDRAG LVN_BEGINDRAG // LVN_FIRST-9
#define GVN_BEGINLABELEDIT LVN_BEGINLABELEDIT // LVN_FIRST-5
#define GVN_BEGINRDRAG LVN_BEGINRDRAG
#define GVN_COLUMNCLICK LVN_COLUMNCLICK
#define GVN_DELETEITEM LVN_DELETEITEM
#define GVN_ENDLABELEDIT LVN_ENDLABELEDIT // LVN_FIRST-6
#define GVN_SELCHANGING LVN_ITEMCHANGING
#define GVN_SELCHANGED LVN_ITEMCHANGED
#define GVN_GETDISPINFO LVN_GETDISPINFO
#define GVN_ODCACHEHINT LVN_ODCACHEHINT
#define GVN_BEGINDELETEITEMS (LVN_FIRST-79)
#define GVN_ENDDELETEITEMS (LVN_FIRST-80)
#define GVN_BEGINPASTE (LVN_FIRST-81)
#define GVN_ENDPASTE (LVN_FIRST-82)
Next I added code to a few places in GridCtrl.cpp to send the notifications:
void CGridCtrl::ClearCells(CCellRange Selection)
{
SendMessageToParent(Selection.GetTopLeft().row,
Selection.GetTopLeft().col, GVN_BEGINDELETEITEMS);
for (int row = Selection.GetMinRow();
row <= Selection.GetMaxRow(); row++)
{
for (int col = Selection.GetMinCol();
col <= Selection.GetMaxCol(); col++)
{
if ( m_arRowHeights[row] > 0
&& m_arColWidths[col] > 0)
{
ValidateAndModifyCellContents(row, col, _T(""));
}
}
}
SendMessageToParent(Selection.GetTopLeft().row,
Selection.GetTopLeft().col, GVN_ENDDELETEITEMS);
Refresh();
}
BOOL CGridCtrl::PasteTextToGrid(CCellID cell, COleDataObject* pDataObject)
{
...
CString strText = szBuffer;
delete szBuffer;
SendMessageToParent(cell.row, cell.col, GVN_BEGINPASTE);
...
strText.UnlockBuffer();
Refresh();
SendMessageToParent(cell.row, cell.col, GVN_ENDPASTE);
return TRUE;
}
Processing the notifications in the application
Any application can ignore the new notifications if it wants to and the cost is negligible, but my application now uses them to avoid calling CheckPoint
multiple times for a single 'transaction'. The extra code is quite simple. First the new notifications are added to the message map:
ON_NOTIFY(GVN_BEGINDELETEITEMS, IDC_GRID, OnGridBeginDelete)
ON_NOTIFY(GVN_ENDDELETEITEMS, IDC_GRID, OnGridEndDelete)
ON_NOTIFY(GVN_BEGINPASTE, IDC_GRID, OnGridBeginPaste)
ON_NOTIFY(GVN_ENDPASTE, IDC_GRID, OnGridEndPaste)
Then the handler functions for the new notifications:
void CMyView::OnGridBeginDelete(NMHDR *pNotifyStruct, LRESULT* pResult)
{
*pResult = true;
CMyDoc *pDoc = GetDocument();
pDoc->DisableCheckPoint();
}
void CMyView::OnGridEndDelete(NMHDR *pNotifyStruct, LRESULT *pResult)
{
*pResult = true;
CMyDoc *pDoc = GetDocument();
NM_GRIDVIEW *pItem = (NM_GRIDVIEW *)pNotifyStruct;
pDoc->EnableCheckPoint();
CString cs;
cs.Format("Delete cells from row %d,column %d",
pItem->iRow, pItem->iColumn);
pDoc->CheckPoint(cs);
}
void CMyView::OnGridBeginPaste(NMHDR *pNotifyStruct, LRESULT* pResult)
{
*pResult = true;
CMyDoc *pDoc = GetDocument();
pDoc->DisableCheckPoint();
}
void CMyView::OnGridEndPaste(NMHDR *pNotifyStruct, LRESULT* pResult)
{
*pResult = true;
CMyDoc *pDoc = GetDocument();
NM_GRIDVIEW *pItem = (NM_GRIDVIEW *)pNotifyStruct;
pDoc->EnableCheckPoint();
CString cs;
cs.Format("Paste cells to row %d,column %d",
pItem->iRow, pItem->iColumn);
pDoc->CheckPoint(cs);
}
The result of all this is simply to cause CheckPoint
to ignore all calls between the paired *BEGIN*
and *END*
messages. The CheckPoint
call in the END message handler adds a single entry into the undo list for the whole transaction. A single call to Undo
will now undo the entire delete, cut or paste operation. Notice the CString
argument to CheckPoint
. This is available because I added Alex Lemaresquier's code. If you're not using his enhancement just omit the argument and the associated code.
Enabling undo in cells being edited
All that was left was to be able to handle undo in the edit control when editing a cell. I did this simply by using the standard edit control's built-in undo facility. It's not very good and there's no separate redo (it's a single-level undo that performs a redo if you call the function a second time), but it's what users are accustomed to. It needed a few additions to the grid code:
In GridCtrl.h:
#ifndef GRIDCONTROL_NO_CLIPBOARD
afx_msg void OnUpdateEditCopy(CCmdUI* pCmdUI);
afx_msg void OnUpdateEditCut(CCmdUI* pCmdUI);
afx_msg void OnUpdateEditPaste(CCmdUI* pCmdUI);
#endif
#ifndef GRIDCTRL_NO_UNDO
afx_msg BOOL DoEditUndo(UINT nID);
afx_msg void OnUpdateEditUndo(CCmdUI* pCmdUI);
afx_msg void OnUpdateEditRedo(CCmdUI* pCmdUI);
#endif
In GridCtrl.cpp:
...
ON_NOTIFY(GVN_ENDLABELEDIT, IDC_INPLACE_CONTROL, OnEndInPlaceEdit)
#ifndef GRIDCTRL_NO_UNDO
ON_COMMAND_EX(ID_EDIT_UNDO, DoEditUndo)
ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, OnUpdateEditUndo)
ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, OnUpdateEditRedo)
#endif
END_MESSAGE_MAP()
and
void CGridCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if (!IsValid(m_idCurrentCell))
{
CWnd::OnKeyDown(nChar, nRepCnt, nFlags);
return;
}
CCellID next = m_idCurrentCell;
BOOL bChangeLine = FALSE;
BOOL bHorzScrollAction = FALSE;
BOOL bVertScrollAction = FALSE;
if (IsCTRLpressed())
{
switch (nChar)
{
case 'A':
OnEditSelectAll();
break;
#ifndef GRIDCONTROL_NO_CLIPBOARD
case 'X':
OnEditCut();
break;
case VK_INSERT:
case 'C':
OnEditCopy();
break;
case 'V':
OnEditPaste();
break;
#endif
#ifndef GRIDCTRL_NO_UNDO
case 'Z':
DoEditUndo();
break;
#endif
}
}
...
Also in GridCtrl.cpp:
#ifndef GRIDCTRL_NO_UNDO
BOOL CGridCtrl::DoEditUndo(UINT )
{
if (!IsEditable())
return FALSE;
CCellID cell = GetFocusCell();
if (IsValid(cell) && IsItemEditing(cell.row, cell.col))
{
CGridCellBase *pCell = GetCell(cell.row, cell.col);
ASSERT(pCell);
if (pCell)
{
CWnd* pEditWnd = pCell->GetEditWnd();
if (pEditWnd && pEditWnd->IsKindOf(RUNTIME_CLASS(CEdit)))
((CEdit*)pEditWnd)->Undo();
}
return TRUE;
}
return FALSE;
}
void CGridCtrl::OnUpdateEditUndo(CCmdUI* pCmdUI)
{
BOOL bCanUndo = FALSE;
CCellID cell = GetFocusCell();
if (IsValid(cell) && IsItemEditing(cell.row, cell.col))
{
CGridCellBase *pCell = GetCell(cell.row, cell.col);
ASSERT(pCell);
if (pCell)
{
CWnd *pEditWnd = pCell->GetEditWnd();
if (pEditWnd && pEditWnd->IsKindOf(RUNTIME_CLASS(CEdit)))
bCanUndo = ((CEdit*)pEditWnd)->CanUndo();
}
pCmdUI->Enable(bCanUndo);
}
else
pCmdUI->m_bContinueRouting = TRUE;
}
void CGridCtrl::OnUpdateEditRedo(CCmdUI* pCmdUI)
{
CCellID cell = GetFocusCell();
if (IsValid(cell) && IsItemEditing(cell.row, cell.col))
{
CGridCellBase *pCell = GetCell(cell.row, cell.col);
ASSERT(pCell);
if (pCell)
pCmdUI->Enable(FALSE);
}
else
pCmdUI->m_bContinueRouting = TRUE;
}
#endif
Acknowledgements
I recognise that my own contribution in all this is pretty small (but perhaps pretty in a small way).
My thanks are due, of course, to Chris Maunder, Keith Rule and Alex Lemaresquier.
P.S.
If Alex Lemaresquier is feminine I ask her to accept my apologies for referring to her as 'him' in this article! I would be delighted to hear from her so that I can make the necessary corrections. On the other hand, of course, if he's masculine I'd like to know so that I can remove this stupid P.S.
Started my career as an electronics engineer.
Started software development in 4004 assembler.
Progressed to 8080, Z80 and 6802 assembler in the days when you built your own computer from discrete components.
Dabbled in Macro-11 and Coral66 by way of a small digression.
Moved on to C, first on Z80s and then on PCs when they were invented.
Continued to C++ and then C#
Now working mostly in C# and XAML while maintaining about half a million lines of C++