65.9K
CodeProject 正在变化。 阅读更多。
Home

QML TreeModel 和 TreeView

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (7投票s)

2013 年 8 月 6 日

CPOL

6分钟阅读

viewsIcon

66472

使用 ListModel 和 ListView 实现 QML TreeModel 和 TreeView

引言

看起来 QML 中对 TreeView 和 Model 的快速实现存在迫切需求。本文将教你一种实现方法。请注意,为了实现 Model 和 View,我没有使用任何桌面组件。所有功能都可以通过旧的 QML 功能实现。你在最终程序中看到的额外导入仅仅是为了用户交互,它们并不是严格必需的。最终程序应该可以无需修改地运行。 

背景

受众应略微熟悉 QML。

首先,让我们考虑向 ListModel 添加数据的两种方法

示例 1

<1> 静态方式

ListModel {
   ListElement {
      role0: "Something"
      role1: 0
   }
   ListElement {
      role0: "SomethingElse"
      role1: 1
   }
}

<2> 动态实现相同功能

ListModel { 
   Component.onCompleted: {
      append({"role0": qsTr("Something"), "role1": 0})
      append({"role0": qsTr("SomethingElse"), "role1": 1})
   }
}

示例 2:一个更复杂的 Model

<1> 静态方式

ListModel {
   ListElement {
      role0: "ABC"
      contents: [
         ListElement {
            someRole0: "aqs"
            someRole1: 123
         },
         ListElement {
            someRole0: "qwer"
            someRole1: 12378
         }
      ]
   }  
   ListElement {
      role0: "ABC"
      contents: [
         ListElement {
            someRole0: "aqs"
            someRole1: 123
         },
         ListElement {
            someRole0: "qwer"
            someRole1: 12378
         }
      ]
   } 
}   

<2> 动态实现相同功能

ListModel {
   Component.onCompleted: {
      for(var i = 0; i < 2; ++i) {
         append({
                   "role0": qsTr("ABC"),
                   "contents": [
                      {"someRole0": qsTr("aqs"), "someRole1": 123},
                      {"someRole0": qsTr("qwer"), "someRole1": 12378}
                   ]
                })
      }
   }
} 

希望你已经明白了。要了解更多,请阅读有关 JavaScript 对象的内容。

使用代码

现在回到主题: 

在使用 ListModelListView 进行简单的 TreeView 实现时,这里使用了两个关键概念。

首先,我们使用一个递归组件——一个组件,它可以在运行时添加它自身,也就是它自己。这样的组件可能看起来像这样:

Component {
   id: objRecursiveComponent
   
   Column {
      Item {
         id: objData1
      }
           
      Repeater {
         model: someModel
         delegate: objRecursiveComponent
      }
   }
}

这里的 objData<n=1,2....> 可以看作是节点的成员数据,而 Repeater 用于添加进一步的子节点——这正是树的结构。

如果 someModel 是数字且大于 0,或者是一个长度大于 0 的对象列表,那么这将导致无限递归。我们必须提供一种方法,使得 someModel 对于 objRecursiveComponent 的每个新实例化对象都是可控的。一个简单的设想是公开一个对象列表,其中每个对象都有一个普通的 role 字段(objData1 将使用它),以及另一个 role 字段,它是一个包含与自身相似对象的对象列表。这个第二个成员可以作为 Repeatermodel 成员的源 Model(即上面的 someModel)。如果第二个成员是一个空的对象列表,递归就会停止,不会创建/附加进一步的子节点。任何此类对象列表的(动态)更新将由于 QML 绑定而直接影响树,添加节点到树或从树中删除节点。

因此,让我们构建一个允许在任何合法位置动态添加节点的树。一旦理解了这一点,其他操作都应该很简单。此外,树的每个节点将包含两个数据成员——一个名称字段(用于节点的名称),另一个是链(类似于上面的 Repeater),用于附加子节点(子节点)。

第二个概念是 Model 的 JavaScript 对象。如果你理解了本文背景部分中的示例,那么这个应该很容易理解。

ListModel 的 JavaScript 对象应该看起来像这样:

{"name": qsTr("Node-Name"), "level": 0, "subNode": []} 

level 字段仅用于一些附加信息,这将帮助我们廉价地缩进子节点——当然,我随时都不提供任何理想的解决方案,只是提供一点帮助来入门。

这是一个 ListModel

ListModel {
    id: objModel
}

要添加节点“Zero”、“One”和“Two”,你只需执行:

objModel.append({"name": qsTr("Zero"), "level": 0, "subNode": []})
objModel.append({"name": qsTr("One"), "level": 0, "subNode": []})
objModel.append({"name": qsTr("Two"), "level": 0, "subNode": []}) 

要将节点“Three”和“Four”添加到节点“One”下,你将编写:

