Qt Model/View架构详解(四):高级特性

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   │
│  (源模型)   │      │  (代理模型)     │      │  (视图)   │
└────────────┘      └────────────────┘      └──────────┘
                          │
                          ├─ 排序
                          ├─ 过滤
                          └─ 转换

代理模型的优势

  1. 不修改源数据 - 所有操作都在代理层完成
  2. 多视图支持 - 同一源模型可以有多个不同的代理
  3. 即时响应 - 源模型数据变化时自动更新
  4. 易于维护 - 业务逻辑和展示逻辑分离

常见的代理模型

代理模型 用途
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();
}

关键点

  1. 视图使用代理模型 - view->setModel(proxyModel),而不是 sourceModel
  2. 代理模型使用源模型 - proxyModel->setSourceModel(sourceModel)
  3. 数据操作仍在源模型 - 添加、删除数据仍然通过 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()

完整示例 - 带搜索和排序的表格应用

关键要点

  1. 代理模型不存储数据,只转换数据
  2. 视图使用代理模型,代理模型使用源模型
  3. 数据修改仍然在源模型上进行
  4. 代理模型会自动响应源模型的变化
  5. 可以串联多个代理模型实现复杂功能
  • 代理模型的概念
  • 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()实现特殊排序规则

多列排序 - 按优先级比较多个列

智能排序 - 自动识别数字、日期、版本号、文件大小

关键要点

  1. lessThan()接收的是源模型的索引
  2. 可以根据列号使用不同的排序规则
  3. 自动类型识别可以提升用户体验
  4. 调用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()

多条件过滤 - 组合多个过滤条件

实时搜索 - 即时响应用户输入

关键要点

  1. filterAcceptsRow()返回true表示显示该行
  2. 调用invalidateFilter()触发重新过滤
  3. 可以组合正则表达式和自定义过滤
  4. 过滤不影响源模型数据
  5. 实时搜索提升用户体验
  • 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() - 源索引→代理索引

实战应用 - 删除、修改、选择的正确姿势

常见陷阱 - 索引混用导致的错误

关键要点

  1. 视图操作使用代理索引
  2. 数据操作使用源索引
  3. 必须进行索引映射
  4. 检查映射后索引的有效性
  5. 批量删除时从后往前
  • mapToSource() - 代理索引到源索引
  • mapFromSource() - 源索引到代理索引
  • 在过滤/排序后操作数据

第9章总结

🎉 第9章 排序与过滤 已全部完成!

本章涵盖了:

  • ✅ 9.1 QSortFilterProxyModel介绍(代理模型概念、设置)
  • ✅ 9.2 排序功能(自定义排序、智能排序)
  • ✅ 9.3 过滤功能(正则表达式、自定义过滤、多条件)
  • ✅ 9.4 代理模型的映射(索引转换、数据操作)

核心知识点

  1. 代理模型是数据转换的中间层
  2. lessThan()控制排序规则
  3. filterAcceptsRow()控制过滤逻辑
  4. 必须进行索引映射才能正确操作数据
  5. 代理模型不存储数据,所有修改在源模型上进行

接下来可以继续学习第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种模式满足不同需求

关键要点

  1. 必须同时启用drag和drop才能拖放
  2. InternalMove适合单视图内重排序
  3. DragDrop适合跨视图数据传输
  4. 放置指示器提升用户体验
  • 启用拖放: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() - 设置拖放标志

关键要点

  1. supportedDropActions()定义允许的操作
  2. mimeData()将模型数据编码为MIME数据
  3. dropMimeData()解码并插入数据
  4. flags()必须包含相应的拖放标志
  5. 使用标准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配合使用

自定义拖放 - 重写模型方法实现特殊需求

关键要点

  1. 列表重排序使用InternalMove模式
  2. 树形拖放需要设置ItemIsDropEnabled标志
  3. 跨视图拖放通常使用CopyAction
  4. 从后往前删除避免索引错乱
  5. 可以通过mimeData()支持跨应用拖放
  • 实战:列表项重排序
  • 实战:树形节点拖放
  • 实战:跨视图拖放

第10章总结

🎉 第10章 拖放支持 已全部完成!

