treeview

QML自定义一个TreeView,使用ListView递归


在 Qt5 的 QtQuick.Controls 2.x 中还没有 TreeView 这个控件(在 Qt6 中出了一个继承自 TableView 的 TreeView),而且 QtQuick.Controls 1.x 中的也需要配合 C++ model 来自定义,对于一些简单的需求还要写 C++ model 就显得费事儿。

参照别人的自定义 TreeView 实现,我也使用 ListView 嵌套的方式做了一个简易的 TreeView,主要功能就是根据 json 结构来生成一个树状列表。

(2022-05-28)在最初的版本没考虑 ListView 超过 cache 范围自动销毁的问题,改版后勾选等状态放到外部数组中,释放后重新创建 delegate item 时,会从外部数组中取值进行判断该 item 是否勾选。

1.展示
先看看ui效果图-1和自己的demo图-2。三级目录,其中最后一级是勾选框。我用黑色表示无子项,白色有子项已展开,灰色有子项未展开,绿色已勾选,红色未勾选。实际使用时会替换为image。

     

//PS.后面完善了下,效果如下图

代码github:https://github.com/gongjianbo/QmlTreeView

主要代码:

//TreeView.qml

import QtQuick 2.7
import QtQuick.Controls 2.7
 
//代码仅供参考,很多地方都不完善
//还有一些样式没有导出,可以根据需求自己定义
//因为ListView有回收策略,除非cacheBuffer设置很大,所以状态不能保存在delegate.item中
//需要用外部变量或者model来存储delegate.item的状态
Rectangle {
    id: control
 
    property string currentItem //当前选中item
 
    property int spacing: 10    //项之间距离
    property int indent: 5      //子项缩进距离,注意实际还有icon的距离
    property string onSrc: "qrc:/img/on.png"
    property string offSrc: "qrc:/img/off.png"
    property string checkedSrc: "qrc:/img/check.png"
    property string uncheckSrc: "qrc:/img/uncheck.png"
 
    property var checkedArray: [] //当前已勾选的items
    property bool autoExpand: true
 
    //背景
    color: Qt.rgba(2/255,19/255,23/255,128/255)
    border.color: "darkCyan"
    property alias model: list_view.model
    ListView {
        id: list_view
        anchors.fill: parent
        anchors.margins: 10
        //model: //model由外部设置,通过解析json
        property string viewFlag: ""
        delegate: list_delegate
        clip: true
        onModelChanged: {
            console.log('model change')
            checkedArray=[]; //model切换的时候把之前的选中列表清空
        }
    }
    Component {
        id: list_delegate
        Row{
            id: list_itemgroup
            spacing: 5
            property string parentFlag: ListView.view.viewFlag
            //以字符串来标记item
            //字符串内容为parent.itemFlag+model.index
            property string itemFlag: parentFlag+"-"+(model.index+1)
 
            //canvas 画项之间的连接线
            Canvas {
                id: list_canvas
                width: item_titleicon.width+10
                height: list_itemcol.height
                //开了反走样,线会模糊看起来加粗了
                antialiasing: false
                //最后一项的连接线没有尾巴
                property bool isLastItem: (model.index===list_itemgroup.ListView.view.count-1)
                onPaint: {
                    var ctx = getContext("2d")
                    var i=0
                    //ctx.setLineDash([4,2]); 遇到个大问题,不能画虚线
                    // setup the stroke
                    ctx.strokeStyle = Qt.rgba(201/255,202/255,202/255,1)
                    ctx.lineWidth=1
                    // create a path
                    ctx.beginPath()
                    //用短线段来实现虚线效果,判断里-3是防止width(4)超过判断长度
                    //此外还有5的偏移是因为我image是透明背景的,为了不污染到图标
                    //这里我是虚线长4,间隔2,加起来就是6一次循环
                    //效果勉强
                    ctx.moveTo(width/2,0) //如果第一个item虚线是从左侧拉过来,要改很多
                    for(i=0;i<list_itemrow.height/2-5-3;i+=6){
                        ctx.lineTo(width/2,i+4);
                        ctx.moveTo(width/2,i+6);
                    }
 
                    ctx.moveTo(width/2+5,list_itemrow.height/2)
                    for(i=width/2+5;i<width-3;i+=6){
                        ctx.lineTo(i+4,list_itemrow.height/2);
                        ctx.moveTo(i+6,list_itemrow.height/2);
                    }
 
                    if(!isLastItem){
                        ctx.moveTo(width/2,list_itemrow.height/2+5)
                        for(i=list_itemrow.height/2+5;i<height-3;i+=6){
                            ctx.lineTo(width/2,i+4);
                            ctx.moveTo(width/2,i+6);
                        }
                        //ctx.lineTo(10,height)
                    }
                    // stroke path
                    ctx.stroke()
                }
 
                //项图标框--可以是ractangle或者image
                Image {
                    id: item_titleicon
                    visible: false
                    //如果是centerIn的话展开之后就跑到中间去了
                    anchors.left: parent.left
                    anchors.top: parent.top
                    anchors.leftMargin: list_canvas.width/2-width/2
                    anchors.topMargin: list_itemrow.height/2-width/2
                    //根据是否有子项/是否展开加载不同的图片/颜色
                    //color: item_repeater.count
                    //      ?item_sub.visible?"white":"gray"
                    //:"black"
                    //这里没子项或者子项未展开未off,展开了为on
                    source: item_repeater.count?item_sub.visible?offSrc:onSrc:offSrc
 
                    MouseArea{
                        anchors.fill: parent
                        onClicked: {
                            if(item_repeater.count)
                                item_sub.visible=!item_sub.visible;
                        }
                    }
                }
 
                //项勾选框--可以是ractangle或者image
                Image {
                    id: item_optionicon
                    visible: false
                    width: 10
                    height: 10
                    anchors.left: parent.left
                    anchors.top: parent.top
                    anchors.leftMargin: list_canvas.width/2-width/2
                    anchors.topMargin: list_itemrow.height/2-width/2
                    property bool checked: isChecked(itemFlag)
                    //勾选框
                    //color: checked
                    //       ?"lightgreen"
                    //       :"red"
                    source: checked?checkedSrc:uncheckSrc
                    MouseArea {
                        anchors.fill: parent
                        onClicked: {
                            item_optionicon.checked=!item_optionicon.checked;
                            if(item_optionicon.checked){
                                check(itemFlag);
                            }else{
                                uncheck(itemFlag);
                            }
                            var str="checked ";
                            for(var i in checkedArray)
                                str+=String(checkedArray[i])+" ";
                            console.log(str)
                        }
                    }
                }
 
            }
 
            //项内容:包含一行item和子项的listview
            Column {
                id: list_itemcol
 
                //这一项的内容,这里只加了一个text
                Row {
                    id: list_itemrow
                    width: control.width
                    height: item_text.contentHeight+control.spacing
                    spacing: 5
 
                    Rectangle {
                        height: item_text.contentHeight+control.spacing
                        width: parent.width
                        anchors.verticalCenter: parent.verticalCenter
                        color: (control.currentItem===itemFlag)
                               ?Qt.rgba(101/255,255/255,255/255,38/255)
                               :"transparent"
 
                        Text {
                            id: item_text
                            anchors.left: parent.left
                            anchors.verticalCenter: parent.verticalCenter
                            text: modelData.text
                            font.pixelSize: 14
                            font.family: "Microsoft YaHei UI"
                            color: Qt.rgba(101/255,1,1,1)
                        }
 
                        MouseArea {
                            anchors.fill: parent
                            onClicked: {
                                control.currentItem=itemFlag;
                                console.log("selected",itemFlag)
                            }
                        }
                    }
 
                    Component.onCompleted: {
                        if(modelData.istitle){
                            item_titleicon.visible=true;
                        }else if(modelData.isoption){
                            item_optionicon.visible=true;
                        }
                    }
                }
 
                //放子项
                Column {
                    id: item_sub
                    //也可以像check一样用一个expand数组保存展开状态
                    visible: control.autoExpand
                    //上级左侧距离=小图标宽+x偏移
                    x: control.indent
                    Item {
                        width: 10
                        height: item_repeater.contentHeight
                        //需要加个item来撑开,如果用Repeator获取不到count
                        ListView {
                            id: item_repeater
                            anchors.fill: parent
                            delegate: list_delegate
                            model: modelData.subnodes
                            property string viewFlag: itemFlag
                        }
                    }
                }
 
            }
        }//end list_itemgroup
    }//end list_delegate
 
    //勾选时放入arr中
    function check(itemFlag){
        checkedArray.push(itemFlag);
    }
 
    //取消勾选时从arr移除
    function uncheck(itemFlag){
        var i = checkedArray.length;
 
        while (i--) {
            if (checkedArray[i] === itemFlag) {
                checkedArray.splice(i,1);
                break;
            }
        }
    }
 
    //判断是否check
    function isChecked(itemFlag){
        var i = checkedArray.length;
 
        while (i--) {
            if (checkedArray[i] === itemFlag) {
                return true;
            }
        }
        return false;
    }
}
//main.qml

