仓库源文-树组件的定制.md),站点原文-树组件的定制)


title: 玩转Qml(17)-树组件的定制 photos: /images/Qml13/1.png tags:


简介

最近遇到一些需求,要在Qt/Qml中开发树结构,并能够导入、导出json格式。

于是我写了一个简易的Demo,并做了一些性能测试。

在这里将源码、实现原理、以及性能测试都记录、分享出来,算是抛砖引玉吧,希望有更多人来讨论、交流。

TreeEdit源码

起初的代码在单独的仓库

github https://github.com/jaredtao/TreeEdit

gitee镜像 https://gitee.com/jaredtao/Tree

后续会收录到《玩转Qml》配套的开源项目TaoQuick中

github https://github.com/jaredtao/TaoQuick

gitee镜像 https://gitee.com/jaredtao/TaoQuick

效果预览

看一下最终效果

预览

Qml实现的树结构编辑器, 功能包括:

树结构的缩进 节点展开、折叠 添加节点 删除节点 重命名节点 搜索 导入 导出 节点属性编辑(完善中)

原理说明

数据model的实现,使用C++,继承于QAbstractListModel,并实现rowCount、data等方法。

model本身是List结构的,在此基础上,对model数据进行扩展以模拟树结构,例如增加了 “节点深度”、“是否有子节点”等数据段。

view使用Qml Controls 2中的ListView模拟实现(Controls 1 中的TreeView即将被废弃)。

关键代码

model

基本model的声明如下:

template <typename T>
class TaoListModel : public QAbstractListModel {
public:
    //声明父类
    using Super = QAbstractListModel;

    TaoListModel(QObject* parent = nullptr);
    TaoListModel(const QList<T>& nodeList, QObject* parent = nullptr);

    const QList<T>& nodeList() const
    {
        return m_nodeList;
    }
    void setNodeList(const QList<T>& nodeList);

    int rowCount(const QModelIndex& parent) const override;

    QVariant data(const QModelIndex& index, int role) const override;
    bool setData(const QModelIndex& index, const QVariant& value, int role) override;


    bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
    bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;

    Qt::ItemFlags flags(const QModelIndex& index) const override;
    Qt::DropActions supportedDropActions() const override;

protected:
    QList<T> m_nodeList;
};

其中数据成员使用 QList m_nodeList 存储, 大部分成员函数是对此数据的操作。

Json格式的model声明如下:

const static QString cDepthKey = QStringLiteral("TModel_depth");
const static QString cExpendKey = QStringLiteral("TModel_expend");
const static QString cChildrenExpendKey = QStringLiteral("TModel_childrenExpend");
const static QString cHasChildendKey = QStringLiteral("TModel_hasChildren");
const static QString cParentKey = QStringLiteral("TModel_parent");
const static QString cChildrenKey = QStringLiteral("TModel_children");

const static QString cRecursionKey = QStringLiteral("subType");
const static QStringList cFilterKeyList = { cDepthKey, cExpendKey, cChildrenExpendKey, cHasChildendKey, cParentKey, cChildrenKey };
class TaoJsonTreeModel : public TaoListModel<QJsonObject> {
    Q_OBJECT
    Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
    //声明父类
    using Super = TaoListModel<QJsonObject>;
    //从json文件读入数据
    Q_INVOKABLE void loadFromJson(const QString& jsonPath, const QString& recursionKey = cRecursionKey);
    //导出到json文件
    Q_INVOKABLE bool saveToJson(const QString& jsonPath, bool compact = false) const;
    Q_INVOKABLE void clear();
    //设置指定节点的数值
    Q_INVOKABLE void setNodeValue(int index, const QString &key, const QVariant &value);
    //在index添加子节点。刷新父级,返回新项index
    Q_INVOKABLE int addNode(int index, const QJsonObject& json);
    Q_INVOKABLE int addNode(const QModelIndex& index, const QJsonObject& json)
    {
        return addNode(index.row(), json);
    }
    //删除。递归删除所有子级,刷新父级
    Q_INVOKABLE void remove(int index);
    Q_INVOKABLE void remove(const QModelIndex& index)
    {
        remove(index.row());
    }
    Q_INVOKABLE QList<int> search(const QString& key, const QString& value, Qt::CaseSensitivity cs = Qt::CaseInsensitive) const;
    //展开子级。只展开一级,不递归
    Q_INVOKABLE void expand(int index);
    Q_INVOKABLE void expand(const QModelIndex& index)
    {
        expand(index.row());
    }
    //折叠子级。递归全部子级。
    Q_INVOKABLE void collapse(int index);
    Q_INVOKABLE void collapse(const QModelIndex& index)
    {
        collapse(index.row());
    }
    //展开到指定项。递归
    Q_INVOKABLE void expandTo(int index);
    Q_INVOKABLE void expandTo(const QModelIndex& index)
    {
        expandTo(index.row());
    }
    //展开全部
    Q_INVOKABLE void expandAll();