objModel.get(1).subNode.append({"name": qsTr("Three"), "level": 1, "subNode": []})
objModel.get(1).subNode.append({"name": qsTr("Four"), "level": 1, "subNode": []})

要将“Five”添加到“Three”下:

objModel.get(1).subNode.get(0).subNode.append({"name": qsTr("Five"), "level": 2, "subNode": []})

希望以上内容都清晰明了。

现在,让我们为 ListView 创建一个 delegate。我们简单的 delegate 将具有以下基本属性:

  • 如果节点不包含进一步的子节点,则节点颜色应为黄色;否则为蓝色。
  • 子节点应有缩进(否则看起来会令人困惑)——为此,我们将使用 ListModel 中对象的 level 字段。
  • 带有子节点的节点应可折叠和展开。

这是一个尝试实现此功能的组件:

Component {
  id: objRecursiveDelegate
  Column {
     id: objRecursiveColumn
     clip: true
     MouseArea {
        width: objRow.implicitWidth
        height: objRow.implicitHeight
    //for collapsing and expanding
        onDoubleClicked: {
       //remember that Repeater is also a child of objRecursiveColumn. Altering
      //it's visiblity serves no purpose and is wasteful so thus we avoid looping
          //over it by  parent.children.length - 1. i starts from 1 because we don't
          //want to affect the visiblity of 0th child which is this MouseArea itself. 
      //Also note that making a child invisible will also make all
          //children/grandchildren of the child invisible too - nice. 
           for(var i = 1; i < parent.children.length - 1; ++i) {
              parent.children[i].visible = !parent.children[i].visible
           }
        }
        Row {
           id: objRow
       //for indentation
           Item {
              height: 1
              width: model.level * 20
           }
           Text {
	  //if collapsed, show + else show -. If no child-nodes then show nothing
              //(cannot be collapsed or expanded)
              text: (objRecursiveColumn.children.length > 2 ?
                          objRecursiveColumn.children[1].visible ?
                          qsTr("-  ") : qsTr("+ ") : qsTr("   ")) + model.name
              color: objRecursiveColumn.children.length > 2 ? "blue" : "yellow"
              font { bold: true; pixelSize: 14 }
           }
        }
     }
     Repeater {
        model: subNode
        delegate: objRecursiveDelegate
     }
  }
}

我们已经讨论了其中较棘手的部分。其余部分,我希望借助代码片段中的一些内联注释,都能轻松理解。请注意,每个 subNode 本身就是一个 ListElement,因此当 Repeater 的 Model 是 subNode 时(如上所示),delegate 将可以访问 model.namemodel.levelmodel.subNode 这些字段/角色,我们在上面的 delegate 中使用了这三者。

到目前为止,我们已经看到了 ListModel 的代码,我们将提供给 ListView 的递归 delegate 的代码,以及一些用于向 ListModel 添加数据的代码。这就是所有必需的。为了更好地理解这个概念,我们将添加用户交互。用户可以向树中的任何合法位置添加节点。当用户仅输入一个字符串(例如,“XYZ”)时,它将被添加为第 0 级的节点 0。如果用户再次仅输入一个字符串(例如,“ABC”),它将被添加为第 0 级的节点 1。现在我们有了第 0 级的两个节点,用户可以向其中任何一个添加子节点。假设需要节点 1(“ABC”)包含一个子节点(“QWE”)。用户将通过输入:1,QWE 来表达此意图。这意味着节点 1(“ABC”)应该包含子节点“QWE”。如果后续输入是 1,RTY,那么“ABC”将有两个子节点“QWE”和“RTY”。要将“UIO”添加到“QWE”下,他将输入:1,0,UIO。这是因为 1 是“ABC”。然后,“ABC”内部的 0 是“QWE”。类似地,要将“123”添加到“UIO”下,用户将输入:1,0,0,123。希望你已经掌握了窍门。

为此,我们将添加一个按钮,点击该按钮将显示一个模态 TextInput。在这里,用户可以输入信息并按回车键。我们将对文本输入设置一个正则表达式验证器,尽管这是可选的(我喜欢正则表达式,因为我大量使用 Vim,所以即使在不需要的地方也会添加它,只是为了好玩 :))。实际重要的部分是我们检查输入是否合法。如果是,则将数据添加到 Model;否则,在控制台显示错误。例如,对于上面的示例,输入 1,3,ASDF 是一个非法输入,因为节点 1(“ABC”)没有节点 3。它只有两个节点“QWE”和“RTY”,即节点 0 和节点 1。

这是代码

