QUndoCommand的使用

目录

  • 引言
  • 基本实现
    • 主要组成
    • 命令(QUndoCommand)
    • 命令栈(QUndoStack)
  • 优化技巧
    • 组合命令
    • 合并命令
  • 完整代码

引言

实现撤销重做(Undo/Redo)是编辑器的必备功能,诸如文本编辑器、电子表格、图像编辑器等。用户在编辑过程中是需要通过该方式修正错误的输入或者是不断调整编辑内容的,而且撤销重做不仅限于上一步,是很多时候可能是之前的几十步,这个在PS中非常常见,因此我们需要一个记录步骤的容器。

包含单个操作的撤销重做、步骤记录容器,可以直接使用Qt Undo Framework,本文主要描述如何使用以及在实际开发中非常重要的技巧。

基本实现

主要组成

在这里插入图片描述

上述视频为示例Demo,左侧为撤销重做列表,用于展示当前容器内的情况,右侧是模拟常见编辑器的内容,有选中框、滑动条。

此处的编辑是采用修改数据模型的方式,界面触发修改后调用对应的修改命令,修改命令去修改数据模型,模型更新后再同时界面刷新,这种更新方式主要考虑到一个数据模型对应多个UI控件的情况,如slider更新后修改skipBox更新,代码如下

class DataModel : public QObject {
    Q_OBJECT
    Q_PROPERTY(int fontSize READ fontSize WRITE setFontSize NOTIFY fontSizeChanged FINAL)

public:
    DataModel(QObject *parent = nullptr);
    ~DataModel();

public:
    int fontSize() const;
    void setFontSize(int value);

signals:
    void fontSizeChanged();

private:
    int font_size_ = 50;
};
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    
    // ...
    // do something
    // ...
    
    // 数据模型
    data_model_ = new DataModel(this);

    // 关联更新
    ui->sizeSlider->setValue(data_model_->fontSize());
    ui->sizeBox->setValue(data_model_->fontSize());
    
    connect(data_model_, &DataModel::fontSizeChanged, this, [this]{
        ui->sizeSlider->setValue(data_model_->fontSize());
        ui->sizeBox->setValue(data_model_->fontSize());
    });

    connect(ui->sizeSlider, &QSlider::sliderMoved, this, [this]{
        auto modify_command = new ModifyFontSizeCommand(data_model_, ui->sizeSlider->value());
        undo_stack_->push(modify_command);
    });
}

实现撤销重做主要涉及QUndoStack、QUndoCommand以及QUndoView。QUndoCommand负责单个操作的撤销重做,需要重写其undo/redo函数。QUndoStack则是用于记录步骤,也就是QUndoCommand的容器。QUndoView则是用来展示容器内的内容,方便Demo演示。

命令(QUndoCommand)

先看最简单的单属性更新命令,只更新数据模型内的lightStyle属性,代码如下:

class ModifyLightStyleCommand : public QUndoCommand {
public:
    ModifyLightStyleCommand(DataModel* target_model, bool enable, QUndoCommand *parent = nullptr);
    ~ModifyLightStyleCommand();

protected:
    void undo() override;
    void redo() override;

private:
    QPointer<DataModel> target_model_;
    bool ori_value_ = false;
    bool new_value_ = false;
};
ModifyLightStyleCommand::ModifyLightStyleCommand(DataModel *target_model, bool enable, QUndoCommand *parent)
    : QUndoCommand(parent)
    , target_model_(target_model)
    , new_value_(enable)
{
    setText("Modify Light Style Commond");
    if(!target_model_.isNull()){
        ori_value_ = target_model_->lightStyle();
    }
}

ModifyLightStyleCommand::~ModifyLightStyleCommand()
{

}

void ModifyLightStyleCommand::undo()
{
    if(target_model_.isNull())
        return;
    target_model_->setLightStyle(ori_value_);
}

void ModifyLightStyleCommand::redo()
{
    if(target_model_.isNull())
        return;
    target_model_->setLightStyle(new_value_);
}

如前文所述,ModifyLightStyleCommand继承QUndoCommand ,重写其undo/redo函数,使用时需要确认新值和旧值,undo时设置新值,redo时设置旧值。setText是用于在QUndoView进行显示。如此就完成了一条属性修改的命令,实际使用时只需要构造命令并入栈即可,代码如下:

connect(ui->lightBox, &QCheckBox::clicked, this, [this](bool checked) {
    auto modify_light = new ModifyLightStyleCommand(data_model_, checked);
    undo_stack_->push(modify_light);
});

当命令入栈时会调用命令的redo函数,源码如下,因此触发命令入栈后,并不需要对数据模型再进行额外的属性修改。

