Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / MFC
Article

Integrating Undo/Redo support with MFC Grid

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
5 Mar 2002CPOL4 min read 96.1K   29   20
Shows how to integrate simple and easy Undo/Redo (by Keith Rule) into an application using the MFC Grid control (by Chris Maunder).

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

// Messages sent to the grid's parent (More will be added in future)
#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
// +++++++ added by pjp ++++++++++++++++++++++++++
#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);  // pjp
    for (int row = Selection.GetMinRow(); 
            row <= Selection.GetMaxRow(); row++)
        {
        for (int col = Selection.GetMinCol(); 
              col <= Selection.GetMaxCol(); col++)
            {
            //  don't clear hidden cells
            if ( m_arRowHeights[row] > 0 
                     && m_arColWidths[col]  > 0)
                {
                ValidateAndModifyCellContents(row, col, _T(""));
                }
            }
        }
    SendMessageToParent(Selection.GetTopLeft().row, 
       Selection.GetTopLeft().col, GVN_ENDDELETEITEMS); // pjp
    Refresh();
    }
BOOL CGridCtrl::PasteTextToGrid(CCellID cell, COleDataObject* pDataObject)
    {
...
    // Now store in generic TCHAR form so we no longer have to deal with
    // ANSI/UNICODE problems
    CString strText = szBuffer;
    delete szBuffer;

    SendMessageToParent(cell.row, cell.col, GVN_BEGINPASTE);    // pjp
...
    strText.UnlockBuffer();
    Refresh();

    SendMessageToParent(cell.row, cell.col, GVN_ENDPASTE);      // pjp

    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(); // don't checkpoint individual changes
    }

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(); // don't checkpoint individual changes
    }

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
//+++++ added by pjp ++++++++++++++++++++++++++++++++++++++++
#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)
//+++++ added by pjp ++++++++++++++++++++++++++++++++++++++++
#ifndef GRIDCTRL_NO_UNDO
// ON_COMMAND_EX allows us to continue routing the command when
// we can't handle it
    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

//+++++ added by pjp ++++++++++++++++++++++++++++++++++++++++
#ifndef GRIDCTRL_NO_UNDO
        case 'Z':
            DoEditUndo();
            break;
#endif
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
            }
        }
...

Also in GridCtrl.cpp:

//+++++ added by pjp ++++++++++++++++++++++++++++++++++++++++
#ifndef GRIDCTRL_NO_UNDO

BOOL CGridCtrl::DoEditUndo(UINT /*nID*/)
    {
    if (!IsEditable())
        return FALSE;

    // Get the Focus cell. If none then there's nothing to do
    CCellID cell = GetFocusCell();

    // If the cell is valid and is being edited,
    // then call its edit window undo function.
    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;  // we processed it, no more routing
        }
    return FALSE;     // continue routing
    }

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
        // we cannot process undo, let someone else have a go
        pCmdUI->m_bContinueRouting = TRUE;
    }

void CGridCtrl::OnUpdateEditRedo(CCmdUI* pCmdUI)
    {
    // intercept to disable the applications's redo when we're cell editing
    CCellID cell = GetFocusCell();

    if (IsValid(cell) && IsItemEditing(cell.row, cell.col))
        {
        CGridCellBase *pCell = GetCell(cell.row, cell.col);
        ASSERT(pCell);
        if (pCell)
            // we're editing in a cell, can't redo
            pCmdUI->Enable(FALSE);
        }
    else
        // we cannot process redo, let someone else have a go
        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.

License

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


Written By
Software Developer (Senior)
England England
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++

Comments and Discussions

 
QuestionHow can i apply this on Dialog Based Application? Pin
Member 1241072227-Jul-16 0:24
Member 1241072227-Jul-16 0:24 
AnswerRe: How can i apply this on Dialog Based Application? Pin
Phil J Pearson27-Jul-16 0:39
Phil J Pearson27-Jul-16 0:39 
GeneralRe: How can i apply this on Dialog Based Application? Pin
Member 1241072227-Jul-16 0:47
Member 1241072227-Jul-16 0:47 
Generalabout ON_COMMAND_EX [modified] Pin
Rockone17-Sep-07 22:18
Rockone17-Sep-07 22:18 
GeneralRe: about ON_COMMAND_EX Pin
Phil J Pearson18-Sep-07 9:17
Phil J Pearson18-Sep-07 9:17 
GeneralRe: about ON_COMMAND_EX Pin
Rockone18-Sep-07 16:09
Rockone18-Sep-07 16:09 
QuestionCould you please send us an example? Pin
urwangshd2-Jul-07 11:21
urwangshd2-Jul-07 11:21 
AnswerRe: Could you please send us an example? Pin
Phil J Pearson2-Jul-07 11:56
Phil J Pearson2-Jul-07 11:56 
QuestionAnother question about DoEditUndo function Pin
PlutoX73-Aug-06 11:58
PlutoX73-Aug-06 11:58 
AnswerRe: Another question about DoEditUndo function Pin
Phil J Pearson5-Aug-06 6:19
Phil J Pearson5-Aug-06 6:19 
Call DoEditUndo(0) - it doesn't use the parameter anyway.


Regards,
Phil
QuestionHow can I add message map for a Doc/View application? Pin
PlutoX73-Aug-06 2:38
PlutoX73-Aug-06 2:38 
AnswerRe: How can I add message map for a Doc/View application? Pin
Phil J Pearson3-Aug-06 11:15
Phil J Pearson3-Aug-06 11:15 
GeneralRe: How can I add message map for a Doc/View application? Pin
PlutoX73-Aug-06 11:54
PlutoX73-Aug-06 11:54 
QuestionHow to serialize the grid ? Pin
CosminU7-Jun-05 22:59
CosminU7-Jun-05 22:59 
AnswerRe: How to serialize the grid ? Pin
Phil J Pearson8-Jun-05 9:47
Phil J Pearson8-Jun-05 9:47 
QuestionWhere is Alex Lemaresquier? Pin
Nish Nishant9-Oct-03 23:49
sitebuilderNish Nishant9-Oct-03 23:49 
AnswerRe: Where is Alex Lemaresquier? Pin
clownstaples7-Feb-06 10:48
clownstaples7-Feb-06 10:48 
GeneralInconsistent definition of DoEditUndo function Pin
Richard Cunday6-Mar-02 4:55
Richard Cunday6-Mar-02 4:55 
GeneralRe: Inconsistent definition of DoEditUndo function Pin
Phil J Pearson6-Mar-02 8:53
Phil J Pearson6-Mar-02 8:53 
GeneralExamples Pin
Jon Newman4-Mar-02 10:20
Jon Newman4-Mar-02 10:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.