실행 취소 프레임워크 예제

이 예제는 Qt 실행 취소 프레임워크로 실행 취소/다시 실행 기능을 구현하는 방법을 보여줍니다.

The Undo Diagram Example

Qt 실행 취소 프레임워크에서 사용자가 수행하는 모든 동작은 QUndoCommand 을 상속하는 클래스에서 구현됩니다. 실행 취소 명령 클래스는 redo() - 또는 처음 실행하는 방법과 undo() 동작을 모두 알고 있습니다. 사용자가 수행하는 각 작업에 대해 명령이 QUndoStack 스택에 배치됩니다. 스택에는 문서에서 실행된 모든 명령이 시간 순서대로 쌓여 있으므로 명령을 실행 취소하고 다시 실행하여 문서의 상태를 앞뒤로 롤백할 수 있습니다. 실행 취소 프레임워크에 대한 개괄적인 소개는 개요 문서를 참조하세요.

실행 취소 예제는 간단한 다이어그램 애플리케이션을 구현합니다. 상자 또는 직사각형 모양의 항목을 추가 및 삭제할 수 있으며, 마우스로 드래그하여 항목을 이동할 수 있습니다. 실행 취소 스택은 명령이 목록 항목으로 표시되는 목록인 QUndoView 에 표시됩니다. 실행 취소 및 다시 실행은 편집 메뉴를 통해 사용할 수 있습니다. 사용자는 실행 취소 보기에서 명령을 선택할 수도 있습니다.

다이어그램을 구현하기 위해 그래픽 보기 프레임워크를 사용합니다. 프레임워크에는 자체 예제(예: 다이어그램 장면 예제)가 있으므로 관련 코드만 간략하게 다루겠습니다.

이 예제는 다음 클래스로 구성됩니다:

  • MainWindow 는 기본 창이며 예제의 위젯을 정렬합니다. 사용자 입력을 기반으로 명령을 생성하고 명령 스택에 유지합니다.
  • AddCommand 장면에 항목을 추가합니다.
  • DeleteCommand 장면에서 항목을 삭제합니다.
  • MoveCommand 항목을 이동하면 MoveCommand는 이동의 시작 및 중지 위치를 기록하고 redo()undo() 이 호출될 때 이에 따라 항목을 이동합니다.
  • DiagramScene QGraphicsScene 를 상속하고 항목이 이동하면 MoveComands 에 신호를 보냅니다.
  • DiagramItem QGraphicsPolygonItem 를 상속하고 다이어그램에서 항목을 나타냅니다.

메인윈도우 클래스 정의

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();

public slots:
    void itemMoved(DiagramItem *movedDiagram, const QPointF &moveStartPosition);

private slots:
    void deleteItem();
    void addBox();
    void addTriangle();
    void about();
    void updateActions();

private:
    void createActions();
    void createMenus();
    void createToolBars();
    void createUndoView();

    QAction *deleteAction = nullptr;
    QAction *addBoxAction = nullptr;
    QAction *addTriangleAction = nullptr;
    QAction *undoAction = nullptr;
    QAction *redoAction = nullptr;
    QAction *exitAction = nullptr;
    QAction *aboutAction = nullptr;

    QMenu *fileMenu = nullptr;
    QMenu *editMenu = nullptr;
    QMenu *itemMenu = nullptr;
    QMenu *helpMenu = nullptr;

    DiagramScene *diagramScene = nullptr;
    QUndoStack *undoStack = nullptr;
    QUndoView *undoView = nullptr;
};

MainWindow 클래스는 실행 취소 스택을 유지합니다. 즉, QUndoCommand를 생성하고 undoActionredoAction 로부터 triggered() 신호를 받으면 스택에서 푸시하고 팝합니다.

메인윈도우 클래스 구현

생성자부터 살펴보겠습니다:

MainWindow::MainWindow()
{
    undoStack = new QUndoStack(this);
    diagramScene = new DiagramScene();

    const QBrush pixmapBrush(QPixmap(":/icons/cross.png").scaled(30, 30));
    diagramScene->setBackgroundBrush(pixmapBrush);
    diagramScene->setSceneRect(QRect(0, 0, 500, 500));

    createActions();
    createMenus();
    createToolBars();

    createUndoView();

    connect(diagramScene, &DiagramScene::itemMoved,
            this, &MainWindow::itemMoved);
    connect(diagramScene, &DiagramScene::selectionChanged,
            this, &MainWindow::updateActions);

    setWindowTitle("Undo Framework");
    QGraphicsView *view = new QGraphicsView(diagramScene);
    setCentralWidget(view);
    adjustSize();
}

