QML之TreeView的使用

本文讲述了在使用QML中的TreeView时,如何自定义Model并处理动态添加和删除Item时界面不自动更新的问题,通过添加`emitlayoutAboutToBeChanged();emitlayoutChanged();`解决了这一问题,同时展示了相关代码和模型类的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先上效果图

QML中TreeView的使用总体比较简单,重点还是在于自定义Model的编写上,也踩了一个大坑,就是动态添加和删除Item的时候,界面似乎没有自动更新,最后加了两行代码才解决:

        emit layoutAboutToBeChanged();

        emit layoutChanged();

如果有大神知道其他解决办法的,请评论区发出来。

注: 测试环境为Opensuse 15.5 + Qt 6.6.2

下面贴出关键代码:

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

import QtQuick 6.5
import NetTool
import QtQuick.Controls 6.5
import Qt.labs.qmlmodels
import content

Window {
    id: window
    width: 400
    height: 300

    visible: true
    title: "NetTool"


    SplitView{
        anchors.fill: parent
        orientation: Qt.Horizontal

        TreeView {
            id:treeView
            SplitView.maximumWidth: Math.max(350, parent.width/2)
            SplitView.minimumWidth: Math.min(200, parent.width/4)
            SplitView.fillHeight: true
            columnSpacing: 0
            rowSpacing: 0

            ScrollBar.horizontal: ScrollBar {}
            ScrollBar.vertical: ScrollBar {}
            ScrollIndicator.horizontal: ScrollIndicator {}
            ScrollIndicator.vertical: ScrollIndicator {}
            boundsBehavior: Flickable.StopAtBounds
            clip: true
            model: TreeModel{
                onRowsInserted:(index, first, last)=> {
                    console.log("onRowsInserted: index=" + index + ", first=" +  first + ", last=" + last)
                }
            }
            selectionModel: ItemSelectionModel {
               // model: tableView.model

                onCurrentChanged: (cur, pre)=>{
                    let item = treeView.itemAtIndex(pre);
                    if(item != null){
                          let itemPre = treeView.itemAtIndex(pre).configWnd
                          itemPre.parent = treeView.itemAtIndex(pre);
                          itemPre.visible = false;
                          console.log("hide " + pre)
                    }

                    let itemCur = treeView.itemAtIndex(cur).configWnd
                    itemCur.visible = true
                    itemCur.parent = main
                    itemCur.port = cur.row     //只是为了测试是否选中了
                    console.log("show " + cur)
                }
            }

            Menu{
                id: menu_delete
                MenuItem {
                    text: qsTr("删除")
                    onTriggered: {
                        console.log("删除row=" + treeView.currentRow + " col=" + treeView.currentColumn)
                        let index = treeView.selectionModel.currentIndex
                        treeView.model.removeRow(index.row, index.parent)
                    }
                }
            }

            Menu{
                id: menu_append
                MenuItem {
                    text: qsTr("添加")
                    onTriggered: {
                        console.log("添加row=" + treeView.currentRow + " col=" + treeView.currentColumn)
                        let index = treeView.selectionModel.currentIndex
                        //treeView.model.insertRow(treeView.model.rowCount(index), index)
                        treeView.model.insertRow(0, index)
                    }
                }
            }

            delegate: TreeViewDelegate{
                id: control
                implicitWidth: treeView.width
                implicitHeight: 35
                leftMargin: 5
                rightMargin: 5
                spacing: 5
                indentation: 20

                function getData(role){
                    let model = control.treeView.model
                    let index = control.treeView.index(row, column)
                    return model.data(index, role)
                }

                function setData(role, value){
                    let model = control.treeView.model
                    let index = control.treeView.index(row, column)
                    model.setData(index, value, role)
                }

                //最初的设想是每一项都关联一个界面,鼠标点击时切换过去
                property var configWnd : {
                    // let strWnd = `import  QtQuick; ConfigUI{anchors.fill: parent; visible:false}`
                    // return Qt.createQmlObject(strWnd, control, "")
                    var component = Qt.createComponent("ConfigUI.qml");   //动态创建QML对象的方式之一
                    return component.createObject(control, {visible:false});
                }

                MouseArea{
                    anchors.fill: parent
                    acceptedButtons: Qt.RightButton | Qt.LeftButton
                    propagateComposedEvents: true

                    onPressed: (mouse)=>{
                        let index = control.treeView.index(row, column)
                        console.log("id=" + control + ", row=" + row + ", col=" + column + ", index=" + index)
                        mouse.accepted = false

                        if(mouse.button === Qt.RightButton)
                        {
                            if(depth === 2)
                                menu_delete.popup()
                            else  if(depth == 1)
                                menu_append.popup()

                            treeView.selectionModel.clearCurrentIndex()
                            treeView.selectionModel.clearSelection()
                            treeView.selectionModel.setCurrentIndex(index, 2)
                        }
                    }
                }
                contentItem: Label {
                    id:content_label
                    text:{
                        return getData(Qt.DisplayRole)
                    }
                }

                TableView.editDelegate: FocusScope {
                    width: parent.width
                    height: parent.height

                    TextField {
                        id: textField
                        x: control.contentItem.x
                        y: (parent.height - height) / 2
                        width: control.contentItem.width
                        text: display
                        focus: true
                    }

                    TableView.onCommit: {
                        let index = control.treeView.index(row, column)
                        TableView.view.model.setData(index, Qt.EditRole, textField.text)
                        content_label.text = textField.text
                    }
                }
            }
        }

        Rectangle{
            id: main
            color:"lightblue"
            SplitView.fillHeight: true
        }
    }

}

