<

Go Premium for a chance to win a PS4. Enter to Win

x

Sudoku, a complete MFC application.  Part 9

Published on
10,104 Points
3,004 Views
1 Endorsement
Last Modified:

Introduction:

The undo support, implementing a stack.

Continuing from the eigth article about sudoku.  

We need a mechanism to keep track of the digits entered so as to implement an undo mechanism.  This should be a ‘Last In First Out’ collection – basically a stack.  MFC supplies various collections, unfortunately a stack is not one of them.  We require what digit was the previous value and to which button.  Where to implement this?  Typically a document would be for the data storage but we are not implementing Sudoku in that way.  Here the buttons themselves (on the view) store the information – so I will implement this undo feature in the view.  An alternative would have been to store the previous values within each button itself – so all we would require was which button was last changed (but I coded it this way before I had that idea, and this way does work - decisions, choices, for a real application plan, plan PLAN! - there is no substitute for that).

First we need to create a new class – CUndoStack – for the storage mechanism.  Solution explorer, Add class, C++ class, Add – enter CUndoStack as the name and accept the defaults.  Modify the header file to be as follows:

#pragma once

class CUndoStack
{
public:
    CUndoStack(UINT nUndoMessage);
    ~CUndoStack(void);

    void AddItem(CWnd* pWndBtn, int digit);
    void RemoveItem();
    bool HasItems();
    void Reset();

private:
    UINT m_nUndoMessage;

    struct strUndo
    {
        int m_iDigit;
        CWnd* m_pWndBtn;
    };
    CList<strUndo*, strUndo*> m_lstUndo;

    bool m_bInRemove;
};

Open in new window


And the c.pp to be as follows

#include "StdAfx.h"
#include "UndoStack.h"

CUndoStack::CUndoStack(UINT nUndoMessage)
: m_nUndoMessage(nUndoMessage), m_bInRemove(false)
{
}

CUndoStack::~CUndoStack(void)
{
    //Clean up the internal list - prevent memory leaks
    Reset();
}

void CUndoStack::AddItem(CWnd* pWndBtn, int digit)
{
    //The removal sends a message to the window - this is to prevent the item being added to the list again
    if(m_bInRemove)
        return;

    strUndo* pUndo = new strUndo;
    pUndo->m_pWndBtn = pWndBtn;
    pUndo->m_iDigit = digit;
    m_lstUndo.AddTail(pUndo);
}

void CUndoStack::RemoveItem()
{
    if(HasItems())    //No items - then nothing to do
    {
        strUndo* pUndo = m_lstUndo.RemoveTail();    //unlink from the internal list

        //Set the internal flag to prevent re-addition to the list of undo items
        m_bInRemove = true;
        pUndo->m_pWndBtn->SendMessage(m_nUndoMessage, (WPARAM)(pUndo->m_iDigit), (LPARAM)0);
        m_bInRemove = false;

        delete pUndo;    //release memory
    }
}

bool CUndoStack::HasItems()
{
    return (m_lstUndo.GetHeadPosition() != NULL);
}

void CUndoStack::Reset()
{
    POSITION pos = m_lstUndo.GetHeadPosition();
    while(pos != NULL)
    {
        delete m_lstUndo.GetNext(pos);    //release memory
    }
    m_lstUndo.RemoveAll();    //Remove from the list - memory no longer valid
}

Open in new window


We have supplied functions to add the latest digit into the collection.  To perform an undo we just remove the top item – the caller does not need to know what it is.  For updating the availability of the undo option on the toolbar we have the HasItems functions and a Reset which is used when a new game is started.  Note the constructor with the nUndoMessage.  To construct an instance of this class an UINT is required.  This is the message that is to be sent to the recipient window for performing the undo action.  Here is a trivial usage of such a technique.  In a real situation it is providing a bit more separation between the class instances.  A generic solution might uses different messages being sent to different types of windows depending upon circumstances.  Passing the message via the constructor means at compile time one could trap the instance of forgetting to set this parameter.  

A minor point with the RemoveTail (in the RemoveItem function) is that it does NOT release any memory assigned with new, all it does it detach it from the list.  Failure to understand things like that are a common source of memory leaks.

Internally to this class is the structure to hold the two pieces of information and a templated list to store the undo items.  We also have a boolean flag (m_bInRemove) which is to stop an item being added to the list as part of the undo.  Sounds odd?  The undo is actually implemented by setting the value in the button to the value it was prior to the change – basically just as if the previous value had been typed in.  Obviously if a number is entered then it is to be added to the undo list.  

We send a message to the GridButton to perform the undo action, basically use the supplied digit – so we require a change in the GridButton.h file.  At the top of the file we add two more definitions:

#define SUD_PRESETVALUE (WM_USER+103)
#define SUD_UNDO (WM_USER+104)

Open in new window


Then add the following as a private member at the end of the class declaration:

    afx_msg LRESULT OnUndo(WPARAM wParam, LPARAM lParam);

