在交互应用程序中撤销和重做(Undo/Redo)能力是很重要的。像常见的软件Office,AutoCAD等,有了撤销功能,用户体验更舒服。一般都会使用Command模式来实现这一功能。
基于Command设计模式
支持命令压缩和命令合成mergeWith
利用Qt的Undo Framework,实现表格操作的撤销,重做(undo,redo)功能。效果如下:
左边部分是利用QAbstractTableModel
,QTableView
实现了简单的学生数据展示功能,并在最上方有两个按钮,一个新增学生,一个删除学生,两个功能。
对模型视图不是很清楚的话,可以查看我的上一篇博客《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;
}
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);
}
}
EditCommand
类的实现,还需要在思考下。