目录
[9.2.1.必须实现的 QAbstractItemModel 虚函数](#9.2.1.必须实现的 QAbstractItemModel 虚函数)
[9.2.3.行 / 列增删功能实现](#9.2.3.行 / 列增删功能实现)
[9.2.4.拖拽 & 剪贴板核心实现](#9.2.4.拖拽 & 剪贴板核心实现)
10.QAbstractItemView::setIndexWidget注意事项
[11.1.核心模块 1:绘制渲染接口](#11.1.核心模块 1:绘制渲染接口)
[11.2.核心模块 2:编辑生命周期接口](#11.2.核心模块 2:编辑生命周期接口)
[11.3.核心模块 3:事件与交互接口](#11.3.核心模块 3:事件与交互接口)
9.QStandardItemModel
QStandardItemModel 是 Qt 内置的通用模型,原生支持树形结构、拖拽移动、复选框、编辑等功能,无需手写底层索引和数据接口,开发效率远高于自定义 QAbstractItemModel,非常适合中小数据量的树形表格场景。
9.1.核心原理
- 每一行由多个
QStandardItem组成(两列对应 2 个 item),统一由父节点的appendRow/insertRow整行插入。 - 拖拽功能由模型和视图共同配合开启,
QStandardItemModel原生支持内部 MIME 数据传输,无需手写拖拽逻辑。 - 整行操作,避免只操作单列导致数据不同步。
9.1.1.QStandardItemPrivate
QStandardItem 是 Qt Model/View 框架里用于承载单条数据节点的类,配合 QStandardItemModel 使用,既可以做表格节点 ,也可以构建树形层级节点。
QStandardItem 对外暴露简洁 API;真正存储数据、子节点、父子关系、行列管理全部在 QStandardItemPrivate。
cpp
class QStandardItemPrivate
{
Q_DECLARE_PUBLIC(QStandardItem)
public:
inline QStandardItemPrivate()
: model(0),
parent(0),
rows(0),
columns(0),
q_ptr(0),
lastKnownIndex(-1)
{ }
inline int childIndex(int row, int column) const {
if ((row < 0) || (column < 0)
|| (row >= rowCount()) || (column >= columnCount())) {
return -1;
}
return (row * columnCount()) + column;
}
inline int childIndex(const QStandardItem *child) const {
const int lastChild = children.size() - 1;
int &childsLastIndexInParent = child->d_func()->lastKnownIndex;
if (childsLastIndexInParent != -1 && childsLastIndexInParent <= lastChild) {
if (children.at(childsLastIndexInParent) == child)
return childsLastIndexInParent;
} else {
childsLastIndexInParent = lastChild / 2;
}
// assuming the item is in the vicinity of the previous index, iterate forwards and
// backwards through the children
int backwardIter = childsLastIndexInParent - 1;
int forwardIter = childsLastIndexInParent;
Q_FOREVER {
if (forwardIter <= lastChild) {
if (children.at(forwardIter) == child) {
childsLastIndexInParent = forwardIter;
break;
}
++forwardIter;
} else if (backwardIter < 0) {
childsLastIndexInParent = -1;
break;
}
if (backwardIter >= 0) {
if (children.at(backwardIter) == child) {
childsLastIndexInParent = backwardIter;
break;
}
--backwardIter;
}
}
return childsLastIndexInParent;
}
QPair<int, int> position() const;
void setChild(int row, int column, QStandardItem *item,
bool emitChanged = false);
inline int rowCount() const {
return rows;
}
inline int columnCount() const {
return columns;
}
void childDeleted(QStandardItem *child);
void setModel(QStandardItemModel *mod);
inline void setParentAndModel(
QStandardItem *par,
QStandardItemModel *mod) {
setModel(mod);
parent = par;
}
void changeFlags(bool enable, Qt::ItemFlags f);
void setItemData(const QMap<int, QVariant> &roles);
const QMap<int, QVariant> itemData() const;
bool insertRows(int row, int count, const QList<QStandardItem*> &items);
bool insertRows(int row, const QList<QStandardItem*> &items);
bool insertColumns(int column, int count, const QList<QStandardItem*> &items);
void sortChildren(int column, Qt::SortOrder order);
QStandardItemModel *model;
QStandardItem *parent;
QVector<QStandardItemData> values;
QVector<QStandardItem*> children;
int rows;
int columns;
QStandardItem *q_ptr;
mutable int lastKnownIndex; // this is a cached value
};
成员变量:
cpp
QStandardItemModel *model; // 所属模型,一棵树上所有item最终归属同一个model
QStandardItem *parent; // 父Item指针;根节点(invisibleRoot)parent = nullptr
QVector<QStandardItemData> values; // 【当前单元格多角色数据】DisplayRole/Icon/CheckState...
QVector<QStandardItem*> children; // 【所有子节点一维数组】重点!二维行列扁平存储
int rows; // 直接子节点行数
int columns; // 直接子节点列数
QStandardItem *q_ptr; // 反向指针,指向外层 QStandardItem
mutable int lastKnownIndex; // 缓存:子item在children数组中的下标(加速查找,mutable允许const函数修改)
children 是一维扁平数组
逻辑:子节点是
rows × columns的二维表格,扁平存进一维 QVector索引公式:
index = row * columns + column对应源码内联函数:
cpp
inline int childIndex(int row, int column) const {
if ((row < 0) || (column < 0) || (row >= rowCount()) || (column >= columnCount()))
return -1;
return (row * columnCount()) + column;
}
示例:rows=2, columns=2
- row=0,col=0 → 0
- row=0,col=1 → 1
- row=1,col=0 → 2
- row=1,col=1 → 3
✅ 优势:连续内存,Vector 访问很快
⚠️ 坑:多列树形场景,整行 item 紧密排列;拖拽 mime 序列化默认只取第 0 列,同行后面列数据容易断裂(你之前拖拽第二列空白的底层根源)
values:保存当前这个 Item 自身的所有角色数据;children 保存它的所有子单元格 Item。 区分:
- values → 我自己的数据
- children → 我的所有子单元格(子树)
9.1.2.QStandardItemData
cpp
class QStandardItemData
{
public:
inline QStandardItemData() : role(-1) {}
inline QStandardItemData(int r, const QVariant &v) : role(r), value(v) {}
inline QStandardItemData(const std::pair<const int&, const QVariant&> &p) : role(p.first), value(p.second) {}
int role;
QVariant value;
inline bool operator==(const QStandardItemData &other) const { return role == other.role && value == other.value; }
};
QStandardItem 存储单条 role → value 的最小单元,用来承载单元格某一个角色的数据。
-
int role对应 Qt 的 Item 角色:Qt::DisplayRole(0):界面显示文本Qt::DecorationRole(1):图标Qt::CheckStateRole(10):复选框状态Qt::UserRole + N:自定义业务数据- 默认构造
role = -1,代表无效角色
-
QVariant value该角色对应的值,支持任意 Qt 可存储类型(字符串、数字、图标、自定义结构体等)
关键实现细节:
-
不使用 QMap,而是用 QVector 存 QStandardItemData Qt 没有直接用
QMap<int, QVariant>常驻内存,而是用 vector 保存结构体:- 少量 role 场景下,线性遍历 vector 开销远低于 map
- 绝大多数 item 只用 2~5 个角色(Display/Decoration/CheckState),性能更好
- 如果一个 item 设置几十上百种自定义 role,线性查找性能会下降
-
同一个 role 不会保存多条记录 setData 相同 role 多次调用,直接覆盖旧 value,不会追加多条 QStandardItemData
-
对比 QModelIndex::data () 当调用
index.data(Qt::DisplayRole),最终:item->d_func()->values遍历匹配 role → 返回 value只读场景优先
index.data(),不要 itemFromIndex 拿到 item 再读,避免懒创建空白 item -
拷贝行为 QStandardItem 深拷贝时,会复制每一条 QStandardItemData(role + QVariant 完整复制),所以大量节点拷贝内存开销高。
因此上面你可以理解成:
cpp
// QStandardItemData ≈ 简易键值对
struct Pair {
int key; // role
QVariant val; // value
};
QVector<Pair> values;
Qt 为了减少小对象内存开销、兼容批量导入,封装成了 QStandardItemData。
9.1.3.QStandardItem
内部核心结构:
1.父子层级存储
每个 QStandardItem 内部维护一个二维子项表(行 × 列):
- 子节点保存在父 Item 内部;
parent()获取父节点,顶层节点父为nullptr;rowCount()返回直接子行数量(非递归全部子孙);- 树形场景一般只用单列子表,表格使用多列
2.多角色数据存储
Item 不只有显示文本,支持按**角色(Qt::ItemDataRole)**存储多份数据:
Qt::DisplayRole:界面显示文字(setText()等价设置该角色)Qt::DecorationRole:图标Qt::CheckStateRole:复选框状态- 自定义角色:
Qt::UserRole + N存放自定义业务数据 - API:
setData(值, 角色)/data(角色)
3.标志(Flags)
控制节点行为:是否可编辑、可选、启用、可拖拽等,setFlags() 修改。
常用构造 & 基础 API
构造
cpp
QStandardItem();
explicit QStandardItem(const QString &text);
explicit QStandardItem(const QIcon &icon, const QString &text);
QStandardItem(const QStandardItem &other); // 拷贝构造
基础数据读写
cpp
void setText(const QString &text);
QString text() const;
void setIcon(const QIcon &icon);
QIcon icon() const;
void setCheckState(Qt::CheckState state);
Qt::CheckState checkState() const;
void setData(const QVariant &value, int role = Qt::UserRole + 1);
QVariant data(int role = Qt::UserRole + 1) const;
void setFlags(Qt::ItemFlags flags);
Qt::ItemFlags flags() const;
父子 / 树形操作核心接口
cpp
// 获取子节点
QStandardItem *child(int row, int column = 0) const;
// 设置子节点
void setChild(int row, int column, QStandardItem *item);
// 追加一行子节点(多列传QList)
void appendRow(QStandardItem *item);
void appendRow(const QList<QStandardItem*> &items);
// 指定行插入
void insertRow(int row, const QList<QStandardItem*> &items);
// 移除并取出子节点(接管内存)
QStandardItem *takeChild(int row, int column = 0);
// 获取父节点
QStandardItem *parent() const;
// 直接子行数
int rowCount() const;
int columnCount() const;
// 设置行列数量
void setRowCount(int rows);
void setColumnCount(int columns);
重要:
appendRow / insertRow之后,模型接管 Item 内存 ,不要手动 delete;使用takeChild取出后,由开发者管理释放
9.1.4.和QStandardItemModel的关系
cpp
class QStandardItemModelPrivate : public QAbstractItemModelPrivate
{
Q_DECLARE_PUBLIC(QStandardItemModel)
public:
QStandardItemModelPrivate();
virtual ~QStandardItemModelPrivate();
void init();
inline QStandardItem *createItem() const {
return itemPrototype ? itemPrototype->clone() : new QStandardItem;
}
inline QStandardItem *itemFromIndex(const QModelIndex &index) const {
Q_Q(const QStandardItemModel);
if (!index.isValid())
return root.data();
if (index.model() != q)
return 0;
QStandardItem *parent = static_cast<QStandardItem*>(index.internalPointer());
if (parent == 0)
return 0;
return parent->child(index.row(), index.column());
}
void sort(QStandardItem *parent, int column, Qt::SortOrder order);
void itemChanged(QStandardItem *item, const QVector<int> &roles = QVector<int>());
void rowsAboutToBeInserted(QStandardItem *parent, int start, int end);
void columnsAboutToBeInserted(QStandardItem *parent, int start, int end);
void rowsAboutToBeRemoved(QStandardItem *parent, int start, int end);
void columnsAboutToBeRemoved(QStandardItem *parent, int start, int end);
void rowsInserted(QStandardItem *parent, int row, int count);
void columnsInserted(QStandardItem *parent, int column, int count);
void rowsRemoved(QStandardItem *parent, int row, int count);
void columnsRemoved(QStandardItem *parent, int column, int count);
void _q_emitItemChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight);
void decodeDataRecursive(QDataStream &stream, QStandardItem *item);
QVector<QStandardItem*> columnHeaderItems;
QVector<QStandardItem*> rowHeaderItems;
QScopedPointer<QStandardItem> root;
const QStandardItem *itemPrototype;
int sortRole;
};
从上面的源码可以看出:
- 一棵 Model 对应一整棵 Item 树 所有 Item 内部
d->model指针最终指向同一个 Model;禁止一个 Item 同时挂到多个 Model。 - Model 不直接存业务数据,数据全部分散保存在各个 QStandardItem 内部
QModelIndex只是轻量索引:row + column + internalPointer(父 Item 裸指针),不持有 Item 对象本身 通过itemFromIndex才能拿到 Item(附带懒创建副作用)- 内存归属:
- 调用
appendRow / insertRow将 Item 加入模型后,Model 接管 Item 生命周期,不需要手动 delete takeChild / takeRow取出 Item,内存管理权交还给开发者
- 调用
关键调用链路
Item → QModelIndex
cpp
QStandardItem *item = ...;
// 拿到item在父中的行列
QPair<int,int> pos = item->d_func()->position();
int r = pos.first;
int c = pos.second;
QStandardItem *p = item->parent();
QModelIndex idx = model->index(r, c, model->indexFromItem(p));
QModelIndex → QStandardItem
cpp
// 有副作用:不存在则懒创建空白item
QStandardItem* item = model->itemFromIndex(idx);
// 无副作用安全读取(自己实现,底层 parent->child(r,c))
9.2.QStandardItemModel的实现
QStandardItemModel也是继承于QAbstractItemModel,和自定义QAbstractItemModel的实现类似
- 提供通用的 表格 / 树形 Model 实现,开发者不用手写
QAbstractItemModel纯虚接口; - 使用
QStandardItem作为数据节点,每个单元格对应一个 Item; - 内置拖拽支持、复制粘贴、排序、编辑、信号自动通知视图;
- 适合中小型数据列表、树形、表格;底层对象开销大,不适合十万级以上大数据。
9.2.1.必须实现的 QAbstractItemModel 虚函数
QAbstractItemModel 规定的 5 个基础纯虚函数,QStandardItemModel 全部实现:
1.index ():构造 QModelIndex
cpp
QModelIndex QStandardItemModel::index(int row, int column, const QModelIndex &parent) const
{
Q_D(const QStandardItemModel);
QStandardItem *parentItem = d->itemFromIndex(parent);
if ((parentItem == 0)
|| (row < 0)
|| (column < 0)
|| (row >= parentItem->rowCount())
|| (column >= parentItem->columnCount())) {
return QModelIndex();
}
return createIndex(row, column, parentItem);
}
重点:
QModelIndex只保存 row /column/ 父 Item 指针,不存当前 Item。
2.parent ():获取父节点索引
cpp
QModelIndex QStandardItemModel::parent(const QModelIndex &child) const
{
if (!child.isValid())
return QModelIndex();
QStandardItem *childItem = itemFromIndex(child);
if (!childItem)
return QModelIndex();
QStandardItem *parentItem = childItem->parent();
if (!parentItem || parentItem == d->invisibleRoot)
return QModelIndex();
auto pos = parentItem->d_func()->position();
return createIndex(pos.first, pos.second, parentItem->parent());
}
3.rowCount ():获取直接子行数
cpp
int QStandardItemModel::rowCount(const QModelIndex &parent) const
{
Q_D(const QStandardItemModel);
QStandardItem *parentItem = itemFromIndex(parent);
return parentItem ? parentItem->rowCount() : 0;
}
4.columnCount ():获取直接子列数
cpp
int QStandardItemModel::columnCount(const QModelIndex &parent) const
{
Q_D(const QStandardItemModel);
QStandardItem *parentItem = itemFromIndex(parent);
return parentItem ? parentItem->columnCount() : 0;
}
5.data ():按角色读取单元格数据
cpp
QVariant QStandardItemModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
QStandardItem *item = itemFromIndex(index);
return item ? item->data(role) : QVariant();
}
读取
QStandardItemPrivate::values里存放的多条QStandardItemData。
9.2.2.配套重写的标准接口(编辑、标志、表头)
1.setData () 修改数据
cpp
bool QStandardItemModel::clearItemData(const QModelIndex &index)
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return false;
Q_D(QStandardItemModel);
QStandardItem *item = d->itemFromIndex(index);
if (!item)
return false;
item->clearData();
return true;
}
void QStandardItem::setData(const QVariant &value, int role)
{
Q_D(QStandardItem);
role = (role == Qt::EditRole) ? Qt::DisplayRole : role;
const QVector<int> roles((role == Qt::DisplayRole) ?
QVector<int>({Qt::DisplayRole, Qt::EditRole}) :
QVector<int>({role}));
QVector<QStandardItemData>::iterator it;
for (it = d->values.begin(); it != d->values.end(); ++it) {
if ((*it).role == role) {
if (value.isValid()) {
if ((*it).value.type() == value.type() && (*it).value == value)
return;
(*it).value = value;
} else {
d->values.erase(it);
}
if (d->model)
d->model->d_func()->itemChanged(this, roles);
return;
}
}
d->values.append(QStandardItemData(role, value));
if (d->model)
d->model->d_func()->itemChanged(this, roles);
}
2.flags () 节点行为标志(可编辑、可选、可拖拽等)
cpp
Qt::ItemFlags QStandardItemModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
QStandardItem *item = itemFromIndex(index);
return item->flags();
}
3.headerData () /setHeaderData () 表头管理
独立维护表头数据(表头不使用 QStandardItem,私有成员单独存储)。
4.sort () 排序
cpp
void QStandardItemModel::sort(int column, Qt::SortOrder order)
{
Q_D(QStandardItemModel);
d->invisibleRoot->sortChildren(column, order);
emit layoutChanged();
}
调用 QStandardItemPrivate::sortChildren 重排一维 children 数组。
9.2.3.行 / 列增删功能实现
所有接口最终调用 Item 的私有方法:insertRows / removeRows / insertColumns
cpp
bool insertRows(int row, int count, const QModelIndex &parent);
bool removeRows(int row, int count, const QModelIndex &parent);
bool insertColumns(int column, int count, const QModelIndex &parent);
bool removeColumns(int column, int count, const QModelIndex &parent);
内部逻辑:
- 获取父 Item;
- 调用
QStandardItemPrivate::insertRows,扩容 children 一维数组; - 发射
rowsInserted / rowsRemoved通知视图刷新;
注意:
appendRow是上层便捷封装,等价于insertRow(rowCount(), items)
9.2.4.拖拽 & 剪贴板核心实现
两个核心虚函数:
1.QMimeData *mimeData(const QModelIndexList &indexes) const
cpp
QMimeData *QStandardItemModel::mimeData(const QModelIndexList &indexes) const
{
// 1. 调用基类 QAbstractItemModel::mimeData,基础剪贴板文本导出(选中内容转纯文本)
QMimeData *data = QAbstractItemModel::mimeData(indexes);
if(!data)
return nullptr;
// 注册自定义MimeType:application/x-qstandarditemmodeldatalist
const QString format = qStandardItemModelDataListMimeType();
if (!mimeTypes().contains(format))
return data;
QByteArray encoded;
QDataStream stream(&encoded, QIODevice::WriteOnly);
QSet<QStandardItem*> itemsSet; // 保存所有选中顶层Item集合(去重后待序列化)
QStack<QStandardItem*> stack; // DFS栈,遍历子树
itemsSet.reserve(indexes.count());
stack.reserve(indexes.count());
// Step1:遍历所有选中index,转 QStandardItem*,全部压入集合和栈
for (int i = 0; i < indexes.count(); ++i) {
// ⚠️ 关键副作用:itemFromIndex() 不存在则懒创建空白Item!
if (QStandardItem *item = itemFromIndex(indexes.at(i))) {
itemsSet << item;
stack.push(item);
} else {
qWarning("QStandardItemModel::mimeData: No item associated with invalid index");
return nullptr;
}
}
// Step2:【核心逻辑】剔除"已经被包含在父节点子树内的子Item",避免重复序列化
// 只保留选中区域的「顶层节点」:如果父节点已经被选中,子节点不再单独序列化
{
QSet<QStandardItem *> seen;
while (!stack.isEmpty()) {
QStandardItem *itm = stack.pop();
if (seen.contains(itm))
continue;
seen.insert(itm);
// 遍历当前item所有子节点
const QVector<QStandardItem*> &childList = itm->d_func()->children;
for (int i = 0; i < childList.count(); ++i) {
QStandardItem *chi = childList.at(i);
if (chi) {
// 子节点从待序列化集合中移除:父序列化时会递归把子树全部带走
itemsSet.erase(itemsSet.constFind(chi));
stack.push(chi);
}
}
}
}
// Step3:剩下 itemsSet 就是真正需要序列化的顶层选中节点
stack.reserve(itemsSet.count());
for (QStandardItem *item : qAsConst(itemsSet))
stack.push(item);
// Step4:DFS递归序列化所有节点写入数据流
while (!stack.isEmpty()) {
QStandardItem *item = stack.pop();
// 如果是选中的顶层节点:额外写入它在父中的 row / column 坐标
if (itemsSet.contains(item))
stream << item->row() << item->column();
// 序列化当前item自身数据(operator<< 调用 QStandardItem::write)
// 再写入:当前item列数、子节点总数量
stream << *item << item->columnCount() << item->d_ptr->children.count();
// 子节点入栈,后续循环序列化子树
stack += item->d_ptr->children;
}
// 将二进制序列化数据塞入QMimeData
data->setData(format, encoded);
return data;
}
拖拽多选时:如果同时选中父节点 + 它的子节点,只序列化父节点,子节点自动跟随父节点序列化,防止重复导出、重复粘贴
示例:选中 A(父)和 A 的子 B → 只序列化 A,A 序列化时递归包含 B
序列化数据流结构
对于每一个顶层选中 Item:
cpp
[row][col] ← 仅顶层选中节点才有这一段
[QStandardItem完整数据]
[int columnCount] ← 当前item拥有多少列
[int childCount] ← 当前item拥有多少直接子节点
然后依次序列化所有子节点(递归)
operator<<底层调用item.write(stream):序列化values(QStandardItemData)所有角色数据
2.bool dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent)
cpp
bool QStandardItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent)
{
Q_D(QStandardItemModel);
// 1. 只支持 Copy / Move 两种动作
if (!data || !(action == Qt::CopyAction || action == Qt::MoveAction))
return false;
// 识别自定义mime格式:application/x-qstandarditemmodeldatalist
const QString format = qStandardItemModelDataListMimeType();
if (!data->hasFormat(format))
// 不支持该格式,交由基类默认处理(文本粘贴等)
return QAbstractItemModel::dropMimeData(data, action, row, column, parent);
// 边界修正:row=-1 代表追加到末尾
if (row > rowCount(parent))
row = rowCount(parent);
if (row == -1)
row = rowCount(parent);
if (column == -1)
column = 0;
QByteArray encoded = data->data(format);
QDataStream stream(&encoded, QIODevice::ReadOnly);
// ========== 2. 读取数据流,递归还原所有拖拽节点 ==========
// top/left/bottom/right:记录拖拽源数据占用的矩形范围(原始行列边界)
int top = INT_MAX;
int left = INT_MAX;
int bottom = 0;
int right = 0;
QVector<int> rows, columns; // 保存每个顶层item原始 r,c
QVector<QStandardItem *> items; // 保存克隆出来的完整item(带子树)
while (!stream.atEnd()) {
int r, c;
QStandardItem *item = d->createItem(); // 基于itemPrototype克隆空白item
stream >> r >> c; // 读出序列化时保存的原始行列
d->decodeDataRecursive(stream, item); // 递归读取,还原整棵子树
rows.append(r);
columns.append(c);
items.append(item);
// 更新源数据所占矩形边界
top = qMin(r, top);
left = qMin(c, left);
bottom = qMax(r, bottom);
right = qMax(c, right);
}
// ========== 3. 计算拖拽数据占用行列,压缩连续行索引 ==========
int dragRowCount = 0;
int dragColumnCount = right - left + 1;
// 标记哪些原始行存在数据,重新映射为连续从0开始的相对行号
QVector<int> rowsToInsert(bottom + 1);
for (int i = 0; i < rows.count(); ++i)
rowsToInsert[rows.at(i)] = 1;
for (int i = 0; i < rowsToInsert.count(); ++i) {
if (rowsToInsert.at(i) == 1){
rowsToInsert[i] = dragRowCount;
++dragRowCount;
}
}
// 将原始绝对行号 → 相对于top的连续偏移行号
for (int i = 0; i < rows.count(); ++i)
rows[i] = top + rowsToInsert.at(rows.at(i));
// 位图:标记目标位置是否已经写入item,防止同一位置重复覆盖
QBitArray isWrittenTo(dragRowCount * dragColumnCount);
// ========== 4. 预先扩容:列不足则插入列,行不足则插入行 ==========
int colCount = columnCount(parent);
if (colCount < dragColumnCount + column) {
insertColumns(colCount, dragColumnCount + column - colCount, parent);
colCount = columnCount(parent);
}
insertRows(row, dragRowCount, parent);
row = qMax(0, row);
column = qMax(0, column);
// 获取目标父Item,无效parent代表根节点
QStandardItem *parentItem = itemFromIndex (parent);
if (!parentItem)
parentItem = invisibleRootItem();
QVector<QPersistentModelIndex> newIndexes(items.size());
// ========== 5. 计算每个item最终放置的目标坐标 ==========
for (int j = 0; j < items.size(); ++j) {
int relativeRow = rows.at(j) - top;
int relativeColumn = columns.at(j) - left;
// 最终放置位置 = 拖拽落点 + 相对偏移
int destinationRow = relativeRow + row;
int destinationColumn = relativeColumn + column;
int flat = (relativeRow * dragColumnCount) + relativeColumn;
// 如果列越界 / 当前位置已经被占用:新建一行放数据
if (destinationColumn >= colCount || isWrittenTo.testBit(flat)) {
destinationColumn = qBound(column, destinationColumn, colCount - 1);
destinationRow = row + dragRowCount;
insertRows(row + dragRowCount, 1, parent);
flat = (dragRowCount * dragColumnCount) + relativeColumn;
isWrittenTo.resize(++dragRowCount * dragColumnCount);
}
if (!isWrittenTo.testBit(flat)) {
newIndexes[j] = index(destinationRow, destinationColumn, parentItem->index());
isWrittenTo.setBit(flat);
}
}
// ========== 6. 将重建好的item挂载到目标父节点(setChild) ==========
for(int k = 0; k < newIndexes.size(); k++) {
if (newIndexes.at(k).isValid()) {
// setChild:挂载item,自动维护children一维数组、行列数、父子model指针
parentItem->setChild(newIndexes.at(k).row(), newIndexes.at(k).column(), items.at(k));
} else {
// 无效位置,销毁克隆出来的item,防止内存泄漏
delete items.at(k);
}
}
return true;
}
关键内部依赖函数说明
d->createItem()使用全局itemPrototype->clone()创建空白 Item,和itemFromIndex懒创建共用一套原型。d->decodeDataRecursive(stream, item)递归解析数据流:还原QStandardItem自身所有角色数据、读取columnCount / childCount,循环克隆所有子节点,完整恢复子树结构。
对应
mimeData中stream << *item << colCnt << childCnt的序列化逻辑。
parentItem->setChild(r,c,item)底层调用QStandardItemPrivate::setChild:- 释放该位置原有 item
- 将新 item 加入
children扁平一维数组 - 更新 rows /columns
- 绑定 parent、model 指针
- 触发数据变更信号
整体数据流闭环
cpp
拖拽选中indexes
↓ mimeData
itemFromIndex(⚠️懒创建风险)→ 收集顶层item → DFS剔除被包含子节点 → 序列化顶层r/c + 递归写入整棵子树
↓ QMimeData
dropMimeData
↓ decodeDataRecursive
重建所有item子树 → 计算目标矩形范围 → 预分配行列空间 → 计算目标坐标 → setChild挂载item
矩形映射机制
Qt 这套实现的设计初衷:支持表格块选区拖拽(矩形多选)
- 拖拽源多个离散单元格,会算出一个包围矩形
[top,left] ~ [bottom,right] - 放到目标落点后,保持单元格之间相对位置不变 示例: 选中
(0,0)、(0,2),包围矩形宽 3 列;拖到 row=5,col=1
- (0,0) → (5,1)
- (0,2) → (5,3)
如果目标位置发生冲突(位置已占用),代码会自动新增一行存放,避免覆盖。
9.2.5.信号自动通知机制
只要修改 Item / 增删行列,Model 自动发射信号:
dataChanged:单元格数据变更rowsInserted / rowsRemoved:行新增删除columnsInserted / columnsRemoved:列新增删除layoutChanged:排序、大规模结构变动
底层:
QStandardItem::setData内部回调所属 model 发送信号。
9.3.为什么只适合中小数据量的树形表格场景?
QStandardItemModel 是「面向单元格对象」的通用容器,为开发便捷牺牲内存与性能;大数据 / 树形层级深时对象爆炸、信号泛滥、无法懒加载,因此上限一般几千~一两万节点。
1.每个单元格 = 独立 QStandardItem 对象
自定义继承
QAbstractItemModel:不创建单元格对象,视图需要哪个单元格,临时从业务结构体(vector/map/db)读取返回(按需获取,无多余对象)。QStandardItemModel:每一格都 new QStandardItem,一行两列 = 2 个对象;1 万行 2 列 = 2 万个堆对象。
QStandardItem 内部自带大量冗余成员:
- 多角色数据存储(DisplayRole、FontRole、ForegroundRole、BackgroundRole、CheckStateRole 等全套)
- 父子树指针、兄弟链表、行 / 列标记、flags、图标、提示文字、编辑状态
- Qt 对象元信息(元对象、信号槽支持、事件相关)
对象堆分配开销巨大:频繁 new/delete、内存碎片、GC / 对象树管理成本;树形场景下父子互相引用,内存占用进一步放大。
2.信号与刷新机制:频繁发射通知,UI 极易卡顿
QStandardItemModel 内置全自动信号通知:
dataChanged / rowsInserted / rowsRemoved / layoutChanged
- 批量插入大量 item:每一行 appendRow 都会触发一轮信号 → 视图反复重绘
- 树形展开 / 折叠、拖拽排序:触发大量 layout 变更信号,整树刷新,而不是局部单元格刷新
自定义 Model 可以精细控制:批量操作时阻塞信号,完成后一次性通知视图刷新;只对变化的 index 发送 dataChanged,最小化重绘。
10.QAbstractItemView::setIndexWidget注意事项
官方文档描述:
cpp
void QAbstractItemView::setIndexWidget(const QModelIndex &index, QWidget *widget)
Sets the given widget on the item at the given index, passing the ownership of the widget to the viewport.
If index is invalid (e.g., if you pass the root index), this function will do nothing.
The given widget's autoFillBackground property must be set to true, otherwise the widget's background will be transparent, showing both the model data and the item at the given index.
If index widget A is replaced with index widget B, index widget A will be deleted. For example, in the code snippet below, the QLineEdit object will be deleted.
setIndexWidget(index, new QLineEdit);
...
setIndexWidget(index, new QTextEdit);
This function should only be used to display static content within the visible area corresponding to an item of data. If you want to display custom dynamic content or implement a custom editor widget, subclass QStyledItemDelegate instead.
This function was introduced in Qt 4.1.
See also indexWidget() and Delegate Classes.
1.所有权与生命周期
- 控件的所有权完全交给视图视口 ,你不需要手动
delete,视图销毁、单元格移除、替换控件时会自动释放。 - 对同一个索引重复调用
setIndexWidget,旧控件会被立即删除,不会残留。
2.渲染与背景
- 控件默认背景透明,会透出单元格底层的模型文本(比如你第三列如果写了文字,会和按钮重叠显示)。
- 必须设置
widget->setAutoFillBackground(true)才能完全覆盖底层内容。
3.拖拽问题
这个函数仅用于在数据项对应的可见区域显示静态内容。如果你要实现自定义动态内容、自定义编辑器控件,请子类化 QStyledItemDelegate。
为什么拖拽移动时 setIndexWidget 一定会出问题
核心本质:绑定的是「位置」,不是「数据节点」
setIndexWidget(index, widget) 是把控件挂载到当前行号、列号对应的单元格位置上,而不是和模型里的某个数据节点深度绑定。
当发生以下操作时,控件和数据就会错位:
- 拖拽移动节点:节点在模型里的行号、父节点发生了变化,但控件还停留在原位置的单元格上,出现 "按钮还在原地,数据已经搬走了" 的错位。
- 排序 / 过滤:行号重新排列,控件和对应的数据行完全错位,点击按钮会操作到错误的行。
- 树形折叠 / 展开:子节点被折叠后行被移除,再次展开时视图不会自动重新挂载控件,按钮直接消失。
- 滚动回收:视图滚动时单元格是复用的,滚出屏幕的行对应的控件不会自动跟随,滚回来后可能出现控件丢失、错乱。
这也是官方明确说它只适合静态内容的原因 ------ 数据结构不变、行数少、不滚动、不排序、不拖拽的场景,它才适用。
setIndexWidget vs 委托方案 选型对比
| 维度 | setIndexWidget | QStyledItemDelegate 自绘 |
|---|---|---|
| 开发复杂度 | 极低,直接 new 控件挂上 | 中等,需要手写绘制和交互逻辑 |
| 拖拽 / 排序 / 过滤兼容性 | ❌ 完全错位,无法正常使用 | ✅ 天然兼容,永远和数据索引对齐 |
| 树形折叠展开 | ❌ 展开后按钮消失,需手动重挂 | ✅ 自动重绘,无任何问题 |
| 大数据性能 | ❌ 每行一个真实控件,内存、句柄占用高,百行以上明显卡顿 | ✅ 纯绘制,十万行也无额外性能开销 |
| 样式自由度 | 受限于控件原生样式 | ✅ 完全自定义,像素级控制 |
| 官方推荐场景 | 静态、少量、无结构变化的单元格 | 动态内容、自定义编辑器、交互复杂的场景 |
11.QStyledItemDelegate详解
cpp
class Q_WIDGETS_EXPORT QStyledItemDelegate : public QAbstractItemDelegate
{
Q_OBJECT
public:
explicit QStyledItemDelegate(QObject *parent = nullptr);
~QStyledItemDelegate();
// painting
void paint(QPainter *painter,
const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
// editing
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
// editor factory
QItemEditorFactory *itemEditorFactory() const;
void setItemEditorFactory(QItemEditorFactory *factory);
virtual QString displayText(const QVariant &value, const QLocale &locale) const;
protected:
virtual void initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const;
bool eventFilter(QObject *object, QEvent *event) override;
bool editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option, const QModelIndex &index) override;
private:
Q_DECLARE_PRIVATE(QStyledItemDelegate)
Q_DISABLE_COPY(QStyledItemDelegate)
};
QStyledItemDelegate 是 Qt Model/View 架构中委托层的标准默认实现 ,继承自 QAbstractItemDelegate,负责单元格的绘制、编辑控件管理与交互事件处理。它遵循 Qt 样式系统,支持 QSS 美化,是所有自定义委托的首选基类。
委托层承担两个核心职责:
- 绘制渲染 :单元格的文本、图标、复选框、背景等所有视觉元素,全部由委托的
paint函数绘制,视图本身不直接绘制单元格。 - 编辑交互:单元格进入编辑状态时,由委托创建编辑器控件、填充数据、回写数据,实现「数据 - 控件」的双向同步。
11.1.核心模块 1:绘制渲染接口
1.paint ------ 单元格绘制入口
cpp
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
视图每刷新一次单元格,就会调用一次该函数,是自定义绘制的唯一入口。进度条、状态灯、按钮、星级评分等所有自定义视觉效果,都通过重写此函数实现。
参数说明
painter:绘图设备,所有绘制操作都通过它完成,使用前建议开启抗锯齿。option:样式选项,包含单元格矩形、选中 / 悬停 / 启用状态、调色板、字体等所有渲染上下文。index:对应的数据模型索引,通过index.data()可获取模型数据。
调用时机
视图重绘、滚动、数据变化、状态变化(选中 / 悬停)时高频调用。
最佳实践
- 函数是
const的,不能修改成员变量,只能读取状态。 - 开头调用
painter->save(),结尾调用painter->restore(),避免画笔、画刷等状态污染其他单元格绘制。 - 简单场景可先调用父类
QStyledItemDelegate::paint()绘制基础文本,再叠加自定义内容;完全自定义则全部自己绘制。
对应你的
ButtonDelegate:重写paint绘制了按钮的背景、边框、文字三态效果,替代了原生文本绘制。
2.sizeHint ------ 单元格建议尺寸
cpp
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
返回单元格的建议尺寸,视图计算行高、列宽时会参考此值。当自定义绘制内容比原生文本大时,必须重写保证内容完整显示。
调用时机
视图布局计算、自动调整行列大小时调用。
对应你的
ButtonDelegate:重写sizeHint保证操作列有足够宽度容纳按钮。
3.initStyleOption ------ 样式选项初始化(保护函数)
cpp
virtual void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const;
核心作用
将模型中的数据(DisplayRole、DecorationRole、CheckStateRole、FontRole 等)填充到 option 中,是父类 paint 的数据准备步骤。
重写场景
需要统一修改所有单元格的对齐方式、字体、显示规则时,重写此函数比重写 paint 更简洁。
示例:所有单元格居中对齐
cpp
void MyDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
{
QStyledItemDelegate::initStyleOption(option, index);
option->displayAlignment = Qt::AlignCenter;
}
4.displayText ------ 显示文本格式化
cpp
virtual QString displayText(const QVariant &value, const QLocale &locale) const;
核心作用
将模型返回的 QVariant 原始数据,格式化为最终显示的字符串。
重写场景
统一处理数字格式化、日期格式化、单位拼接、枚举转文本等,无需在 data() 或 paint() 中逐个处理。
示例:数字保留两位小数
cpp
QString MyDelegate::displayText(const QVariant &value, const QLocale &locale) const
{
if (value.type() == QVariant::Double) {
return QString::number(value.toDouble(), 'f', 2);
}
return QStyledItemDelegate::displayText(value, locale);
}
11.2.核心模块 2:编辑生命周期接口
这四个函数构成了「单元格编辑」的完整闭环,自定义编辑器(下拉框、数字输入、颜色选择器)时必须重写。
1.createEditor ------ 创建编辑器控件
cpp
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
核心作用
用户触发编辑(双击、F2)时调用,创建一个临时编辑器控件(默认是 QLineEdit),父对象为视图视口。
重写场景
需要自定义编辑控件时,比如用 QComboBox 做下拉选择、QSpinBox 做数字输入、QColorDialog 做颜色选择。
示例:下拉框编辑器
cpp
QWidget *ComboDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QComboBox *editor = new QComboBox(parent);
editor->addItems({"正常", "故障", "停机"});
return editor;
}
2.setEditorData ------ 模型数据填充到编辑器
cpp
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
核心作用
编辑器创建完成后调用,从模型中取出 Qt::EditRole 数据,填充到编辑器控件上。
示例
cpp
void ComboDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
QComboBox *combo = static_cast<QComboBox*>(editor);
QString text = index.data(Qt::EditRole).toString();
combo->setCurrentText(text);
}
3.setModelData ------ 编辑器数据回写到模型
cpp
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
核心作用
编辑提交(失去焦点、回车、确认)时调用,读取编辑器当前值,通过 model->setData() 写回模型。
示例
cpp
void ComboDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QComboBox *combo = static_cast<QComboBox*>(editor);
model->setData(index, combo->currentText(), Qt::EditRole);
}
4.updateEditorGeometry ------ 更新编辑器位置尺寸
cpp
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
核心作用
编辑器显示前调用,设置编辑器的位置和大小,使其刚好填满单元格。默认实现已满足绝大多数场景,只有特殊布局需求时才重写。
编辑完整生命周期
cpp
用户双击单元格
→ createEditor 创建编辑器控件
→ setEditorData 填充模型数据
→ 用户在控件上编辑
→ 提交编辑(回车/失去焦点)
→ setModelData 写回模型
→ 编辑器销毁
注意:编辑器是临时对象 ,编辑结束后立即销毁,不会长期占用资源,这也是它比
setIndexWidget更符合架构设计的原因。
11.3.核心模块 3:事件与交互接口
1. editorEvent ------ 单元格交互事件入口(保护函数)
cpp
bool editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option, const QModelIndex &index) override;
核心作用
视图将单元格上的鼠标、键盘事件转发给委托,是无控件实现交互的核心入口。按钮、复选框、进度条点击等交互,都通过重写此函数实现。
参数说明
event:原始事件(鼠标按下、移动、释放、键盘按键等)model:对应数据模型,可修改数据option:单元格样式与矩形信息index:对应模型索引
返回值
true:事件已被委托处理,不再向下传递false:事件未处理,交给父类默认逻辑
典型用法
- 判断事件类型(按下、移动、释放)
- 通过
option.rect计算命中区域(比如按钮矩形) - 命中后更新状态(悬停、按下),通过
dataChanged触发重绘 - 有效点击时执行业务逻辑(发射信号、修改数据)
对应你的
ButtonDelegate:重写editorEvent处理了鼠标移动(悬停)、按下、释放,模拟了完整的按钮点击交互,没有使用任何真实控件。
2. eventFilter ------ 编辑器事件过滤器
cpp
bool eventFilter(QObject *object, QEvent *event) override;
核心作用
监听编辑器控件的事件,默认实现已经处理了回车提交、ESC 取消、焦点失去提交等标准逻辑。自定义编辑器时,可重写它拦截特殊按键、自定义提交时机。
11.4.编辑器工厂接口
cpp
QItemEditorFactory *itemEditorFactory() const;
void setItemEditorFactory(QItemEditorFactory *factory);
核心作用
QItemEditorFactory 是全局编辑器工厂,根据数据类型(QVariant::Type)自动创建对应编辑器。比如:
- 字符串 →
QLineEdit - 整数 →
QSpinBox - 布尔 →
QCheckBox
使用场景
自定义工厂后,可以全局替换某类数据的默认编辑器,不用给每一列单独设置委托,适合全局统一编辑风格的场景。
11.5.自定义委托重写选型表
| 需求场景 | 重写函数 | 说明 |
|---|---|---|
| 自定义绘制(按钮、进度条、状态灯、图标) | paint |
最常用,纯绘制无控件 |
| 自定义点击交互 | editorEvent |
配合 paint 实现无控件交互 |
| 自定义编辑器(下拉、数字、颜色) | createEditor setEditorData setModelData |
标准编辑流程 |
| 调整单元格尺寸 | sizeHint |
保证自定义内容完整显示 |
| 统一文本格式化 | displayText |
数字、日期、单位统一处理 |
| 统一样式(对齐、字体) | initStyleOption |
比重写 paint 更轻量 |
| 拦截编辑器按键事件 | eventFilter |
自定义提交 / 取消逻辑 |
11.6.注意事项
-
优先用委托,少用 setIndexWidget 官方文档明确说明
setIndexWidget仅适合静态内容,动态交互、拖拽排序场景必须用委托方案,这也是你之前拖拽错位问题的根本解法。 -
paint 保持轻量 滚动时会高频调用,不要在里面做复杂计算、IO 操作,数据提前算好存到模型里。
-
状态变化用 dataChanged 局部刷新 悬停、按下状态变化时,只对当前索引发射
dataChanged,只重绘单个单元格,性能最优,不会全屏闪烁。 -
持久化索引跟踪状态 记录悬停、按下的节点时,用
QPersistentModelIndex而非普通QModelIndex,模型行号变化后自动跟踪正确节点,拖拽、排序不会错位。 -
编辑场景用标准编辑器流程 不要用
setIndexWidget做持久编辑器,标准的 createEditor 生命周期更清晰,和模型的交互更规范。
11.7.示例
自定义SpinBox
delegate.h
cpp
#ifndef DELEGATE_H
#define DELEGATE_H
#include <QStyledItemDelegate>
//! [0]
class SpinBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
SpinBoxDelegate(QObject *parent = 0);
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
//! [0]
#endif
delegate.cpp
cpp
#include "delegate.h"
#include <QSpinBox>
//! [0]
SpinBoxDelegate::SpinBoxDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
//! [0]
//! [1]
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &/* option */,
const QModelIndex &/* index */) const
{
QSpinBox *editor = new QSpinBox(parent);
editor->setFrame(false);
editor->setMinimum(0);
editor->setMaximum(100);
return editor;
}
//! [1]
//! [2]
void SpinBoxDelegate::setEditorData(QWidget *editor,
const QModelIndex &index) const
{
int value = index.model()->data(index, Qt::EditRole).toInt();
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->setValue(value);
}
//! [2]
//! [3]
void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const
{
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->interpretText();
int value = spinBox->value();
model->setData(index, value, Qt::EditRole);
}
//! [3]
//! [4]
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
{
editor->setGeometry(option.rect);
}
//! [4]
12.使用示例
运行截图:

