QUndoCommand的使用

目录

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

引言

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

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

基本实现

主要组成

在这里插入图片描述

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

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

class DataModel : public QObject {Q_OBJECTQ_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);});

完整代码

代码下载链接

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023年移动游戏逆势增长,原来消息推送是这么玩的!

Unity公布了2024游戏报告&#xff0c;2023年全球移动游戏DAU中位数对比前两年再度提升&#xff0c;证明了移动游戏仍在增长&#xff1b;全球移动玩家的首日留存率和7日留存率中位数分别下滑1%和0.1%。这两条意味着虽然游戏玩家在增多&#xff0c;但是怎么让他们始终保持兴趣在变…

电脑内存不够用了怎么办?!如何给电脑加速?

大家好呀&#xff0c;在教大家如何给系统清理增加内存空间之前&#xff0c;我们要理清两个概念&#xff1a;系统的内存和储存是两个不同的硬件&#xff0c;用英文说&#xff0c;内存是RAM、储存是ROM&#xff1b;分别对应着内存条和机械硬盘或固态硬盘这两种硬件设备。 我们可以…

怎么倒放视频教程?3个简单易行方法分享

怎么倒放视频教程&#xff1f;视频倒放是一种创意性的视频编辑方式&#xff0c;通过倒序播放视频内容&#xff0c;可以为观众带来全新的视觉体验。无论是为了制作搞笑视频&#xff0c;还是为了创作具有艺术感的短片&#xff0c;倒放视频都是一个非常实用的技巧。同时&#xff0…

【C语言】C语言实现猜单词小游戏(源码+报告)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

用于HUD平视显示器的控制芯片:S2D13V40

一款利用汽车抬头显示技术用于HUD平视显示器的控制芯片:S2D13V40。HUD的全称是Head Up Display&#xff0c;即平视显示器&#xff0c;以前应用于军用飞机上&#xff0c;旨在降低飞行员需要低头查看仪表的频率。起初&#xff0c;HUD通过光学原理&#xff0c;将驾驶相关的信息投射…

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

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

4.机器学习-十大算法之一线性回归算法(LinearRegression)案例讲解

机器学习-十大算法之一线性回归算法案例讲解 一摘要二个人简介三什么是线性回归四LinearRegression使用方法五糖尿病数据线性回归预测1.数据说明2.导包3.导入数据4.脱敏处理5.抽取训练数据和预测数据6.创建模型7.预测8.线性回归评估指标9.研究每个特征和标记结果之间的关系.来分…

YOLOv9改进策略 :主干优化 | 无需TokenMixer也能达成SOTA性能的极简ViT架构 | CVPR2023 RIFormer

💡💡💡本文改进内容: token mixer被验证能够大幅度提升性能,但典型的token mixer为自注意力机制,推理耗时长,计算代价大,而RIFormers是无需TokenMixer也能达成SOTA性能的极简ViT架构 ,在保证性能的同时足够轻量化。 💡💡💡RIFormerBlock引入到YOLOv9,多个数…

GIt的原理和使用(五):模拟多人协作的两种情况

目录 多人协作 多人协作一 准备工作 协作开发 多人协作二 准备工作 额外场景 申请单合并分支 更推荐写法 远程分支删除后&#xff0c;本地git branch -a依然能看到的解决办法 多人协作 多人协作一 目标&#xff1a;在远程master分支下的file.txt文件新增代码“aaa”…

基础框架SSM-----------spring篇

spring系统架构 Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器&#xff08;框架&#xff09;!!!!!!!! IOC(Inversion of Control)控制反转------解耦合 如图使我们所熟悉的基本逻辑&#xff0c;在业务层中调用Dao层的对象&#xff0c;但是当我们Dao层进行修改的时候…

springcloud基本使用二(远程调用)

创建两个springboot maven子项目 子项目名称分别为order-server和user-server 配置user-server子项目: 所需依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependenc…

【数据结构】AVL 树

文章目录 1. AVL 树的概念2. AVL 树节点的定义3. AVL 树的插入4. AVL 树的旋转5. AVL 树的验证6. AVL 树的删除7. AVL 树的性能 前面对 map / multimap / set / multiset 进行了简单的介绍【C】map & set&#xff0c;在其文档介绍中发现&#xff0c;这几个容器有个共同点是…