Qt Model/View 框架完全指南:从入门到性能优化

9 阅读10分钟

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 大数据处理导致卡顿的原因

  1. 频繁的信号发射:每次数据变化都发射信号,导致视图频繁更新
  2. UI线程阻塞:大量数据处理在UI线程中执行,导致界面卡顿
  3. 过度渲染:视图尝试渲染所有数据,即使不可见
  4. 内存占用过高:加载大量数据到内存,导致系统资源不足

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 性能优化

  1. 使用合适的模型:根据数据结构选择合适的模型,如列表数据使用QAbstractListModel
  2. 优化数据访问:实现高效的数据访问方法,避免不必要的计算
  3. 减少信号发射:批量操作时使用blockSignals或begin/end方法
  4. 使用代理模型:如QSortFilterProxyModel处理排序和过滤,避免直接修改原始模型
  5. 优化视图渲染:使用QStyledItemDelegate的paint方法时,避免复杂计算

4.2 内存管理

  1. 及时释放内存:不再使用的模型和视图应及时删除
  2. 避免内存泄漏:注意信号槽连接的生命周期,避免循环引用
  3. 合理使用缓存:缓存常用数据,但避免缓存过多导致内存占用过高

4.3 错误处理

  1. 数据验证:在setData方法中验证数据有效性
  2. 异常处理:使用try-catch处理可能的异常
  3. 错误反馈:向用户提供清晰的错误信息

4.4 测试与调试

  1. 单元测试:为模型和代理编写单元测试
  2. 性能测试:测试大数据量下的性能表现
  3. 调试技巧:使用Qt Creator的调试工具,查看模型数据和视图状态

4.5 国际化与本地化

  1. 使用翻译字符串:所有用户可见的字符串使用tr()函数
  2. 日期时间格式:使用QDateTime::toString()的本地化版本
  3. 数字格式:使用QLocale处理数字格式

4.6 可访问性

  1. 键盘导航:确保所有操作可通过键盘完成
  2. 屏幕阅读器支持:使用QAccessible提供无障碍支持
  3. 高对比度模式:支持系统的高对比度设置

4.7 代码组织与命名规范

  1. 命名规范:使用清晰的命名,如模型类以Model结尾,代理类以Delegate结尾
  2. 代码结构:将模型、视图和代理分离到不同的文件中
  3. 注释:为复杂的模型实现添加详细注释

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 框架是一个强大而灵活的架构,通过分离数据与视图,使得代码更加模块化、可维护。在使用过程中,需要注意以下几点:

  1. 选择合适的模型:根据数据结构选择最适合的模型类
  2. 优化数据更新:使用批量更新、惰性加载等技术减少UI卡顿
  3. 合理使用代理:通过自定义代理实现复杂的编辑和渲染需求
  4. 注意性能优化:特别是在处理大数据时,要采取适当的优化措施
  5. 遵循最佳实践:保持代码清晰、模块化,注意内存管理和错误处理

通过掌握这些技巧,你可以开发出高效、响应迅速的 QT 应用程序,即使在处理大量数据时也能保持良好的用户体验。