void QUndoStack::push(QUndoCommand *cmd)
{
    Q_D(QUndoStack);
    if (!cmd->isObsolete())
       cmd->redo();
       
    // ...
    // do something
    // ...
}

[since 5.9] bool QUndoCommand::isObsolete() const Returns whether the
command is obsolete. The boolean is used for the automatic removal of
commands that are not necessary in the stack anymore. The isObsolete
function is checked in the functions QUndoStack::push(),
QUndoStack::undo(), QUndoStack::redo(), and QUndoStack::setIndex().

解决完如何使用的问题,还有一个就是命令构造后如何析构,谁去触发析构,会不会存在还需要使用者进行内存管理的情况。这些问题还是看源码,如下所示:

QUndoStack::~QUndoStack()
{
    // ...
    // do something
    // ...
    clear();
}

void QUndoStack::clear()
{
    Q_D(QUndoStack);
    if (d->command_list.isEmpty())
        return;
    // ...
    qDeleteAll(d->command_list);
    d->command_list.clear();
    // ...
}

当QUndoStack析构时会调用clear函数,而clear函数内则会调用qDeleteAll析构command_list内记录的命令,再清空command_list。因此,当命令入栈后就不再需要人为管理命令的生命周期,已经托管给QUndoStack。

命令栈(QUndoStack)

解决单条命令的实现之后,接着就是命令的整体调度问题,这个是由QUndoStack完成的,代码如下:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    // ...
    undo_stack_ = new QUndoStack(this);
    ui->undoView->setStack(undo_stack_);
    // ...
    connect(ui->undoBtn, &QPushButton::clicked, undo_stack_, [this]{
        undo_stack_->undo();
    });
    connect(ui->redoBtn, &QPushButton::clicked, undo_stack_, [this]{
        undo_stack_->redo();
    });
    connect(ui->clearBtn, &QPushButton::clicked, undo_stack_, [this]{
        undo_stack_->clear();
    });
    // ...  
}

调用撤销重做并不需要直接操作QUndoCommand,操作QUndoStack即可,撤销则调用undo(),重做则调用redo()。当页面切换希望清空当前记录的命令队列时,也可以直接调用clear()。

优化技巧

基本的调用理解之后,在实际工作时还需要解决一些问题。第一种情况,属性的修改很多使用存在着联动,例如在当前例子中的lightStyle和darkStyle互斥。第二种情况,同一个操作间隔内相同的命令需要合并,例如滑动条按下、拖动到放下属于同一个操作间隔,当前间隔内产生的ModifyFontSizeCommand都应该合并为一条,这样一次撤销就能还原为按下时的数据。

组合命令

针对第一种情况,当然可以把互斥关系写在命令类内,但这会导致目前的两种命令类都需要修改,(ModifyLightStyleCommand、ModifyDarkStyleCommand),如果将这两个类合并成一个类则会让这个类随着业务的增加逐渐膨胀,以后再要增加功能都会去修改此复合类,而且undo/redo函数的实现也会变得负责化。
那有没有在保证其独立性的情况下,实现复合命令呢。答案当然是有的,代码如下:

    connect(ui->lightBox, &QCheckBox::clicked, this, [this](bool checked) {
        auto composite_command = new QUndoCommand();
        composite_command->setText("Modify Light Style Composite Commond");
        new ModifyLightStyleCommand(data_model_, checked, composite_command);
        new ModifyDarkStyleCommand(data_model_, !checked, composite_command);
        undo_stack_->push(composite_command);
    });

为两条命令设置同样的父命令,再将父命令入栈即可。此处的父命令未重写undo/redo,使用的是原有的函数,源码如下:

void QUndoCommand::redo()
{
    for (int i = 0; i < d->child_list.size(); ++i)
        d->child_list.at(i)->redo();
}

void QUndoCommand::undo()
{
    for (int i = d->child_list.size() - 1; i >= 0; --i)
        d->child_list.at(i)->undo();
}

可以看到原有的函数会将子命令挨个执行,如此就完成组合命令的实现,还保持单条命令的原子性及简单化。对于后续业务的扩展也有非常强的适应能力,能够自由组合,不仅限于当前的功能。

合并命令

针对第二种情况,主要是因为使用数据模型,而数据的修改是通过命令完成,而在滑动的过程中,为了保证界面的刷新,就需要不断的调用命令,此时则需要使用到合并命令的功能。需要重写id()和mergeWith()函数,代码如下:

int ModifyFontSizeCommand::id() const
{
    return id_;
}

bool ModifyFontSizeCommand::mergeWith(const QUndoCommand *other)
{
    auto other_command =  dynamic_cast<const ModifyFontSizeCommand *>(other);
    if (!other_command)
        return false;
    new_value_ = other_command->new_value_;
    return true;
}

