当前位置: 首页 > news >正文

Qt QTreeView深度解析:从原理到实战应用

前言

最近项目中需要实现一个文件管理器的树形显示功能,研究了QTreeView的实现机制后,发现Qt的Model/View架构设计真的很优雅。今天整理一下QTreeView的核心原理和实际使用经验,希望能帮到有需要的朋友。

一、QTreeView的核心架构

1.1 Model/View分离设计

QTreeView采用的是Qt的Model/View架构,这种设计把数据和显示完全分离开了。简单说就是:

  • Model负责管理数据
  • View负责显示数据
  • Delegate负责编辑和绘制

这样做的好处是一份数据可以被多个View共享,而且修改数据时View会自动更新,不需要手动刷新界面。

1.2 数据模型层次结构

QTreeView使用QModelIndex来标识树中的每个节点。每个QModelIndex包含三个关键信息:

  • row:当前节点在父节点中的行号
  • column:列号
  • internalPointer:指向实际数据的指针
// QModelIndex的内部结构示意
class QModelIndex {int r;  // rowint c;  // columnvoid *p;  // internalPointerconst QAbstractItemModel *m;  // model指针
};

二、自定义Model的实现原理

要使用QTreeView,核心是实现一个继承自QAbstractItemModel的自定义Model。下面是必须实现的几个关键函数。

2.1 index()函数:创建索引

这个函数用来创建指定位置的QModelIndex。

QModelIndex CustomTreeModel::index(int row, int column, const QModelIndex &parent) const
{if (!hasIndex(row, column, parent))return QModelIndex();TreeNode *parentNode;if (!parent.isValid())parentNode = rootNode;  // 根节点elseparentNode = static_cast<TreeNode*>(parent.internalPointer());TreeNode *childNode = parentNode->child(row);if (childNode)return createIndex(row, column, childNode);elsereturn QModelIndex();
}

这里的关键是createIndex()函数,它把TreeNode指针存到了QModelIndex的internalPointer中,这样以后就能通过QModelIndex快速访问到对应的数据节点。

2.2 parent()函数:获取父索引

这个函数返回指定节点的父节点索引,是实现树形结构的核心。

QModelIndex CustomTreeModel::parent(const QModelIndex &child) const
{if (!child.isValid())return QModelIndex();TreeNode *childNode = static_cast<TreeNode*>(child.internalPointer());TreeNode *parentNode = childNode->parent();if (parentNode == rootNode)return QModelIndex();  // 顶层节点的父节点是无效索引return createIndex(parentNode->row(), 0, parentNode);
}

2.3 rowCount()和columnCount()

这两个函数返回指定节点的子节点数量和列数。

int CustomTreeModel::rowCount(const QModelIndex &parent) const
{TreeNode *parentNode;if (!parent.isValid())parentNode = rootNode;elseparentNode = static_cast<TreeNode*>(parent.internalPointer());return parentNode->childCount();
}int CustomTreeModel::columnCount(const QModelIndex &parent) const
{Q_UNUSED(parent);return 3;  // 假设有3列数据
}

2.4 data()函数:返回显示数据

这个函数根据QModelIndex和role返回对应的数据。