#ifndef TREEMODEL_H
#define TREEMODEL_H
#include <qqml.h>
#include <QAbstractItemModel>
#include <memory>
#include <vector>
#include <map>
#include <QVariant>
using namespace std;

struct TreeNode
{
    shared_ptr<TreeNode> m_pParent;
    vector<shared_ptr<TreeNode>> m_vChild;
    QString m_strName;
    map<QString, QVariant> m_mapAttribute;
};

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT
    QML_ELEMENT
public:
    explicit TreeModel(QObject *parent = nullptr);

    // Header:
    QVariant headerData(int section,
                        Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const override;

    bool setHeaderData(int section,
                       Qt::Orientation orientation,
                       const QVariant &value,
                       int role = Qt::EditRole) override;

    // Basic functionality:
    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;

    // Fetch data dynamically:
    bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;

    bool canFetchMore(const QModelIndex &parent) const override;
    void fetchMore(const QModelIndex &parent) override;

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

    // Editable:
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;

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

    // Add data:
    bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
    bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override;

    // Remove data:
    bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
    bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override;
private:
    vector<shared_ptr<TreeNode>> m_vData;
    QList<QString> m_vHorHeader;
};

#endif // TREEMODEL_H
#include "treemodel.h"

TreeModel::TreeModel(QObject *parent)
    : QAbstractItemModel{parent}
{
    for(auto pro : {"tcp", "udp", "websocket", "http"})
    {
        shared_ptr<TreeNode> p = make_shared<TreeNode>();
        p->m_pParent = nullptr;
        p->m_strName = pro;
        for(auto type : {"客户端", "服务端"})
        {
            shared_ptr<TreeNode> pType = make_shared<TreeNode>();
            pType->m_strName = type;
            pType->m_pParent = p;
            for(int i = 0; i < 10; i++)
            {
                auto pChild = make_shared<TreeNode>();
                pChild->m_strName = QString("127.0.0.1:%1").arg(8000+i);
                pChild->m_pParent = pType;
                pType->m_vChild.push_back(pChild);
            }
            p->m_vChild.push_back(pType);
        }

        m_vData.push_back(p);
    }
}

// Header:
QVariant TreeModel::headerData(int section,
                    Qt::Orientation orientation,
                    int role) const
{
    qDebug() << "headerData";
    if(orientation == Qt::Orientation::Horizontal && role == Qt::DisplayRole && m_vHorHeader.size() < section && section >= 0)
        return m_vHorHeader[section];
    return "";
}