    //折叠全部
    Q_INVOKABLE void collapseAll();


    int count() const;

    Q_INVOKABLE QVariant data(int idx, int role = Qt::DisplayRole) const
    {
        return Super::data(Super::index(idx), role);
    }
signals:
    void countChanged();
    ...
};

TaoJsonTreeModel继承于TaoListModel,并提供大量Q_INVOKABLE函数,以供Qml调用。

view

TreeView的模拟实现如下:

Item {
    id: root
    readonly property string __depthKey: "TModel_depth"
    readonly property string __expendKey: "TModel_expend"
    readonly property string __childrenExpendKey: "TModel_childrenExpend"
    readonly property string __hasChildendKey: "TModel_hasChildren"

    readonly property string __parentKey: "TModel_parent"
    readonly property string __childrenKey: "TModel_children"
    ...
    ListView {
        id: listView
        anchors.fill: parent
        currentIndex: -1
        delegate: Rectangle {
            id: delegateRect
            width: listView.width
            color: (listView.currentIndex === index || area.hovered) ? config.normalColor : config.darkerColor
            // 根据 expaned 判断是否展开,不展开的情况下高度为0
            height: model.display[__expendKey] === true ? 35 : 0
            // 优化。高度为0时visible为false,不渲染。
            visible: height > 0
            property alias editable: nameEdit.editable
            property alias editItem: nameEdit
            TTextInput {
                id: nameEdit
                anchors.verticalCenter: parent.verticalCenter
                //按深度缩进
                x: root.basePadding + model.display[__depthKey] * root.subPadding
                text: model.display["name"]
                height: parent.height
                width: parent.width * 0.8 - x
                editable: false
                onTEditFinished: {
                    sourceModel.setNodeValue(index, "name", displayText)
                }
            }
            TTransArea {
                id: area
                height: parent.height
                width: parent.width - controlIcon.x
                hoverEnabled: true
                acceptedButtons: Qt.LeftButton | Qt.RightButton
                onPressed: {
                    //单击时切换当前选中项
                    if (listView.currentIndex !== index) {
                        listView.currentIndex = index;
                    } else {
                        listView.currentIndex = -1;
                    }
                }
                onTDoubleClicked: {
                    //双击进入编辑状态
                    delegateRect.editable = true;
                    nameEdit.forceActiveFocus()
                    nameEdit.ensureVisible(0)
                }
            }
            Image {
                id: controlIcon
                anchors {
                    verticalCenter: parent.verticalCenter
                    right: parent.right
                    rightMargin: 20
                }
                //有子节点时,显示小图标
                visible: model.display[__hasChildendKey]
                source: model.display[__childrenExpendKey] ? "qrc:/img/collapse.png" : "qrc:/img/expand.png"
                MouseArea {
                    anchors.fill: parent
                    onClicked: {
                        //点击小图标时,切换折叠、展开的状态
                        if (model.display[__hasChildendKey]) {
                            if( true === model.display[__childrenExpendKey]) {
                                collapse(index)
                            } else {
                                expand(index)
                            }
                        }
                    }
                }
            }
        }
    }
    ...
}

model层并没有扩展role,而是在data函数的role为display时直接返回json数据,