//Assume text is the input user supplies to TextInput.
var szSplit = text.split(',')
if(szSplit.length === 1) {
   objModel.append({"name": szSplit[0], "level": 0, "subNode": []})
}
else {
   if(objModel.get(parseInt(szSplit[0])) === undefined) {
      console.log("Error - Given node does not exist !")
      return
   }
   var node = objModel.get(parseInt(szSplit[0]))
   for(var i = 1; i < szSplit.length - 1; ++i) {
      if(node.subNode.get(parseInt(szSplit[i])) === undefined) {
         console.log("Error - Given node does not exist !")
         return
      }
   node = node.subNode.get(parseInt(szSplit[i]))
}
node.subNode.append({"name": szSplit[i], "level": i, "subNode": []})

这没什么特别的。只需根据“,”进行拆分,并将最后一个字符串视为代理组件 objRecursiveDelegate 的文本节点名称数据。

这是完整的代码列表:

import QtQuick 2.0
import QtQuick.Window 2.0
import QtQuick.Layouts 1.0
import QtQuick.Controls 1.0
Rectangle {
   width: 600
   height: 600
   color: "black"
   ListModel {
      id: objModel
   }
   Component {
      id: objRecursiveDelegate
      Column {
         id: objRecursiveColumn
         clip: true
         MouseArea {
            width: objRow.implicitWidth
            height: objRow.implicitHeight 
            onDoubleClicked: {
               for(var i = 1; i < parent.children.length - 1; ++i) {
                  parent.children[i].visible = !parent.children[i].visible
               }
            }
            Row {
               id: objRow
               Item {
                  height: 1
                  width: model.level * 20
               }
               Text {
                  text: (objRecursiveColumn.children.length > 2 ?
                           objRecursiveColumn.children[1].visible ?
                           qsTr("-  ") : qsTr("+ ") : qsTr("   ")) + model.name
                  color: objRecursiveColumn.children.length > 2 ? "blue" : "yellow"
                  font { bold: true; pixelSize: 14 }
               }
            }
         }
         Repeater {
            model: subNode
            delegate: objRecursiveDelegate
         }
      }
   }
   ColumnLayout {
      anchors.fill: parent
      ListView {
         Layout.fillHeight: true
         Layout.fillWidth: true
         model: objModel
         delegate: objRecursiveDelegate
      }
      Window {
         id: objModalInput
         modality: Qt.ApplicationModal
         visible: false
         height: 30
         width: 200
         color: "yellow"
         TextInput {
            anchors.fill: parent
            font { bold: true; pixelSize: 20 }
            verticalAlignment: TextInput.AlignVCenter
            horizontalAlignment: TextInput.AlignHCenter
            validator: RegExpValidator {
               regExp: /(\d{1,},)*.{1,}/
            }
            onFocusChanged: {
               if(focus) {
                  selectAll()
               }
            }
            text: qsTr("node0")
            onAccepted: {
               if(acceptableInput) {
                  objModalInput.close()
                  var szSplit = text.split(',')
                  if(szSplit.length === 1) {
                     objModel.append({"name": szSplit[0], "level": 0, "subNode": []})
                  }
                  else {
                     if(objModel.get(parseInt(szSplit[0])) === undefined) {
                        console.log("Error - Given node does not exist !")
                        return
                     }
                     var node = objModel.get(parseInt(szSplit[0]))
                     for(var i = 1; i < szSplit.length - 1; ++i) {
                        if(node.subNode.get(parseInt(szSplit[i])) === undefined) {
                           console.log("Error - Given node does not exist !")
                           return
                        }
                        node = node.subNode.get(parseInt(szSplit[i]))
                     }
                     node.subNode.append({"name": szSplit[i], "level": i, "subNode": []})
                  }
               }
            }
         }
      }
      Button { 
         text: "add data to tree" 
         onClicked: {
            objModalInput.show()
         }
      }
   }
}

以下是可用于入门的操作(按顺序执行):

  1. 点击按钮 -> 点击模态窗口并只按回车键
  2. 点击按钮 -> 只按回车键(这次模态窗口将自动获得焦点)
  3. 重复 <2> 3 次
  4. 点击按钮 -> 转到模态窗口文本的开头(按“Home”)-> 输入 2,(将其余文本保留原样)-> 按回车键
  5. 重复 <2> 3 次
  6. 点击按钮 -> 转到文本中的 2, -> 输入 1,(此时文本应显示为:2,1,node0)-> 按回车键
  7. 重复 <2> 3 次
  8. 双击最外面的蓝色节点
  9. 重复 <8>
  10. 对其他蓝色节点重复此操作

到目前为止,你已经掌握了它。

既然你知道在 QML/JavaScript 中创建 TreeView 是多么简单,你可以继续修改它、使用它,或者从头开始编写一些东西,并构建具有信号槽/拖放/拖放插入/洗牌/排序等功能的复杂 TreeModel。我已经这样做了(在复习了良好的算法之后,我往往会变得生疏,因为如今一切都是现成的),并且效果非常好。

© . All rights reserved.