背景

        近来在做Qt界面设计相关的项目,由于主窗体的框架是用QtQuick搭建的,所以必须从传统的C++开发转向JS风格的QML开发,QML我理解他的话就是一个JavaScript风格的描述性说明语言,至于QtQuick则相当于Qt官方为qml开发提供的一个通用强大的组件库吧,QML与QtQuick的关系相当于C++与STL吧。扯远了,说白了就是在项目的开发过程中,需要构建一个提供导航功能的流程树,其所有节点默认是展开的,点击打开其子节点对应的界面。

        树控件TreeView是在Qt5.5版本以后才有的,其功能也十分强大,可直接代替之前ListView等控件对于树结构的不灵活构建,可使用MVC数据与视图分离的方式灵活配置树结构和数据。

点击三角符号才会展开,或者实现以下代码,通过单击或双击父节点展开子项,但也需要一项一项的展开,如下代码:

property bool isExpand: true
onClicked: {
    if(isExpand) {
        emit: myTree.expand(index);
        isExpand = false;
    }
    else {
        emit: myTree.collapse(index);
        isExpand = true;
    }
}

 问题

        不像QTreeView,它有expandAll()的方法,可在加载界面和数据后直接展开全部节点。qml的TreeView控件的帮助文档中只提供了单个节点的展开和收缩方法如下代码:

// 参数为单个节点的index
void collapse(QModelIndex index);

//Collapses the model item specified by the index.
//See also collapsed and isExpanded.

void expand(QModelIndex index);

//Expands the model item specified by the index.
//See also expanded and isExpanded.

        并且在网上找了各种文章,也没发现Qt提供了展开所有包含子项的树节点,甚至在一些贴吧里还发现了提出此问题的同行,至今没发现解决办法。由于项目的需求,这个问题是我必须迈过的坎,通过大概一周的尝试和写demo,最终以自己的方式解决,虽然此方法看起来不是很聪明的样子,但是也完全解决了此问题,希望能给遇到此问题的同行提供一些思路。

思路

        我们再来拆解下问题,项目加载TreeView时实现默认(自动)展开所有节点(包含子项的节点),而qml  TreeView中只提供了展开单个树节点的方法,那么这问题可转化为,找到树上所有包含子项的节点的QModelIndex然后依次调用void expand(QModelIndex index);方法即可。此时的难点就变成了遍历此树,找出包含子项的节点的index。

        由于TreeView采用了MVC的方式构建,树的数据是在C++中的TreeModel类中实现的,因此解决此问题必须的使用qml和c++混合编程,这时我们的思路就是:项目加载完qml的view和model后,qml调用c++的TreeModel类的方法,此方法中遍历树数据结构,找到包含子项的节点,则发射TreeModel定义的信号,信号中包含此节点的QModelIndex,qml的tree控件响应此信号,调用展开方法。遍历结束,则全部节点展开完成。流程图如下图:

treeselect默认展开已选中的数据_qml

         实现了,转了一个圈(qml --> c++ --> qml)啊,是不是有点笨。

实现

         此篇文章是在读者已经掌握了实现MCV框架的qml TreeView控件的所有知识的基础上阅读的,如果对qml和qml TreeView不熟,建议先恶补一下基础知识,此处推荐一个大佬的博客链接,也是我学习qml从头看完的帖子, 

        插一句,Qt自带的帮助文档真的特别特别方便和全面,建议从事Qt开发的同行,时时刻刻都应该打开帮助。

       

        扯远了扯远了,我们赶快进入正题吧!

1. TreeView加载完毕调用C++ TreeModel类的槽函数

        采用qml的方法如下代码:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQml.Models 2.2     // 使用TreeView
import my.qt.TreeModel 1.0  // 注册到元对象中的C++类

Window {

    // 属性 略

    TreeModel {
        id: treeModel;  // C++类
    }

    TreeView {
        
        // 略

    }
    
    // qml加载完毕 默认执行此段
    Component.onCompleted: {
        treeModel.handleTreeExpand(); // C++槽函数
    }

}

2. C++ TreeModel类槽函数handleTreeExpand的实现

        TreeModel的槽函数handleTreeExpand主要实现,遍历树的所以节点,找出包含子项的节点,发射TreeModel中定义的信号void expandTreeNode(QModelIndex index); 这部分的代码如下:

TreeModel.h

#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>
#include "TreeItem.h" // 用来保存树节点的数据结构类,不明白请参考MVC的实现

class TreeItem;
class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    
    // ...

signals:
    // 发射展开信号和要展开的节点index
    void expandTreeNode(QModelIndex index);

public:
    
    // C++处理展开槽函数 遍历树结构 找到需展开的节点
    void handleTreeExpand();

private:
    // 处理父节点下的子节点 递归 知道没有子节点返回
    void handleChildItemExpand(TreeItem* parent);


    TreeItem* m_rootItem; // 保存树结构的根节点

}

TreeModel.cpp

#include "TreeModel.h"

// ... 

// 槽函数实现
void TreeModel::handleTreeExpand()
{
    // 根节点下一级子节点的个数
    int count = m_rootItem->childCount(); 
    for(int i = 0; i < count; ++i)
    {
        // 依次遍历一级子节点 
        auto childItem = m_rootItem->child(i);
        for(0 < chileItem->childCount())
        {
            // 包含子项的节点 展开
            emit expandTreeNode(createIndex(i, 0, childItem));
            
            // 处理 此一级节点的子节点
            handleChildItemExpand(childItem);
        }
    }
}

void TreeModel::handleChildItemExpand()
{
    int count = parent->childCount();
    for(int i = 0; i < count; ++i)
    {
        for(0 < chileItem->childCount())
        {
            // 包含子项的节点 展开
            emit expandTreeNode(createIndex(i, 0, childItem));
            
            // 递归处理 此级节点的子节点
            handleChildItemExpand(childItem);
        }
    }
}

3. qml中连接C++的信号expandTreeNode实现节点展开

        直接上代码实现吧

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQml.Models 2.2     // 使用TreeView
import my.qt.TreeModel 1.0  // 注册到元对象中的C++类

Window {

    // 属性 略

    MyTreeModel {
        id: treeModel;  // C++类
    }

    TreeView {
        id: myTree;        
        // 略
    }
    
    // qml加载完毕 默认执行此段
    Component.onCompleted: {
        treeModel.handleTreeExpand(); // C++槽函数
    }
    
    // 连接C++中定义的信号
    Connections {
        target: treeModel;
        onExpandTreeNode: {  // 注意qml中对于信号的写法
            emit: myTree.expand(index);
        }
    }
}

        看到这里,肯定会有童鞋问了:怎样把C++类注册到qml元对象中?qml中可以访问C++类的哪些属性?害,知识就在那,学不学看你。

写在最后

        如果我说这个问题的思路,方法和实现已经全部讲完,可能某些童鞋会一脸懵吧,甚至会说:这算什么文章嘛? 代码断断续续,这省那略的,完全看不懂;全篇一个实现效果图都没有,运行结果也没有,确定解决了吗?只看到了啰里啰唆的废话了。

        这是我写完后看了这篇文章的感受,代替一些童鞋说了出来,如果有同感的,欢迎点评。

        还是得解释一下滴,这个问题是我在公司项目中遇到的,测试代码和demo都是在内网工作机上实现的,并且项目这部分工作提交了后,我才有功夫写这篇文章的,所以这个方法是完全能解决这个问题的。当然还是得在QML与C++混合编程,并且采用了MCV的框架的背景下才行。如果有实现方面的问题欢迎随时评论交流。