实现的功能有:插入节点,编辑节点内容,拖拽实现移动等
详细实现代码:
ButtonDelegate.h
cpp
#ifndef BUTTONDELEGATE_H
#define BUTTONDELEGATE_H
#include <QStyledItemDelegate>
#include <QPersistentModelIndex>
class ButtonDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit ButtonDelegate(const QString& btnText = "删除", QObject *parent = nullptr);
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
bool editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) override;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
// 可配置参数:边距、尺寸限制
void setMargins(int leftRight, int topBottom);
void setMinSize(int minWidth, int minHeight);
void setMaxHeight(int maxHeight);
signals:
void buttonClicked(const QModelIndex& index) const;
private:
QRect buttonRect(const QRect& cellRect) const;
QString m_btnText;
QPersistentModelIndex m_hoverIndex;
QPersistentModelIndex m_pressedIndex;
// 自适应配置参数
int m_marginX = 0;//8; // 左右内边距
int m_marginY = 0;//4; // 上下内边距
int m_minWidth = 50; // 按钮最小宽度
int m_minHeight = 22;// 按钮最小高度
int m_maxHeight = 30;// 按钮最大高度
};
#endif // BUTTONDELEGATE_H
ButtonDelegate.cpp
cpp
#include "ButtonDelegate.h"
#include <QPainter>
#include <QMouseEvent>
ButtonDelegate::ButtonDelegate(const QString& btnText, QObject *parent)
: QStyledItemDelegate(parent), m_btnText(btnText)
{
}
void ButtonDelegate::setMargins(int leftRight, int topBottom)
{
m_marginX = leftRight;
m_marginY = topBottom;
}
void ButtonDelegate::setMinSize(int minWidth, int minHeight)
{
m_minWidth = minWidth;
m_minHeight = minHeight;
}
void ButtonDelegate::setMaxHeight(int maxHeight)
{
m_maxHeight = maxHeight;
}
// ==================== 核心:自适应按钮矩形计算 ====================
QRect ButtonDelegate::buttonRect(const QRect &cellRect) const
{
// 1. 扣除边距后的理论尺寸
int theoryWidth = cellRect.width() - 2 * m_marginX;
int theoryHeight = cellRect.height() - 2 * m_marginY;
// 2. 限制最小尺寸
int btnWidth = qMax(theoryWidth, m_minWidth);
int btnHeight = qMax(theoryHeight, m_minHeight);
// 3. 限制最大高度,防止行高过高按钮太丑
btnHeight = qMin(btnHeight, m_maxHeight);
// 4. 单元格内居中放置
int x = cellRect.left() + (cellRect.width() - btnWidth) / 2;
int y = cellRect.top() + (cellRect.height() - btnHeight) / 2;
return QRect(x, y, btnWidth, btnHeight);
}
// ==================== 绘制:自适应圆角 ====================
void ButtonDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
// 绘制单元格背景(选中态)
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
} else {
painter->fillRect(option.rect, option.palette.base());
}
//QRect btnRect = buttonRect(option.rect);
QRect btnRect = option.rect;
int radius = btnRect.height() / 4; // 圆角随按钮高度自适应
// 状态判断
bool isPressed = (m_pressedIndex == index);
bool isHover = (m_hoverIndex == index);
// 配色(删除按钮红色系,可自行修改)
QColor bgColor, borderColor, textColor;
if (isPressed) {
bgColor = QColor(0xE5, 0x39, 0x35);
borderColor = QColor(0xC6, 0x28, 0x28);
textColor = Qt::white;
} else if (isHover) {
bgColor = QColor(0xFF, 0xEB, 0xEE);
borderColor = QColor(0xEF, 0x9A, 0x9A);
textColor = QColor(0xC6, 0x28, 0x28);
} else {
bgColor = Qt::white;
//borderColor = QColor(0xE0, 0xE0, 0xE0); //#FFCDD2
borderColor = QColor(0xFF, 0xCD, 0xD2);
textColor = QColor(0xE5, 0x39, 0x35);
}
// 绘制圆角按钮
painter->setBrush(bgColor);
painter->setPen(QPen(borderColor, 1));
//painter->drawRoundedRect(btnRect, radius, radius);
painter->drawRoundedRect(btnRect, 0, 0);
// 绘制文字
painter->setPen(textColor);
QFont font = painter->font();
font.setPixelSize(20);
painter->setFont(font);
QString text = QString("%1 %2").arg(m_btnText).arg(index.row());
painter->drawText(btnRect, Qt::AlignCenter, text);
painter->restore();
}
// ==================== 交互逻辑(命中检测自动适配新矩形) ====================
bool ButtonDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index)
{
//QRect btnRect = buttonRect(option.rect);
QRect btnRect = option.rect;
switch (event->type())
{
case QEvent::MouseMove: {
QMouseEvent* me = static_cast<QMouseEvent*>(event);
if (btnRect.contains(me->pos())) {
if (m_hoverIndex != index) {
QModelIndex oldHover = m_hoverIndex;
m_hoverIndex = QPersistentModelIndex(index);
if (oldHover.isValid())
const_cast<QAbstractItemModel*>(model)->dataChanged(oldHover, oldHover);
const_cast<QAbstractItemModel*>(model)->dataChanged(index, index);
}
} else {
if (m_hoverIndex == index) {
m_hoverIndex = QPersistentModelIndex();
const_cast<QAbstractItemModel*>(model)->dataChanged(index, index);
}
}
return true;
}
case QEvent::MouseButtonPress: {
QMouseEvent* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::LeftButton && btnRect.contains(me->pos())) {
m_pressedIndex = QPersistentModelIndex(index);
const_cast<QAbstractItemModel*>(model)->dataChanged(index, index);
return true;
}
break;
}
case QEvent::MouseButtonRelease: {
QMouseEvent* me = static_cast<QMouseEvent*>(event);
bool wasPressed = (m_pressedIndex == index);
m_pressedIndex = QPersistentModelIndex();
if (me->button() == Qt::LeftButton && btnRect.contains(me->pos()) && wasPressed) {
emit buttonClicked(index);
}
const_cast<QAbstractItemModel*>(model)->dataChanged(index, index);
return true;
}
case QEvent::Leave: {
if (m_hoverIndex == index) {
m_hoverIndex = QPersistentModelIndex();
const_cast<QAbstractItemModel*>(model)->dataChanged(index, index);
}
break;
}
default:
break;
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
QSize ButtonDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
Q_UNUSED(option);
Q_UNUSED(index);
// 建议单元格最小尺寸:保证按钮能正常显示
return QSize(m_minWidth + 2*m_marginX, m_minHeight + 2*m_marginY);
}
mainwindow.h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QStandardItemModel>
#include <QTreeView>
#include "ButtonDelegate.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onAddTopLevel();
void onAddChild();
void onInsertSibling();
void onRemove();
void onClear();
// 复选框状态变化处理(级联核心)
void onItemChanged(QStandardItem *item);
// ========== 新增:按钮相关槽函数 ==========
void onRowsInserted(const QModelIndex &parent, int first, int last);
void onDeleteBtnClicked(const QPersistentModelIndex &index);
private:
QList<QStandardItem*> createRowItems(const QString& name, const QString& remark = "");
void initUI();
// 向下级联:同步所有子节点勾选状态
void updateChildrenCheckState(QStandardItem *parentItem, Qt::CheckState state);
// 向上汇总:根据子节点更新父节点勾选状态
void updateParentCheckState(QStandardItem *childItem);
// ========== 新增:给指定索引挂载删除按钮 ==========
void addDeleteButton(const QModelIndex &index);
private:
Ui::MainWindow *ui;
bool m_isUpdatingCheck = false; // 防递归标志
QTreeView* m_treeView;
QStandardItemModel* m_model;
ButtonDelegate* m_btnDelegate;
};
#endif // MAINWINDOW_H
mainwindow.cpp
cpp
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include "CustomTreeModel.h"
#include <QInputDialog>
#include "delegate.h"
#include "BtnDelegate.h"
TreeItem* node4 = nullptr;
CustomTreeModel* treeModel = nullptr;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
m_treeView = ui->treeView_2;
treeModel = new CustomTreeModel(this);
ui->treeView->setModel(treeModel);
TreeItem* node1 = treeModel->addChild(nullptr, QString::fromLocal8Bit("一级节点1"));
treeModel->addChild(node1, QString::fromLocal8Bit("子节点1-1"));
treeModel->addChild(node1, QString::fromLocal8Bit("子节点1-2"));
TreeItem* node2 = treeModel->addChild(nullptr, QString::fromLocal8Bit("一级节点2"));
treeModel->addChild(node2, QString::fromLocal8Bit("子节点2-1"));
TreeItem* node3 = treeModel->addChild(nullptr, QString::fromLocal8Bit("一级节点3"));
treeModel->addChild(node3, QString::fromLocal8Bit("子节点3-1"));
treeModel->addChild(node3, QString::fromLocal8Bit("子节点3-2"));
treeModel->addChild(node3, QString::fromLocal8Bit("子节点3-3"));
treeModel->addChild(node3, QString::fromLocal8Bit("子节点3-4"));
node4 = treeModel->addChild(node3, QString::fromLocal8Bit("子节点3-5"));
treeModel->addChild(node4, QString::fromLocal8Bit("子节点3-5-1"));
treeModel->addChild(node4, QString::fromLocal8Bit("子节点3-5-2"));
treeModel->addChild(node4, QString::fromLocal8Bit("子节点3-5-3"));
treeModel->addChild(node4, QString::fromLocal8Bit("子节点3-5-4"));
treeModel->addChild(nullptr, QString::fromLocal8Bit("一级节点4"));
ui->treeView->expandAll();
connect(ui->pushButton, &QPushButton::clicked, this, [&](){
treeModel->addChild(node4, QString::fromLocal8Bit("子节点3-6-1"));
treeModel->addChild(node4, QString::fromLocal8Bit("子节点3-6-2"));
treeModel->addChild(node4, QString::fromLocal8Bit("子节点3-6-3"));
node4 = treeModel->addChild(node4, QString::fromLocal8Bit("子节点3-6-4"));
treeModel->addChild(nullptr, QString::fromLocal8Bit("一级节点4"));
ui->treeView->expandAll();
});
initUI();
// 初始化测试数据
QList<QStandardItem*> row1 = createRowItems(QString::fromLocal8Bit("车间A"), QString::fromLocal8Bit("1号厂房"));
QList<QStandardItem*> row2 = createRowItems(QString::fromLocal8Bit("车间B"), QString::fromLocal8Bit("11号厂房"));
QList<QStandardItem*> row3 = createRowItems(QString::fromLocal8Bit("车间C"), QString::fromLocal8Bit("2号厂房"));
QList<QStandardItem*> row4 = createRowItems(QString::fromLocal8Bit("车间D"), QString::fromLocal8Bit("5号厂房"));
QList<QStandardItem*> row5 = createRowItems(QString::fromLocal8Bit("车间E"), QString::fromLocal8Bit("6号厂房"));
QList<QStandardItem*> row6 = createRowItems(QString::fromLocal8Bit("车间F"), QString::fromLocal8Bit("9号厂房"));
QList<QStandardItem*> row7 = createRowItems(QString::fromLocal8Bit("车间G"), QString::fromLocal8Bit("110号厂房"));
QList<QStandardItem*> row8 = createRowItems(QString::fromLocal8Bit("车间11"), QString::fromLocal8Bit("88号厂房"));
QList<QStandardItem*> row9 = createRowItems(QString::fromLocal8Bit("车间12"), QString::fromLocal8Bit("8443号厂房"));
QList<QStandardItem*> row10 = createRowItems(QString::fromLocal8Bit("车间21"), QString::fromLocal8Bit("erre号厂房"));
QList<QStandardItem*> row11 = createRowItems(QString::fromLocal8Bit("车间22"), QString::fromLocal8Bit("33号厂房"));
QList<QStandardItem*> row12 = createRowItems(QString::fromLocal8Bit("车间23"), QString::fromLocal8Bit("5445号厂房"));
QList<QStandardItem*> row13 = createRowItems(QString::fromLocal8Bit("车间56"), QString::fromLocal8Bit("999号厂房"));
m_model->appendRow(row1);
m_model->appendRow(row2);
m_model->appendRow(row3);
m_model->appendRow(row4);
m_model->appendRow(row5);
m_model->appendRow(row6);
m_model->appendRow(row7);
m_model->appendRow(row8);
m_model->appendRow(row9);
m_model->appendRow(row10);
m_model->appendRow(row11);
m_model->appendRow(row12);
m_model->appendRow(row13);
row1.first()->appendRow(createRowItems(QString::fromLocal8Bit("产线A1"), QString::fromLocal8Bit("运行中")));
row1.first()->appendRow(createRowItems(QString::fromLocal8Bit("产线A2"),QString::fromLocal8Bit( "停机")));
row1.first()->appendRow(createRowItems(QString::fromLocal8Bit("产线A3"),QString::fromLocal8Bit( "停机")));
row1.first()->appendRow(createRowItems(QString::fromLocal8Bit("产线A4"),QString::fromLocal8Bit( "停机")));
row2.first()->appendRow(createRowItems(QString::fromLocal8Bit("产线B1"), QString::fromLocal8Bit("运行中")));
row2.first()->appendRow(createRowItems(QString::fromLocal8Bit("产线B2"),QString::fromLocal8Bit( "停机")));
row3.first()->appendRow(createRowItems(QString::fromLocal8Bit("产线C3"),QString::fromLocal8Bit( "停机")));
row4.first()->appendRow(createRowItems(QString::fromLocal8Bit("产线D4"),QString::fromLocal8Bit( "停机")));
m_model->appendRow(createRowItems(QString::fromLocal8Bit("车间B"), QString::fromLocal8Bit("2号厂房")));
m_treeView->expandAll();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::initUI()
{
// 顶部按钮栏
//QHBoxLayout* btnLayout = new QHBoxLayout;
QPushButton* btnAddTop = new QPushButton(QString::fromLocal8Bit("添加顶层"));
QPushButton* btnAddChild = new QPushButton(QString::fromLocal8Bit("添加子节点"));
QPushButton* btnInsert = new QPushButton(QString::fromLocal8Bit("插入兄弟"));
QPushButton* btnRemove = new QPushButton(QString::fromLocal8Bit("删除选中"));
QPushButton* btnClear = new QPushButton(QString::fromLocal8Bit("清空全部"));
ui->horizontalLayout_2->addWidget(btnAddTop);
ui->horizontalLayout_2->addWidget(btnAddChild);
ui->horizontalLayout_2->addWidget(btnInsert);
ui->horizontalLayout_2->addWidget(btnRemove);
ui->horizontalLayout_2->addWidget(btnClear);
connect(btnAddTop, &QPushButton::clicked, this, &MainWindow::onAddTopLevel);
connect(btnAddChild, &QPushButton::clicked, this, &MainWindow::onAddChild);
connect(btnInsert, &QPushButton::clicked, this, &MainWindow::onInsertSibling);
connect(btnRemove, &QPushButton::clicked, this, &MainWindow::onRemove);
connect(btnClear, &QPushButton::clicked, this, &MainWindow::onClear);
// 树形视图
m_model = new QStandardItemModel(this);
m_model->setColumnCount(3);
m_model->setHeaderData(0, Qt::Horizontal, QString::fromLocal8Bit("节点名称"));
m_model->setHeaderData(1, Qt::Horizontal, QString::fromLocal8Bit("备注信息"));
m_model->setHeaderData(2, Qt::Horizontal, QString::fromLocal8Bit("操作")); // 新增操作
// 复选框级联信号
connect(m_model, &QStandardItemModel::itemChanged,
this, &MainWindow::onItemChanged);
#if 0
// ========== 新增:监听行插入,自动挂载按钮 ==========
connect(m_model, &QStandardItemModel::rowsInserted,
this, &MainWindow::onRowsInserted);
#endif
//m_treeView = new QTreeView(this);
m_treeView->setModel(m_model);
m_treeView->setSelectionBehavior(QAbstractItemView::SelectRows);
m_treeView->setSelectionMode(QAbstractItemView::SingleSelection);
m_treeView->setDragDropMode(QAbstractItemView::InternalMove);
m_treeView->setDropIndicatorShown(true);
m_treeView->setDefaultDropAction(Qt::MoveAction);
m_treeView->setAlternatingRowColors(true);
m_treeView->header()->setStretchLastSection(false);
m_treeView->setColumnWidth(0, 500);
m_treeView->setColumnWidth(1, 500);
m_treeView->setColumnWidth(2, 120); // 操作列固定宽度
// ========== 替换为委托方案 ==========
m_btnDelegate = new ButtonDelegate(QString::fromLocal8Bit("删除"), this);
connect(m_btnDelegate, &ButtonDelegate::buttonClicked, this, [this](const QModelIndex& index){
if (!index.isValid())
return;
QString text;
QStandardItem* parentItem = m_model->itemFromIndex(index);
if (parentItem){
text = parentItem->text();
}
});
m_treeView->setItemDelegateForColumn(2, m_btnDelegate);
// 给第2列(操作列)设置专属委托
//SpinBoxDelegate* pSpinBox = new SpinBoxDelegate(this);
//BtnDelegate* pSpinBox = new BtnDelegate(this);
//m_treeView->setItemDelegateForColumn(2, pSpinBox);
resize(600, 500);
}
// 创建一行节点:第一列带三态复选框,第二列纯文本
QList<QStandardItem*> MainWindow::createRowItems(const QString& name, const QString& remark)
{
QList<QStandardItem*> row;
// 第一列:名称 + 三态复选框 + 拖拽能力
QStandardItem* nameItem = new QStandardItem(name);
nameItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled |
Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled |
Qt::ItemIsUserCheckable | Qt::ItemIsTristate);
nameItem->setCheckState(Qt::Unchecked); // 默认未勾选
row.append(nameItem);
// 第二列:备注,无复选框、不可拖拽接收
QStandardItem* remarkItem = new QStandardItem(remark);
remarkItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable |
Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
row.append(remarkItem);
// ========== 新增:第三列 操作列 ==========
QStandardItem* opItem = new QStandardItem();
// 仅启用,不可选中、不可编辑,避免选中行时背景覆盖按钮
opItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
row.append(opItem);
return row;
}
// 行插入触发:给所有新行的第三列挂载删除按钮
void MainWindow::onRowsInserted(const QModelIndex &parent, int first, int last)
{
for (int row = first; row <= last; ++row) {
// 获取第三列的模型索引
QModelIndex btnIndex = m_model->index(row, 2, parent);
addDeleteButton(btnIndex);
}
}
// 给指定索引挂载删除按钮
void MainWindow::addDeleteButton(const QModelIndex &index)
{
if (!index.isValid() || index.column() != 2)
return;
// 创建按钮,优化样式适配表格
QPushButton* delBtn = new QPushButton(QString::fromLocal8Bit("删除"));
delBtn->setFlat(true); // 扁平化,与表格融合更好
#if 1
delBtn->setStyleSheet(R"(
QPushButton {
font-size: 10pt;
padding: 2px 6px;
height: 22px;
color: #E53935;
border: 1px solid #FFCDD2;
border-radius: 3px;
background: #FFF;
}
QPushButton:hover {
background: #FFEBEE;
}
QPushButton:pressed {
background: #FFCDD2;
}
)");
#endif
// 用持久化索引跟踪节点:拖拽移动后行号变化也能准确定位
QPersistentModelIndex persistentIdx(index);
// 绑定点击事件,lambda捕获持久化索引
connect(delBtn, &QPushButton::clicked, this, [this, persistentIdx]() {
//onDeleteBtnClicked(persistentIdx);
});
// 将按钮挂载到视图指定单元格
m_treeView->setIndexWidget(index, delBtn);
}
// 按钮点击:删除对应节点
void MainWindow::onDeleteBtnClicked(const QPersistentModelIndex &index)
{
if (!index.isValid())
return;
QModelIndex parentIdx = index.parent();
int row = index.row();
// 删除行(会自动触发rowsRemoved,按钮自动销毁)
if (parentIdx.isValid()) {
QStandardItem* parentItem = m_model->itemFromIndex(parentIdx);
if (parentItem)
parentItem->removeRow(row);
} else {
m_model->removeRow(row);
}
}
// ==================== 复选框级联核心逻辑 ====================
void MainWindow::onItemChanged(QStandardItem *item)
{
// 只处理第一列的勾选变化,且级联过程中忽略重复触发
if (item->column() != 0 || m_isUpdatingCheck)
return;
m_isUpdatingCheck = true;
Qt::CheckState state = item->checkState();
// 1. 向下级联:所有子节点同步当前状态
updateChildrenCheckState(item, state);
// 2. 向上汇总:更新所有父节点的汇总状态
updateParentCheckState(item);
m_isUpdatingCheck = false;
}
// 向下递归:设置所有子孙节点的勾选状态
void MainWindow::updateChildrenCheckState(QStandardItem *parentItem, Qt::CheckState state)
{
if (!parentItem) return;
for (int i = 0; i < parentItem->rowCount(); ++i) {
QStandardItem* child = parentItem->child(i, 0);
if (!child) continue;
child->setCheckState(state);
// 递归处理孙节点
updateChildrenCheckState(child, state);
}
}
// 向上递归:根据子节点状态汇总父节点勾选状态
void MainWindow::updateParentCheckState(QStandardItem *childItem)
{
QStandardItem* parent = childItem->parent();
if (!parent) return; // 已到顶层,终止递归
int checkedCount = 0;
int partialCount = 0;
int total = parent->rowCount();
for (int i = 0; i < total; ++i) {
QStandardItem* child = parent->child(i, 0);
if (!child) continue;
switch (child->checkState()) {
case Qt::Checked: checkedCount++; break;
case Qt::PartiallyChecked: partialCount++; break;
default: break;
}
}
// 计算父节点状态
Qt::CheckState parentState;
if (checkedCount == total) {
parentState = Qt::Checked;
} else if (checkedCount == 0 && partialCount == 0) {
parentState = Qt::Unchecked;
} else {
parentState = Qt::PartiallyChecked;
}
parent->setCheckState(parentState);
// 继续向上递归更新祖父节点
updateParentCheckState(parent);
}
// ==================== 增删操作接口 ====================
void MainWindow::onAddTopLevel()
{
bool ok;
QString text = QInputDialog::getText(this, QString::fromLocal8Bit("新增"), QString::fromLocal8Bit("节点名称:"), QLineEdit::Normal, QString::fromLocal8Bit("新节点"), &ok);
if (ok && !text.isEmpty()) {
m_model->appendRow(createRowItems(text, ""));
}
}
void MainWindow::onAddChild()
{
QModelIndex current = m_treeView->currentIndex();
bool ok;
QString text = QInputDialog::getText(this, QString::fromLocal8Bit("新增子节点"), QString::fromLocal8Bit("节点名称:"), QLineEdit::Normal, QString::fromLocal8Bit("子节点"), &ok);
if (!ok || text.isEmpty()) return;
if (!current.isValid()) {
m_model->appendRow(createRowItems(text, ""));
return;
}
QStandardItem* parent = m_model->itemFromIndex(current.siblingAtColumn(0));
if (parent) {
parent->appendRow(createRowItems(text, ""));
m_treeView->expand(current);
}
}
void MainWindow::onInsertSibling()
{
QModelIndex current = m_treeView->currentIndex();
bool ok;
QString text = QInputDialog::getText(this, QString::fromLocal8Bit("插入兄弟节点"), QString::fromLocal8Bit("节点名称:"), QLineEdit::Normal, QString::fromLocal8Bit("兄弟节点"), &ok);
if (!ok || text.isEmpty()) return;
QModelIndex parentIdx = current.parent();
int row = current.isValid() ? current.row() : m_model->rowCount();
QList<QStandardItem*> rowItems = createRowItems(text, "");
if (parentIdx.isValid()) {
QStandardItem* parent = m_model->itemFromIndex(parentIdx);
parent->insertRow(row, rowItems);
} else {
m_model->insertRow(row, rowItems);
}
}
void MainWindow::onRemove()
{
QModelIndex current = m_treeView->currentIndex();
if (!current.isValid()) return;
QModelIndex parentIdx = current.parent();
int row = current.row();
if (parentIdx.isValid()) {
QStandardItem* parent = m_model->itemFromIndex(parentIdx);
parent->removeRow(row);
} else {
m_model->removeRow(row);
}
}
void MainWindow::onClear()
{
m_model->removeRows(0, m_model->rowCount());
}
界面布局:
