You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于QIdentityProxyModel实现扁平表转树形结构的技术疑问

实现扁平表转树形分组的Qt代理模型

核心方法实现说明

要实现类似DevExtreme Reactive Grid的分组树形结构,必须重写parent、index、mapToSource、mapFromSource这四个方法,原因如下:

  • parent:定义节点间的层级关联,是构建树形结构的核心
  • index:根据行、列和父节点生成代理模型的索引,区分分组节点与数据节点
  • mapToSource/mapFromSource:完成代理模型与源模型的索引映射,确保数据读写、交互逻辑正确

仅实现mapToSource和mapFromSource无法构建树形层级,因为QIdentityProxyModel默认保持扁平结构,没有层级逻辑。

修正后的实现代码

以下是按第一列分组的完整代理模型实现,可扩展至前n列分组:

#include <QApplication>
#include <QSqlDatabase>
#include <QSqlTableModel>
#include <QSqlError>
#include <QSqlQuery>
#include <QTreeView>
#include <QIdentityProxyModel>
#include <QVariant>
#include <QMap>

// 存储分组信息:分组值、对应的源行索引列表
struct GroupInfo {
    QVariant groupValue;
    QList<int> sourceRows;
};

class GroupByColumnsProxyModel : public QIdentityProxyModel {
    Q_OBJECT
public:
    explicit GroupByColumnsProxyModel(int groupColumnCount = 1, QObject *parent = nullptr)
        : QIdentityProxyModel(parent), m_groupColumnCount(groupColumnCount) {}

    ~GroupByColumnsProxyModel() override = default;

    void setSourceModel(QAbstractItemModel* sourceModel) override {
        QIdentityProxyModel::setSourceModel(sourceModel);
        if (sourceModel) {
            refreshGroups();
            connect(sourceModel, &QAbstractItemModel::dataChanged, this, &GroupByColumnsProxyModel::refreshGroups);
            connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &GroupByColumnsProxyModel::refreshGroups);
            connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &GroupByColumnsProxyModel::refreshGroups);
        }
    }

    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        if (!parent.isValid()) {
            // 顶层节点:返回分组数量
            return m_groups.size();
        }

        // 判断父节点是分组节点还是数据节点
        if (parent.internalPointer() == nullptr) {
            // 分组节点:返回该分组下的源数据行数
            const GroupInfo& group = m_groups.at(parent.row());
            return group.sourceRows.size();
        } else {
            // 数据节点没有子节点
            return 0;
        }
    }

    int columnCount(const QModelIndex &parent = QModelIndex()) const override {
        // 保持与源模型列数一致
        return QIdentityProxyModel::columnCount(parent);
    }

    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override {
        if (!hasIndex(row, column, parent)) {
            return QModelIndex();
        }

        if (!parent.isValid()) {
            // 顶层分组节点,用nullptr作为内部指针标记
            return createIndex(row, column, nullptr);
        }

        if (parent.internalPointer() == nullptr) {
            // 分组下的数据节点,用源行索引作为内部指针
            const GroupInfo& group = m_groups.at(parent.row());
            int sourceRow = group.sourceRows.at(row);
            return createIndex(row, column, reinterpret_cast<void*>(sourceRow + 1)); // +1避免与0混淆
        }

        return QModelIndex();
    }

    QModelIndex parent(const QModelIndex &child) const override {
        if (!child.isValid()) {
            return QModelIndex();
        }

        if (child.internalPointer() == nullptr) {
            // 分组节点没有父节点
            return QModelIndex();
        }

        // 数据节点,找到对应的分组
        int sourceRow = reinterpret_cast<quintptr>(child.internalPointer()) - 1;
        for (int groupIdx = 0; groupIdx < m_groups.size(); ++groupIdx) {
            const GroupInfo& group = m_groups.at(groupIdx);
            if (group.sourceRows.contains(sourceRow)) {
                return createIndex(groupIdx, child.column(), nullptr);
            }
        }

        return QModelIndex();
    }

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
        if (!index.isValid()) {
            return QVariant();
        }

        if (index.internalPointer() == nullptr) {
            // 分组节点,显示分组列的值
            const GroupInfo& group = m_groups.at(index.row());
            if (role == Qt::DisplayRole && index.column() < m_groupColumnCount) {
                return group.groupValue;
            }
            // 分组节点非分组列显示空或自定义内容
            return QVariant();
        }

        // 数据节点,映射到源模型获取数据
        int sourceRow = reinterpret_cast<quintptr>(index.internalPointer()) - 1;
        QModelIndex sourceIndex = sourceModel()->index(sourceRow, index.column());
        return sourceModel()->data(sourceIndex, role);
    }

    QModelIndex mapToSource(const QModelIndex &proxyIndex) const override {
        if (!proxyIndex.isValid() || proxyIndex.internalPointer() == nullptr) {
            // 分组节点无对应源索引
            return QModelIndex();
        }
        int sourceRow = reinterpret_cast<quintptr>(proxyIndex.internalPointer()) - 1;
        return sourceModel()->index(sourceRow, proxyIndex.column());
    }

    QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override {
        if (!sourceIndex.isValid()) {
            return QModelIndex();
        }

        // 找到源行对应的分组和行索引
        int sourceRow = sourceIndex.row();
        for (int groupIdx = 0; groupIdx < m_groups.size(); ++groupIdx) {
            const GroupInfo& group = m_groups.at(groupIdx);
            int rowInGroup = group.sourceRows.indexOf(sourceRow);
            if (rowInGroup != -1) {
                return index(rowInGroup, sourceIndex.column(), createIndex(groupIdx, 0, nullptr));
            }
        }

        return QModelIndex();
    }

