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



