bool TreeModel::setHeaderData(int section,
                   Qt::Orientation orientation,
                   const QVariant &value,
                   int role)
{
    qDebug() << "setHeaderData";
    if(orientation == Qt::Orientation::Horizontal && role == Qt::DisplayRole)
    {
        m_vHorHeader[section] = value.toString();
        emit headerDataChanged(orientation, section, section);
        return true;
    }

    return false;
}


// Basic functionality:
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if(hasIndex(row, column, parent) == false)
        return {};

    TreeNode* p = (TreeNode*)parent.internalPointer();
    if(p == nullptr)
        return createIndex(row, 0, m_vData[row].get());
    return createIndex(row, 0, p->m_vChild[row].get());
}

QModelIndex TreeModel::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return {};

    TreeNode* p = (TreeNode*)index.internalPointer();
    int row = 0;
    if(p == nullptr)
        return createIndex(-1, -1);

    if(p->m_pParent == nullptr)
        return createIndex(-1, -1);

    for(auto& item : p->m_pParent->m_vChild)
    {
        if(item.get() == p)
            return createIndex(row, 0,  p->m_pParent.get());
        row++;
    }
    return createIndex(-1, -1);
}


int TreeModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid() && parent.column() > 0)
        return 0;

    if(!parent.isValid())
        return m_vData.size();
    TreeNode* p = (TreeNode*)parent.internalPointer();
    return p->m_vChild.size();
}

int TreeModel::columnCount(const QModelIndex &parent) const
{
    return 1;
}

bool TreeModel::hasChildren(const QModelIndex &parent ) const
{
    if(!parent.isValid())
        return false;
    TreeNode* p = (TreeNode*)parent.internalPointer();
    if(p == nullptr)
        return false;
    return !p->m_vChild.empty();
}

bool TreeModel::canFetchMore(const QModelIndex &parent) const
{
    return false;
}

void TreeModel::fetchMore(const QModelIndex &parent)
{
}

QVariant TreeModel::data(const QModelIndex &index, int role ) const
{
    TreeNode* p = (TreeNode*)index.internalPointer();
    if(p == nullptr)
        return "";
    return p->m_strName;
}

// Editable:
bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    TreeNode* p = (TreeNode*)index.internalPointer();
    p->m_strName = value.toString();
    emit dataChanged(index, index, {role});
    return true;
}

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::NoItemFlags;

    return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; // FIXME: Implement me!
}

// Add data:
bool TreeModel::insertRows(int row, int count, const QModelIndex &parent)
{
    qDebug() << "insertRows(): row=" << row << ", count=" << count;
    emit layoutAboutToBeChanged();
    beginInsertRows(parent, row, row + count - 1);
    TreeNode* p = (TreeNode*)parent.constInternalPointer();
    shared_ptr<TreeNode> pParent = nullptr;
    for(auto& item : p->m_pParent->m_vChild)
    {
        if(item.get() == p)
        {
            pParent = item;
            break;
        }
    }
    for(int i = 0; i < count; i++)
    {
        auto pNode = make_shared<TreeNode>();
        pNode->m_strName = "未命名";
        pNode->m_pParent = pParent;
        pParent->m_vChild.insert(pParent->m_vChild.begin()+row, pNode);
    }
    endInsertRows();
    emit layoutChanged();
    return true;
}

bool TreeModel::insertColumns(int column, int count, const QModelIndex &parent)
{
    return true;
}

// Remove data:
bool TreeModel::removeRows(int row, int count, const QModelIndex &parent)
{
    qDebug() << "removeRows()";
    emit layoutAboutToBeChanged();
    beginRemoveRows(parent, row, row + count - 1);
    TreeNode* p = (TreeNode*)parent.constInternalPointer();
    auto it = p->m_vChild.begin() + row;
    p->m_vChild.erase(it, it+count);
    endRemoveRows();
    emit layoutChanged();
    return true;
}

bool TreeModel::removeColumns(int column, int count, const QModelIndex &parent)
{
    beginRemoveColumns(parent, column, column + count - 1);
    // FIXME: Implement me!
    endRemoveColumns();
    return true;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值