一、目的
在 Qt 的 QTableWidget
中,insertRow(0)
(头插)和 insertRow(rowCount())
(尾插)在性能上存在显著差异。
二、QAbstractItemModel:: insertRows 原文解释
QAbstractItemModel Class | Qt Core 5.15.18
AI 解释
QAbstractItemModel:: insertRows
是 Qt 模型/视图框架中用于在模型中插入行的一个虚函数。以下是对该函数的详细解释,包括其功能、参数、返回值以及如何正确实现自定义模型中的插入操作。
函数声明
[virtual] bool QAbstractItemModel::insertRows(int row, int count, const QModelIndex &parent = QModelIndex())
功能概述
insertRows
函数用于在模型中指定位置插入一行或多行数据。具体来说,它会在给定的 row
位置之前插入 count
行新数据。这些新行将成为由 parent
模型索引表示的父项的子项。
参数说明
-
int row
指定插入行的位置。如果row
为0
,新行将被插入到父项的最前面;如果row
等于rowCount(parent)
,新行将被追加到父项的最后面。 -
int count
要插入的行数。可以一次性插入多行数据。 -
const QModelIndex &parent = QModelIndex()
表示插入行的父索引。如果parent
是无效索引(默认值),则插入的行将成为顶级行(即没有父项)。否则,新行将作为parent
所表示的父项的子项插入。
返回值
-
bool
返回true
表示插入操作成功;返回false
表示插入操作失败。
注意事项
- 基类实现
QAbstractItemModel
的基类实现是空的,直接调用会返回false
。因此,如果你的自定义模型需要支持插入行的操作,必须重写(reimplement)此函数。 - 通知机制
在实际插入数据之前和之后,必须调用beginInsertRows
和endInsertRows
函数。这两个函数用于通知视图和其他相关组件模型数据的变化,以确保界面能够正确更新。 - 实现逻辑
当重写insertRows
时,除了调用beginInsertRows
和endInsertRows
来通知变化外,还需要在内部数据结构中实际插入新的数据行。
示例代码
以下是一个简单的自定义模型中重写 insertRows
的示例:
#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>
#include <QList>
class MyModel : public QAbstractItemModel {
Q_OBJECT
public:
// 构造函数和其他必要的成员函数...
// 重写 insertRows 函数
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
// 开始插入操作的通知
beginInsertRows(parent, row, row + count - 1);
// 在内部数据结构中插入新行
if (!parent.isValid()) {
// 插入顶级行
for (int i = 0; i < count; ++i) {
m_data.insert(row, QVariant()); // 假设 m_data 是 QList<QVariant>
}
} else {
// 插入子行(假设父项有子项列表)
MyItem *parentItem = static_cast<MyItem*>(parent.internalPointer());
if (parentItem) {
parentItem->appendChildItems(count); // 假设有相应的方法添加子项
}
}
// 结束插入操作的通知
endInsertRows();
return true; // 插入成功
}
// 其他必要的虚函数实现...
private:
QList<QVariant> m_data; // 示例数据结构,实际应用中可能更复杂
};
详细解释
-
调用
beginInsertRows
beginInsertRows(parent, row, row + count - 1);
这个函数通知所有连接到模型的视图,即将在
parent
父索引下的row
到row + count - 1
位置插入新行。这使视图能够预先调整其布局和缓冲区,以适应即将到来的数据变化。 -
修改内部数据结构
根据parent
是否有效,决定是插入顶级行还是子行:- 顶级行:直接在
m_data
列表的指定位置插入新数据。 - 子行:通过
parent
获取对应的父项对象,并调用相应的方法在父项下插入新的子项。
- 顶级行:直接在
-
调用
endInsertRows
endInsertRows();
这个函数通知视图插入操作已完成,视图可以更新显示新的数据行。
-
返回值
返回true
表示插入操作成功。如果插入过程中出现错误,可以返回false
,但通常需要提供适当的错误处理机制。
使用场景
- 动态添加数据:当用户通过界面按钮或其他方式请求添加新行时,模型通过
insertRows
插入新数据。 - 批量插入:一次性插入多行数据,提高效率。
- 层级结构:在树形结构模型中,为特定父项插入子行。
性能考虑
- 通知开销:频繁调用
beginInsertRows
和endInsertRows
可能导致性能下降。因此,应尽量批量插入数据,而不是逐行插入。 - 数据结构的效率:确保内部数据结构支持高效的插入操作。例如,使用链表可以快速在头部插入,而动态数组在尾部插入更高效。
总结
QAbstractItemModel::insertRows
是实现自定义模型时用于插入新行的关键函数。通过正确重写此函数,并结合 beginInsertRows
和 endInsertRows
的通知机制,可以确保模型数据的动态变化能够正确反映在关联的视图上,同时保持良好的性能和用户体验。
三、QTableWidget::insertRow(int row) 源码
路径
- Qt\5.15.2\Src\qtbase\src\widgets\itemviews\qtablewidget. cpp
- Qt\5.15.2\Src\qtbase\src\widgets\itemviews\qtablewidget_p.h
代码解析
void QTableWidget::insertRow(int row)
{
Q_D(QTableWidget);
d->tableModel()->insertRows(row);
}
bool QTableModel::insertRows(int row, int count, const QModelIndex &)
{
if (count < 1 || row < 0 || row > verticalHeaderItems.count())
return false;
beginInsertRows(QModelIndex(), row, row + count - 1);
int rc = verticalHeaderItems.count();
int cc = horizontalHeaderItems.count();
verticalHeaderItems.insert(row, count, 0);
if (rc == 0)
tableItems.resize(cc * count);
else
tableItems.insert(tableIndex(row, 0), cc * count, 0);
endInsertRows();
return true;
}
class QTableModel : public QAbstractTableModel
{
Q_OBJECT
friend class QTableWidget;
.....
private:
const QTableWidgetItem *prototype;
QVector<QTableWidgetItem*> tableItems;
QVector<QTableWidgetItem*> verticalHeaderItems;
QVector<QTableWidgetItem*> horizontalHeaderItems;
// A cache must be mutable if get-functions should have const modifiers
mutable QModelIndexList cachedIndexes; };
从这段代码可以看出,QTableWidget
的插入行操作实际上是通过其内部的 QTableModel
来实现的,具体步骤包括:
- 插入表头项:
verticalHeaderItems.insert(row, count, 0);
- 插入表格数据项:
- 如果当前表格没有行 (
rc == 0
),则直接调整tableItems
的大小。 - 否则,在指定位置插入新的数据项,
tableItems.insert(tableIndex(row, 0), cc * count, 0);
- 如果当前表格没有行 (
性能差异分析
1. 性能差异的核心原因
头插 (insertRow(0)
)
- 表头项操作:
verticalHeaderItems.insert(row, count, 0)
在QVector
头部插入元素,需要将后续所有元素向后移动,时间复杂度为 O (n)(n 为当前行数)。 - 表格数据操作:
tableItems.insert(tableIndex(row, 0), cc * count, 0)
在数据数组头部插入新元素,同样需要移动后续所有数据,时间复杂度为 O (n * m)(m 为列数)。 - 视图更新:
触发beginInsertRows
和endInsertRows
,通知视图重新计算所有行的位置,导致界面重绘开销较大。
尾插 (insertRow(rowCount())
)
- 表头项操作:
在QVector
尾部追加元素,时间复杂度为 O (1)(假设预分配了足够内存)。 - 表格数据操作:
- 若表格为空,通过
tableItems.resize(cc * count)
初始化内存(O (1))。 - 若表格非空,直接追加到
QVector
末尾(O (1))。
- 若表格为空,通过
- 视图更新:
视图仅需扩展显示区域,渲染开销更低。
2. 插入操作的时间复杂度
- 尾插 (
insertRow(rowCount())
):- 表头项插入:在
QVector
的末尾插入元素,时间复杂度为 O(1)。 - 表项数据插入:如果表格为空,仅需调整大小;否则,由于是在末尾插入,
QVector::insert
在有预留空间的情况下也是 O(1)。但如果有重新分配内存的需求,可能会涉及到 O(n) 的复制操作,但这种情况在尾部插入时较少发生。
- 表头项插入:在
- 头插 (
insertRow(0)
):- 表头项插入:在
QVector
的开头插入元素,需要移动所有现有元素,时间复杂度为 O(n)。 - 表项数据插入:同样,在开头插入需要移动所有现有的数据项,时间复杂度为 O(n)。如果表格较大,这种移动操作的开销会显著增加。
- 表头项插入:在
3. 实际性能影响
- 数据量较小:
- 当表格中的行数较少(例如几十行)时,头插和尾插的性能差异可能不明显,用户几乎感觉不到延迟。
- 数据量较大:
- 当表格包含数千行甚至更多行时,头插操作由于需要频繁移动大量元素,会导致明显的性能下降,甚至可能造成界面卡顿或响应延迟。
- 尾插操作由于主要在末尾添加元素,性能相对稳定,几乎不受插入次数的影响。
4. 内存与缓存的影响
- 内存重新分配:
QVector
在插入元素时,如果当前容量不足以容纳新元素,会进行内存重新分配和元素复制。头插操作由于频繁移动元素,可能更频繁地触发内存重新分配,增加开销。
- 缓存局部性:
- 尾插操作更有利于 CPU 缓存的利用,因为新元素通常被添加到内存的连续区域。而头插操作打乱了数据的连续性,导致缓存命中率降低,进一步影响性能。
5. 实际性能对比
操作类型 | 时间复杂度 | 内存移动次数 | 适用场景 | 性能影响 |
---|---|---|---|---|
头插 | O(n) | 高(n 次移动) | 按倒序插入少量数据 | 高(避免频繁使用) |
尾插 | O(1) | 低(尾部追加) | 常规数据追加、大规模插入 | 低(推荐优先使用) |
- 小数据量场景(如数十行):两者差异可忽略。
- 大数据量场景(如数万行): - 头插单行耗时可能是尾插的 10 倍以上(因需移动全部数据)。 - 尾插性能稳定,适合高频插入操作。
四、 示例测试
以下是一个使用 QElapsedTimer
和 QTableWidget
测试 insertRow(0)
(头插)和 insertRow(rowCount())
(尾插)性能差异的完整示例代码。代码通过批量插入数据并测量时间,直观展示两者的性能差距:
#include <QApplication>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QElapsedTimer>
#include <QTimer>
#include <QDebug>
// 测试函数:插入指定行数到表格的头部或尾部
void testInsertPerformance(QTableWidget* table, int rowCount, const QString& testName) {
table->clearContents();
table->setRowCount(0);
// 禁用视图更新以提高测试准确性
table->setUpdatesEnabled(false);
QElapsedTimer timer;
timer.start();
for (int i = 0; i < rowCount; ++i) {
int row = (testName == "Head Insert") ? 0 : table->rowCount();
table->insertRow(row);
table->setItem(row, 0, new QTableWidgetItem(QString("Name %1").arg(i)));
table->setItem(row, 1, new QTableWidgetItem(QString::number(i)));
table->setItem(row, 2, new QTableWidgetItem(QString("City %1").arg(i % 10)));
}
auto elapsed_ms = timer.elapsed();
table->setUpdatesEnabled(true); // 恢复视图更新
qDebug() << testName << "Performance:";
qDebug() << " Time elapsed:" << elapsed_ms << "ms";
qDebug() << " Time elapsed:" << elapsed_ms << "ms";
qDebug() << " Memory used:" << table->sizeHint().height() * sizeof(QTableWidgetItem*) / 1024.<< "KB";
qDebug() << " Memory used:" << table->sizeHint().width() * sizeof(QTableWidgetItem*) / 1024.<< "KB";
}
int main(int argc, char* argv[]) {
QApplication a(argc, argv);
// 创建测试表格(3列)
QTableWidget table;
table.setColumnCount(3);
table.setHorizontalHeaderLabels({ "Name", "Age", "City" });
table.resize(600, 400);
table.show();
#if 1
// 测试头插性能(插入100000行)
testInsertPerformance(&table, 100000, "Head Insert");
// 等待用户操作后测试尾插性能
QTimer::singleShot(2000, [&]() {
testInsertPerformance(&table, 100000, "Tail Insert");
});
#else
//测试尾插性能
testInsertPerformance(&table, 100000, "Tail Insert");
// 等待用户操作后测试头插性能
QTimer::singleShot(2000, [&]() {
testInsertPerformance(&table, 100000, "Head Insert");
});
#endif
return a.exec();
}
代码解析与测试结果
1. 核心逻辑
- 禁用视图更新:通过
setUpdatesEnabled(false)
暂停界面刷新,避免渲染开销干扰时间测量。 - 批量插入数据:循环插入指定行数,每行包含姓名、年龄、城市三列数据。
- 时间测量:使用
QElapsedTimer
记录插入操作的耗时。 - 内存估算:通过
sizeHint().height()
估算表格占用的内存(粗略计算)。
2. 测试结果示例
操作类型 | 插入行数 | 耗时(ms) | 内存占用(KB) | |
---|---|---|---|---|
头插 | 100,000 | 9,224 | 1.5 | |
尾插 | 100,000 | 2509 | 1.5 | |
注:实际结果可能因硬件和 Qt 版本略有差异,但头插耗时通常比尾插高 。
性能差异原因
-
头插 (
insertRow(0)
):- 数据移动:
QVector
在头部插入元素需移动后续所有元素,时间复杂度为 O(n)。 - 内存重新分配:频繁插入导致内存多次重新分配,增加开销。
- 数据移动:
-
尾插 (
insertRow(rowCount())
):- 尾部追加:
QVector
在尾部插入元素时间复杂度为 O(1)(预分配内存时)。 - 内存连续性:数据连续存储,缓存命中率高。
- 尾部追加:
优化建议
- 预分配内存:若已知数据量,提前调用
table->setRowCount(rowCount)
预分配内存。 - 改用自定义模型:对超大数据集,使用
QAbstractTableModel
替代QTableWidget
,通过虚拟化技术减少内存占用。
五、 优化建议
基于上述分析,以下是一些优化建议:
-
优先使用尾插:
- 如果业务逻辑允许,尽量使用
insertRow(rowCount())
进行尾插操作,以获得更好的性能表现。
- 如果业务逻辑允许,尽量使用
-
批量插入:
- 如果需要插入多行,尽量一次性批量插入,而不是逐行插入。例如,先收集所有需要插入的数据,然后调用一次
setRowCount
。
// 示例:批量插入多行 tableWidget->setRowCount(10000); for (int i = 0; i < 10000; ++i) { // 填充数据 }
- 如果需要插入多行,尽量一次性批量插入,而不是逐行插入。例如,先收集所有需要插入的数据,然后调用一次
-
使用模型/视图架构:
- 如果需要频繁进行插入、删除等操作,考虑直接使用
QAbstractTableModel
或其子类QStandardItemModel
,而不是QTableWidget
。QAbstractTableModel
提供了更高的灵活性和性能优化空间,尤其是在处理大规模数据时。 - 对于超大数据集,推荐使用
QTableView
+ 自定义QAbstractTableModel
/QStandardItemModel
,通过虚拟化技术减少内存和渲染开销。例如:
class CustomTableModel : public QAbstractTableModel { // 实现 data()、rowCount()、columnCount() 等虚函数 }; QTableView *tableView = new QTableView; tableView->setModel(new CustomTableModel); // 或者 tableView->setModel(new QStandardItemModel);
- 如果需要频繁进行插入、删除等操作,考虑直接使用
-
延迟更新:
- 若必须头插,可先禁用视图更新(
setUpdatesEnabled(false)
),插入多行后再统一刷新界面:
tableWidget->setUpdatesEnabled(false); for (int i = 0; i < 100; ++i) { tableWidget->insertRow(0); // 批量头插 } tableWidget->setUpdatesEnabled(true);
- 在进行大量插入操作前,可以暂时禁用视图的更新,操作完成后再恢复。这可以通过
setUpdatesEnabled(false)
和setUpdatesEnabled(true)
实现,但需要注意处理好数据的一致性。
tableWidget->setUpdatesEnabled(false); // 执行批量插入操作 tableWidget->setUpdatesEnabled(true);
- 若必须头插,可先禁用视图更新(
-
懒加载技术:
- 仅在可见区域加载数据,结合滚动事件动态插入行(参考 QT 懒加载技术 的
UpdateAlarmList
实现):
void AlarmCenter::wheelEvent(QWheelEvent *event) { // 根据滚动条位置动态加载数据 int visibleRows = tableViewHeight / rowHeight; int startRow = currentRow - visibleRows / 2; UpdateAlarmList(startRow, visibleRows); }
- 仅在可见区域加载数据,结合滚动事件动态插入行(参考 QT 懒加载技术 的
-
性能对比总结:
指标 | 头插 | 尾插 |
---|---|---|
时间复杂度 | O (n)(数据移动) | O (1)(尾部追加) |
内存操作 | 高(频繁重新分配和复制) | 低(预分配或追加) |
界面渲染 | 高(全表重绘) | 低(仅扩展区域) |
适用场景 | 倒序插入少量数据 | 常规追加、大规模插入 |
总结
- 头插 (
insertRow(0)
):- 性能较低,尤其是在数据量较大时,由于需要在开头插入元素,导致所有现有元素需要移动,时间复杂度为 O(n)。
- 尾插 (
insertRow(rowCount())
):- 性能较高,在末尾插入元素通常为 O(1),即使有内存重新分配,也相对高效。
因此,在使用 QTableWidget::insertRow
时,尾插性能远优于头插,应优先考虑尾插操作,以获得更好的性能表现。如果业务逻辑确实需要频繁进行头插操作,建议重新评估设计,或者考虑使用更适合频繁插入删除操作的模型/视图架构(如 QAbstractTableModel
)配合自定义的数据结构优化性能。