基于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"
关键实现说明
- 分组结构维护:通过
GroupInfo和m_groups存储分组值与对应的源行索引,refreshGroups方法负责根据源模型数据更新分组 - 节点类型区分:用
internalPointer区分分组节点(nullptr)和数据节点(存储源行索引+1,避免空指针混淆) - 层级逻辑实现:
rowCount根据父节点类型返回分组数或分组内数据行数index根据父节点生成对应类型的代理索引parent根据节点类型找到对应的父分组节点
- 索引映射:
mapToSource/mapFromSource仅处理数据节点与源模型的映射,分组节点无对应源索引 - 扩展性:若要按前n列分组,只需修改
refreshGroups中的分组键生成逻辑(比如用多个列值组合成复合键)
内容的提问来源于stack exchange,提问作者tobilocker
相关产品推荐
相关产品推荐