int QUndoCommand::id() const Returns the ID of this command. A command
ID is used in command compression. It must be an integer unique to
this command’s class, or -1 if the command doesn’t support
compression. If the command supports compression this function must be
overridden in the derived class to return the correct ID. The base
implementation returns -1. QUndoStack::push() will only try to merge
two commands if they have the same ID, and the ID is not -1.

id()默认为-1,只有当其不为-1,且当前栈内的命令和将要入栈的命令的id相同时,才会调用mergeWith()函数。mergeWith()函数的作用则是让当前命令继承新命令的值,如上述代码所示,将other_command的value赋给当前命令。

void QUndoStack::push(QUndoCommand *cmd)
{
    // ...
    bool try_merge = cur != nullptr
                     && cur->id() != -1
                     && cur->id() == cmd->id()
                     && (macro || d->index != d->clean_index);
    if (try_merge && cur->mergeWith(cmd)) {
        delete cmd;
        // ...
    }
}

为了更深入了解,这里再贴上源码片段,可以看到如前文所述,先判断id是否为-1,再判断id是否相同,最后调用mergeWith,如果成功则析构cmd。

外部调用需要为该操作间隔设置统一的id,可以使用当日的毫秒时间戳,例如在滑动条按下时记录当前的时间戳,在滑动条滑动的过程中通过构造函数初始化id,示例代码如下:

    connect(ui->sizeSlider, &QSlider::sliderPressed, this, [this]{
        ui->sizeSlider->setProperty("command", QTime::currentTime().msecsSinceStartOfDay());
    });

    connect(ui->sizeSlider, &QSlider::sliderMoved, this, [this]{
        int command_id = ui->sizeSlider->property("command").toInt();
        auto modify_command = new ModifyFontSizeCommand(data_model_, ui->sizeSlider->value(), command_id);
        undo_stack_->push(modify_command);
    });

完整代码

代码下载链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/509631.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx 日志输出配置json格式

nginx日志输出配置json格式 nginx服务器日志相关指令主要有两条&#xff1a; (1) 一条是log_format&#xff0c;用来设置日志格式 (2) 另外一条是access_log&#xff0c;用来指定日志文件的存放路径、格式和缓存大小。 log_format指令用来设置日志的记录格式&#xff0c;它的语…

go入门到精通

初识Go语言 Go语言介绍 Go语言是什么 2009年11月10日&#xff0c;Go语言正式成为开源编程语言家庭的一员。 Go语言&#xff08;或称Golang&#xff09;是云计算时代的C语言。Go语言的诞生是为了让程序员有更高的生产效率&#xff0c;Go语言专门针对多处理器系统应用程序的编…

【Leetcode】2952. 需要添加的硬币的最小数量

文章目录 题目思路代码复杂度分析时间复杂度空间复杂度 结果总结 题目 题目链接&#x1f517; 给你一个下标从 0 0 0 开始的整数数组 c o i n s coins coins&#xff0c;表示可用的硬币的面值&#xff0c;以及一个整数 t a r g e t target target 。 如果存在某个 c o i …

SpringCloud学习(1)-consul

consul下载安装及使用 1.consul简介 Consul是一种开源的、分布式的服务发现和配置管理工具&#xff0c;能够帮助开发人员构建和管理现代化的分布式系统。它提供了一套完整的功能&#xff0c;包括服务注册与发现、健康检查、KV存储、多数据中心支持等&#xff0c;可以帮助开发人…

【C语言】InfiniBand内核驱动_mlx4_ib_post_send

一、注释 以下是_mlx4_ib_post_send函数的注释&#xff0c;该函数用于处理InfiniBand工作请求&#xff08;WRs&#xff09;的发送过程&#xff1a; static int _mlx4_ib_post_send(struct ib_qp *ibqp, const struct ib_send_wr *wr,const struct ib_send_wr **bad_wr, bool …

备考ICA----Istio实验15---开启 mTLS 自动双向认证实验

备考ICA----Istio实验15—开启mTLS自动双向认证实验 在某些生成环境下,我们希望微服务和微服务之间使用加密通讯方式来确保不被中间人代理. 默认情况下Istio 使用 PERMISSIVE模式配置目标工作负载,PERMISSIVE模式时,服务可以使用明文通讯.为了只允许双向 TLS 流量&#xff0c;…

XGB回归预测

关键代码 import numpy as np import matplotlib.pyplot as plt from xgboost import XGBRegressor #pip install xgboost -i https://pypi.tuna.tsinghua.edu.cn/simple import pandas as pd import joblib#处理中文字体 plt.rcParams[font.family] [sans-serif] plt.rcPar…

XMind 2024 下载地址及安装教程