QVariant CustomTreeModel::data(const QModelIndex &index, int role) const
{if (!index.isValid())return QVariant();TreeNode *node = static_cast<TreeNode*>(index.internalPointer());if (role == Qt::DisplayRole || role == Qt::EditRole) {switch (index.column()) {case 0: return node->name();case 1: return node->type();case 2: return node->size();default: return QVariant();}}else if (role == Qt::DecorationRole && index.column() == 0) {// 返回图标return node->icon();}else if (role == Qt::TextAlignmentRole) {if (index.column() == 2)return Qt::AlignRight + Qt::AlignVCenter;}return QVariant();
}

三、数据节点类的设计

为了方便管理树形数据,通常需要设计一个TreeNode类。

class TreeNode
{
public:explicit TreeNode(const QString &name, TreeNode *parent = nullptr): m_name(name), m_parent(parent){if (parent)parent->appendChild(this);}~TreeNode(){qDeleteAll(m_children);}void appendChild(TreeNode *child){m_children.append(child);child->m_parent = this;}TreeNode *child(int row){if (row < 0 || row >= m_children.size())return nullptr;return m_children.at(row);}int childCount() const{return m_children.size();}int row() const{if (m_parent)return m_parent->m_children.indexOf(const_cast<TreeNode*>(this));return 0;}TreeNode *parent(){return m_parent;}QString name() const { return m_name; }QString type() const { return m_type; }qint64 size() const { return m_size; }void setType(const QString &type) { m_type = type; }void setSize(qint64 size) { m_size = size; }private:QString m_name;QString m_type;qint64 m_size;TreeNode *m_parent;QList<TreeNode*> m_children;
};

这个设计的关键点:

  1. 每个节点都知道自己的父节点和所有子节点
  2. row()函数返回节点在父节点中的位置
  3. 使用指针管理父子关系,效率高

四、完整示例代码

下面是一个完整的文件树管理器实现,可以直接运行。

// treemodel.h
#ifndef TREEMODEL_H
#define TREEMODEL_H#include <QAbstractItemModel>
#include <QIcon>class TreeNode;class TreeModel : public QAbstractItemModel
{Q_OBJECTpublic:explicit TreeModel(QObject *parent = nullptr);~TreeModel();QVariant data(const QModelIndex &index, int role) const override;Qt::ItemFlags flags(const QModelIndex &index) const override;QVariant headerData(int section, Qt::Orientation orientation,int role = Qt::DisplayRole) const override;QModelIndex index(int row, int column,const QModelIndex &parent = QModelIndex()) const override;QModelIndex parent(const QModelIndex &index) const override;int rowCount(const QModelIndex &parent = QModelIndex()) const override;int columnCount(const QModelIndex &parent = QModelIndex()) const override;bool setData(const QModelIndex &index, const QVariant &value,int role = Qt::EditRole) override;bool insertRows(int position, int rows,const QModelIndex &parent = QModelIndex()) override;bool removeRows(int position, int rows,const QModelIndex &parent = QModelIndex()) override;private:TreeNode *rootNode;QIcon folderIcon;QIcon fileIcon;
};#endif // TREEMODEL_H
// treemodel.cpp
#include "treemodel.h"
#include <QStringList>class TreeNode
{
public:explicit TreeNode(const QString &name, const QString &type = "", qint64 size = 0, TreeNode *parent = nullptr): m_name(name), m_type(type), m_size(size), m_parent(parent){}~TreeNode(){qDeleteAll(m_children);}void appendChild(TreeNode *child){m_children.append(child);child->m_parent = this;}TreeNode *child(int row){if (row < 0 || row >= m_children.size())return nullptr;return m_children.at(row);}int childCount() const { return m_children.size(); }int row() const{if (m_parent)return m_parent->m_children.indexOf(const_cast<TreeNode*>(this));return 0;}TreeNode *parent() { return m_parent; }QString name() const { return m_name; }QString type() const { return m_type; }qint64 size() const { return m_size; }void setName(const QString &name) { m_name = name; }void setType(const QString &type) { m_type = type; }void setSize(qint64 size) { m_size = size; }bool insertChildren(int position, int count){if (position < 0 || position > m_children.size())return false;for (int row = 0; row < count; ++row) {TreeNode *node = new TreeNode("New Item", "file", 0, this);m_children.insert(position, node);}return true;}bool removeChildren(int position, int count){if (position < 0 || position + count > m_children.size())return false;for (int row = 0; row < count; ++row)delete m_children.takeAt(position);return true;}private:QString m_name;QString m_type;qint64 m_size;TreeNode *m_parent;QList<TreeNode*> m_children;
};TreeModel::TreeModel(QObject *parent): QAbstractItemModel(parent)
{// 初始化根节点rootNode = new TreeNode("Root");// 构建示例数据TreeNode *documents = new TreeNode("Documents", "folder", 0, rootNode);rootNode->appendChild(documents);TreeNode *projects = new TreeNode("Projects", "folder", 0, documents);documents->appendChild(projects);TreeNode *qt_project = new TreeNode("QtProject", "folder", 0, projects);projects->appendChild(qt_project);TreeNode *main_cpp = new TreeNode("main.cpp", "C++ Source", 2048, qt_project);qt_project->appendChild(main_cpp);TreeNode *widget_h = new TreeNode("widget.h", "C++ Header", 1024, qt_project);qt_project->appendChild(widget_h);TreeNode *pictures = new TreeNode("Pictures", "folder", 0, rootNode);rootNode->appendChild(pictures);TreeNode *photo1 = new TreeNode("photo1.jpg", "JPEG Image", 524288, pictures);pictures->appendChild(photo1);folderIcon = QIcon::fromTheme("folder");fileIcon = QIcon::fromTheme("text-x-generic");
}TreeModel::~TreeModel()
{delete rootNode;
}QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{if (!hasIndex(row, column, parent))return QModelIndex();TreeNode *parentNode;if (!parent.isValid())parentNode = rootNode;elseparentNode = static_cast<TreeNode*>(parent.internalPointer());TreeNode *childNode = parentNode->child(row);if (childNode)return createIndex(row, column, childNode);return QModelIndex();
}QModelIndex TreeModel::parent(const QModelIndex &index) const
{if (!index.isValid())return QModelIndex();TreeNode *childNode = static_cast<TreeNode*>(index.internalPointer());TreeNode *parentNode = childNode->parent();if (parentNode == rootNode)return QModelIndex();return createIndex(parentNode->row(), 0, parentNode);
}int TreeModel::rowCount(const QModelIndex &parent) const
{TreeNode *parentNode;if (parent.column() > 0)return 0;if (!parent.isValid())parentNode = rootNode;elseparentNode = static_cast<TreeNode*>(parent.internalPointer());return parentNode->childCount();
}int TreeModel::columnCount(const QModelIndex &parent) const
{Q_UNUSED(parent);return 3;
}QVariant TreeModel::data(const QModelIndex &index, int role) const
{if (!index.isValid())return QVariant();TreeNode *node = static_cast<TreeNode*>(index.internalPointer());if (role == Qt::DisplayRole || role == Qt::EditRole) {switch (index.column()) {case 0: return node->name();case 1: return node->type();case 2: if (node->size() > 0)return QString("%1 KB").arg(node->size() / 1024.0, 0, 'f', 2);return QVariant();default: return QVariant();}}else if (role == Qt::DecorationRole && index.column() == 0) {if (node->type() == "folder")return folderIcon;else if (!node->type().isEmpty())return fileIcon;}else if (role == Qt::TextAlignmentRole) {if (index.column() == 2)return int(Qt::AlignRight | Qt::AlignVCenter);}return QVariant();
}Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{if (!index.isValid())return Qt::NoItemFlags;return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const
{if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {switch (section) {case 0: return tr("Name");case 1: return tr("Type");case 2: return tr("Size");default: return QVariant();}}return QVariant();
}bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{if (role != Qt::EditRole)return false;TreeNode *node = static_cast<TreeNode*>(index.internalPointer());if (index.column() == 0)node->setName(value.toString());emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});return true;
}bool TreeModel::insertRows(int position, int rows, const QModelIndex &parent)
{TreeNode *parentNode;if (!parent.isValid())parentNode = rootNode;elseparentNode = static_cast<TreeNode*>(parent.internalPointer());beginInsertRows(parent, position, position + rows - 1);bool success = parentNode->insertChildren(position, rows);endInsertRows();return success;
}bool TreeModel::removeRows(int position, int rows, const QModelIndex &parent)
{TreeNode *parentNode;if (!parent.isValid())parentNode = rootNode;elseparentNode = static_cast<TreeNode*>(parent.internalPointer());beginRemoveRows(parent, position, position + rows - 1);bool success = parentNode->removeChildren(position, rows);endRemoveRows();return success;
}
// main.cpp
#include <QApplication>
#include <QTreeView>
#include <QVBoxLayout>
#include <QWidget>
#include <QPushButton>
#include <QHBoxLayout>
#include "treemodel.h"int main(int argc, char *argv[])
{QApplication app(argc, argv);QWidget window;window.setWindowTitle("QTreeView Demo");window.resize(600, 400);QVBoxLayout *mainLayout = new QVBoxLayout(&window);// 创建TreeViewQTreeView *treeView = new QTreeView;TreeModel *model = new TreeModel;treeView->setModel(model);// 设置TreeView属性treeView->setAlternatingRowColors(true);treeView->setSelectionBehavior(QAbstractItemView::SelectRows);treeView->setSelectionMode(QAbstractItemView::SingleSelection);treeView->setEditTriggers(QAbstractItemView::DoubleClicked);// 调整列宽treeView->setColumnWidth(0, 250);treeView->setColumnWidth(1, 200);// 展开第一层treeView->expandToDepth(1);mainLayout->addWidget(treeView);// 添加操作按钮QHBoxLayout *buttonLayout = new QHBoxLayout;QPushButton *addButton = new QPushButton("Add Row");QPushButton *removeButton = new QPushButton("Remove Row");QObject::connect(addButton, &QPushButton::clicked, [treeView, model]() {QModelIndex index = treeView->currentIndex();model->insertRows(0, 1, index);});QObject::connect(removeButton, &QPushButton::clicked, [treeView, model]() {QModelIndex index = treeView->currentIndex();if (index.isValid()) {model->removeRows(index.row(), 1, index.parent());}});buttonLayout->addWidget(addButton);buttonLayout->addWidget(removeButton);buttonLayout->addStretch();mainLayout->addLayout(buttonLayout);window.show();return app.exec();
}

