QT Model-View 框架完全指南
1. 核心概念与架构
1.1 Model-View-Delegate 架构
QT 的 Model-View 架构采用了经典的 MVC 设计模式,但做了一些调整,引入了 Delegate(代理)概念,形成了 Model-View-Delegate 架构:
- Model(模型):负责数据的存储和管理,提供数据访问接口
- View(视图):负责数据的展示,不直接管理数据
- Delegate(代理):负责数据的编辑和渲染,处理用户交互
1.2 数据模型层次
QT 提供了多种数据模型,按层次分为:
- QAbstractItemModel:所有模型的基类
- QAbstractListModel:列表数据模型的基类
- QAbstractTableModel:表格数据模型的基类
- QStandardItemModel:通用的项模型,可用于列表、表格和树形结构
- QFileSystemModel:文件系统模型
- QSqlTableModel:数据库表模型
- QSortFilterProxyModel:排序和过滤代理模型
1.3 信号槽机制
Model-View 架构通过信号槽机制实现数据与视图的通信:
- 模型数据变化时,通过
dataChanged()信号通知视图 - 模型结构变化时,通过
rowsInserted()、rowsRemoved()等信号通知视图 - 视图通过
setModel()方法与模型关联
2. 常用控件使用方法
2.1 QListView
功能:显示单列列表数据
使用方法:
// 创建模型
QStandardItemModel *model = new QStandardItemModel(this);
// 添加数据
for (int i = 0; i < 10; ++i) {
QStandardItem *item = new QStandardItem(QString("Item %1").arg(i));
model->appendRow(item);
}
// 创建视图并关联模型
QListView *listView = new QListView(this);
listView->setModel(model);
常见属性:
viewMode:设置视图模式(列表或图标)selectionMode:设置选择模式wordWrap:设置是否自动换行
2.2 QTableView
功能:显示表格数据
使用方法:
// 创建模型
QStandardItemModel *model = new QStandardItemModel(5, 3, this);
model->setHorizontalHeaderLabels({"Name", "Age", "Gender"});
// 添加数据
for (int row = 0; row < 5; ++row) {
for (int col = 0; col < 3; ++col) {
QStandardItem *item = new QStandardItem(QString("Row %1, Col %2").arg(row).arg(col));
model->setItem(row, col, item);
}
}
// 创建视图并关联模型
QTableView *tableView = new QTableView(this);
tableView->setModel(model);
常见属性:
showGrid:设置是否显示网格alternatingRowColors:设置是否使用交替行颜色sortingEnabled:设置是否启用排序
2.3 QTreeView
功能:显示树形结构数据
使用方法:
// 创建模型
QStandardItemModel *model = new QStandardItemModel(this);
QStandardItem *rootItem = model->invisibleRootItem();
// 添加数据
for (int i = 0; i < 3; ++i) {
QStandardItem *parentItem = new QStandardItem(QString("Parent %1").arg(i));
rootItem->appendRow(parentItem);
for (int j = 0; j < 2; ++j) {
QStandardItem *childItem = new QStandardItem(QString("Child %1-%2").arg(i).arg(j));
parentItem->appendRow(childItem);
}
}
// 创建视图并关联模型
QTreeView *treeView = new QTreeView(this);
treeView->setModel(model);
treeView->expandAll();
常见属性:
indentation:设置缩进距离expandsOnDoubleClick:设置是否双击展开headerHidden:设置是否隐藏表头
2.4 QColumnView
功能:显示多列层次数据
使用方法:
// 创建模型(与QTreeView相同)
QStandardItemModel *model = new QStandardItemModel(this);
QStandardItem *rootItem = model->invisibleRootItem();
// 添加数据
// ...(与QTreeView相同)
// 创建视图并关联模型
QColumnView *columnView = new QColumnView(this);
columnView->setModel(model);
2.5 QHeaderView
功能:管理视图的表头
使用方法:
// 获取表格视图的水平表头
QHeaderView *header = tableView->horizontalHeader();
// 设置表头属性
header->setSectionResizeMode(QHeaderView::Stretch); // 自动拉伸
header->setSectionsClickable(true); // 允许点击
header->setSortIndicator(0, Qt::AscendingOrder); // 设置排序指示器
2.6 代理控件
QItemDelegate:默认代理,提供基本的编辑功能
QStyledItemDelegate:基于样式的代理,支持更丰富的外观
自定义代理:
class CustomDelegate : public QStyledItemDelegate {
public:
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
// 创建自定义编辑器
QLineEdit *editor = new QLineEdit(parent);
return editor;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const override {
// 设置编辑器数据
QString value = index.model()->data(index, Qt::EditRole).toString();
QLineEdit *lineEdit = static_cast<QLineEdit*>(editor);
lineEdit->setText(value);
}
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
// 将编辑器数据写回模型
QLineEdit *lineEdit = static_cast<QLineEdit*>(editor);
QString value = lineEdit->text();
model->setData(index, value, Qt::EditRole);
}
};
// 使用自定义代理
tableView->setItemDelegate(new CustomDelegate());
3. 数据更新与性能优化
3.1 数据更新方法
单条数据更新:
// 更新单个数据项
model->setData(model->index(row, column), newValue, Qt::EditRole);
// 或者使用更底层的方法
model->beginResetModel();
// 更新数据
model->endResetModel();
批量数据更新:
// 批量更新前禁用信号
model->blockSignals(true);
// 执行批量更新
for (int i = 0; i < 1000; ++i) {
model->setData(model->index(i, 0), newValue);
}
// 重新启用信号并通知视图
model->blockSignals(false);
model->dataChanged(model->index(0, 0), model->index(999, 0));
3.2 大数据处理导致卡顿的原因
- 频繁的信号发射:每次数据变化都发射信号,导致视图频繁更新
- UI线程阻塞:大量数据处理在UI线程中执行,导致界面卡顿
- 过度渲染:视图尝试渲染所有数据,即使不可见
- 内存占用过高:加载大量数据到内存,导致系统资源不足
3.3 解决方法
3.3.1 批量更新
// 使用beginInsertRows和endInsertRows
model->beginInsertRows(QModelIndex(), model->rowCount(), model->rowCount() + 999);
for (int i = 0; i < 1000; ++i) {
// 添加数据
}
model->endInsertRows();
// 或者使用beginResetModel和endResetModel
model->beginResetModel();
// 大量数据操作
model->endResetModel();
3.3.2 惰性加载
// 自定义模型实现惰性加载
class LazyModel : public QAbstractListModel {
public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
// 返回总数据量,但实际只加载可见部分
return totalItems;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid())
return QVariant();
// 检查数据是否已加载
if (!dataLoaded.contains(index.row())) {
// 加载数据
loadData(index.row());
}
// 返回数据
return dataCache[index.row()];
}
private:
void loadData(int row) const {
// 模拟加载数据
dataCache[row] = QString("Item %1").arg(row);
dataLoaded.insert(row);
}
mutable QMap<int, QString> dataCache;
mutable QSet<int> dataLoaded;
int totalItems = 100000;
};
3.3.3 多线程处理
// 创建工作线程
QThread *workerThread = new QThread(this);
DataLoader *loader = new DataLoader();
loader->moveToThread(workerThread);
// 连接信号槽
connect(workerThread, &QThread::started, loader, &DataLoader::loadData);
connect(loader, &DataLoader::dataLoaded, this, &MainWindow::onDataLoaded);
connect(loader, &DataLoader::finished, workerThread, &QThread::quit);
connect(loader, &DataLoader::finished, loader, &DataLoader::deleteLater);
connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater);
// 启动线程
workerThread->start();
// 数据加载完成后的处理
void MainWindow::onDataLoaded(const QList<QString> &data) {
// 在UI线程中更新模型
model->beginResetModel();
// 更新数据
model->endResetModel();
}
3.3.4 缓存机制
// 实现缓存机制
class CachedModel : public QAbstractListModel {
public:
// ...
private:
QList<QString> dataCache;
int cacheSize = 100;
int currentStartIndex = 0;
void updateCache(int startIndex) {
// 更新缓存
dataCache.clear();
currentStartIndex = startIndex;
for (int i = 0; i < cacheSize && i + startIndex < totalItems; ++i) {
dataCache.append(QString("Item %1").arg(i + startIndex));
}
}
};
3.3.5 分页显示
// 实现分页模型
class PagedModel : public QAbstractListModel {
public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return pageSize;
}
void setPage(int page) {
currentPage = page;
beginResetModel();
loadPageData(page);
endResetModel();
}
private:
void loadPageData(int page) {
// 加载指定页的数据
pageData.clear();
int startIndex = page * pageSize;
for (int i = 0; i < pageSize && i + startIndex < totalItems; ++i) {
pageData.append(QString("Item %1").arg(i + startIndex));
}
}
int currentPage = 0;
int pageSize = 50;
int totalItems = 100000;
QList<QString> pageData;
};
3.3.6 虚拟滚动
// 使用QAbstractItemModel的canFetchMore和fetchMore方法
class VirtualModel : public QAbstractListModel {
public:
bool canFetchMore(const QModelIndex &parent) const override {
return fetchedCount < totalItems;
}
void fetchMore(const QModelIndex &parent) override {
int remaining = totalItems - fetchedCount;
int itemsToFetch = qMin(100, remaining);
beginInsertRows(QModelIndex(), fetchedCount, fetchedCount + itemsToFetch - 1);
for (int i = 0; i < itemsToFetch; ++i) {
data.append(QString("Item %1").arg(fetchedCount + i));
}
fetchedCount += itemsToFetch;
endInsertRows();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return fetchedCount;
}
private:
int totalItems = 100000;
int fetchedCount = 0;
QList<QString> data;
};
// 启用虚拟滚动
listView->setUniformItemSizes(true); // 提高性能
listView->setLayoutMode(QListView::Batched); // 批处理布局
4. 其他注意事项与最佳实践
4.1 性能优化
- 使用合适的模型:根据数据结构选择合适的模型,如列表数据使用QAbstractListModel
- 优化数据访问:实现高效的数据访问方法,避免不必要的计算
- 减少信号发射:批量操作时使用blockSignals或begin/end方法
- 使用代理模型:如QSortFilterProxyModel处理排序和过滤,避免直接修改原始模型
- 优化视图渲染:使用QStyledItemDelegate的paint方法时,避免复杂计算
4.2 内存管理
- 及时释放内存:不再使用的模型和视图应及时删除
- 避免内存泄漏:注意信号槽连接的生命周期,避免循环引用
- 合理使用缓存:缓存常用数据,但避免缓存过多导致内存占用过高
4.3 错误处理
- 数据验证:在setData方法中验证数据有效性
- 异常处理:使用try-catch处理可能的异常
- 错误反馈:向用户提供清晰的错误信息
4.4 测试与调试
- 单元测试:为模型和代理编写单元测试
- 性能测试:测试大数据量下的性能表现
- 调试技巧:使用Qt Creator的调试工具,查看模型数据和视图状态
4.5 国际化与本地化
- 使用翻译字符串:所有用户可见的字符串使用tr()函数
- 日期时间格式:使用QDateTime::toString()的本地化版本
- 数字格式:使用QLocale处理数字格式
4.6 可访问性
- 键盘导航:确保所有操作可通过键盘完成
- 屏幕阅读器支持:使用QAccessible提供无障碍支持
- 高对比度模式:支持系统的高对比度设置
4.7 代码组织与命名规范
- 命名规范:使用清晰的命名,如模型类以Model结尾,代理类以Delegate结尾
- 代码结构:将模型、视图和代理分离到不同的文件中
- 注释:为复杂的模型实现添加详细注释
5. 实例演示
5.1 完整的Model-View应用
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QStandardItemModel>
#include <QTableView>
#include <QPushButton>
#include <QVBoxLayout>
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onAddRow();
void onRemoveRow();
void onUpdateData();
private:
QStandardItemModel *model;
QTableView *tableView;
QPushButton *addButton;
QPushButton *removeButton;
QPushButton *updateButton;
QVBoxLayout *layout;
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
// 创建模型
model = new QStandardItemModel(0, 3, this);
model->setHorizontalHeaderLabels({"Name", "Age", "Gender"});
// 创建视图
tableView = new QTableView(this);
tableView->setModel(model);
tableView->setSortingEnabled(true);
// 创建按钮
addButton = new QPushButton("Add Row", this);
removeButton = new QPushButton("Remove Row", this);
updateButton = new QPushButton("Update Data", this);
// 连接信号槽
connect(addButton, &QPushButton::clicked, this, &MainWindow::onAddRow);
connect(removeButton, &QPushButton::clicked, this, &MainWindow::onRemoveRow);
connect(updateButton, &QPushButton::clicked, this, &MainWindow::onUpdateData);
// 创建布局
layout = new QVBoxLayout();
layout->addWidget(tableView);
layout->addWidget(addButton);
layout->addWidget(removeButton);
layout->addWidget(updateButton);
// 设置中央部件
QWidget *centralWidget = new QWidget(this);
centralWidget->setLayout(layout);
setCentralWidget(centralWidget);
// 添加初始数据
for (int i = 0; i < 5; ++i) {
QList<QStandardItem*> row;
row.append(new QStandardItem(QString("Person %1").arg(i)));
row.append(new QStandardItem(QString("%1").arg(20 + i)));
row.append(new QStandardItem(i % 2 == 0 ? "Male" : "Female"));
model->appendRow(row);
}
}
MainWindow::~MainWindow() {
}
void MainWindow::onAddRow() {
int row = model->rowCount();
QList<QStandardItem*> newRow;
newRow.append(new QStandardItem(QString("Person %1").arg(row)));
newRow.append(new QStandardItem(QString("%1").arg(20 + row)));
newRow.append(new QStandardItem(row % 2 == 0 ? "Male" : "Female"));
model->appendRow(newRow);
}
void MainWindow::onRemoveRow() {
if (model->rowCount() > 0) {
model->removeRow(model->rowCount() - 1);
}
}
void MainWindow::onUpdateData() {
// 批量更新数据
model->blockSignals(true);
for (int row = 0; row < model->rowCount(); ++row) {
QModelIndex index = model->index(row, 1);
int age = model->data(index).toInt();
model->setData(index, age + 1);
}
model->blockSignals(false);
model->dataChanged(model->index(0, 1), model->index(model->rowCount() - 1, 1));
}
// main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
5.2 自定义模型示例
// custommodel.h
#ifndef CUSTOMMODEL_H
#define CUSTOMMODEL_H
#include <QAbstractTableModel>
#include <QList>
class Person {
public:
Person(const QString &name, int age, const QString &gender) :
name(name), age(age), gender(gender) {}
QString name;
int age;
QString gender;
};
class CustomModel : public QAbstractTableModel {
Q_OBJECT
public:
CustomModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
void addPerson(const Person &person);
void removePerson(int row);
void updatePerson(int row, const Person &person);
private:
QList<Person> persons;
};
#endif // CUSTOMMODEL_H
// custommodel.cpp
#include "custommodel.h"
CustomModel::CustomModel(QObject *parent) : QAbstractTableModel(parent) {
}
int CustomModel::rowCount(const QModelIndex &parent) const {
if (parent.isValid())
return 0;
return persons.size();
}
int CustomModel::columnCount(const QModelIndex &parent) const {
if (parent.isValid())
return 0;
return 3;
}
QVariant CustomModel::data(const QModelIndex &index, int role) const {
if (!index.isValid())
return QVariant();
if (index.row() >= persons.size() || index.row() < 0)
return QVariant();
if (role == Qt::DisplayRole || role == Qt::EditRole) {
const Person &person = persons.at(index.row());
switch (index.column()) {
case 0:
return person.name;
case 1:
return person.age;
case 2:
return person.gender;
default:
return QVariant();
}
}
return QVariant();
}
QVariant CustomModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (role != Qt::DisplayRole)
return QVariant();
if (orientation == Qt::Horizontal) {
switch (section) {
case 0:
return "Name";
case 1:
return "Age";
case 2:
return "Gender";
default:
return QVariant();
}
}
return QVariant();
}
bool CustomModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (index.isValid() && role == Qt::EditRole) {
int row = index.row();
Person person = persons.value(row);
switch (index.column()) {
case 0:
person.name = value.toString();
break;
case 1:
person.age = value.toInt();
break;
case 2:
person.gender = value.toString();
break;
default:
return false;
}
persons.replace(row, person);
emit dataChanged(index, index, {role});
return true;
}
return false;
}
Qt::ItemFlags CustomModel::flags(const QModelIndex &index) const {
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
void CustomModel::addPerson(const Person &person) {
beginInsertRows(QModelIndex(), rowCount(), rowCount());
persons.append(person);
endInsertRows();
}
void CustomModel::removePerson(int row) {
if (row < 0 || row >= persons.size())
return;
beginRemoveRows(QModelIndex(), row, row);
persons.removeAt(row);
endRemoveRows();
}
void CustomModel::updatePerson(int row, const Person &person) {
if (row < 0 || row >= persons.size())
return;
persons.replace(row, person);
emit dataChanged(index(row, 0), index(row, 2));
}
6. 总结
QT 的 Model-View 框架是一个强大而灵活的架构,通过分离数据与视图,使得代码更加模块化、可维护。在使用过程中,需要注意以下几点:
- 选择合适的模型:根据数据结构选择最适合的模型类
- 优化数据更新:使用批量更新、惰性加载等技术减少UI卡顿
- 合理使用代理:通过自定义代理实现复杂的编辑和渲染需求
- 注意性能优化:特别是在处理大数据时,要采取适当的优化措施
- 遵循最佳实践:保持代码清晰、模块化,注意内存管理和错误处理
通过掌握这些技巧,你可以开发出高效、响应迅速的 QT 应用程序,即使在处理大量数据时也能保持良好的用户体验。