利用QtQuick 2.0(qml)实现叶子节点可以拖动的强大的树形结构
引子:Qt是跨平台开发的利器,其中Qml更是利器中的神器。Qml很多人用过,开发自由灵活美观的程序界面是其优点,不管是桌面程序开发还是移动应用开发,都可以胜任,Qml的UI对触摸的支持也很好。如果用C++开发内部的有大量运算处理的代码,用qml来开发界面就完美无缺了。
下面通过一个简洁的小例子讲讲用qml开发一个复杂的树形结构,并且该树形结构的叶子可以自由拖动。
下图看起来是一个很普通的树:
但是它的叶子节点是可以自由拖动的,比如可以方便的把这个树拖动成下面这个样子:
非叶子节点既可以展开,也可以收起:
利用Qml强大的界面美化功能,我们可以很容易的把这棵树做成一个很美丽的树结构。
好了,基本功能就是这样,进入正题,是怎么实现的呢?废话不说,上代码:
importQtQuick2.0
importQtQuick.Controls1.0
importQtQuick.Window2.0
importQtQuick.Layouts1.0
importQtQuick.Controls1.0
Rectangle{
id:objRoot
objectName:"objRoot"
width:600
height:600
color:"black"
ListModel{
id:objModel
objectName:"objModel"
Component.onCompleted:{
objModel.append({"name":qsTr("Zero"),"level":0,"parentModel":objModel,"subNode":[]})//添加第一层的节点
objModel.append({"name":qsTr("One"),"level":0,"parentModel":objModel,"subNode":[]})
objModel.append({"name":qsTr("Two"),"level":0,"parentModel":objModel,"subNode":[]})
objModel.get(1).subNode.append({"name":qsTr("Three"),"level":1,"parentModel":objModel.get(1).subNode,"subNode":[]})//添加第二层的节点
objModel.get(1).subNode.append({"name":qsTr("Four"),"level":1,"parentModel":objModel.get(1).subNode,"subNode":[]})
objModel.get(1).subNode.get(0).subNode.append({"name":qsTr("Five"),"level":2,"parentModel":objModel.get(1).subNode.get(0).subNode,"subNode":[]})//添加第三层的节点
}
}
Component{
id:objRecursiveDelegate
Column{
id:objRecursiveColumn
objectName:"objRecursiveColumn"
propertyintm_iIndex:model.index//当前节点的序号
propertyvarm_parentModel:model.parentModel//指向当前节点的父节点
propertyvarm_currentModel:model
clip:true
MouseArea{
id:objMouseArea
objectName:"objMouseArea"
width:objRow.implicitWidth
height:objRow.implicitHeight
onDoubleClicked:{
for(vari=0;i<parent.children.length;++i){
if(parent.children[i].objectName!=="objMouseArea"){
parent.children[i].visible=!parent.children[i].visible
}
}
}
drag.target:objDragRect
onReleased:{
if(objDragRect.Drag.target){
objDragRect.Drag.drop()
}
}
Row{
id:objRow
Item{
id:objIndentation
height:20
width:model.level*20
}
Rectangle{//处理拖动后落下操作的矩形
id:objDisplayRowRect
height:objNodeName.implicitHeight+5
width:objCollapsedStateIndicator.width+objNodeName.implicitWidth+5
border.color:"green"
border.width:2
color:"#31312c"
DropArea{
//keys:[model.parentModel]//设置接受落下操作的范围
anchors.fill:parent
onEntered:objValidDropIndicator.visible=true
onExited:objValidDropIndicator.visible=false
onDropped:{//执行落下操作
objValidDropIndicator.visible=false
//判断是否有子节点
if(drag.source.m_objTopParent.m_parentModel.get(drag.source.m_objTopParent.m_iIndex).subNode.count===0)//如果没有子节点
{
objRecursiveColumn.m_parentModel.get(model.index).subNode.append({"name":drag.source.m_objTopParent.m_parentModel.get(drag.source.m_objTopParent.m_iIndex).name,
"level":objRecursiveColumn.m_parentModel.get(model.index).level+1,
"parentModel":objRecursiveColumn.m_parentModel.get(model.index).subNode,
"subNode":[]}); //在新的位置添加一个子节点
// console.debug(objRecursiveColumn.m_parentModel.get(model.index).name)//拖动的目标位置名称
drag.source.m_objTopParent.m_parentModel.remove(drag.source.m_objTopParent.m_iIndex,1); //移除原来位置的节点
}
}
Rectangle{//进入范围显示动画的矩形
id:objValidDropIndicator
anchors.fill:parent
visible:false
onVisibleChanged:{
visible?objAnim.start():objAnim.stop()
}
SequentialAnimationoncolor{
id:objAnim
loops:Animation.Infinite
running:false
ColorAnimation{from:"#31312c";to:"green";duration:400}
ColorAnimation{from:"green";to:"#31312c";duration:400}
}
}
}
Rectangle{
id:objDragRect
propertyvarm_objTopParent:objRecursiveColumn
Drag.active:objMouseArea.drag.active
//Drag.keys:[model.parentModel]
border.color:"magenta"
border.width:2
opacity:.85
states:State{
when:objMouseArea.drag.active
AnchorChanges{
target:objDragRect
anchors{horizontalCenter:undefined;verticalCenter:undefined}
}
ParentChange{
target:objDragRect
parent:objRoot
}
}
anchors{horizontalCenter:parent.horizontalCenter;verticalCenter:parent.verticalCenter}
height:objDisplayRowRect.height
width:objDisplayRowRect.width
visible:Drag.active
color:"red"
Text{
anchors.fill:parent
horizontalAlignment:Text.AlignHCenter
verticalAlignment:Text.AlignVCenter
text:model.name
font{bold:true;pixelSize:18}
color:"blue"
}
}
Text{
id:objCollapsedStateIndicator
anchors{left:parent.left;top:parent.top;bottom:parent.bottom}
width:20
horizontalAlignment:Text.AlignHCenter
verticalAlignment:Text.AlignVCenter
text:objRepeater.count>0?objRepeater.visible?qsTr("-"):qsTr("+"):qsTr("")
font{bold:true;pixelSize:18}
color:"yellow"
}
Text{
id:objNodeName
anchors{left:objCollapsedStateIndicator.right;top:parent.top;bottom:parent.bottom}
text:model.name
color:objRepeater.count>0?"yellow":"white"
font{bold:true;pixelSize:18}
verticalAlignment:Text.AlignVCenter
}
}
}
}
Rectangle{
id:objSeparator
anchors{left:parent.left;right:parent.right;}
height:1
color:"black"
}
Repeater{
id:objRepeater
objectName:"objRepeater"
model:subNode
delegate:objRecursiveDelegate
}
}
}
ColumnLayout{
objectName:"objColLayout"
anchors.fill:parent
ScrollView{
Layout.fillHeight:true
Layout.fillWidth:true
ListView{
objectName:"objListView"
model:objModel
delegate:objRecursiveDelegate
interactive:false
}
}
}
}
关于Qml语言的基本语法就不说啦,教程一大堆,下面详述本程序是怎么实现这个树结构的。
首先引入一些必要的包:
importQtQuick2.0
importQtQuick.Controls1.0
importQtQuick.Window2.0
importQtQuick.Layouts1.0
importQtQuick.Controls1.0
然后是我们程序的根元素objRoot。在这个根元素中,首先定义了一个ListModel,其中存储了树结构所有节点的数据。
向其中插入元素,不是本文的重点,在这里不做详述,但是本例中还是使用了相关插入元素的语句:
objModel.append({"name":qsTr("Zero"),"level":0,"parentModel":objModel,"subNode":[]})//添加第一层的节点
objModel.append({"name":qsTr("One"),"level":0,"parentModel":objModel,"subNode":[]})
objModel.append({"name":qsTr("Two"),"level":0,"parentModel":objModel,"subNode":[]})
objModel.get(1).subNode.append({"name":qsTr("Three"),"level":1,"parentModel":objModel.get(1).subNode,"subNode":[]})//添加第二层的节点
objModel.get(1).subNode.append({"name":qsTr("Four"),"level":1,"parentModel":objModel.get(1).subNode,"subNode":[]})
objModel.get(1).subNode.get(0).subNode.append({"name":qsTr("Five"),"level":2,"parentModel":objModel.get(1).subNode.get(0).subNode,"subNode":[]})//添加第三层的节点
其中name是每个节点的名字,level是节点所在的层次,parentModel是其父节点,subNode是其子节点。
有了数据,还得显示,在后面定义了一个ListView,ListModel中的数据就是通过ListView显示成了一个树结构。ListModel是内容,ListView是形式。
那么ListView具体怎么显示ListModel中的每一个节点数据呢,这个是通过另一个元素来定义的,这个元素是id为objRecursiveDelegate的Component元素。这个元素定义了每个节点具体怎么显示,在节点中如果有子节点,那么又要递归的显示子节点,所以其中有一个Repeater元素,起到了递归显示子节点的作用,如下:
Repeater{
id:objRepeater
objectName:"objRepeater"
model:subNode//子节点
delegate:objRecursiveDelegate//递归调用自己显示子节点
}
双击展开和收起节点,通过下面这个方法来实现:
onDoubleClicked:{
for(vari=0;i<parent.children.length;++i){
if(parent.children[i].objectName!=="objMouseArea")//鼠标区域不能参与,否则鼠标区域一旦隐藏了,就出不来了,哈哈
{
parent.children[i].visible=!parent.children[i].visible
}
}
}
显示的问题解决了,那么拖动的问题如何解决呢?
首先,鼠标区域objMouseArea必须定义自己的拖动目标对象:
drag.target:objDragRect
被拖动的对象objDragRect在被拖动时将会被显示出来:
Drag.active:objMouseArea.drag.active
visible:Drag.active
同时必须在拖动时取消对objDragRect的锚定,否则是拖不动的,呵呵:
states:State{
when:objMouseArea.drag.active
AnchorChanges{
target:objDragRect
anchors{horizontalCenter:undefined;verticalCenter:undefined}
}
ParentChange{
target:objDragRect
parent:objRoot
}
}
拖动时,一旦进入了某些区域,UI上会给出提示,objValidDropIndicator这个对象通过一个动画完成了任务:
SequentialAnimationoncolor{
id:objAnim
loops:Animation.Infinite
running:false
ColorAnimation{from:"#31312c";to:"green";duration:400}
ColorAnimation{from:"green";to:"#31312c";duration:400}
}
拖动总要有落下的时候,我们的DropArea元素会处理我们把东西放下时应执行的动作:
onDropped:{//执行落下操作
objValidDropIndicator.visible=false
//判断是否有子节点
if(drag.source.m_objTopParent.m_parentModel.get(drag.source.m_objTopParent.m_iIndex).subNode.count===0)//如果没有子节点
{
objRecursiveColumn.m_parentModel.get(model.index).subNode.append({"name":drag.source.m_objTopParent.m_parentModel.get(drag.source.m_objTopParent.m_iIndex).name,
"level":objRecursiveColumn.m_parentModel.get(model.index).level+1,
"parentModel":objRecursiveColumn.m_parentModel.get(model.index).subNode,
"subNode":[]}); //在新的位置添加一个子节点
drag.source.m_objTopParent.m_parentModel.remove(drag.source.m_objTopParent.m_iIndex,1); //移除原来位置的节点
}
}
每个节点有没有子节点,是通过文字的颜色来区分的:
color:objRepeater.count>0?"yellow":"white"
每个非叶子节点,有没有展开也是要区分的:
text:objRepeater.count>0?objRepeater.visible?qsTr("-"):qsTr("+"):qsTr("")
分割线objSeparator要不要真的无所谓
好了程序大体上就是这么工作的,其中定义的一些属性,如果你看懂了上面的方法代码,很快就能理解这些属性是什么意思了。
是不是很简单呢,如果你有任何问题,欢迎讨论。