private:
    void refreshGroups() {
        if (!sourceModel()) return;

        m_groups.clear();
        QMap<QVariant, GroupInfo> groupMap;

        // 按第一列分组,可扩展为前n列的组合键
        for (int row = 0; row < sourceModel()->rowCount(); ++row) {
            QVariant groupKey = sourceModel()->data(sourceModel()->index(row, 0));
            if (groupMap.contains(groupKey)) {
                groupMap[groupKey].sourceRows.append(row);
            } else {
                GroupInfo info;
                info.groupValue = groupKey;
                info.sourceRows.append(row);
                groupMap.insert(groupKey, info);
            }
        }

        m_groups = groupMap.values();
        emit layoutChanged();
    }

    int m_groupColumnCount;
    QList<GroupInfo> m_groups;
};

static bool createConnection()
{
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(":memory:");
    if (!db.open()) {
        return false;
    }
    QSqlQuery query;
    query.exec("create table simple (A int, B int)");
    query.exec("insert into simple values(1, 2)");
    query.exec("insert into simple values(1, 3)");
    query.exec("insert into simple values(2, 4)");
    return true;
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    if (!createConnection())
        return EXIT_FAILURE;

    QSqlTableModel *model = new QSqlTableModel;
    model->setTable("simple");
    model->select();

    QTreeView *view = new QTreeView;
    GroupByColumnsProxyModel *proxy = new GroupByColumnsProxyModel(1);
    proxy->setSourceModel(model);

    view->setModel(proxy);
    view->expandAll(); // 默认展开所有分组
    view->show();

    return a.exec();
}
#include "main.moc"

关键实现说明

  1. 分组结构维护:通过GroupInfo和m_groups存储分组值与对应的源行索引,refreshGroups方法负责根据源模型数据更新分组
  2. 节点类型区分:用internalPointer区分分组节点(nullptr)和数据节点(存储源行索引+1,避免空指针混淆)
  3. 层级逻辑实现:
    • rowCount根据父节点类型返回分组数或分组内数据行数
    • index根据父节点生成对应类型的代理索引
    • parent根据节点类型找到对应的父分组节点
  4. 索引映射:mapToSource/mapFromSource仅处理数据节点与源模型的映射,分组节点无对应源索引
  5. 扩展性:若要按前n列分组,只需修改refreshGroups中的分组键生成逻辑(比如用多个列值组合成复合键)

内容的提问来源于stack exchange,提问作者tobilocker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 22:48:52