생성자에서 DiagramScene 과 QGraphicsView 를 설정합니다. 항목을 선택했을 때만 deleteAction 이 활성화되기를 원하므로 장면의 selectionChanged() 신호에 updateActions() 슬롯에 연결합니다.

다음은 createUndoView() 함수입니다:

void MainWindow::createUndoView()
{
    QDockWidget *undoDockWidget = new QDockWidget;
    undoDockWidget->setWindowTitle(tr("Command List"));
    undoDockWidget->setWidget(new QUndoView(undoStack));
    addDockWidget(Qt::RightDockWidgetArea, undoDockWidget);
}

QUndoView 는 실행 취소 스택의 각 QUndoCommand 에 대해 setText() 함수로 설정된 텍스트를 목록으로 표시하는 위젯입니다. 이 함수를 도킹 위젯에 넣습니다.

다음은 createActions() 함수입니다:

void MainWindow::createActions()
{
    deleteAction = new QAction(QIcon(":/icons/remove.png"), tr("&Delete Item"), this);
    deleteAction->setShortcut(tr("Del"));
    connect(deleteAction, &QAction::triggered, this, &MainWindow::deleteItem);
    ...
    undoAction = undoStack->createUndoAction(this, tr("&Undo"));
    undoAction->setIcon(QIcon(":/icons/undo.png"));
    undoAction->setShortcuts(QKeySequence::Undo);

    redoAction = undoStack->createRedoAction(this, tr("&Redo"));
    redoAction->setIcon(QIcon(":/icons/redo.png"));
    redoAction->setShortcuts(QKeySequence::Redo);

createActions() 함수는 위에 표시된 방식으로 모든 예제 작업을 설정합니다. createUndoAction () 및 createRedoAction() 메서드는 스택의 상태에 따라 비활성화 및 활성화되는 액션을 만드는 데 도움이 됩니다. 또한 실행 취소 명령의 text()에 따라 액션의 텍스트가 자동으로 업데이트됩니다. 다른 액션의 경우 MainWindow 클래스에 슬롯을 구현했습니다.

    ...
    updateActions();
}

void MainWindow::updateActions()
{
    deleteAction->setEnabled(!diagramScene->selectedItems().isEmpty());
}

모든 액션이 생성되면 씬의 selectionChanged 신호에 연결된 동일한 함수를 호출하여 상태를 업데이트합니다.

createMenus()createToolBars() 함수는 메뉴와 도구 모음에 액션을 추가합니다:

void MainWindow::createMenus()
{
    fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(exitAction);

    editMenu = menuBar()->addMenu(tr("&Edit"));
    editMenu->addAction(undoAction);
    editMenu->addAction(redoAction);
    editMenu->addSeparator();
    editMenu->addAction(deleteAction);
    ...
    helpMenu = menuBar()->addMenu(tr("&About"));
    helpMenu->addAction(aboutAction);
}

void MainWindow::createToolBars()
{
    QToolBar *editToolBar = new QToolBar;
    editToolBar->addAction(undoAction);
    editToolBar->addAction(redoAction);
    editToolBar->addSeparator();
    editToolBar->addAction(deleteAction);
    ...
    addToolBar(editToolBar);
    addToolBar(itemToolBar);
}

다음은 itemMoved() 슬롯입니다:

void MainWindow::itemMoved(DiagramItem *movedItem,
                           const QPointF &oldPosition)
{
    undoStack->push(new MoveCommand(movedItem, oldPosition));
}

스택에서 redo() 함수를 호출하는 MoveCommand를 푸시하기만 하면 됩니다.

다음은 deleteItem() 슬롯입니다:

void MainWindow::deleteItem()
{
    if (diagramScene->selectedItems().isEmpty())
        return;

    QUndoCommand *deleteCommand = new DeleteCommand(diagramScene);
    undoStack->push(deleteCommand);
}

