Search code examples
c++qtqt5qt4command-pattern

Undo-Redo functionality using Command-Pattern in Qt for FitInView feature


I have QGraphicsView which contains some QGraphicsItem. This view has some feature like zoom-in, zoom-out, fitIn, Undo-Redo.
My fitIn feature is not working in Undo-Redo functionality.
( To implement Undo-Redo I have used Command-Pattern in Qt. )

myCommand.h

class myCommand: public QUndoCommand
{
public:
    myCommand();
    myCommand(QTransform t, int hr, int vr, QGraphicsView* v);
    void undo();
    void redo();
    QTransform t;
    int hScrollBar;
    int  vScrollBar;
    QGraphicsView* mView;
};

myCommand.cpp

myCommand::myCommand(QTransform t, int hr, int vr, QGraphicsView *v)
{
    this->t = t;
    this->hScrollBar= hr;
    this->vScrollBar= vr;
    this->mView = v;
}

void myCommand::undo()
{
    mView->setTransform(t);
    mView->horizontalScrollBar()->setValue(hScrollBar);
    mView->verticalScrollBar()->setValue(vScrollBar);
}

void myCommand::redo()
{
   myView mv;
   mv.FitInView();
}

myView.cpp

 void myView::FitIn()
    {
        FitInView();
        QTransform t = view->transform();
        int hrValue = view->horizontalScrollBar()->value();
        int vrValue = view->verticalScrollBar()->value();
        myCommand* Command1 = new myCommand(t,hrValue,vrValue,view);
        undoStack->push(Command1);   
    }

void myView::DrawDesign()
{
   // Logic for inserting all the Rectangles and polylines.
   QTimer::singleShot(100, this, [&]() {
        FitInView();
    });
}

void myView::FitInView()
{
    QRectF bounds = scene->sceneRect();
    QRectF rect  {0,0,200,200};
    if (bounds.width() < 200)
    {
        rect .setWidth(bounds.width());
        bounds.setWidth(200);
    }
    if (bounds.height() < 200)
    {
        rect.setWidth(bounds.height());
        bounds.setHeight(200);
    }
    view->fitInView(bounds, Qt::KeepAspectRatio);
    view->updateGeometry();
}

myView.h

public:
      QUndoStack* undoStack;

FitInView fits my design perfectly but it does not work in Undo-Redo feature.
I think I am making a mistake in myCommand::undo() and myCommand::redo() function.


Solution

  • Based on the many question you posted on the Qt's Undo Framework, it seems to me you are missing an essential part of the Command pattern:

    The Command pattern is based on the idea that all editing in an application is done by creating instances of command objects. Command objects apply changes to the document and are stored on a command stack. Furthermore, each command knows how to undo its changes to bring the document back to its previous state. [1]

    So all changes, should be implemented inside the QUndoCommand (follow the link to discover a basic usage example), i.e. inside QUndoCommand::redo. Once the command is pushed on the QUndoStack, using QUndoStack::push, the change (i.e. QCommand::redo) is automatically performed:

    Pushes cmd on the stack or merges it with the most recently executed command. In either case, executes cmd by calling its redo() function. [4]

    Steps to create a new QUndoCommand

    1. Implement the desired changes inside QUndoCommand::redo (and not somewhere else).
    2. Write the inverse of this command inside QUndoCommand::undo. If needed, capture the initial state inside the QUndoCommand constructor, to easily revert QUndoCommand::redo.

    Applied to your example

    So, the only action that myView::FitIn should perform, is pushing the command on the undo stack:

    void myView::FitIn()
    {
        undoStack->push(new fitCommand(view));   
    }
    

    Implement your changes inside the redo command:

    void fitCommand::redo()
    {
        mView->FitInView(); // Can be implemented using QGraphicsView::fitInView
    }
    

    As the inverse operation of fitting is not uniquely defined, we store the initial state inside the QUndoCommand constructor (to be able to restore the initial state inside QUndoCommand::undo):

    fitCommand::fitCommand(myView *view)
    {
        this->t = view->transform();
        this->hScrollBar= view->horizontalScrollBar()->value();
        this->vScrollBar= view->verticalScrollBar()->value();
        this->mView = view;
    }
    

    Now we can implement the undo command to revert the redo command:

    void fitCommand::undo()
    {
        mView->setTransform(t);
        mView->horizontalScrollBar()->setValue(hScrollBar);
        mView->verticalScrollBar()->setValue(vScrollBar);
    }
    

    Usefull reading material: