Qt Undo Framework学习,实现撤销、重做功能

0. 引言

在交互应用程序中撤销和重做(Undo/Redo)能力是很重要的。像常见的软件Office,AutoCAD等,有了撤销功能,用户体验更舒服。一般都会使用Command模式来实现这一功能。

1. Qt undo/redo框架特性

  • 基于Command设计模式

  • 支持命令压缩和命令合成mergeWith

2. Qt Undo框架包含的类

  • QtUndoCommand - 用于修改document的对象的基类
  • QtUndoStack - QtUndoCommand对象的堆栈
  • QtUndoGroup - undo堆栈的组。很多应用程序允许用户同时打开超过一个文档,该类允许你把一组undo堆栈按一单个stack对待。(暂时不是很理解,没使用过。。。)
  • QtUndoView - 继承自QListWidget,用来展示undo堆栈的内容,以字符串形式

3. 代码演示

利用Qt的Undo Framework,实现表格操作的撤销,重做(undo,redo)功能。效果如下:

Qt Undo Framework学习,实现撤销、重做功能_第1张图片

3.1 Model/View视图模型

左边部分是利用QAbstractTableModelQTableView实现了简单的学生数据展示功能,并在最上方有两个按钮,一个新增学生,一个删除学生,两个功能。

对模型视图不是很清楚的话,可以查看我的上一篇博客《Qt Model/View结构原理之QAbstractTableModel基本使用》

//mainWidget.cpp代码示例:

//初始化table数据
	QList<StdInfo> students;
	for (int i = 0; i < 10; i++)
	{
		StdInfo std;
		std.Name = QString("Std%1").arg(i);
		std.Age = 10;
		std.Sex = StdInfo::Boy;
		students.append(std);
	}

	MyTableModel* modelStd = new MyTableModel(this);
	modelStd->setInitData(students);
	QTableView* view = new QTableView(this);
	view->setSelectionBehavior(QAbstractItemView::SelectRows);//设置整行选中
	view->setModel(modelStd);
	
	QHBoxLayout* headLayout = new QHBoxLayout;
	MyButton* addStd = new MyButton("New");
	MyButton* delStd = new MyButton("Delete");
	headLayout->addWidget(addStd);
	headLayout->addWidget(delStd);
	headLayout->addStretch(1);

QGridLayout* mainLayout = new QGridLayout;
	mainLayout->setSpacing(10);//设置间距
mainLayout->addLayout(headLayout, 0,0);//往网格不同坐标添加不同的组件
	mainLayout->addWidget(view, 1, 0);
	mainLayout->addWidget(undoView, 0, 1, 2, 1);
	mainLayout->setColumnStretch(0, 2);
	mainLayout->setColumnStretch(1, 1);
	setLayout(mainLayout);

模型视图架构中重点是是model的设计和实现:

//MyTableModel.h

#pragma once
#include 
#include 
#include 
#pragma execution_character_set("utf-8");
struct StdInfo
{
	enum StdSex {Boy,Girl};
	QString Name;
	StdSex Sex;
	int Age;
};

Q_DECLARE_METATYPE(StdInfo)   //注册元数据类型

class MyTableModel : public QAbstractTableModel
{
	Q_OBJECT
public:
	explicit MyTableModel(QObject* parent = nullptr);
	void setInitData(QList<StdInfo>& data);
public:
	//返回行数
	int rowCount(const QModelIndex& parent = QModelIndex()) const override;
	//返回列数
	int columnCount(const QModelIndex& parent = QModelIndex()) const override;
	//根据模型索引返回当前的数据
	QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
	//
	Qt::ItemFlags flags(const QModelIndex& index) const override;
	//设置value数据给index处的role角色
	bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;

	bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
	bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;

	bool myInsertRows(int row, StdInfo std);
	bool updataRows(int row, StdInfo std);

private:
	int m_columnNum;                        //列大小
	int m_rowNum;                           //行大小
	QList<StdInfo> m_datas;                    //数据
};

//MyTableModel.cpp

#include "mytablemodel.h"
#include 
#include 
#include 

MyTableModel::MyTableModel(QObject* parent) : QAbstractTableModel(parent)
{
	m_columnNum = 3;
}

void MyTableModel::setInitData(QList<StdInfo>& data)
{
	//重置model数据之前调用beginResetModel,此时会触发modelAboutToBeReset信号
	beginResetModel();
	//重置model中的数据
	m_datas = data;
	m_rowNum = m_datas.size();
	//数据设置结束后调用endResetModel,此时会触发modelReset信号
	endResetModel();
}

int MyTableModel::rowCount(const QModelIndex& parent) const
{
	if (parent.isValid()) 
	{
		return 0;
	}
	else {
		return m_rowNum;
	}
}

int MyTableModel::columnCount(const QModelIndex& parent) const
{
	if (parent.isValid()) {
		return 0;
	}
	else {
		return m_columnNum;
	}
}

QVariant MyTableModel::data(const QModelIndex& index, int role) const
{
	if (!index.isValid()) {
		return QVariant();
	}
	if (index.row() < m_datas.count())
	{
		if (role == Qt::DisplayRole || role == Qt::EditRole) {
			switch (index.column())
			{
			case 0:
				return m_datas[index.row()].Name;
				break;
			case 1:
				return m_datas[index.row()].Sex == StdInfo::Boy ? "男" : "女";
				break;
			case 2:
				return m_datas[index.row()].Age;
				break;
			}
		}
		else if (role == Qt::TextAlignmentRole) {    //对其方式
			return Qt::AlignCenter;
		}
		else if (role == Qt::UserRole)
		{
			return QVariant::fromValue(m_datas[index.row()]);
		}
	}
	return QVariant();
}

Qt::ItemFlags MyTableModel::flags(const QModelIndex& index) const
{
	if (!index.isValid())
		return Qt::NoItemFlags;
	return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}

bool MyTableModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
	if (index.isValid() && role == Qt::EditRole)
	{
		const int row = index.row();
		switch (index.column())
		{
		case 0:
			m_datas[index.row()].Name = value.toString();
			break;
			m_datas[index.row()].Sex = static_cast<StdInfo::StdSex>(value.toInt());
			break;
			m_datas[index.row()].Age = value.toInt();
			break;
		}
		//发送信号触发刷新
		emit dataChanged(index, index, QVector<int>() << role);
		return true;
	}
	else
	{
		return false;
	}
}

bool MyTableModel::insertRows(int row, int count, const QModelIndex& parent /*= QModelIndex()*/)
{
	if (row < 0 || count<1 || row>rowCount())
		return false;
	//需要将操作放到beginInsertRows和endInsertRows两个函数调用之间
	beginInsertRows(parent, row, row + count-1);
	for (int i = row; i < row+count; i++)
	{
		StdInfo std;
		std.Name = "StuName0";
		std.Sex = StdInfo::Boy;
		std.Age = 10;
		m_datas.insert(i, std);
	}
	endInsertRows();
	setInitData(m_datas);
	return true;
}

bool MyTableModel::removeRows(int row, int count, const QModelIndex& parent /*= QModelIndex()*/)
{
	if (row < 0 || count<1 || row + count>rowCount())
		return false;
	//需要将操作放到beginRemoveRows和endRemoveRows两个函数调用之间
	beginRemoveRows(parent, row, row + count - 1);
	for (int i = row+count-1; i >= row; i--)
	{
		//移除该行数据
		m_datas.removeAt(i);
	}
	endRemoveRows();
	setInitData(m_datas);
	return true;
}

bool MyTableModel::myInsertRows(int row, StdInfo std)
{
	if (row < 0 || row >rowCount())
		return false;
	m_datas.insert(row, std);
	setInitData(m_datas);
	return true;
}