五、QTreeView的高级应用

5.1 自定义委托(Delegate)

如果需要自定义编辑器或者特殊的绘制效果,可以使用QStyledItemDelegate。

class CustomDelegate : public QStyledItemDelegate
{Q_OBJECTpublic:CustomDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {}QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,const QModelIndex &index) const override{if (index.column() == 1) {// 第二列使用下拉框编辑QComboBox *editor = new QComboBox(parent);editor->addItems({"folder", "file", "C++ Source", "C++ Header"});return editor;}return QStyledItemDelegate::createEditor(parent, option, index);}void setEditorData(QWidget *editor, const QModelIndex &index) const override{if (QComboBox *comboBox = qobject_cast<QComboBox*>(editor)) {QString value = index.model()->data(index, Qt::EditRole).toString();int idx = comboBox->findText(value);if (idx >= 0)comboBox->setCurrentIndex(idx);} else {QStyledItemDelegate::setEditorData(editor, index);}}void setModelData(QWidget *editor, QAbstractItemModel *model,const QModelIndex &index) const override{if (QComboBox *comboBox = qobject_cast<QComboBox*>(editor)) {model->setData(index, comboBox->currentText(), Qt::EditRole);} else {QStyledItemDelegate::setModelData(editor, model, index);}}
};// 使用方法
treeView->setItemDelegate(new CustomDelegate(treeView));

5.2 性能优化技巧

在处理大量数据时,需要注意几个性能问题:

  1. 延迟加载:只在展开节点时才加载子节点数据
bool TreeModel::canFetchMore(const QModelIndex &parent) const
{TreeNode *node;if (!parent.isValid())node = rootNode;elsenode = static_cast<TreeNode*>(parent.internalPointer());// 判断是否还有数据可以加载return node->hasMoreData();
}void TreeModel::fetchMore(const QModelIndex &parent)
{TreeNode *node;if (!parent.isValid())node = rootNode;elsenode = static_cast<TreeNode*>(parent.internalPointer());// 加载更多数据int first = node->childCount();int count = node->fetchMoreChildren();  // 获取更多子节点beginInsertRows(parent, first, first + count - 1);// 数据已经在fetchMoreChildren中加载endInsertRows();
}
  1. 使用QModelIndex的缓存:避免重复创建相同的QModelIndex

  2. 合理使用信号:批量修改数据时,使用layoutAboutToBeChanged和layoutChanged信号

void TreeModel::sortData()
{emit layoutAboutToBeChanged();// 执行排序操作// ...emit layoutChanged();
}

5.3 右键菜单实现

treeView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(treeView, &QTreeView::customContextMenuRequested, [treeView, model](const QPoint &pos) {QModelIndex index = treeView->indexAt(pos);if (!index.isValid())return;QMenu menu;QAction *addAction = menu.addAction("Add Child");QAction *deleteAction = menu.addAction("Delete");QAction *selected = menu.exec(treeView->viewport()->mapToGlobal(pos));if (selected == addAction) {model->insertRows(0, 1, index);treeView->expand(index);} else if (selected == deleteAction) {model->removeRows(index.row(), 1, index.parent());}
});

六、常见问题及解决方案

6.1 数据更新后界面不刷新

这通常是因为修改数据后没有发射dataChanged信号。

// 错误做法
node->setName("New Name");  // 只修改了数据// 正确做法
node->setName("New Name");
emit dataChanged(index, index);

6.2 展开/折叠状态保存

// 保存展开状态
QList<QPersistentModelIndex> expandedIndexes;
void saveExpandState(QTreeView *view, const QModelIndex &parent = QModelIndex())
{for (int i = 0; i < view->model()->rowCount(parent); ++i) {QModelIndex idx = view->model()->index(i, 0, parent);if (view->isExpanded(idx)) {expandedIndexes.append(idx);saveExpandState(view, idx);}}
}// 恢复展开状态
void restoreExpandState(QTreeView *view)
{for (const QPersistentModelIndex &idx : expandedIndexes) {if (idx.isValid())view->expand(idx);}
}

6.3 选中特定节点

// 根据节点数据查找并选中
void selectNodeByName(QTreeView *view, const QString &name, const QModelIndex &parent = QModelIndex())
{for (int i = 0; i < view->model()->rowCount(parent); ++i) {QModelIndex idx = view->model()->index(i, 0, parent);if (view->model()->data(idx).toString() == name) {view->setCurrentIndex(idx);view->scrollTo(idx);return;}selectNodeByName(view, name, idx);}
}

七、总结

QTreeView的核心就是理解Model/View架构和QModelIndex的工作机制。掌握了这几个关键点后,就能灵活处理各种树形数据显示需求:

  1. 继承QAbstractItemModel并实现必要的虚函数
  2. 设计合理的TreeNode类来管理数据
  3. 正确使用createIndex和internalPointer
  4. 修改数据后及时发射信号通知View更新

实际项目中,根据具体需求可能还需要实现拖拽、自定义绘制、过滤排序等功能,但基本原理都是一样的。

上面的完整代码可以直接编译运行,建议自己动手试试,修改一些参数看看效果,这样理解会更深刻。

有什么问题欢迎在评论区讨论。


开发环境:Qt 5.15+ / Qt 6.x
编译器:MSVC 2019 / GCC 9+ / Clang 10+
测试平台:Windows 10 / Ubuntu 20.04

http://www.dtcms.com/a/614369.html

相关文章:

  • 建设网站需要哪些经营范围免费发帖推广的平台
  • 用C语言编译好玩的程序 | 探索编程世界的奇妙乐趣
  • 【开发备忘】GeoServer相关两则:发布时间维ImageMosaic+客户端WMS样式
  • 山东外贸国际网站建设南昌做网站多少钱
  • 3-Linux驱动开发-简单内核模块代码详解
  • 北京住房城乡建设部网站云南技术网站建设销售
  • Unity游戏开发客户端面试题分享
  • Hugging Face Transformers:AI模型的“瑞士军刀“
  • 中国纪检监察报网站wordpress古腾堡汉化
  • L2 MAC层核心机制介绍
  • 互动即价值?社交型数字资产深度融合社交行为与价值创造:
  • c#.net List对象和字典对象Dictionary,HashSet对比
  • 自己做免费网站的视频做跨境电商要什么费用
  • 唯品会 item_get 接口对接全攻略:从入门到精通
  • 互联网站账户e服务平台怎么建立一个网站好
  • 网站标题如何修改江苏高端品牌网站建设
  • 软考程序员2021年C语言链表案例题解答
  • nfs练习作业
  • 红黑树分析 1
  • Linux:监控命令
  • 官方网站开发用什么语言厦门建网站的公司
  • 做网站设计需要哪些软件兖州做网站
  • 马来西亚医疗旅游理事会举办“2025马来西亚深圳医疗旅游周“发力中国医疗旅游市场
  • wordpress 微信授权做网站建设优化的公司排名
  • 网站制作招聘音乐网站可做哪些内容
  • 一块中国好屏,和智能终端共舞
  • 240. Java 集合 - 使用集合工厂方法创建和处理数据
  • 南京app定制台州商品关键词优化
  • 代码生成工具Amazon CodeWhisperer介绍
  • 做视频网站的流程注册公司流程需要多久