Qt Model/View架构详解
重要程度 : ⭐⭐⭐⭐⭐
实战价值 : 处理复杂数据展示(表格、树形结构、列表)
学习目标 : 掌握Qt的Model/View设计模式,能够自定义Model和Delegate处理复杂数据展示需求
本篇要点: 了解和会使用Qt Model/View的一些高级特性。
📚 目录
第四部分:高级特性 (第9-11章)
第9章 排序与过滤(QSortFilterProxyModel)
- 9.1 QSortFilterProxyModel介绍
- 9.2 排序功能
- 9.3 过滤功能
- 9.4 代理模型的映射
第10章 拖放支持
- 10.1 Model/View中的拖放
- 10.2 模型中的拖放接口
- 10.3 拖放实战
第11章 高级主题
- 11.1 懒加载(Lazy Loading)
- 11.2 模型测试与调试
- 11.3 性能优化
- 11.4 多线程与Model/View
- 11.5 自定义代理模型
第9章 排序与过滤
QSortFilterProxyModel是Qt提供的代理模型,可以在不修改原始数据的情况下对视图中的数据进行排序和过滤。
9.1 QSortFilterProxyModel介绍
9.1.1 代理模型的概念
什么是代理模型:
代理模型(Proxy Model)是一个位于源模型(Source Model)和视图(View)之间的中间层,它不存储实际数据,而是对源模型的数据进行转换、过滤或排序后再展示给视图。
代理模式架构:
原始数据流:
┌────────────┐ ┌──────────┐
│ Model │ ───> │ View │
│ (源模型) │ │ (视图) │
└────────────┘ └──────────┘
使用代理模型:
┌────────────┐ ┌────────────────┐ ┌──────────┐
│ Model │ ───> │ Proxy Model │ ───> │ View │
│ (源模型) │ │ (代理模型) │ │ (视图) │
└────────────┘ └────────────────┘ └──────────┘
│
├─ 排序
├─ 过滤
└─ 转换
代理模型的优势:
- 不修改源数据 - 所有操作都在代理层完成
- 多视图支持 - 同一源模型可以有多个不同的代理
- 即时响应 - 源模型数据变化时自动更新
- 易于维护 - 业务逻辑和展示逻辑分离
常见的代理模型:
| 代理模型 | 用途 |
|---|---|
| QSortFilterProxyModel | 排序和过滤 |
| QIdentityProxyModel | 简单的1:1映射(用于子类化) |
| QAbstractProxyModel | 自定义代理模型的基类 |
9.1.2 QSortFilterProxyModel的作用
主要功能:
cpp
QSortFilterProxyModel proxyModel;
// 1. 排序功能
proxyModel.sort(0, Qt::AscendingOrder); // 按第0列升序排序
proxyModel.sort(1, Qt::DescendingOrder); // 按第1列降序排序
// 2. 过滤功能
proxyModel.setFilterRegularExpression("搜索关键词"); // 正则表达式过滤
proxyModel.setFilterKeyColumn(0); // 设置过滤的列
proxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive); // 忽略大小写
// 3. 自定义过滤
// 重写 filterAcceptsRow() 实现复杂的过滤逻辑
// 4. 自定义排序
// 重写 lessThan() 实现自定义排序规则
典型应用场景:
cpp
// 场景1:数据表格的搜索功能
// 用户在搜索框输入关键词,实时过滤表格内容
// 场景2:按列排序
// 点击表头时,对该列数据进行排序
// 场景3:多条件过滤
// 同时按多个条件筛选数据(如:价格范围 + 分类)
// 场景4:自定义排序
// 按自定义规则排序(如:中文拼音、数字大小、日期)
9.1.3 设置源模型和视图
基本设置步骤:
cpp
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// 1. 创建源模型
QStandardItemModel *sourceModel = new QStandardItemModel(5, 3);
sourceModel->setHorizontalHeaderLabels({"姓名", "年龄", "城市"});
// 添加数据
sourceModel->setItem(0, 0, new QStandardItem("张三"));
sourceModel->setItem(0, 1, new QStandardItem("25"));
sourceModel->setItem(0, 2, new QStandardItem("北京"));
sourceModel->setItem(1, 0, new QStandardItem("李四"));
sourceModel->setItem(1, 1, new QStandardItem("30"));
sourceModel->setItem(1, 2, new QStandardItem("上海"));
sourceModel->setItem(2, 0, new QStandardItem("王五"));
sourceModel->setItem(2, 1, new QStandardItem("22"));
sourceModel->setItem(2, 2, new QStandardItem("广州"));
// 2. 创建代理模型
QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel;
// 3. 设置源模型
proxyModel->setSourceModel(sourceModel);
// 4. 创建视图并设置代理模型
QTableView *view = new QTableView;
view->setModel(proxyModel); // 注意:视图使用代理模型,不是源模型
// 5. 启用排序
view->setSortingEnabled(true);
view->resize(500, 300);
view->show();
return app.exec();
}
关键点:
- 视图使用代理模型 -
view->setModel(proxyModel),而不是sourceModel - 代理模型使用源模型 -
proxyModel->setSourceModel(sourceModel) - 数据操作仍在源模型 - 添加、删除数据仍然通过
sourceModel进行
完整示例:带搜索和排序的表格:
cpp
#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QLineEdit>
#include <QTableView>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <QHeaderView>
class FilterableTableWidget : public QWidget {
Q_OBJECT
private:
QStandardItemModel *m_sourceModel;
QSortFilterProxyModel *m_proxyModel;
QTableView *m_view;
QLineEdit *m_searchBox;
public:
FilterableTableWidget(QWidget *parent = nullptr) : QWidget(parent) {
setupUI();
loadData();
connectSignals();
}
private:
void setupUI() {
// 搜索框
m_searchBox = new QLineEdit;
m_searchBox->setPlaceholderText("输入关键词搜索...");
// 源模型
m_sourceModel = new QStandardItemModel(this);
m_sourceModel->setHorizontalHeaderLabels({"姓名", "年龄", "城市", "职位"});
// 代理模型
m_proxyModel = new QSortFilterProxyModel(this);
m_proxyModel->setSourceModel(m_sourceModel);
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); // 忽略大小写
m_proxyModel->setFilterKeyColumn(-1); // -1表示搜索所有列
// 视图
m_view = new QTableView;
m_view->setModel(m_proxyModel); // 使用代理模型
m_view->setSortingEnabled(true); // 启用排序
m_view->setAlternatingRowColors(true);
m_view->horizontalHeader()->setStretchLastSection(true);
// 布局
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(m_searchBox);
layout->addWidget(m_view);
setLayout(layout);
resize(600, 400);
}
void loadData() {
// 添加示例数据
QList<QList<QString>> data = {
{"张三", "25", "北京", "工程师"},
{"李四", "30", "上海", "设计师"},
{"王五", "22", "广州", "产品经理"},
{"赵六", "28", "深圳", "工程师"},
{"孙七", "35", "杭州", "架构师"},
{"周八", "26", "成都", "测试工程师"},
{"吴九", "29", "武汉", "运维工程师"},
{"郑十", "31", "南京", "数据分析师"}
};
for (const QList<QString> &row : data) {
QList<QStandardItem*> items;
for (const QString &text : row) {
items << new QStandardItem(text);
}
m_sourceModel->appendRow(items);
}
}
void connectSignals() {
// 搜索框文本变化时,更新过滤
connect(m_searchBox, &QLineEdit::textChanged,
m_proxyModel, &QSortFilterProxyModel::setFilterRegularExpression);
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
FilterableTableWidget widget;
widget.setWindowTitle("可搜索和排序的表格");
widget.show();
return app.exec();
}
#include "main.moc"
多代理模型链:
可以将多个代理模型串联起来,实现更复杂的功能:
cpp
// 源模型
QStandardItemModel *sourceModel = new QStandardItemModel;
// 第一层代理:过滤
QSortFilterProxyModel *filterProxy = new QSortFilterProxyModel;
filterProxy->setSourceModel(sourceModel);
filterProxy->setFilterRegularExpression("关键词");
// 第二层代理:排序
QSortFilterProxyModel *sortProxy = new QSortFilterProxyModel;
sortProxy->setSourceModel(filterProxy); // 使用第一层代理作为源
sortProxy->sort(0, Qt::AscendingOrder);
// 视图使用最后一层代理
view->setModel(sortProxy);
// 数据流:
// sourceModel -> filterProxy -> sortProxy -> view
注意事项:
cpp
// ❌ 错误:视图直接使用源模型
view->setModel(sourceModel);
// ✓ 正确:视图使用代理模型
view->setModel(proxyModel);
// ❌ 错误:对代理模型添加数据
proxyModel->appendRow(...); // 代理模型通常不支持直接修改
// ✓ 正确:对源模型添加数据
sourceModel->appendRow(...); // 修改会自动反映到代理模型
// 获取数据时需要注意索引映射(将在9.4节详细讲解)
QModelIndex proxyIndex = view->currentIndex(); // 代理索引
QModelIndex sourceIndex = proxyModel->mapToSource(proxyIndex); // 映射到源索引
本节小结:
✅ 代理模型概念 - 数据转换的中间层
✅ QSortFilterProxyModel - Qt提供的排序和过滤代理
✅ 基本设置 - setSourceModel()和view->setModel()
✅ 完整示例 - 带搜索和排序的表格应用
关键要点:
- 代理模型不存储数据,只转换数据
- 视图使用代理模型,代理模型使用源模型
- 数据修改仍然在源模型上进行
- 代理模型会自动响应源模型的变化
- 可以串联多个代理模型实现复杂功能
- 代理模型的概念
- QSortFilterProxyModel的作用
- 设置源模型和视图
9.2 排序功能
QSortFilterProxyModel提供了强大的排序功能,可以对数据按指定规则进行排序。
9.2.1 启用排序
视图中启用排序:
cpp
QTableView *view = new QTableView;
view->setModel(proxyModel);
// 启用排序(点击表头排序)
view->setSortingEnabled(true);
// 用户点击表头时,会自动调用 proxyModel->sort(column, order)
程序化排序:
cpp
// 按第0列升序排序
proxyModel->sort(0, Qt::AscendingOrder);
// 按第1列降序排序
proxyModel->sort(1, Qt::DescendingOrder);
// 禁用排序(恢复原始顺序)
proxyModel->sort(-1);
排序顺序:
cpp
Qt::AscendingOrder // 升序(从小到大,A-Z)
Qt::DescendingOrder // 降序(从大到小,Z-A)
9.2.2 自定义排序规则:lessThan()
默认排序是基于字符串比较的,对于数字、日期等需要自定义排序规则。
重写lessThan()方法:
cpp
class CustomSortProxyModel : public QSortFilterProxyModel {
protected:
bool lessThan(const QModelIndex &left,
const QModelIndex &right) const override {
// left 和 right 是源模型的索引
// 获取数据
QVariant leftData = sourceModel()->data(left);
QVariant rightData = sourceModel()->data(right);
// 自定义比较逻辑
// 返回 true 表示 left < right
return leftData.toString() < rightData.toString();
}
};
数字排序:
cpp
class NumberSortProxyModel : public QSortFilterProxyModel {
protected:
bool lessThan(const QModelIndex &left,
const QModelIndex &right) const override {
// 检查是否是年龄列(假设年龄在第1列)
if (left.column() == 1) {
int leftAge = sourceModel()->data(left).toInt();
int rightAge = sourceModel()->data(right).toInt();
return leftAge < rightAge;
}
// 其他列使用默认排序
return QSortFilterProxyModel::lessThan(left, right);
}
};
9.2.3 多列排序
虽然QSortFilterProxyModel本身不直接支持多列排序,但可以通过自定义实现:
cpp
class MultiColumnSortProxyModel : public QSortFilterProxyModel {
private:
QList<int> m_sortColumns; // 排序列的优先级
QList<Qt::SortOrder> m_sortOrders;
public:
void setSortColumns(const QList<int> &columns,
const QList<Qt::SortOrder> &orders) {
m_sortColumns = columns;
m_sortOrders = orders;
invalidate(); // 重新排序
}
protected:
bool lessThan(const QModelIndex &left,
const QModelIndex &right) const override {
// 按优先级比较多列
for (int i = 0; i < m_sortColumns.size(); ++i) {
int column = m_sortColumns[i];
Qt::SortOrder order = m_sortOrders[i];
QModelIndex leftIdx = left.sibling(left.row(), column);
QModelIndex rightIdx = right.sibling(right.row(), column);
QVariant leftData = sourceModel()->data(leftIdx);
QVariant rightData = sourceModel()->data(rightIdx);
if (leftData != rightData) {
bool result = QSortFilterProxyModel::lessThan(leftIdx, rightIdx);
return (order == Qt::AscendingOrder) ? result : !result;
}
}
return false; // 完全相等
}
};
9.2.4 实战:自定义数字排序、日期排序
完整示例:实现智能排序。
cpp
class SmartSortProxyModel : public QSortFilterProxyModel {
public:
explicit SmartSortProxyModel(QObject *parent = nullptr)
: QSortFilterProxyModel(parent) {}
protected:
bool lessThan(const QModelIndex &left,
const QModelIndex &right) const override {
QVariant leftData = sourceModel()->data(left);
QVariant rightData = sourceModel()->data(right);
// 1. 数字排序
bool leftOk, rightOk;
double leftNumber = leftData.toDouble(&leftOk);
double rightNumber = rightData.toDouble(&rightOk);
if (leftOk && rightOk) {
return leftNumber < rightNumber;
}
// 2. 日期排序
QDate leftDate = QDate::fromString(leftData.toString(), "yyyy-MM-dd");
QDate rightDate = QDate::fromString(rightData.toString(), "yyyy-MM-dd");
if (leftDate.isValid() && rightDate.isValid()) {
return leftDate < rightDate;
}
// 3. 版本号排序(如"1.2.3")
if (isVersionString(leftData.toString()) &&
isVersionString(rightData.toString())) {
return compareVersions(leftData.toString(),
rightData.toString()) < 0;
}
// 4. 文件大小排序(如"1.5 MB")
qint64 leftSize = parseFileSize(leftData.toString());
qint64 rightSize = parseFileSize(rightData.toString());
if (leftSize >= 0 && rightSize >= 0) {
return leftSize < rightSize;
}
// 5. 默认字符串排序
return QSortFilterProxyModel::lessThan(left, right);
}
private:
bool isVersionString(const QString &str) const {
QRegularExpression re("^\\d+(\\.\\d+)*$");
return re.match(str).hasMatch();
}
int compareVersions(const QString &v1, const QString &v2) const {
QStringList parts1 = v1.split('.');
QStringList parts2 = v2.split('.');
int maxLen = qMax(parts1.size(), parts2.size());
for (int i = 0; i < maxLen; ++i) {
int num1 = (i < parts1.size()) ? parts1[i].toInt() : 0;
int num2 = (i < parts2.size()) ? parts2[i].toInt() : 0;
if (num1 != num2) {
return num1 - num2;
}
}
return 0;
}
qint64 parseFileSize(const QString &sizeStr) const {
QRegularExpression re("^([0-9.]+)\\s*(B|KB|MB|GB|TB)?$",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch match = re.match(sizeStr.trimmed());
if (!match.hasMatch()) {
return -1;
}
double size = match.captured(1).toDouble();
QString unit = match.captured(2).toUpper();
if (unit == "KB") size *= 1024;
else if (unit == "MB") size *= 1024 * 1024;
else if (unit == "GB") size *= 1024 * 1024 * 1024;
else if (unit == "TB") size *= 1024LL * 1024 * 1024 * 1024;
return static_cast<qint64>(size);
}
};
使用示例:
cpp
// 创建模型和数据
QStandardItemModel *model = new QStandardItemModel(4, 4);
model->setHorizontalHeaderLabels({"名称", "大小", "日期", "版本"});
model->setItem(0, 0, new QStandardItem("文件A"));
model->setItem(0, 1, new QStandardItem("1.5 MB"));
model->setItem(0, 2, new QStandardItem("2024-01-15"));
model->setItem(0, 3, new QStandardItem("1.2.3"));
model->setItem(1, 0, new QStandardItem("文件B"));
model->setItem(1, 1, new QStandardItem("500 KB"));
model->setItem(1, 2, new QStandardItem("2024-03-20"));
model->setItem(1, 3, new QStandardItem("2.0.1"));
model->setItem(2, 0, new QStandardItem("文件C"));
model->setItem(2, 1, new QStandardItem("2.1 GB"));
model->setItem(2, 2, new QStandardItem("2023-12-01"));
model->setItem(2, 3, new QStandardItem("1.10.5"));
// 使用智能排序代理
SmartSortProxyModel *proxyModel = new SmartSortProxyModel;
proxyModel->setSourceModel(model);
QTableView *view = new QTableView;
view->setModel(proxyModel);
view->setSortingEnabled(true);
本节小结:
✅ 启用排序 - setSortingEnabled()和sort()方法
✅ 自定义排序 - 重写lessThan()实现特殊排序规则
✅ 多列排序 - 按优先级比较多个列
✅ 智能排序 - 自动识别数字、日期、版本号、文件大小
关键要点:
- lessThan()接收的是源模型的索引
- 可以根据列号使用不同的排序规则
- 自动类型识别可以提升用户体验
- 调用invalidate()触发重新排序
- 启用排序
- 自定义排序规则:lessThan()
- 多列排序
- 实战:自定义数字排序、日期排序
9.3 过滤功能
QSortFilterProxyModel提供了灵活的过滤功能,可以根据各种条件筛选数据。
9.3.1 filterRegularExpression - 正则表达式过滤
基本使用:
cpp
QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel;
proxyModel->setSourceModel(sourceModel);
// 设置过滤正则表达式
proxyModel->setFilterRegularExpression("关键词");
// 忽略大小写
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
// 设置过滤列(-1表示所有列)
proxyModel->setFilterKeyColumn(-1);
正则表达式示例:
cpp
// 精确匹配
proxyModel->setFilterRegularExpression("^张三$");
// 包含关键词
proxyModel->setFilterRegularExpression("工程师");
// 以...开头
proxyModel->setFilterRegularExpression("^北京");
// 以...结尾
proxyModel->setFilterRegularExpression("com$");
// 多个关键词(OR)
proxyModel->setFilterRegularExpression("张三|李四|王五");
// 数字范围
proxyModel->setFilterRegularExpression("^[2-3][0-9]$"); // 20-39
9.3.2 filterKeyColumn - 过滤列设置
cpp
// 过滤第0列
proxyModel->setFilterKeyColumn(0);
// 过滤第1列
proxyModel->setFilterKeyColumn(1);
// 过滤所有列
proxyModel->setFilterKeyColumn(-1);
实时搜索示例:
cpp
QLineEdit *searchBox = new QLineEdit;
searchBox->setPlaceholderText("搜索...");
// 实时过滤
connect(searchBox, &QLineEdit::textChanged, [=](const QString &text) {
proxyModel->setFilterRegularExpression(text);
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
});
9.3.3 filterAcceptsRow() - 自定义过滤逻辑
重写filterAcceptsRow():
cpp
class CustomFilterProxyModel : public QSortFilterProxyModel {
protected:
bool filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const override {
// sourceRow 是源模型中的行号
// sourceParent 是父索引(对于树形模型)
// 获取该行的数据
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
QString name = sourceModel()->data(index).toString();
// 自定义过滤条件
if (name.startsWith("张")) {
return true; // 显示
}
return false; // 隐藏
}
};
多条件过滤示例:
cpp
class MultiConditionFilterModel : public QSortFilterProxyModel {
private:
int m_minAge;
int m_maxAge;
QString m_city;
public:
void setAgeRange(int min, int max) {
m_minAge = min;
m_maxAge = max;
invalidateFilter(); // 重新过滤
}
void setCity(const QString &city) {
m_city = city;
invalidateFilter();
}
protected:
bool filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const override {
// 获取年龄(第1列)
QModelIndex ageIndex = sourceModel()->index(sourceRow, 1, sourceParent);
int age = sourceModel()->data(ageIndex).toInt();
// 年龄过滤
if (age < m_minAge || age > m_maxAge) {
return false;
}
// 城市过滤
if (!m_city.isEmpty()) {
QModelIndex cityIndex = sourceModel()->index(sourceRow, 2, sourceParent);
QString city = sourceModel()->data(cityIndex).toString();
if (city != m_city) {
return false;
}
}
// 通过所有过滤条件
return true;
}
};
9.3.4 实战:多条件组合过滤
完整示例:实现高级过滤功能。
cpp
class AdvancedFilterWidget : public QWidget {
Q_OBJECT
private:
QStandardItemModel *m_sourceModel;
MultiConditionFilterModel *m_proxyModel;
QTableView *m_view;
QLineEdit *m_nameFilter;
QSpinBox *m_minAge;
QSpinBox *m_maxAge;
QComboBox *m_cityFilter;
public:
AdvancedFilterWidget(QWidget *parent = nullptr) : QWidget(parent) {
setupUI();
loadData();
connectSignals();
}
private:
void setupUI() {
// 过滤控件
m_nameFilter = new QLineEdit;
m_nameFilter->setPlaceholderText("搜索姓名...");
m_minAge = new QSpinBox;
m_minAge->setRange(0, 100);
m_minAge->setValue(0);
m_maxAge = new QSpinBox;
m_maxAge->setRange(0, 100);
m_maxAge->setValue(100);
m_cityFilter = new QComboBox;
m_cityFilter->addItem("全部");
m_cityFilter->addItems({"北京", "上海", "广州", "深圳", "杭州"});
// 过滤条件布局
QHBoxLayout *filterLayout = new QHBoxLayout;
filterLayout->addWidget(new QLabel("姓名:"));
filterLayout->addWidget(m_nameFilter);
filterLayout->addWidget(new QLabel("年龄:"));
filterLayout->addWidget(m_minAge);
filterLayout->addWidget(new QLabel("-"));
filterLayout->addWidget(m_maxAge);
filterLayout->addWidget(new QLabel("城市:"));
filterLayout->addWidget(m_cityFilter);
filterLayout->addStretch();
// 模型和视图
m_sourceModel = new QStandardItemModel(this);
m_sourceModel->setHorizontalHeaderLabels({"姓名", "年龄", "城市", "职位"});
m_proxyModel = new MultiConditionFilterModel;
m_proxyModel->setSourceModel(m_sourceModel);
m_view = new QTableView;
m_view->setModel(m_proxyModel);
m_view->setSortingEnabled(true);
// 主布局
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(filterLayout);
mainLayout->addWidget(m_view);
setLayout(mainLayout);
resize(700, 500);
}
void loadData() {
QList<QList<QString>> data = {
{"张三", "25", "北京", "工程师"},
{"李四", "30", "上海", "设计师"},
{"王五", "22", "广州", "产品经理"},
{"赵六", "28", "深圳", "工程师"},
{"孙七", "35", "杭州", "架构师"},
{"周八", "26", "北京", "测试工程师"},
{"吴九", "29", "上海", "运维工程师"},
{"郑十", "31", "广州", "数据分析师"}
};
for (const QList<QString> &row : data) {
QList<QStandardItem*> items;
for (const QString &text : row) {
items << new QStandardItem(text);
}
m_sourceModel->appendRow(items);
}
}
void connectSignals() {
// 姓名过滤
connect(m_nameFilter, &QLineEdit::textChanged, [=](const QString &text) {
m_proxyModel->setFilterRegularExpression(text);
m_proxyModel->setFilterKeyColumn(0); // 只过滤姓名列
m_proxyModel->setFilterCaseSensitivity(Qt::Case Insensitive);
});
// 年龄范围过滤
connect(m_minAge, QOverload<int>::of(&QSpinBox::valueChanged),
this, &AdvancedFilterWidget::updateAgeFilter);
connect(m_maxAge, QOverload<int>::of(&QSpinBox::valueChanged),
this, &AdvancedFilterWidget::updateAgeFilter);
// 城市过滤
connect(m_cityFilter, QOverload<int>::of(&QComboBox::currentIndexChanged),
[=](int index) {
QString city = (index == 0) ? "" : m_cityFilter->currentText();
m_proxyModel->setCity(city);
});
}
void updateAgeFilter() {
m_proxyModel->setAgeRange(m_minAge->value(), m_maxAge->value());
}
};
9.3.5 实战:实时搜索框
简洁的实时搜索实现:
cpp
class SearchableTableWidget : public QWidget {
Q_OBJECT
private:
QLineEdit *m_searchBox;
QTableView *m_view;
QSortFilterProxyModel *m_proxyModel;
QLabel *m_resultLabel;
public:
SearchableTableWidget(QWidget *parent = nullptr) : QWidget(parent) {
// 搜索框
m_searchBox = new QLineEdit;
m_searchBox->setPlaceholderText("🔍 搜索...");
// 结果标签
m_resultLabel = new QLabel("显示全部数据");
// 模型
QStandardItemModel *sourceModel = new QStandardItemModel(this);
// ... 添加数据 ...
m_proxyModel = new QSortFilterProxyModel(this);
m_proxyModel->setSourceModel(sourceModel);
m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setFilterKeyColumn(-1); // 搜索所有列
// 视图
m_view = new QTableView;
m_view->setModel(m_proxyModel);
// 搜索逻辑
connect(m_searchBox, &QLineEdit::textChanged, [=](const QString &text) {
m_proxyModel->setFilterRegularExpression(text);
// 更新结果统计
int totalRows = sourceModel->rowCount();
int visibleRows = m_proxyModel->rowCount();
if (text.isEmpty()) {
m_resultLabel->setText(QString("显示全部 %1 条数据").arg(totalRows));
} else {
m_resultLabel->setText(QString("找到 %1 / %2 条匹配数据")
.arg(visibleRows).arg(totalRows));
}
});
// 布局
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(m_searchBox);
layout->addWidget(m_view);
layout->addWidget(m_resultLabel);
setLayout(layout);
}
};
本节小结:
✅ 正则表达式过滤 - setFilterRegularExpression()
✅ 指定列过滤 - setFilterKeyColumn()
✅ 自定义过滤 - 重写filterAcceptsRow()
✅ 多条件过滤 - 组合多个过滤条件
✅ 实时搜索 - 即时响应用户输入
关键要点:
- filterAcceptsRow()返回true表示显示该行
- 调用invalidateFilter()触发重新过滤
- 可以组合正则表达式和自定义过滤
- 过滤不影响源模型数据
- 实时搜索提升用户体验
- filterRegularExpression - 正则表达式过滤
- filterKeyColumn - 过滤列设置
- filterAcceptsRow() - 自定义过滤逻辑
- 实战:多条件组合过滤
- 实战:实时搜索框
9.4 代理模型的映射
使用代理模型时,需要在代理索引和源索引之间进行映射。
9.4.1 mapToSource() - 代理索引到源索引
用途:将代理模型中的索引映射到源模型中的索引。
cpp
// 获取视图中的当前索引(这是代理模型的索引)
QModelIndex proxyIndex = view->currentIndex();
// 映射到源模型的索引
QModelIndex sourceIndex = proxyModel->mapToSource(proxyIndex);
// 现在可以使用源索引操作源模型
sourceModel->setData(sourceIndex, newValue);
sourceModel->removeRow(sourceIndex.row());
9.4.2 mapFromSource() - 源索引到代理索引
用途:将源模型中的索引映射到代理模型中的索引。
cpp
// 源模型中的索引
QModelIndex sourceIndex = sourceModel->index(5, 0);
// 映射到代理模型的索引
QModelIndex proxyIndex = proxyModel->mapFromSource(sourceIndex);
// 检查该行是否在代理模型中可见
if (proxyIndex.isValid()) {
// 选中该项
view->setCurrentIndex(proxyIndex);
}
9.4.3 在过滤/排序后操作数据
完整示例:
cpp
class DataManagementWidget : public QWidget {
Q_OBJECT
private:
QStandardItemModel *m_sourceModel;
QSortFilterProxyModel *m_proxyModel;
QTableView *m_view;
public:
DataManagementWidget(QWidget *parent = nullptr) : QWidget(parent) {
setupUI();
loadData();
}
private:
void setupUI() {
// 模型
m_sourceModel = new QStandardItemModel(this);
m_sourceModel->setHorizontalHeaderLabels({"ID", "姓名", "分数"});
m_proxyModel = new QSortFilterProxyModel(this);
m_proxyModel->setSourceModel(m_sourceModel);
// 视图
m_view = new QTableView;
m_view->setModel(m_proxyModel);
m_view->setSortingEnabled(true);
// 按钮
QPushButton *deleteBtn = new QPushButton("删除选中行");
QPushButton *modifyBtn = new QPushButton("修改选中行");
QPushButton *selectBtn = new QPushButton("选择第3行(源模型)");
connect(deleteBtn, &QPushButton::clicked, this, &DataManagementWidget::deleteSelected);
connect(modifyBtn, &QPushButton::clicked, this, &DataManagementWidget::modifySelected);
connect(selectBtn, &QPushButton::clicked, this, &DataManagementWidget::selectSourceRow);
// 布局
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(m_view);
QHBoxLayout *btnLayout = new QHBoxLayout;
btnLayout->addWidget(deleteBtn);
btnLayout->addWidget(modifyBtn);
btnLayout->addWidget(selectBtn);
btnLayout->addStretch();
layout->addLayout(btnLayout);
setLayout(layout);
}
void loadData() {
for (int i = 0; i < 10; ++i) {
QList<QStandardItem*> row;
row << new QStandardItem(QString::number(i + 1));
row << new QStandardItem(QString("学生%1").arg(i + 1));
row << new QStandardItem(QString::number(60 + i * 5));
m_sourceModel->appendRow(row);
}
}
private slots:
void deleteSelected() {
QModelIndexList selected = m_view->selectionModel()->selectedRows();
if (selected.isEmpty()) {
QMessageBox::warning(this, "提示", "请先选择要删除的行");
return;
}
// ⚠️ 重要:需要映射到源模型索引
QList<int> sourceRows;
for (const QModelIndex &proxyIndex : selected) {
QModelIndex sourceIndex = m_proxyModel->mapToSource(proxyIndex);
sourceRows << sourceIndex.row();
}
// 从后向前删除(避免行号变化)
std::sort(sourceRows.begin(), sourceRows.end(), std::greater<int>());
for (int row : sourceRows) {
m_sourceModel->removeRow(row);
}
}
void modifySelected() {
QModelIndex proxyIndex = m_view->currentIndex();
if (!proxyIndex.isValid()) {
return;
}
// 映射到源索引
QModelIndex sourceIndex = m_proxyModel->mapToSource(proxyIndex);
// 修改数据(在源模型上)
bool ok;
QString newName = QInputDialog::getText(this, "修改", "新姓名:",
QLineEdit::Normal, "", &ok);
if (ok) {
m_sourceModel->setData(sourceIndex.sibling(sourceIndex.row(), 1),
newName);
}
}
void selectSourceRow() {
// 源模型中的第3行
QModelIndex sourceIndex = m_sourceModel->index(2, 0);
// 映射到代理索引
QModelIndex proxyIndex = m_proxyModel->mapFromSource(sourceIndex);
if (proxyIndex.isValid()) {
m_view->setCurrentIndex(proxyIndex);
QMessageBox::information(this, "成功", "已选中第3行");
} else {
QMessageBox::warning(this, "提示", "第3行被过滤,不可见");
}
}
};
常见错误和解决方案:
cpp
// ❌ 错误:直接使用代理索引操作源模型
QModelIndex proxyIndex = view->currentIndex();
sourceModel->removeRow(proxyIndex.row()); // 错误的行号!
// ✓ 正确:先映射再操作
QModelIndex proxyIndex = view->currentIndex();
QModelIndex sourceIndex = proxyModel->mapToSource(proxyIndex);
sourceModel->removeRow(sourceIndex.row());
// ❌ 错误:假设映射后的索引一定有效
QModelIndex sourceIndex = sourceModel->index(5, 0);
QModelIndex proxyIndex = proxyModel->mapFromSource(sourceIndex);
view->setCurrentIndex(proxyIndex); // 如果被过滤,proxyIndex无效!
// ✓ 正确:检查有效性
QModelIndex sourceIndex = sourceModel->index(5, 0);
QModelIndex proxyIndex = proxyModel->mapFromSource(sourceIndex);
if (proxyIndex.isValid()) {
view->setCurrentIndex(proxyIndex);
} else {
qDebug() << "该行已被过滤";
}
批量操作要点:
cpp
// 删除多行时,总是从后往前删除
QList<int> sourceRows;
for (const QModelIndex &proxyIndex : selectedIndexes) {
sourceRows << proxyModel->mapToSource(proxyIndex).row();
}
// 排序并倒序
std::sort(sourceRows.begin(), sourceRows.end(), std::greater<int>());
// 从后往前删除
for (int row : sourceRows) {
sourceModel->removeRow(row);
}
本节小结:
✅ mapToSource() - 代理索引→源索引
✅ mapFromSource() - 源索引→代理索引
✅ 实战应用 - 删除、修改、选择的正确姿势
✅ 常见陷阱 - 索引混用导致的错误
关键要点:
- 视图操作使用代理索引
- 数据操作使用源索引
- 必须进行索引映射
- 检查映射后索引的有效性
- 批量删除时从后往前
- mapToSource() - 代理索引到源索引
- mapFromSource() - 源索引到代理索引
- 在过滤/排序后操作数据
第9章总结:
🎉 第9章 排序与过滤 已全部完成!
本章涵盖了:
- ✅ 9.1 QSortFilterProxyModel介绍(代理模型概念、设置)
- ✅ 9.2 排序功能(自定义排序、智能排序)
- ✅ 9.3 过滤功能(正则表达式、自定义过滤、多条件)
- ✅ 9.4 代理模型的映射(索引转换、数据操作)
核心知识点:
- 代理模型是数据转换的中间层
- lessThan()控制排序规则
- filterAcceptsRow()控制过滤逻辑
- 必须进行索引映射才能正确操作数据
- 代理模型不存储数据,所有修改在源模型上进行
接下来可以继续学习第10章"拖放支持"!
第10章 拖放支持
Qt的Model/View架构内置了强大的拖放功能,可以轻松实现项的拖动、重排序和跨视图数据传输。
10.1 Model/View中的拖放
10.1.1 启用拖放:setDragEnabled()、setAcceptDrops()
基本启用:
cpp
QListView *view = new QListView;
// 1. 启用拖动
view->setDragEnabled(true);
// 2. 接受放置
view->setAcceptDrops(true);
// 3. 设置拖放模式
view->setDragDropMode(QAbstractItemView::InternalMove);
视图的拖放设置:
cpp
// 完整的拖放配置
QTableView *view = new QTableView;
// 启用拖动
view->setDragEnabled(true);
// 接受放置
view->setAcceptDrops(true);
// 显示放置指示器
view->setDropIndicatorShown(true);
// 设置默认放置动作
view->setDefaultDropAction(Qt::MoveAction);
// 设置选择模式(通常需要多选)
view->setSelectionMode(QAbstractItemView::ExtendedSelection);
view->setSelectionBehavior(QAbstractItemView::SelectRows);
10.1.2 setDropIndicatorShown()
放置指示器:
cpp
// 显示放置位置指示器
view->setDropIndicatorShown(true);
// 隐藏放置位置指示器
view->setDropIndicatorShown(false);
指示器样式:
指示器会在拖放时显示在目标位置,通常是一条线或高亮区域。
cpp
// 设置样式(通过样式表)
view->setStyleSheet(R"(
QAbstractItemView::indicator {
border: 2px dashed #007ACC;
background: rgba(0, 122, 204, 0.2);
}
)");
10.1.3 拖放模式:DragOnly、DropOnly、DragDrop
拖放模式枚举:
cpp
enum DragDropMode {
NoDragDrop, // 不支持拖放
DragOnly, // 只能拖出
DropOnly, // 只能放入
DragDrop, // 拖放都支持
InternalMove // 内部移动(同一视图内重排序)
};
使用示例:
cpp
// 只能拖出(作为数据源)
sourceView->setDragDropMode(QAbstractItemView::DragOnly);
// 只能放入(作为接收者)
targetView->setDragDropMode(QAbstractItemView::DropOnly);
// 完整的拖放(可以拖入拖出)
multiView->setDragDropMode(QAbstractItemView::DragDrop);
// 内部重排序(最常用)
listView->setDragDropMode(QAbstractItemView::InternalMove);
完整配置示例:
cpp
QListView *listView = new QListView;
// 启用拖放
listView->setDragEnabled(true);
listView->setAcceptDrops(true);
listView->setDropIndicatorShown(true);
// 设置为内部移动模式
listView->setDragDropMode(QAbstractItemView::InternalMove);
// 设置默认放置动作
listView->setDefaultDropAction(Qt::MoveAction);
// 使用标准模型
QStandardItemModel *model = new QStandardItemModel;
for (int i = 0; i < 10; ++i) {
model->appendRow(new QStandardItem(QString("项目 %1").arg(i + 1)));
}
listView->setModel(model);
本节小结:
✅ 启用拖放 - setDragEnabled()和setAcceptDrops()
✅ 放置指示器 - setDropIndicatorShown()显示拖放位置
✅ 拖放模式 - 5种模式满足不同需求
关键要点:
- 必须同时启用drag和drop才能拖放
- InternalMove适合单视图内重排序
- DragDrop适合跨视图数据传输
- 放置指示器提升用户体验
- 启用拖放:setDragEnabled()、setAcceptDrops()
- setDropIndicatorShown()
- 拖放模式:DragOnly、DropOnly、DragDrop
10.2 模型中的拖放接口
要实现自定义拖放行为,需要在模型中实现相关接口。
10.2.1 supportedDropActions() - 支持的放置动作
放置动作类型:
cpp
Qt::CopyAction // 复制
Qt::MoveAction // 移动
Qt::LinkAction // 链接
Qt::IgnoreAction // 忽略
重写方法:
cpp
class CustomModel : public QStandardItemModel {
public:
Qt::DropActions supportedDropActions() const override {
// 支持复制和移动
return Qt::CopyAction | Qt::MoveAction;
// 或者只支持移动
// return Qt::MoveAction;
}
};
10.2.2 mimeTypes() - MIME类型
定义支持的MIME类型:
cpp
class CustomModel : public QStandardItemModel {
public:
QStringList mimeTypes() const override {
QStringList types;
// 内部数据格式
types << "application/x-qabstractitemmodeldatalist";
// 自定义格式
types << "application/x-mytask";
// 纯文本
types << "text/plain";
return types;
}
};
10.2.3 mimeData() - 生成MIME数据
创建拖动数据:
cpp
class CustomModel : public QStandardItemModel {
public:
QMimeData* mimeData(const QModelIndexList &indexes) const override {
QMimeData *mimeData = new QMimeData;
// 1. 使用默认格式(推荐)
QByteArray encodedData;
QDataStream stream(&encodedData, QIODevice::WriteOnly);
for (const QModelIndex &index : indexes) {
if (index.isValid()) {
QString text = data(index, Qt::DisplayRole).toString();
stream << text;
}
}
mimeData->setData("application/x-qabstractitemmodeldatalist", encodedData);
// 2. 添加纯文本格式(方便跨应用)
QStringList textList;
for (const QModelIndex &index : indexes) {
textList << data(index, Qt::DisplayRole).toString();
}
mimeData->setText(textList.join("\n"));
return mimeData;
}
};
10.2.4 dropMimeData() - 接收放置数据
处理放置数据:
cpp
class CustomModel : public QStandardItemModel {
public:
bool dropMimeData(const QMimeData *data,
Qt::DropAction action,
int row, int column,
const QModelIndex &parent) override {
// 忽略动作
if (action == Qt::IgnoreAction) {
return true;
}
// 检查MIME类型
if (!data->hasFormat("application/x-qabstractitemmodeldatalist")) {
return false;
}
// 确定插入位置
int beginRow;
if (row != -1) {
beginRow = row;
} else if (parent.isValid()) {
beginRow = parent.row();
} else {
beginRow = rowCount(QModelIndex());
}
// 解码数据
QByteArray encodedData = data->data("application/x-qabstractitemmodeldatalist");
QDataStream stream(&encodedData, QIODevice::ReadOnly);
QStringList newItems;
while (!stream.atEnd()) {
QString text;
stream >> text;
newItems << text;
}
// 插入数据
insertRows(beginRow, newItems.size(), QModelIndex());
for (int i = 0; i < newItems.size(); ++i) {
QModelIndex idx = index(beginRow + i, column, QModelIndex());
setData(idx, newItems[i]);
}
return true;
}
};
10.2.5 flags()中的拖放标志
设置项标志:
cpp
class CustomModel : public QStandardItemModel {
public:
Qt::ItemFlags flags(const QModelIndex &index) const override {
Qt::ItemFlags defaultFlags = QStandardItemModel::flags(index);
if (index.isValid()) {
// 可拖动
return defaultFlags | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
} else {
// 根节点(或空白区域)可以接受放置
return defaultFlags | Qt::ItemIsDropEnabled;
}
}
};
标志说明:
cpp
Qt::ItemIsDragEnabled // 项可以被拖动
Qt::ItemIsDropEnabled // 项可以接受放置
本节小结:
✅ supportedDropActions() - 定义支持的放置动作
✅ mimeTypes() - 定义MIME类型
✅ mimeData() - 创建拖动数据
✅ dropMimeData() - 处理放置数据
✅ flags() - 设置拖放标志
关键要点:
- supportedDropActions()定义允许的操作
- mimeData()将模型数据编码为MIME数据
- dropMimeData()解码并插入数据
- flags()必须包含相应的拖放标志
- 使用标准MIME类型便于跨应用拖放
- supportedDropActions() - 支持的放置动作
- mimeTypes() - MIME类型
- mimeData() - 生成MIME数据
- dropMimeData() - 接收放置数据
- flags()中的拖放标志
10.3 拖放实战
10.3.1 实战:列表项重排序
完整实现一个可拖放重排序的列表。
cpp
#include <QApplication>
#include <QListView>
#include <QStandardItemModel>
#include <QVBoxLayout>
#include <QWidget>
#include <QPushButton>
class DraggableListWidget : public QWidget {
Q_OBJECT
private:
QListView *m_listView;
QStandardItemModel *m_model;
public:
DraggableListWidget(QWidget *parent = nullptr) : QWidget(parent) {
setupUI();
loadData();
setWindowTitle("可拖放重排序的列表");
resize(400, 500);
}
private:
void setupUI() {
// 创建模型
m_model = new QStandardItemModel(this);
// 创建视图
m_listView = new QListView;
m_listView->setModel(m_model);
// 启用拖放
m_listView->setDragEnabled(true);
m_listView->setAcceptDrops(true);
m_listView->setDropIndicatorShown(true);
m_listView->setDragDropMode(QAbstractItemView::InternalMove);
m_listView->setDefaultDropAction(Qt::MoveAction);
// 设置选择模式
m_listView->setSelectionMode(QAbstractItemView::ExtendedSelection);
// 添加按钮
QPushButton *addBtn = new QPushButton("添加项");
QPushButton *removeBtn = new QPushButton("删除选中项");
connect(addBtn, &QPushButton::clicked, this, &DraggableListWidget::addItem);
connect(removeBtn, &QPushButton::clicked, this, &DraggableListWidget::removeSelected);
// 布局
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(m_listView);
QHBoxLayout *btnLayout = new QHBoxLayout;
btnLayout->addWidget(addBtn);
btnLayout->addWidget(removeBtn);
btnLayout->addStretch();
layout->addLayout(btnLayout);
setLayout(layout);
}
void loadData() {
QStringList items = {
"📝 任务1:完成文档",
"💼 任务2:代码审查",
"🔧 任务3:修复Bug",
"📊 任务4:数据分析",
"✉ 任务5:回复邮件",
"📞 任务6:客户会议"
};
for (const QString &text : items) {
QStandardItem *item = new QStandardItem(text);
item->setFlags(item->flags() | Qt::ItemIsDragEnabled);
m_model->appendRow(item);
}
}
private slots:
void addItem() {
int count = m_model->rowCount() + 1;
QStandardItem *item = new QStandardItem(
QString("新任务%1").arg(count));
item->setFlags(item->flags() | Qt::ItemIsDragEnabled);
m_model->appendRow(item);
}
void removeSelected() {
QModelIndexList selected = m_listView->selectionModel()->selectedIndexes();
// 从后往前删除
QList<int> rows;
for (const QModelIndex &index : selected) {
rows << index.row();
}
std::sort(rows.begin(), rows.end(), std::greater<int>());
for (int row : rows) {
m_model->removeRow(row);
}
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
DraggableListWidget widget;
widget.show();
return app.exec();
}
#include "main.moc"
10.3.2 实战:树形节点拖放
实现树形结构的拖放,支持节点移动和层级调整。
cpp
class TreeDragDropWidget : public QWidget {
Q_OBJECT
private:
QTreeView *m_treeView;
QStandardItemModel *m_model;
public:
TreeDragDropWidget(QWidget *parent = nullptr) : QWidget(parent) {
setupUI();
loadTreeData();
setWindowTitle("可拖放的树形结构");
resize(500, 600);
}
private:
void setupUI() {
// 创建模型
m_model = new QStandardItemModel(this);
m_model->setHorizontalHeaderLabels({"项目结构"});
// 创建树视图
m_treeView = new QTreeView;
m_treeView->setModel(m_model);
// 启用拖放
m_treeView->setDragEnabled(true);
m_treeView->setAcceptDrops(true);
m_treeView->setDropIndicatorShown(true);
m_treeView->setDragDropMode(QAbstractItemView::InternalMove);
// 设置选择模式
m_treeView->setSelectionMode(QAbstractItemView::ExtendedSelection);
// 展开所有节点
m_treeView->expandAll();
// 布局
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(m_treeView);
setLayout(layout);
}
void loadTreeData() {
// 创建项目结构
QStandardItem *rootItem = m_model->invisibleRootItem();
// 第一层:项目
QStandardItem *project1 = createDraggableItem("📁 项目A");
QStandardItem *project2 = createDraggableItem("📁 项目B");
// 第二层:模块
QStandardItem *moduleA1 = createDraggableItem("📂 模块A1");
QStandardItem *moduleA2 = createDraggableItem("📂 模块A2");
QStandardItem *moduleB1 = createDraggableItem("📂 模块B1");
// 第三层:文件
moduleA1->appendRow(createDraggableItem("📄 文件1.cpp"));
moduleA1->appendRow(createDraggableItem("📄 文件2.cpp"));
moduleA2->appendRow(createDraggableItem("📄 文件3.h"));
moduleB1->appendRow(createDraggableItem("📄 文件4.cpp"));
// 组装树结构
project1->appendRow(moduleA1);
project1->appendRow(moduleA2);
project2->appendRow(moduleB1);
rootItem->appendRow(project1);
rootItem->appendRow(project2);
}
QStandardItem* createDraggableItem(const QString &text) {
QStandardItem *item = new QStandardItem(text);
// 设置为可拖放
item->setFlags(item->flags() | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
return item;
}
};
10.3.3 实战:跨视图拖放
实现两个视图之间的数据传输。
cpp
class CrossViewDragDropWidget : public QWidget {
Q_OBJECT
private:
QListView *m_sourceView;
QListView *m_targetView;
QStandardItemModel *m_sourceModel;
QStandardItemModel *m_targetModel;
public:
CrossViewDragDropWidget(QWidget *parent = nullptr) : QWidget(parent) {
setupUI();
loadData();
setWindowTitle("跨视图拖放");
resize(700, 400);
}
private:
void setupUI() {
// 源模型和视图
m_sourceModel = new QStandardItemModel(this);
m_sourceView = new QListView;
m_sourceView->setModel(m_sourceModel);
// 源视图:只能拖出
m_sourceView->setDragEnabled(true);
m_sourceView->setDragDropMode(QAbstractItemView::DragOnly);
m_sourceView->setSelectionMode(QAbstractItemView::ExtendedSelection);
// 目标模型和视图
m_targetModel = new QStandardItemModel(this);
m_targetView = new QListView;
m_targetView->setModel(m_targetModel);
// 目标视图:只能放入
m_targetView->setAcceptDrops(true);
m_targetView->setDropIndicatorShown(true);
m_targetView->setDragDropMode(QAbstractItemView::DropOnly);
m_targetView->setDefaultDropAction(Qt::CopyAction); // 复制而不是移动
// 标签
QLabel *sourceLabel = new QLabel("可用项(拖动到右侧)");
QLabel *targetLabel = new QLabel("已选择项");
// 布局
QVBoxLayout *sourceLayout = new QVBoxLayout;
sourceLayout->addWidget(sourceLabel);
sourceLayout->addWidget(m_sourceView);
QVBoxLayout *targetLayout = new QVBoxLayout;
targetLayout->addWidget(targetLabel);
targetLayout->addWidget(m_targetView);
QHBoxLayout *mainLayout = new QHBoxLayout;
mainLayout->addLayout(sourceLayout);
mainLayout->addWidget(createSeparator());
mainLayout->addLayout(targetLayout);
setLayout(mainLayout);
}
void loadData() {
// 加载源数据
QStringList sourceItems = {
"🍎 苹果",
"🍌 香蕉",
"🍊 橙子",
"🍇 葡萄",
"🍓 草莓",
"🥝 猕猴桃",
"🍑 桃子",
"🍒 樱桃"
};
for (const QString &text : sourceItems) {
QStandardItem *item = new QStandardItem(text);
item->setFlags(item->flags() | Qt::ItemIsDragEnabled);
m_sourceModel->appendRow(item);
}
// 目标初始为空
m_targetModel->setHorizontalHeaderLabels({"已选择"});
}
QFrame* createSeparator() {
QFrame *line = new QFrame;
line->setFrameShape(QFrame::VLine);
line->setFrameShadow(QFrame::Sunken);
return line;
}
};
自定义拖放行为示例:
cpp
class CustomDragDropModel : public QStandardItemModel {
public:
explicit CustomDragDropModel(QObject *parent = nullptr)
: QStandardItemModel(parent) {}
// 支持复制和移动
Qt::DropActions supportedDropActions() const override {
return Qt::CopyAction | Qt::MoveAction;
}
// 自定义MIME数据
QMimeData* mimeData(const QModelIndexList &indexes) const override {
QMimeData *mimeData = QStandardItemModel::mimeData(indexes);
// 添加纯文本格式(可以拖到其他应用)
QStringList textList;
for (const QModelIndex &index : indexes) {
textList << data(index, Qt::DisplayRole).toString();
}
mimeData->setText(textList.join(", "));
return mimeData;
}
// 自定义放置处理
bool dropMimeData(const QMimeData *data,
Qt::DropAction action,
int row, int column,
const QModelIndex &parent) override {
// 记录日志
qDebug() << "Drop action:" << action;
qDebug() << "Drop position:" << row << column;
// 调用默认实现
return QStandardItemModel::dropMimeData(data, action, row, column, parent);
}
};
本节小结:
✅ 列表重排序 - InternalMove模式的典型应用
✅ 树形拖放 - 支持层级结构调整
✅ 跨视图拖放 - DragOnly和DropOnly配合使用
✅ 自定义拖放 - 重写模型方法实现特殊需求
关键要点:
- 列表重排序使用InternalMove模式
- 树形拖放需要设置ItemIsDropEnabled标志
- 跨视图拖放通常使用CopyAction
- 从后往前删除避免索引错乱
- 可以通过mimeData()支持跨应用拖放
- 实战:列表项重排序
- 实战:树形节点拖放
- 实战:跨视图拖放
第10章总结:
🎉 第10章 拖放支持 已全部完成!
本章涵盖了:
- ✅ 10.1 Model/View中的拖放(启用、指示器、模式)
- ✅ 10.2 模型中的拖放接口(5个关键方法)
- ✅ 10.3 拖放实战(列表重排序、树形拖放、跨视图)
核心知识点:
- 启用拖放需要setDragEnabled()和setAcceptDrops()
- DragDropMode控制拖放行为
- 模型需要实现supportedDropActions()等接口
- flags()必须包含拖放标志
- InternalMove适合单视图重排序
接下来可以继续学习第11章"高级主题"!
第11章 高级主题
本章涵盖Qt Model/View架构的高级技术,包括性能优化、懒加载、多线程处理等实用技巧。
11.1 懒加载(Lazy Loading)
懒加载(也称按需加载)可以显著提升大数据集的性能,避免一次性加载所有数据。
11.1.1 canFetchMore() - 是否可继续获取
方法签名:
cpp
virtual bool canFetchMore(const QModelIndex &parent) const;
作用:告诉视图是否还有更多数据可以加载。
基本实现:
cpp
class LazyLoadModel : public QAbstractListModel {
private:
QList<QString> m_displayedData; // 已显示的数据
QList<QString> m_fullData; // 完整数据集
int m_fetchSize = 50; // 每次加载的数量
public:
bool canFetchMore(const QModelIndex &parent) const override {
if (parent.isValid()) {
return false; // 对于列表模型,只处理根项
}
// 检查是否还有未显示的数据
return m_displayedData.size() < m_fullData.size();
}
};
11.1.2 fetchMore() - 获取更多数据
方法签名:
cpp
virtual void fetchMore(const QModelIndex &parent);
作用:加载更多数据到模型中。
基本实现:
cpp
class LazyLoadModel : public QAbstractListModel {
public:
void fetchMore(const QModelIndex &parent) override {
if (parent.isValid()) {
return;
}
int currentSize = m_displayedData.size();
int remainingSize = m_fullData.size() - currentSize;
int fetchCount = qMin(m_fetchSize, remainingSize);
if (fetchCount <= 0) {
return;
}
// 通知视图将要插入行
beginInsertRows(QModelIndex(), currentSize, currentSize + fetchCount - 1);
// 加载数据
for (int i = 0; i < fetchCount; ++i) {
m_displayedData.append(m_fullData[currentSize + i]);
}
// 完成插入
endInsertRows();
}
};
11.1.3 实战:大数据集的分批加载
完整实现一个支持懒加载的列表模型。
cpp
#include <QApplication>
#include <QListView>
#include <QAbstractListModel>
#include <QVBoxLayout>
#include <QWidget>
#include <QLabel>
#include <QPushButton>
class LazyLoadListModel : public QAbstractListModel {
Q_OBJECT
private:
QStringList m_displayedData; // 已显示的数据
QStringList m_fullData; // 完整数据集(模拟大数据)
int m_fetchSize; // 每次加载的数量
public:
explicit LazyLoadListModel(int totalSize = 10000, int fetchSize = 100,
QObject *parent = nullptr)
: QAbstractListModel(parent), m_fetchSize(fetchSize) {
// 模拟大数据集
for (int i = 0; i < totalSize; ++i) {
m_fullData << QString("项目 #%1 - 数据内容...").arg(i + 1);
}
// 初始加载第一批数据
fetchMore(QModelIndex());
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
if (parent.isValid()) {
return 0;
}
return m_displayedData.size();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.row() >= m_displayedData.size()) {
return QVariant();
}
if (role == Qt::DisplayRole) {
return m_displayedData.at(index.row());
}
return QVariant();
}
bool canFetchMore(const QModelIndex &parent) const override {
if (parent.isValid()) {
return false;
}
return m_displayedData.size() < m_fullData.size();
}
void fetchMore(const QModelIndex &parent) override {
if (parent.isValid()) {
return;
}
int currentSize = m_displayedData.size();
int remainingSize = m_fullData.size() - currentSize;
int fetchCount = qMin(m_fetchSize, remainingSize);
if (fetchCount <= 0) {
return;
}
qDebug() << "Fetching more data:"
<< "current =" << currentSize
<< "fetching =" << fetchCount
<< "total =" << m_fullData.size();
beginInsertRows(QModelIndex(), currentSize, currentSize + fetchCount - 1);
for (int i = 0; i < fetchCount; ++i) {
m_displayedData.append(m_fullData[currentSize + i]);
}
endInsertRows();
emit dataLoadingProgress(m_displayedData.size(), m_fullData.size());
}
int totalDataSize() const {
return m_fullData.size();
}
signals:
void dataLoadingProgress(int loaded, int total);
};
class LazyLoadDemoWidget : public QWidget {
Q_OBJECT
private:
LazyLoadListModel *m_model;
QListView *m_listView;
QLabel *m_statusLabel;
public:
LazyLoadDemoWidget(QWidget *parent = nullptr) : QWidget(parent) {
// 创建模型(10000条数据,每次加载100条)
m_model = new LazyLoadListModel(10000, 100, this);
// 状态标签
m_statusLabel = new QLabel;
updateStatus();
// 创建视图
m_listView = new QListView;
m_listView->setModel(m_model);
// 连接信号
connect(m_model, &LazyLoadListModel::dataLoadingProgress,
this, &LazyLoadDemoWidget::updateStatus);
// 手动加载按钮
QPushButton *loadMoreBtn = new QPushButton("手动加载更多");
connect(loadMoreBtn, &QPushButton::clicked, [this]() {
m_model->fetchMore(QModelIndex());
});
// 布局
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(m_statusLabel);
layout->addWidget(m_listView);
layout->addWidget(loadMoreBtn);
setLayout(layout);
setWindowTitle("懒加载示例");
resize(600, 500);
}
private slots:
void updateStatus() {
int loaded = m_model->rowCount();
int total = m_model->totalDataSize();
double percent = (loaded * 100.0) / total;
m_statusLabel->setText(QString("已加载: %1 / %2 (%3%)")
.arg(loaded)
.arg(total)
.arg(percent, 0, 'f', 1));
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
LazyLoadDemoWidget widget;
widget.show();
return app.exec();
}
#include "main.moc"
懒加载的触发时机:
视图会在以下情况自动调用fetchMore():
- 滚动到底部时
- 显示区域需要更多数据时
- 调用setRootIndex()时
本节小结:
✅ canFetchMore() - 判断是否还有数据可加载
✅ fetchMore() - 执行数据加载
✅ 实战应用 - 大数据集的分批加载
关键要点:
- 使用beginInsertRows()和endInsertRows()通知视图
- 视图会自动调用fetchMore()
- 适合处理大数据集(数据库查询、文件加载等)
- 显著提升初始加载速度
- canFetchMore() - 是否可继续获取
- fetchMore() - 获取更多数据
- 实战:大数据集的分批加载
11.2 模型测试与调试
11.2.1 QAbstractItemModelTester
Qt提供了专门的测试工具来验证模型的正确性。
cpp
#include <QAbstractItemModelTester>
// 在构造函数或初始化代码中
MyCustomModel *model = new MyCustomModel;
// 创建测试器(会自动检查模型的各种操作)
QAbstractItemModelTester *tester = new QAbstractItemModelTester(
model,
QAbstractItemModelTester::FailureReportingMode::QtTest,
this
);
// 测试器会在以下情况报告错误:
// - index()返回的索引无效
// - parent()返回不一致
// - rowCount/columnCount返回负数
// - data()返回的数据类型不一致
// - 插入/删除操作没有正确发送信号
// - 等等...
测试模式:
cpp
// 1. Warning模式(只警告,不中断)
QAbstractItemModelTester::FailureReportingMode::Warning
// 2. QtTest模式(使用Qt Test框架)
QAbstractItemModelTester::FailureReportingMode::QtTest
// 3. Fatal模式(发现错误立即终止)
QAbstractItemModelTester::FailureReportingMode::Fatal
11.2.2 模型的单元测试
使用Qt Test框架对模型进行单元测试。
cpp
#include <QtTest/QtTest>
#include "mycustommodel.h"
class TestMyCustomModel : public QObject {
Q_OBJECT
private slots:
void initTestCase() {
// 测试前的初始化
}
void testRowCount() {
MyCustomModel model;
// 初始应该为空
QCOMPARE(model.rowCount(), 0);
// 添加数据后
model.addItem("Test Item");
QCOMPARE(model.rowCount(), 1);
}
void testData() {
MyCustomModel model;
model.addItem("Test");
QModelIndex index = model.index(0, 0);
QVERIFY(index.isValid());
QString data = model.data(index, Qt::DisplayRole).toString();
QCOMPARE(data, QString("Test"));
}
void testInsertRows() {
MyCustomModel model;
// 监听信号
QSignalSpy spy(&model, &MyCustomModel::rowsInserted);
model.insertRows(0, 2);
// 验证信号被发送
QCOMPARE(spy.count(), 1);
QCOMPARE(model.rowCount(), 2);
}
void testRemoveRows() {
MyCustomModel model;
model.addItem("Item 1");
model.addItem("Item 2");
QCOMPARE(model.rowCount(), 2);
model.removeRows(0, 1);
QCOMPARE(model.rowCount(), 1);
}
void cleanupTestCase() {
// 测试后的清理
}
};
QTEST_MAIN(TestMyCustomModel)
#include "test_mycustommodel.moc"
11.2.3 常见错误和调试技巧
常见错误:
cpp
// ❌ 错误1:忘记调用begin/endInsertRows
void addItem(const QString &text) {
m_data.append(text); // 直接添加,没有通知视图
}
// ✓ 正确
void addItem(const QString &text) {
int row = m_data.size();
beginInsertRows(QModelIndex(), row, row);
m_data.append(text);
endInsertRows();
}
// ❌ 错误2:index()返回无效索引但没有检查
QVariant data(const QModelIndex &index, int role) const override {
return m_data.at(index.row()); // 可能越界!
}
// ✓ 正确
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid() || index.row() >= m_data.size()) {
return QVariant();
}
return m_data.at(index.row());
}
// ❌ 错误3:parent()实现不一致
QModelIndex parent(const QModelIndex &child) const override {
// 列表模型应该总是返回无效索引
return child.parent(); // 错误!
}
// ✓ 正确
QModelIndex parent(const QModelIndex &child) const override {
Q_UNUSED(child);
return QModelIndex(); // 列表模型没有parent
}
调试技巧:
cpp
// 1. 添加调试输出
QVariant data(const QModelIndex &index, int role) const override {
qDebug() << "data() called:" << index.row() << index.column() << role;
// ...
}
// 2. 验证索引
QModelIndex index(int row, int column,
const QModelIndex &parent) const override {
if (row < 0 || row >= rowCount(parent) ||
column < 0 || column >= columnCount(parent)) {
qWarning() << "Invalid index requested:" << row << column;
return QModelIndex();
}
// ...
}
// 3. 使用Q_ASSERT检查前提条件
void setData(const QModelIndex &index, const QVariant &value, int role) {
Q_ASSERT(index.isValid());
Q_ASSERT(index.row() < m_data.size());
// ...
}
本节小结:
✅ QAbstractItemModelTester - 自动化模型测试
✅ 单元测试 - 使用Qt Test框架
✅ 常见错误 - 避免典型陷阱
✅ 调试技巧 - 快速定位问题
关键要点:
- 始终使用QAbstractItemModelTester验证模型
- 编写单元测试覆盖关键功能
- 检查索引有效性
- 正确使用begin/end通知方法
- 使用Q_ASSERT检查前提条件
- QAbstractItemModelTester
- 模型的单元测试
- 常见错误和调试技巧
11.3 性能优化
11.3.1 避免不必要的dataChanged信号
问题:
cpp
// ❌ 错误:频繁发送信号
for (int i = 0; i < 1000; ++i) {
QModelIndex index = this->index(i, 0);
setData(index, newValue); // 每次都发送dataChanged信号
}
优化:
cpp
// ✓ 正确:批量更新后发送一次信号
for (int i = 0; i < 1000; ++i) {
m_data[i] = newValue; // 直接修改数据
}
// 发送一次信号通知范围变化
QModelIndex topLeft = index(0, 0);
QModelIndex bottomRight = index(999, 0);
emit dataChanged(topLeft, bottomRight);
11.3.2 批量操作的优化
插入多行:
cpp
// ❌ 低效:逐个插入
for (int i = 0; i < 100; ++i) {
insertRow(m_data.size()); // 100次通知
}
// ✓ 高效:批量插入
beginInsertRows(QModelIndex(), m_data.size(), m_data.size() + 99);
for (int i = 0; i < 100; ++i) {
m_data.append(newData);
}
endInsertRows(); // 只通知一次
删除多行:
cpp
// ✓ 从后往前删除(避免索引变化)
QList<int> rowsToDelete = {5, 10, 15, 20};
std::sort(rowsToDelete.begin(), rowsToDelete.end(), std::greater<int>());
for (int row : rowsToDelete) {
beginRemoveRows(QModelIndex(), row, row);
m_data.removeAt(row);
endRemoveRows();
}
11.3.3 大数据量的处理策略
策略1:懒加载(见11.1节)
策略2:数据分页:
cpp
class PagedModel : public QAbstractTableModel {
private:
int m_pageSize = 100;
int m_currentPage = 0;
QList<DataRow> m_currentPageData;
public:
void setPage(int page) {
beginResetModel();
m_currentPage = page;
loadPageData(page);
endResetModel();
}
private:
void loadPageData(int page) {
int offset = page * m_pageSize;
// 从数据库或文件加载该页数据
m_currentPageData = database->fetchRows(offset, m_pageSize);
}
};
策略3:虚拟化:
cpp
// 不存储所有数据,按需计算
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DisplayRole) {
// 按需计算/生成数据
return calculateDataForIndex(index.row(), index.column());
}
return QVariant();
}
11.3.4 索引缓存
问题:频繁创建相同的索引
cpp
// ❌ 低效:重复创建索引
for (int i = 0; i < 1000; ++i) {
QModelIndex idx = model->index(5, 3); // 每次都创建
// 使用idx...
}
优化:
cpp
// ✓ 高效:缓存索引
QModelIndex cachedIndex = model->index(5, 3);
for (int i = 0; i < 1000; ++i) {
// 使用cachedIndex...
}
持久化索引:
cpp
// 对于可能变化的模型,使用QPersistentModelIndex
QPersistentModelIndex persistentIdx = model->index(5, 3);
// 即使模型数据变化,persistentIdx也会自动更新
model->insertRow(0); // 在前面插入行
// persistentIdx现在指向第6行(自动调整)
本节小结:
✅ 避免频繁信号 - 批量更新后发送一次
✅ 批量操作 - 使用begin/end方法批量处理
✅ 大数据策略 - 懒加载、分页、虚拟化
✅ 索引缓存 - 避免重复创建索引
关键要点:
- 批量操作优于逐个操作
- 一次性发送dataChanged比多次发送高效
- 大数据集使用懒加载或分页
- 缓存频繁使用的索引
- 使用QPersistentModelIndex应对模型变化
- 避免不必要的dataChanged信号
- 批量操作的优化
- 大数据量的处理策略
- 索引缓存
11.4 多线程与Model/View
11.4.1 模型的线程安全问题
核心原则:Model/View组件不是线程安全的,必须在主线程(GUI线程)中使用。
问题示例:
cpp
// ❌ 错误:在工作线程中直接修改模型
QThread *worker = QThread::create([model]() {
model->insertRow(0); // 崩溃!不能在工作线程中调用
});
worker->start();
11.4.2 在线程中更新模型
正确方式1:使用信号槽:
cpp
class DataLoader : public QObject {
Q_OBJECT
public slots:
void loadData() {
// 在工作线程中执行耗时操作
QList<QString> data = loadFromDatabase();
// 通过信号发送到主线程
emit dataLoaded(data);
}
signals:
void dataLoaded(const QList<QString> &data);
};
// 在主线程中
DataLoader *loader = new DataLoader;
QThread *thread = new QThread;
loader->moveToThread(thread);
connect(loader, &DataLoader::dataLoaded, [model](const QList<QString> &data) {
// 这里在主线程执行
for (const QString &item : data) {
model->appendRow(new QStandardItem(item));
}
});
connect(thread, &QThread::started, loader, &DataLoader::loadData);
thread->start();
正确方式2:使用QMetaObject::invokeMethod:
cpp
// 在工作线程中
QThread *worker = QThread::create([model]() {
QList<QString> data = expensiveOperation();
// 在主线程中执行
QMetaObject::invokeMethod(model, [model, data]() {
for (const QString &item : data) {
model->appendRow(new QStandardItem(item));
}
}, Qt::QueuedConnection);
});
worker->start();
11.4.3 使用信号槽跨线程更新
完整示例:
cpp
class AsyncDataModel : public QStandardItemModel {
Q_OBJECT
public:
explicit AsyncDataModel(QObject *parent = nullptr)
: QStandardItemModel(parent) {
// 创建工作线程
m_workerThread = new QThread(this);
m_dataLoader = new DataLoader;
m_dataLoader->moveToThread(m_workerThread);
// 连接信号
connect(m_workerThread, &QThread::started,
m_dataLoader, &DataLoader::loadData);
connect(m_dataLoader, &DataLoader::dataReady,
this, &AsyncDataModel::onDataReady);
m_workerThread->start();
}
~AsyncDataModel() {
m_workerThread->quit();
m_workerThread->wait();
}
private slots:
void onDataReady(const QList<DataItem> &items) {
// 在主线程中更新模型
for (const DataItem &item : items) {
QStandardItem *stdItem = new QStandardItem(item.name);
appendRow(stdItem);
}
}
private:
QThread *m_workerThread;
DataLoader *m_dataLoader;
};
class DataLoader : public QObject {
Q_OBJECT
public slots:
void loadData() {
// 模拟耗时操作
QList<DataItem> items;
for (int i = 0; i < 1000; ++i) {
items << DataItem{QString("Item %1").arg(i)};
QThread::msleep(1); // 模拟延迟
}
emit dataReady(items);
}
signals:
void dataReady(const QList<DataItem> &items);
};
最佳实践:
cpp
// 1. 数据加载在工作线程
// 2. 模型更新在主线程
// 3. 使用信号槽通信
// 工作线程
void workerFunction() {
// 耗时操作
QVector<Record> records = database.fetchAll();
// 发送到主线程
emit dataFetched(records);
}
// 主线程
void onDataFetched(const QVector<Record> &records) {
// 更新模型(安全)
model->beginResetModel();
model->updateData(records);
model->endResetModel();
}
本节小结:
✅ 线程安全 - Model/View必须在主线程
✅ 跨线程更新 - 使用信号槽或invokeMethod
✅ 完整示例 - 异步数据加载模型
关键要点:
- 所有Model/View操作必须在主线程
- 耗时操作在工作线程执行
- 使用信号槽跨线程通信
- 不要在工作线程中直接操作模型
- Qt::QueuedConnection确保在主线程执行
- 模型的线程安全问题
- 在线程中更新模型
- 使用信号槽跨线程更新
11.5 自定义代理模型
11.5.1 QAbstractProxyModel
QAbstractProxyModel是所有代理模型的基类。
基本结构:
cpp
class CustomProxyModel : public QAbstractProxyModel {
public:
// 必须实现的方法
QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
};
11.5.2 QIdentityProxyModel
QIdentityProxyModel提供1:1映射,适合作为自定义代理的基类。
使用场景:
cpp
// 1. 修改显示数据但不改变结构
class DisplayModifierProxy : public QIdentityProxyModel {
public:
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DisplayRole) {
QVariant value = QIdentityProxyModel::data(index, role);
return "【" + value.toString() + "】"; // 添加装饰
}
return QIdentityProxyModel::data(index, role);
}
};
// 2. 添加额外的角色数据
class ExtraDataProxy : public QIdentityProxyModel {
public:
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::UserRole + 1) {
// 返回额外的计算数据
return calculateExtra(index);
}
return QIdentityProxyModel::data(index, role);
}
};
11.5.3 实战:自定义数据转换代理模型
单位转换代理:
cpp
class UnitConverterProxy : public QIdentityProxyModel {
Q_OBJECT
public:
enum Unit {
Meter,
Kilometer,
Mile
};
explicit UnitConverterProxy(QObject *parent = nullptr)
: QIdentityProxyModel(parent), m_currentUnit(Meter) {}
void setUnit(Unit unit) {
m_currentUnit = unit;
// 通知所有数据已变化
if (sourceModel()) {
emit dataChanged(index(0, 0),
index(rowCount() - 1, columnCount() - 1));
}
}
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DisplayRole && index.column() == 1) {
// 假设第1列是距离数据(米)
double meters = QIdentityProxyModel::data(index, role).toDouble();
double converted = convertDistance(meters);
return QString("%1 %2").arg(converted, 0, 'f', 2)
.arg(unitString());
}
return QIdentityProxyModel::data(index, role);
}
private:
Unit m_currentUnit;
double convertDistance(double meters) const {
switch (m_currentUnit) {
case Meter:
return meters;
case Kilometer:
return meters / 1000.0;
case Mile:
return meters / 1609.34;
}
return meters;
}
QString unitString() const {
switch (m_currentUnit) {
case Meter: return "m";
case Kilometer: return "km";
case Mile: return "mi";
}
return "m";
}
};
格式化代理:
cpp
class FormattingProxy : public QIdentityProxyModel {
public:
QVariant data(const QModelIndex &index, int role) const override {
QVariant value = QIdentityProxyModel::data(index, role);
if (role == Qt::DisplayRole) {
int column = index.column();
// 根据列格式化数据
if (column == 0) {
// 日期列
QDate date = value.toDate();
return date.toString("yyyy-MM-dd");
} else if (column == 1) {
// 货币列
double amount = value.toDouble();
return QString("¥%L1").arg(amount, 0, 'f', 2);
} else if (column == 2) {
// 百分比列
double percent = value.toDouble();
return QString("%1%").arg(percent, 0, 'f', 1);
}
}
return value;
}
};
本节小结:
✅ QAbstractProxyModel - 代理模型基类
✅ QIdentityProxyModel - 1:1映射代理
✅ 实战应用 - 单位转换、数据格式化
关键要点:
- QIdentityProxyModel简化代理实现
- 只需重写需要修改的方法
- 适合数据转换、格式化等场景
- 不改变模型结构,只改变显示
- 可以组合多个代理实现复杂功能
- QAbstractProxyModel
- QIdentityProxyModel
- 实战:自定义数据转换代理模型
第11章总结:
🎉 第11章 高级主题 已全部完成!
本章涵盖了:
- ✅ 11.1 懒加载(canFetchMore、fetchMore、大数据分批加载)
- ✅ 11.2 模型测试与调试(QAbstractItemModelTester、单元测试、常见错误)
- ✅ 11.3 性能优化(信号优化、批量操作、大数据策略、索引缓存)
- ✅ 11.4 多线程与Model/View(线程安全、跨线程更新)
- ✅ 11.5 自定义代理模型(QIdentityProxyModel、数据转换)
核心知识点:
- 懒加载显著提升大数据集性能
- QAbstractItemModelTester自动验证模型正确性
- 批量操作优于逐个操作
- Model/View必须在主线程使用
- QIdentityProxyModel简化代理开发
需要继续实战,可看12章Qt Model/View项目实战!