import QtQuick 2.7
import QtQuick.Controls 2.7
import QtQuick.Window 2.7
 
Window {
    id: root_window
    visible: true
    width: 640
    height: 480
    title: qsTr("QmlTreeView By GongJianBo")
    color: Qt.rgba(3/255,26/255,35/255,1)
 
    //滚动条可以自己设置
    //ListView横向滚动条需要设置如下两个参数(如果是竖向的ListView)
    //contentWidth: 500
    //flickableDirection: Flickable.AutoFlickIfNeeded
    TreeView{
        id: item_tree
        width: parent.width/2
        anchors{
            left: parent.left
            top: parent.top
            bottom: parent.bottom
            margins: 10
        }
        //model: []
 
        //set model data
        Component.onCompleted: {
            console.log(1)
            root_window.setTestDataA();
            console.log(2)
        }
    }
 
    Column{
        anchors{
            right: parent.right
            top: parent.top
            margins: 10
        }
        spacing: 10
        Button{
            text: "ChangeModel"
            checkable: true
            //changed model data
            onClicked: {
                if(checked){
                    root_window.setTestDataB();
                }else{
                    root_window.setTestDataA();
                }
            }
        }
        Button{
            text: "AutoExpand"
            onClicked: item_tree.autoExpand=!item_tree.autoExpand
        }
    }
 
    function setTestDataA(){
        item_tree.model=JSON.parse('[
        {
            "text":"1 one",
            "istitle":true,
            "subnodes":[
                {"text":"1-1 two","istitle":true},
                {
                    "text":"1-2 two",
                    "istitle":true,
                    "subnodes":[
                        {"text":"1-2-1 three","isoption":true},
                        {"text":"1-2-2 three","isoption":true}
                    ]
                }
            ]
        },
        {
            "text":"2 one",
            "istitle":true,
            "subnodes":[
                {"text":"2-1 two","istitle":true},
                {
                    "text":"2-2 two",
                    "istitle":true,
                    "subnodes":[
                        {"text":"2-2-1 three","isoption":true},
                        {"text":"2-2-2 three","isoption":true}
                    ]
                }
            ]
        },
        {
            "text":"3 one",
            "istitle":true,
            "subnodes":[
                {"text":"3-1 two","istitle":true},
                {"text":"3-2 two","istitle":true}
            ]
        },
        {
            "text":"4 one",
            "istitle":true,
            "subnodes":[
                {"text":"4-1 two","istitle":true},
                {
                    "text":"4-2 two",
                    "istitle":true,
                    "subnodes":[
                        {"text":"4-2-1 three","isoption":true},
                        {"text":"4-2-2 three","isoption":true}
                    ]
                }
            ]
        },
        {
            "text":"5 one",
            "istitle":true,
            "subnodes":[
                {"text":"5-1 two","istitle":true},
                {
                    "text":"5-2 two",
                    "istitle":true,
                    "subnodes":[
                        {"text":"5-2-1 three","isoption":true},
                        {"text":"5-2-2 three","isoption":true}
                    ]
                }
            ]
        },
        {
            "text":"6 one",
            "istitle":true,
            "subnodes":[
                {"text":"6-1 two","istitle":true},
                {"text":"6-2 two","istitle":true}
            ]
        }
    ]')
    }
 
    function setTestDataB(){
        item_tree.model=JSON.parse('[
        {
            "text":"1 one",
            "istitle":true,
            "subnodes":[
                {
                    "text":"1-1 two",
                    "istitle":true,
                    "subnodes":[
                        {"text":"1-1-1 three","isoption":true},
                        {"text":"1-1-2 three","isoption":true}
                    ]
                },
                {
                    "text":"1-2 two",
                    "istitle":true,
                    "subnodes":[
                        {"text":"1-2-1 three","isoption":true},
                        {"text":"1-2-2 three","isoption":true}
                    ]
                }
            ]
        },
        {
            "text":"2 one",
            "istitle":true,
            "subnodes":[
                {"text":"2-1 two","istitle":true},
                {
                    "text":"2-2 two",
                    "istitle":true,
                    "subnodes":[
                        {"text":"2-2-1 three","isoption":true},
                        {"text":"2-2-2 three","isoption":true}
                    ]
                }
            ]
        },
        {"text":"3 one","istitle":true},
        {
            "text":"4 one",
            "istitle":true,
            "subnodes":[
                {"text":"4-1 two","istitle":true},
                {"text":"4-2 two","istitle":true}
            ]
        }
    ]')
    }
}
2.参考
思路参考:https://github.com/peihaowang/QmlTreeWidget