삭제하려면 항목이 선택되어 있어야 합니다. 항목이 선택되어 있지 않더라도 deleteAction 이 활성화되어 있을 수 있으므로 선택되어 있는지 확인해야 합니다. 이는 항목이 선택되어 있을 때 신호나 이벤트를 포착하지 못하기 때문에 발생할 수 있습니다.

다음은 addBox() 슬롯입니다:

void MainWindow::addBox()
{
    QUndoCommand *addCommand = new AddCommand(DiagramItem::Box, diagramScene);
    undoStack->push(addCommand);
}

addBox() 함수는 AddCommand를 생성하여 실행 취소 스택에 푸시합니다.

다음은 addTriangle() 슬롯입니다:

void MainWindow::addTriangle()
{
    QUndoCommand *addCommand = new AddCommand(DiagramItem::Triangle,
                                              diagramScene);
    undoStack->push(addCommand);
}

addTriangle() 함수는 AddCommand를 생성하여 실행 취소 스택에 푸시합니다.

다음은 about() 의 구현입니다:

void MainWindow::about()
{
    QMessageBox::about(this, tr("About Undo"),
                       tr("The <b>Undo</b> example demonstrates how to "
                          "use Qt's undo framework."));
}

about 슬롯은 aboutAction 에 의해 트리거되며 예제에 대한 정보 상자를 표시합니다.

AddCommand 클래스 정의

class AddCommand : public QUndoCommand
{
public:
    AddCommand(DiagramItem::DiagramType addType, QGraphicsScene *graphicsScene,
               QUndoCommand *parent = nullptr);
    ~AddCommand();

    void undo() override;
    void redo() override;

private:
    DiagramItem *myDiagramItem;
    QGraphicsScene *myGraphicsScene;
    QPointF initialPosition;
};

AddCommand 클래스는 DiagramScene에 DiagramItem 그래픽 항목을 추가합니다.

AddCommand 클래스 구현

생성자부터 시작하겠습니다:

AddCommand::AddCommand(DiagramItem::DiagramType addType,
                       QGraphicsScene *scene, QUndoCommand *parent)
    : QUndoCommand(parent), myGraphicsScene(scene)
{
    static int itemCount = 0;

    myDiagramItem = new DiagramItem(addType);
    initialPosition = QPointF((itemCount * 15) % int(scene->width()),
                              (itemCount * 15) % int(scene->height()));
    scene->update();
    ++itemCount;
    setText(QObject::tr("Add %1")
        .arg(createCommandString(myDiagramItem, initialPosition)));
}

먼저 DiagramScene에 추가할 DiagramItem을 생성합니다. setText () 함수를 사용하면 명령을 설명하는 QString 을 설정할 수 있습니다. 이를 사용하여 QUndoView 및 메인 창의 메뉴에서 사용자 지정 메시지를 가져옵니다.

void AddCommand::undo()
{
    myGraphicsScene->removeItem(myDiagramItem);
    myGraphicsScene->update();
}

undo() 장면에서 항목을 제거합니다.

void AddCommand::redo()
{
    myGraphicsScene->addItem(myDiagramItem);
    myDiagramItem->setPos(initialPosition);
    myGraphicsScene->clearSelection();
    myGraphicsScene->update();
}

생성자에서는 이 작업을 수행하지 않으므로 항목의 위치를 설정합니다.

DeleteCommand 클래스 정의

class DeleteCommand : public QUndoCommand
{
public:
    explicit DeleteCommand(QGraphicsScene *graphicsScene, QUndoCommand *parent = nullptr);

    void undo() override;
    void redo() override;

private:
    DiagramItem *myDiagramItem;
    QGraphicsScene *myGraphicsScene;
};

DeleteCommand 클래스는 장면에서 항목을 제거하는 기능을 구현합니다.

DeleteCommand 클래스 구현

DeleteCommand::DeleteCommand(QGraphicsScene *scene, QUndoCommand *parent)
    : QUndoCommand(parent), myGraphicsScene(scene)
{
    QList<QGraphicsItem *> list = myGraphicsScene->selectedItems();
    list.first()->setSelected(false);
    myDiagramItem = static_cast<DiagramItem *>(list.first());
    setText(QObject::tr("Delete %1")
        .arg(createCommandString(myDiagramItem, myDiagramItem->pos())));
}