所以delegate中统一使用model.display[xxx]的方式访问数据。

性能测试

测试环境

CPU: Intel i5-8400 2.8GHz 六核

内存: 16GB

OS: Windows10 1909

Qt: 5.12.6

编译器: msvc 2017 x64

测试框架: QTest

测试方法

数据生成

使用node表示根节点的数量,depth表示每个根节点下面嵌套节点的层数。

例如: node 等于 100, depth 等于10,则数据如下:

预览

顶层有100个节点,每个节点下面再嵌套10层,共计节点 100 + 100 * 10 = 1100.

生成json数据的代码如下:

...
//单元测试类
class LoadTest : public QObject {
    Q_OBJECT

public:
    LoadTest();
    ~LoadTest();

    static void genJson(const QPoint& point);

    ...
//私有槽函数会被QTest调用
private slots:
    //初始化
    void initTestCase();
    //清理
    void cleanupTestCase();
    //测试导入
    void test_load();
    //测试导入前,准备数据
    void test_load_data();
    //测试导出
    void test_save();
    //测试导出前,准备数据
    void test_save_data();
};
...
//节点最大值
const int nodeMax = 10000;
//嵌套深度最大值
const int depthMax = 100;

void LoadTest::genJson(const QPoint& point)
{
    using namespace TaoCommon;
    int node = point.x();
    int depth = point.y();
    QJsonArray arr;
    for (int i = 0; i < node; ++i) {
        QJsonObject obj;
        obj["name"] = QString("node_%1").arg(i);
        QVector<QJsonArray> childrenArr = { depth, QJsonArray { QJsonObject {} } };
        //最后一个节点,嵌套层级最深的。
        childrenArr[depth - 1][0] = QJsonObject { { "name", QString("node_%1_%2").arg(i).arg(depth - 1) } };
        //从后往前倒推。
        for (int j = depth - 2; j >= 0; --j) {
            childrenArr[j][0] = QJsonObject { { cRecursionKey, childrenArr[j + 1] }, { "name", QString("node_%1_%2").arg(i).arg(j) } };
        }
        obj[cRecursionKey] = childrenArr[0];
        arr.append(obj);
    }
    writeJsonFile(qApp->applicationDirPath() + QString("/%1_%2.json").arg(node).arg(depth), arr);
}

void LoadTest::initTestCase()
{
    QList<QPoint> list;
    for (int i = 1; i <= nodeMax; i *= 10) {
        for (int j = 1; j <= depthMax; j *= 10) {
            list.append({ i, j });
        }
    }
    auto result = QtConcurrent::map(list, &LoadTest::genJson);
    result.waitForFinished();
}

初始化函数initTestCase中,组织了一个QList,然后使用QtConcurrent::map并发调用genJson函数,生成数据json文件。

node和depth每次扩大10倍。

经过测试,嵌套层数在100以上时,Qt可能会崩溃。要么是QJsonDocument无法解析,要么是Qml挂掉。所以不使用100以上的嵌套级别。

测试过程

QTest十分好用,简单易上手,参考帮助文件即可

例如测试加载的代码如下:

void LoadTest::prepareData()
{
    //添加两列数据
    QTest::addColumn<int>("node");
    QTest::addColumn<int>("depth");
    //添加行
    for (int i = 1; i <= nodeMax; i *= 10) {
        for (int j = 1; j <= depthMax; j *= 10) {
            QTest::newRow(QString("%1_%2").arg(i).arg(j).toStdString().c_str()) << i << j;
        }
    }
}
void LoadTest::test_load_data()
{
    //准备数据
    prepareData();
}
void LoadTest::test_load()
{
    using namespace TaoCommon;
    //取数据
    QFETCH(int, node);
    QFETCH(int, depth);
    TaoJsonTreeModel model;
    //性能测试
    QBENCHMARK
    {
        model.loadFromJson(qApp->applicationDirPath() + QString("/%1_%2.json").arg(node).arg(depth));
    }
}

测试结果

预览

一秒内最多可以加载的数据量在十万级别,包括

10000 x 10耗时在 386毫秒,1000 x 100 耗时在671毫秒。