ListView 文档:https://doc.qt.io/qt-5/qml-qtquick-listview.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/358430.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

3d模型上的材质怎么删除---模大狮模型网

在大多数3D软件中&#xff0c;可以通过以下步骤来删除3D模型上的材质&#xff1a; 选择要删除材质的模型&#xff1a;首先&#xff0c;从场景中选择包含目标材质的模型。可以使用选择工具或按名称查找模型。 进入编辑模式&#xff1a;将模型切换到编辑模式。这通常需要选择相应…

力扣(leetcode)第118题杨辉三角(Python)

118.杨辉三角 题目链接&#xff1a;118.杨辉三角 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 示例 1: 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] …

MIDI码深度解析

MIDI 协议即数字音乐接口&#xff08;Musical Instrument Digital Interface&#xff09;&#xff0c;是电子乐器、合成器等演奏设备之间的一种即时通信协议&#xff0c;用于硬件之间的实时演奏数据传递。如果理解还不够深刻&#xff0c;官方如下解释&#xff1a; 常用midi硬件…

超强的AI写简历软件

你们在制作简历时&#xff0c;是不是基本只关注两件事&#xff1a;简历模板&#xff0c;还有基本信息的填写。 当你再次坐下来更新你的简历时&#xff0c;可能会发现自己不自觉地选择了那个“看起来最好看的模板”&#xff0c;填写基本信息&#xff0c;却没有深入思考如何使简历…