Open in new window


and in the GridButton.cpp file add a message map entry and the function definition:

BEGIN_MESSAGE_MAP(CGridButton, CButton)
    ON_MESSAGE(SUD_UNDO, &CGridButton::OnUndo)
END_MESSAGE_MAP()
and

LRESULT CGridButton::OnUndo(WPARAM wParam, LPARAM lParam)
{
    SetValue((int)wParam);
    return 0;
}

Open in new window


This usage of the currently existing function SetValue will ensure that things are performed as if the value was typed in.  The SUD_UNDO is the custom message being used, but what is with the SUD_PRESETVALUE ?  Well to implement the undo functionality we need to ‘know’ what the previous value was.  So when a button has the value changed by keyboard entry we want the view to update the undo list.  The current implementation however sets the value then informs the view, so the simplest alternative is to instruct the parent prior to updating the internal value.  In other word inside the Pretranslate Message function we change

        if(iKey >= 0)
        {
            SetValue(iKey);
            return TRUE;
        }

Open in new window


into:

        if(iKey >= 0)
        {
            GetParent()->SendMessage(SUD_PRESETVALUE, (WPARAM)this, (LPARAM)GetValue());
            SetValue(iKey);
            GetParent()->SendMessage(SUD_SETVALUE, (WPARAM)this, (LPARAM)iKey);
            return TRUE;
        }

Open in new window


So the parent receives the previous value of the button, then the value is changed and the interface updated.

Onto the view.  Put the following into the SudokuView.h file as private members of the class declaration:

    CUndoStack m_stackUndo;
    afx_msg LRESULT OnPreSetValue(WPARAM wParam, LPARAM lParam);
    afx_msg void OnEditUndo();
    afx_msg void OnUpdateEditUndo(CCmdUI *pCmdUI);

Open in new window


(don’t forget to #include the UndoStack.h file as well)
And in the SudokuView.cpp file add the following to the message map entries

    ON_MESSAGE(SUD_PRESETVALUE, &CSudokuView::OnPreSetValue)
    ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CSudokuView::OnUpdateEditUndo)
    ON_COMMAND(ID_EDIT_UNDO, &CSudokuView::OnEditUndo)

Open in new window


The constructor also needs the following change to the parameter list (else the compiler will object about the CUndoStack object).

CSudokuView::CSudokuView()
    : CFormView(CSudokuView::IDD)
    , m_bInitialised(false)
    , m_stackUndo(SUD_UNDO)
{

Open in new window


And the following function definitions:

LRESULT CSudokuView::OnPreSetValue(WPARAM wParam, LPARAM lParam)
{
    m_stackUndo.AddItem((CWnd*)wParam, (int)lParam);
    return 0;
}

void CSudokuView::OnEditUndo()
{
    m_stackUndo.RemoveItem();
}

void CSudokuView::OnUpdateEditUndo(CCmdUI *pCmdUI)
{
    pCmdUI->Enable(m_stackUndo.HasItems());
}

Open in new window


And one final change, the OnUpdate function needs an extra line of code for the

case CSudokuDoc::eLoadGame: 
….
        //reset the undo stack
        m_stackUndo.Reset();
        Invalidate();
        break;

Open in new window


Now you should be able to compile and run and see the undo functionality in operation.  Note here we have added the message map entries by hand – simpler to use the wizard isn’t it.  However there are times when the wizard can't help and one needs to do it oneself.

Conclusion:

We have implemented a simple undo facility via a class to represent a stack (LIFO = Last In First Out).


Click here for the source code for this article


Previous article in the series is here:  Sudoku in MFC: Part 8
There we found how to locate on disc where the running application file is located, how to generate and use random numbers and try...catch to handle exceptions so you code runs with crashing.

Next article in the series is here:  Sudoku in MFC: Part 10
Here we will be working with a modal dialog, a multi-selection list and some more SQL code to maintain the database of games.


Two points to bear in mind.
You may use the code but you are not allowed to distribute the resulting application either for free or for a reward (monetary or otherwise).  At least not without my express permission.
I will perform some things to demonstrate a point – it is not to be taken as that meaning it is a ‘best’ practice, in fact an alternative might be simpler and suitable.  Some points in the code would even be called poor design and a possible source of errors.
1
Comment
Author:AndyAinscow
0 Comments

Featured Post

Important Lessons on Recovering from Petya

In their most recent webinar, Skyport Systems explores ways to isolate and protect critical databases to keep the core of your company safe from harm.

Join & Write a Comment

This video will show you how to get GIT to work in Eclipse.   It will walk you through how to install the EGit plugin in eclipse and how to checkout an existing repository.
Is your data getting by on basic protection measures? In today’s climate of debilitating malware and ransomware—like WannaCry—that may not be enough. You need to establish more than basics, like a recovery plan that protects both data and endpoints.…
Suggested Courses
Course of the Month5 days, 21 hours left to enroll

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month