삭제할 항목이 선택되어 있지 않으면 DeleteCommand를 생성할 수 없고 한 번에 하나의 항목만 선택할 수 있으므로 선택된 항목이 하나 있어야 한다는 것을 알고 있습니다. 항목이 장면에 다시 삽입되는 경우 해당 항목은 선택 해제되어야 합니다.

void DeleteCommand::undo()
{
    myGraphicsScene->addItem(myDiagramItem);
    myGraphicsScene->update();
}

항목은 단순히 장면에 다시 삽입됩니다.

void DeleteCommand::redo()
{
    myGraphicsScene->removeItem(myDiagramItem);
}

항목이 장면에서 제거됩니다.

MoveCommand 클래스 정의

class MoveCommand : public QUndoCommand
{
public:
    enum { Id = 1234 };

    MoveCommand(DiagramItem *diagramItem, const QPointF &oldPos,
                QUndoCommand *parent = nullptr);

    void undo() override;
    void redo() override;
    bool mergeWith(const QUndoCommand *command) override;
    int id() const override { return Id; }

private:
    DiagramItem *myDiagramItem;
    QPointF myOldPos;
    QPointF newPos;
};

mergeWith()는 한 항목을 연속적으로 이동하기 위해 다시 구현됩니다. 즉, 항목이 첫 번째 이동의 시작 위치로 다시 이동합니다.

MoveCommand 클래스 구현

MoveCommand의 생성자는 다음과 같습니다:

MoveCommand::MoveCommand(DiagramItem *diagramItem, const QPointF &oldPos,
                         QUndoCommand *parent)
    : QUndoCommand(parent), myDiagramItem(diagramItem)
    , myOldPos(oldPos), newPos(diagramItem->pos())
{
}

실행 취소와 다시 실행을 위해 이전 위치와 새 위치를 각각 저장합니다.

void MoveCommand::undo()
{
    myDiagramItem->setPos(myOldPos);
    myDiagramItem->scene()->update();
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));
}

항목의 이전 위치를 설정하고 장면을 업데이트하기만 하면 됩니다.

void MoveCommand::redo()
{
    myDiagramItem->setPos(newPos);
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));
}

항목을 새 위치로 설정합니다.

bool MoveCommand::mergeWith(const QUndoCommand *command)
{
    const MoveCommand *moveCommand = static_cast<const MoveCommand *>(command);
    DiagramItem *item = moveCommand->myDiagramItem;

    if (myDiagramItem != item)
        return false;

    newPos = item->pos();
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));

    return true;
}

MoveCommand가 생성될 때마다 이 함수가 호출되어 이전 명령과 병합할지 여부를 확인합니다. 스택에 유지되는 것은 이전 명령 객체입니다. 이 함수는 명령이 병합되면 참을 반환하고, 그렇지 않으면 거짓을 반환합니다.

먼저 두 번 이동한 항목이 동일한 항목인지 확인하고, 이 경우 명령을 병합합니다. 실행 취소 시 이동 시퀀스에서 마지막 위치를 차지하도록 항목의 위치를 업데이트합니다.

다이어그램씬 클래스 정의

class DiagramScene : public QGraphicsScene
{
    Q_OBJECT

public:
    DiagramScene(QObject *parent = nullptr);

signals:
    void itemMoved(DiagramItem *movedItem, const QPointF &movedFromPosition);

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

private:
    QGraphicsItem *movingItem = nullptr;
    QPointF oldPos;
};

DiagramScene은 마우스로 DiagramItem을 이동하는 기능을 구현합니다. 이동이 완료되면 신호를 방출합니다. 이 신호는 MoveCommands를 만드는 MainWindow 에 의해 포착됩니다. 그래픽 프레임워크 문제만 다루기 때문에 DiagramScene의 구현은 살펴보지 않습니다.

main() 함수

프로그램의 main() 함수는 다음과 같습니다:

int main(int argv, char *args[])
{
    QApplication app(argv, args);

    MainWindow mainWindow;
    mainWindow.show();

    return app.exec();
}

다이어그램 장면의 배경에 그리드를 그리므로 리소스 파일을 사용합니다. 나머지 함수는 MainWindow 을 생성하여 최상위 창으로 표시합니다.

예제 프로젝트 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.