本章涵盖了:

  • ✅ 10.1 Model/View中的拖放(启用、指示器、模式)
  • ✅ 10.2 模型中的拖放接口(5个关键方法)
  • ✅ 10.3 拖放实战(列表重排序、树形拖放、跨视图)

核心知识点

  1. 启用拖放需要setDragEnabled()和setAcceptDrops()
  2. DragDropMode控制拖放行为
  3. 模型需要实现supportedDropActions()等接口
  4. flags()必须包含拖放标志
  5. 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():

  1. 滚动到底部时
  2. 显示区域需要更多数据时
  3. 调用setRootIndex()时

本节小结

canFetchMore() - 判断是否还有数据可加载

fetchMore() - 执行数据加载

实战应用 - 大数据集的分批加载

关键要点

  1. 使用beginInsertRows()和endInsertRows()通知视图
  2. 视图会自动调用fetchMore()
  3. 适合处理大数据集(数据库查询、文件加载等)
  4. 显著提升初始加载速度
  • 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框架

常见错误 - 避免典型陷阱

调试技巧 - 快速定位问题

关键要点

  1. 始终使用QAbstractItemModelTester验证模型
  2. 编写单元测试覆盖关键功能
  3. 检查索引有效性
  4. 正确使用begin/end通知方法
  5. 使用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方法批量处理

大数据策略 - 懒加载、分页、虚拟化

索引缓存 - 避免重复创建索引

关键要点

  1. 批量操作优于逐个操作
  2. 一次性发送dataChanged比多次发送高效
  3. 大数据集使用懒加载或分页
  4. 缓存频繁使用的索引
  5. 使用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

完整示例 - 异步数据加载模型

关键要点

  1. 所有Model/View操作必须在主线程
  2. 耗时操作在工作线程执行
  3. 使用信号槽跨线程通信
  4. 不要在工作线程中直接操作模型
  5. 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映射代理

实战应用 - 单位转换、数据格式化

关键要点

  1. QIdentityProxyModel简化代理实现
  2. 只需重写需要修改的方法
  3. 适合数据转换、格式化等场景
  4. 不改变模型结构,只改变显示
  5. 可以组合多个代理实现复杂功能
  • QAbstractProxyModel
  • QIdentityProxyModel
  • 实战:自定义数据转换代理模型

第11章总结

🎉 第11章 高级主题 已全部完成!

本章涵盖了:

  • ✅ 11.1 懒加载(canFetchMore、fetchMore、大数据分批加载)
  • ✅ 11.2 模型测试与调试(QAbstractItemModelTester、单元测试、常见错误)
  • ✅ 11.3 性能优化(信号优化、批量操作、大数据策略、索引缓存)
  • ✅ 11.4 多线程与Model/View(线程安全、跨线程更新)
  • ✅ 11.5 自定义代理模型(QIdentityProxyModel、数据转换)

核心知识点

  1. 懒加载显著提升大数据集性能
  2. QAbstractItemModelTester自动验证模型正确性
  3. 批量操作优于逐个操作
  4. Model/View必须在主线程使用
  5. QIdentityProxyModel简化代理开发

需要继续实战,可看12章Qt Model/View项目实战!


相关推荐
txinyu的博客2 小时前
std::function
服务器·开发语言·c++
多多*2 小时前
图解Redis的分布式锁的历程 从单机到集群
java·开发语言·javascript·vue.js·spring·tomcat·maven
电商API&Tina2 小时前
Python请求淘宝商品评论API接口全指南||taobao评论API
java·开发语言·数据库·python·json·php
学嵌入式的小杨同学3 小时前
【嵌入式 C 语言实战】交互式栈管理系统:从功能实现到用户交互全解析
c语言·开发语言·arm开发·数据结构·c++·算法·链表
小杍随笔3 小时前
【Rust Cargo 目录迁移到 D 盘:不改变安装路径和环境变量的终极方案】
开发语言·后端·rust
Henry Zhu1233 小时前
Qt Model/View架构详解(五):综合实战项目
开发语言·qt·架构
孞㐑¥3 小时前
算法—滑动窗口
开发语言·c++·经验分享·笔记·算法
AI-小柒3 小时前
从零入门大语言模型(LLM):系统学习路线与实践指南
大数据·开发语言·人工智能·学习·信息可视化·语言模型·自然语言处理
hhy_smile3 小时前
Python environment and installation
开发语言·python