XMind是一款流行的思维导图软件&#xff0c;它帮助用户以图形化的方式组织和呈现思维、概念和信息。XMind可以应用于各个领域&#xff0c;如项目管理、思维导图、会议记录、学习笔记等。 XMind提供了直观和易于使用的界面&#xff0c;用户可以通过拖放和连线来创建思维导图。它…

String、StringBuffer、StringBuilder类

最近在复习 Java 基础的时候&#xff0c;看到了 String 这块的内容&#xff0c;我突发奇想&#xff0c;可以将 String、StringBuffer、StringBuilder 这些知识点整合在一起记忆。我之前背的那个答案其实有点琐碎&#xff0c;而且不太好理解&#xff0c;还繁杂&#xff0c;所以我…

PS从入门到精通视频各类教程整理全集,包含素材、作业等(5)

PS从入门到精通视频各类教程整理全集&#xff0c;包含素材、作业等 最新PS以及插件合集&#xff0c;可在我以往文章中找到 由于阿里云盘有分享次受限制和文件大小限制&#xff0c;今天先分享到这里&#xff0c;后续持续更新 初寒调色案例及练习图 等文件 https://www.alipan.…

Android Studio 识别不到物理机设备

问题 Android Studio 识别不到物理机设备 详细问题 笔者进行Android 项目开发&#xff0c;之前一直可以连接上物理机设备&#xff0c;可能由于笔者对于驱动程序进行更新修改的原因&#xff0c;突然无法连接物理机设备。搜索无数资料&#xff0c;使用无数解决方案&#xff08…

src挖掘技巧总结分享

src挖洞技术分享 src推荐刚入门的新手首选公益src如漏洞盒子、补天src&#xff0c;因为漏洞盒子收录范围广&#xff0c;只要是国内的站点都收入&#xff0c;相比其它src平台挖掘难度非常适合新手。后续可以尝试先从一些小的src厂商入手。 首先是熟能生巧&#xff0c;我一开始挖…

spring(3)

spring6 1、bean生命周期1.1 bean生命周期之五步1.2bean生命周期之七步1.3 bean生命周期之十步1.4 bean作用域与管理周期 2、把自己new的对象交给spring管理3、Bean循环依赖3.1 setsingleton3.2 构造singleton3.3 propotypeset注入3.4 bean循环依赖源码分析&#xff1a;3.5 常见…

图论模板详解

目录 Floyd算法 例题&#xff1a;蓝桥公园 Dijkstra算法 例题&#xff1a;蓝桥王国 SPFA算法 例题&#xff1a;随机数据下的最短路问题 总结 最小生成树MST Prim算法 Kruskal算法 例题&#xff1a;聪明的猴子 Floyd算法 最简单的最短路径算法&#xff0c;使用邻接…

BGP联盟、对等体组、按组打包

BGP联盟 将大的AS划分为几个子AS&#xff08;成员AS&#xff09;&#xff0c;每个子AS内部建立全连接的IBGP邻居&#xff0c;子AS之间建立EBGP邻接关系。 联盟AS&#xff1a;大AS&#xff0c;就是常说的AS号&#xff0c;一般使用公有AS号。 成员AS&#xff1a;小AS&#xff…

MongoDB 启动异常

Failed to start up WiredTiger under any compatibility version. 解决方案: 删除WiredTiger.lock 和 mongod.lock两个文件&#xff0c;在重新启动。回重新生成新的文件。

Unicode在线编码和解码工具推荐(实用)

Unicode在线编码 - Unicode编码工具 - Unicode在线生成 - Unicode在线解码 - WGCLOUD

研发效能·创享大会—IDCF五周年专场

时光流转&#xff0c;IDCF即将迎来五周年的庆典。在这个意义非凡的时刻&#xff0c;我们精心筹备了一场盛大的聚会【研发效能创享大会—IDCF五周年专场】。 IDCF自2019年成立以来&#xff0c;携手百余位技术领头人共同打造DevOps技术学习平台&#xff0c;与30万社群伙伴联动&a…

数据类型和类型检测

Data Type And Type Checking 1.编程语言中的数据类型 类型和变量 一个类型是一系列值的集合&#xff0c;这些集合可以抽象出一个相同的特点&#xff0c;并且可以相互实现计算 例如&#xff1a; 布尔类型&#xff1a;true or false整形&#xff1a;1,2,3…浮点数类型&#xf…

OM6650AM支持蓝牙5.1协议栈与2.4GHz私有协议的双模无线连接SoC芯片

OM6650AM是一款超低功耗、同时支持蓝牙5.1协议栈与2.4GHz私有协议的双模无线连接SoC芯片&#xff0c;采用4.0 mm x 4.0 mm QFN32封装&#xff0c;具有丰富的资源&#xff0c;极低的功耗&#xff0c;优异的射频性能&#xff0c;可广泛应用于车载数字钥匙模组、胎压检测、PKE钥匙…