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:指向实际数据的指针
cpp 复制代码
// QModelIndex的内部结构示意
class QModelIndex {
    int r;  // row
    int c;  // column
    void *p;  // internalPointer
    const QAbstractItemModel *m;  // model指针
};

二、自定义Model的实现原理

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

2.1 index()函数:创建索引

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

cpp 复制代码
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;  // 根节点
    else
        parentNode = static_cast<TreeNode*>(parent.internalPointer());

    TreeNode *childNode = parentNode->child(row);
    if (childNode)
        return createIndex(row, column, childNode);
    else
        return QModelIndex();
}

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

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

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

cpp 复制代码
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()

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

cpp 复制代码
int CustomTreeModel::rowCount(const QModelIndex &parent) const
{
    TreeNode *parentNode;
    if (!parent.isValid())
        parentNode = rootNode;
    else
        parentNode = 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返回对应的数据。

cpp 复制代码
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类。

cpp 复制代码
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. 使用指针管理父子关系,效率高

四、完整示例代码

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

cpp 复制代码
// treemodel.h
#ifndef TREEMODEL_H
#define TREEMODEL_H

#include <QAbstractItemModel>
#include <QIcon>

class TreeNode;

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    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
cpp 复制代码
// 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;
    else
        parentNode = 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;
    else
        parentNode = 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;
    else
        parentNode = 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;
    else
        parentNode = static_cast<TreeNode*>(parent.internalPointer());

    beginRemoveRows(parent, position, position + rows - 1);
    bool success = parentNode->removeChildren(position, rows);
    endRemoveRows();

    return success;
}
cpp 复制代码
// 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);

    // 创建TreeView
    QTreeView *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。

cpp 复制代码
class CustomDelegate : public QStyledItemDelegate
{
    Q_OBJECT

public:
    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. 延迟加载:只在展开节点时才加载子节点数据
cpp 复制代码
bool TreeModel::canFetchMore(const QModelIndex &parent) const
{
    TreeNode *node;
    if (!parent.isValid())
        node = rootNode;
    else
        node = static_cast<TreeNode*>(parent.internalPointer());

    // 判断是否还有数据可以加载
    return node->hasMoreData();
}

void TreeModel::fetchMore(const QModelIndex &parent)
{
    TreeNode *node;
    if (!parent.isValid())
        node = rootNode;
    else
        node = 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信号

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

5.3 右键菜单实现

cpp 复制代码
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信号。

cpp 复制代码
// 错误做法
node->setName("New Name");  // 只修改了数据

// 正确做法
node->setName("New Name");
emit dataChanged(index, index);

6.2 展开/折叠状态保存

cpp 复制代码
// 保存展开状态
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 选中特定节点

cpp 复制代码
// 根据节点数据查找并选中
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

相关推荐
q***96582 小时前
Spring Data JDBC 详解
java·数据库·spring
ooooooctober2 小时前
PHP代码审计框架性思维的建立
android·开发语言·php
Hello,C++!2 小时前
linux下libcurl的https简单例子
linux·数据库·https
864记忆2 小时前
Qt Widgets 模块中的函数详解
开发语言·qt
white-persist3 小时前
差异功能定位解析:C语言与C++(区别在哪里?)
java·c语言·开发语言·网络·c++·安全·信息可视化
q***72873 小时前
Golang 构建学习
开发语言·学习·golang
hmbbcsm3 小时前
练习python题目小记(五)
开发语言·python
kokunka3 小时前
C#类修饰符功能与范围详解
java·开发语言·c#
仟濹3 小时前
【Java 基础】3 面向对象 - this
java·开发语言·python