15EG使用ps点亮mio的led

创建工程模板在hello_world中已经介绍过了&#xff0c;这里直接从配置完zynq 开始 因为要用到ps的GPIO&#xff0c;所以要对ZYNQ进行额外的配置&#xff0c;双击ZYNQ打开配置->打开IO口配置->勾选GPIO0 MIO外设。我们可以在原理图中看到mio的led引脚为MIO24和MIO25&#…

Django模型(四)

一、数据操作初始化 from django.db import models# Create your models here. class Place(models.Model):"""位置信息"""name = models.CharField(max_length=32,verbose_name=地名)address = models.CharField(max_length=64,null=True,verbo…

代码随想录算法训练营29期|day34 任务以及具体任务

第八章 贪心算法 part03 1005.K次取反后最大化的数组和 class Solution {public int largestSumAfterKNegations(int[] nums, int K) {// 将数组按照绝对值大小从大到小排序&#xff0c;注意要按照绝对值的大小nums IntStream.of(nums).boxed().sorted((o1, o2) -> Math.ab…

再学css

盒模型 有两种&#xff0c; IE盒子模型、W3C盒子模型&#xff1b;盒模型&#xff1a; 内容(content)、填充(padding)、边界(margin)、 边框(border)&#xff1b;区 别&#xff1a; IE的content部分把 border 和 padding计算了进去; 标准盒子模型的模型图 从上图可以看到&#x…

Coremail启动鸿蒙原生应用开发,打造全场景邮件办公新体验

1月18日&#xff0c;华为在深圳举行鸿蒙生态千帆启航仪式&#xff0c;Coremail出席仪式并与华为签署鸿蒙合作协议&#xff0c;宣布正式启动鸿蒙原生应用开发。作为首批拥抱鸿蒙的邮件领域伙伴&#xff0c;Coremail的加入标志着鸿蒙生态版图进一步完善。 Coremail是国内自建邮件…

MIT6.1810/Fall 2022(which was called 6.S081 then) Lab5-7

Lab: Copy-on-Write Fork for xv6 8.4 Copy On Write Fork - MIT6.S081 先理解COW机制 Implement copy-on-write fork 您的任务是在xv6内核中实现写时复制分叉。如果修改后的内核成功地执行了cowtest和usertests -q程序&#xff0c;那么就完成了。 为了帮助您测试实现&#…

【计算机网络】——TCP协议

&#x1f4d1;前言 本文主要是【计算机网络】——传输层TCP协议的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f304;每日一句…

获取依赖aar包的两种方式-在android studio里引入 如:glide

背景&#xff1a;我需要获取aar依赖到内网开发&#xff0c;内网几乎代表没网。 一、 如何需要获取依赖aar包 方式一&#xff1a;在官方的github中下载,耗时不建议 要从开发者网站、GitHub 存储库或其他来源获取 ‘com.github.bumptech.glide:glide:4.12.0’ AAR 包&#xff…

下拉框联动 类似于请求第一个框之后,携带参数请求后端接口,渲染第二个下来框

直接上代码 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>1233</title></head><body><div><!-- <table><tr><td><div class"mdui-col"><input><…

PyTorch][chapter 12][李宏毅深度学习][Semi-supervised Linear Methods-1]

这里面介绍半监督学习里面一些常用的方案&#xff1a; K-means ,HAC, PCA 等 目录&#xff1a; K-means HAC PCA 一 K-means 【预置条件】 N 个样本分成k 个 簇 step1: 初始化簇中心点 (随机从X中抽取k个样本点作为&#xff09; Repeat: For all in X: 根据其到 &…

仿真APP在金属波纹管液压胀形工艺设计中的应用

一、背景介绍 金属波纹管是带有波纹状截面的金属管状零件&#xff0c;在工业中应用广泛。金属波纹管特殊的截面形状使其具备较好的柔韧性&#xff0c;能够在一定范围内伸缩弯曲。这一特性赋予波纹管两大用途&#xff1a;一是作为变形补偿器&#xff0c;可用于补偿管道设备由于…

转盘寿司 - 华为OD统一考试

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 寿司店周年庆&#xff0c;正在举办优惠活动回馈新老用户。 寿司转盘上总共有 n 盘寿司&#xff0c; prices[i] 是第 i 盘寿司的价格。 如果客户选择了第 i 盘寿…

【论文阅读】Long-Tailed Recognition via Weight Balancing(CVPR2022)附MaxNorm的代码

目录 论文使用方法weight decayMaxNorm 如果使用原来的代码报错的可以看下面这个 论文 问题&#xff1a;真实世界中普遍存在长尾识别问题&#xff0c;朴素训练产生的模型在更高准确率方面偏向于普通类&#xff0c;导致稀有的类别准确率偏低。 key:解决LTR的关键是平衡各方面&a…

网络原理-TCP/IP(1)

应用层 我们之前编写完了基本的java socket, 要知道,我们之前所写的所有代码都在应用层中,都是为了完成某项业务,如翻译等.关于应用层,后面会有专门的讲解,在此处先讲一下基础知识. 应用层对应着应用程序,是程序员打交道最多的一层,调用系统提供的网络api写出的代码都是应用层…

如何安装配置HFS并实现无公网ip远程访问本地电脑共享文件

文章目录 前言1.下载安装cpolar1.1 设置HFS访客1.2 虚拟文件系统 2. 使用cpolar建立一条内网穿透数据隧道2.1 保留隧道2.2 隧道名称2.3 成功使用cpolar创建二级子域名访问本地hfs 总结 前言 在大厂的云存储产品热度下降后&#xff0c;私人的NAS热度快速上升&#xff0c;其中最…

【webrtc】m98 : vs2019 直接构建webrtc及moduletest工程 2

字数有限制,我们继续 【webrtc】m98 : vs2019 直接构建webrtc及unitest工程 1modules_unittests 构建 Build started... 1>------ Build started: Project: modules_unittests, Configuration: GN Win32 ------ 1>ninja: Entering directory `G:\CDN\rtcCli\m98\src\o…