bool MyTableModel::updataRows(int row, StdInfo std)
{
	if (row < 0 || row >rowCount())
		return false;
	m_datas.replace(row, std);
	setInitData(m_datas);
	return true;
}

3.2 重写QUndoCommand才能实现撤销和重做功能

QUndoCommand是一个框架,我们首先需要子类化QUndoCommand,然后实现两个函数即可

void undo() override;
void redo() override;
//Commands.h
#pragma once

#include 
#include "MyTableModel.h"

class AddCommand : public QUndoCommand
{
public:
	AddCommand(MyTableModel* model, int row, QUndoCommand* parent = nullptr);
	~AddCommand();

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

private:
	MyTableModel* _model;
	int _row;
};


class DeleteCommand : public QUndoCommand
{
public:
	explicit DeleteCommand(MyTableModel* model, int row,  QUndoCommand* parent = nullptr);
	~DeleteCommand();

	void undo() override;
	void redo() override;
private:
	MyTableModel* _model;
	int _row;
	StdInfo _std;
};

class EditCommand :public QUndoCommand
{
public:
	explicit EditCommand(MyTableModel* model, int row, StdInfo oldV, StdInfo newV, QUndoCommand* parent = nullptr);
	~EditCommand();

	void undo() override;
	void redo() override;
private:
	MyTableModel* _model;
	int _row;
	int _column;
	StdInfo _oldStd;
	StdInfo _newStd;
};
//Commands.cpp
#include "AddCommand.h"

AddCommand::AddCommand(MyTableModel* model, int row, QUndoCommand* parent /*= nullptr*/)
	: QUndoCommand(parent), _model(model), _row(row)
{
	setText(QString("Insert a row behind:%1").arg(row));
}

AddCommand::~AddCommand()
{
}

void AddCommand::undo()
{
	if (_model!=nullptr)
	{
		_model->removeRow(_row);
	}
}

void AddCommand::redo()
{
	if (_model!=nullptr)
	{
		_model->insertRow(_row);
	}
}

DeleteCommand::DeleteCommand(MyTableModel* model, int row, QUndoCommand* parent /*= nullptr*/)
	:QUndoCommand(parent), _model(model), _row(row)
{
	_std.Name = _model->index(row, 0).data(Qt::UserRole).value<StdInfo>().Name;
	_std.Sex = _model->index(row, 0).data(Qt::UserRole).value<StdInfo>().Sex;
	_std.Age = _model->index(row, 0).data(Qt::UserRole).value<StdInfo>().Age;
	setText(QString("Remove a row:%1").arg(row));
}

DeleteCommand::~DeleteCommand()
{

}

void DeleteCommand::undo()
{
	if (_model != nullptr)
	{
		_model->myInsertRows(_row, _std);
	}
}

void DeleteCommand::redo()
{
	if (_model != nullptr)
	{
		_model->removeRow(_row);
	}
}

EditCommand::EditCommand(MyTableModel* model, int row, StdInfo oldV, StdInfo newV, QUndoCommand* parent /*= nullptr*/)
	:QUndoCommand(parent), _model(model), _row(row), _oldStd(oldV), _newStd(newV)
{
	setText(QString("Edit a row:%1").arg(row));
}

EditCommand::~EditCommand()
{

}

void EditCommand::undo()
{
	if (_model != nullptr)
	{
		_model->updataRows(_row, _oldStd);
	}
}

void EditCommand::redo()
{
	if (_model != nullptr)
	{
		_model->updataRows(_row, _newStd);
	}
}

4. 最后

  1. QUndoCommand, QUndoStack, QUndoView这一套是被称作框架的,既然是框架,那么就有一定的使用规则,我们只有理解了这些规则,就能给我们实际开发提供很大的便利。
  2. 本例中,暂未实现EditCommand类的实现,还需要在思考下。

你可能感兴趣的:(Qt,c++开发实战,qt,QUndoCommand,Ctrl+Z,撤销)