目录
一、界面演示
二、设备列表
三、抖动单元格
四、设备模型
五、设备编辑
本项目的交流QQ群:701889554
物联网实战--入门篇https://blog.csdn.net/ypp240124016/category_12609773.html
物联网实战--驱动篇https://blog.csdn.net/ypp240124016/category_12631333.html
一、界面演示
设备管理演示
二、设备列表
参考了几个现有的智能家居相关的APP,他们的设备管理模式基本差不多,都是这种单元格的方式,在QML里,就是GridView组件了,以下是设备列表相关的前端QML代码:
import QtQuick 2.7
import QtQuick.Controls 2.0
import "../base"
Rectangle
{
property var groupName: ""
id:id_rootRect
height: parent.height
width: parent.width-20
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
DevDelDialog//设备删除对话框
{
id:id_devDelDialog
onSiqOkClicked:
{
var dev_list=[]
var ptr=0
for(var i=0; i<id_cellModel.count; i++)
{
if(id_cellModel.get(i).select_state)//选中
{
dev_list[ptr++]=id_cellModel.get(i).dev_sn
}
}
theCenterMan.requestDelDevice(dev_list)
funClose()
}
}
DevRenameDialog//设备重命名对话框
{
id:id_devRenameDialog
onSiqOkClicked:
{
theCenterMan.requestRenameDevice(devSn, text)
funClose()
}
}
GridView {
id:id_gridView
property int dragItemIndex: -1
property var dragActive: false
property int selectCnts: 0
property var sortFlag: false
anchors.fill: parent
clip: true
cacheBuffer: 10000
implicitWidth: 150
implicitHeight: 150
cellWidth: width*0.5
cellHeight: cellWidth*0.8
move:Transition {
NumberAnimation { properties: "x,y"; duration: 200 }
}
moveDisplaced:Transition {
NumberAnimation { properties: "x,y"; duration: 200 }
}
model: ListModel{
id:id_cellModel
}
delegate: Rectangle{//单元格矩形
id:id_cellRect
// property var selectState: false
color: "transparent"
// border.color: "blue"
// border.width: 1
width: id_gridView.cellWidth
height: id_gridView.cellHeight
Rectangle //拖动矩形
{
id:id_dragRect
radius: 12
width: id_cellRect.width-15
height:id_cellRect.height-15
anchors.centerIn:id_cellRect
color: id_moveMouseArea.drag.active ? "transparent" : "white"
onParentChanged:
{
if(parent!=null)
{
theCenterMan.showSimpleView(dev_sn, id_dragRect)
}
}
Rectangle{ //勾选框
id:id_selectRect
width: 24
height: width
radius: width/2
border.width: 1
border.color: "#D0D0D0"
visible: id_gridView.dragActive
anchors
{
top:parent.top
topMargin:15
right:parent.right
rightMargin:15
}
Image {
id: id_selectImage
anchors.fill: parent
visible: select_state
mipmap: true
source: "qrc:/imagesRC/mainImages/check.png"
}
}
MouseArea
{
id:id_moveMouseArea
anchors.fill: parent
// drag.target: id_dragRect
drag.axis: Drag.YAxis | Drag.XAxis
drag.onActiveChanged: {
if (id_moveMouseArea.drag.active)
{
id_gridView.dragItemIndex = index;
console.log("drag index=", index)
}
id_dragRect.Drag.drop();
}
onClicked:
{
if(id_gridView.dragActive)
{
select_state=!select_state
if(select_state)
id_gridView.selectCnts++
else
id_gridView.selectCnts--
// console.log("selectCnts=", id_gridView.selectCnts)
}
}
onPressAndHold:
{
id_gridView.dragActive=true
select_state=true
id_gridView.selectCnts=1
}
onReleased:
{
// drag.target=null
// id_gridView.dragActive=false
// id_dropAnimation.stop()
}
}
states: [
State {
when: id_dragRect.Drag.active
ParentChange {
target: id_dragRect
parent: id_rootRect
}
AnchorChanges {
target: id_dragRect
anchors.horizontalCenter: undefined
anchors.verticalCenter: undefined
}
}
]
Drag.active: id_moveMouseArea.drag.active
Drag.hotSpot.x: id_dragRect.width / 2
Drag.hotSpot.y: id_dragRect.height / 2
}
DropArea { //拖拽
id: dropArea
anchors.fill: parent
onDropped:{
console.log("onDropped")
var other_index = id_gridView.indexAt(id_moveMouseArea.mouseX + id_cellRect.x, id_moveMouseArea.mouseY + id_cellRect.y);
console.log("other_index:",other_index,"id_gridView.dragItemIndex:",id_gridView.dragItemIndex);
if(other_index!==id_gridView.dragItemIndex && other_index>=0)
{
id_cellModel.move(id_gridView.dragItemIndex,other_index, 1);
id_gridView.sortFlag=true//有排序动作
}
}
Rectangle {
id: dropRectangle
anchors.centerIn:parent
width: id_dragRect.width
height: id_dragRect.height
color: "transparent"
radius: 12
states: [
State {
when: dropArea.containsDrag
PropertyChanges {
target: dropRectangle
color: "lightsteelblue"
opacity:0.3
}
}
]
}//end Rectangle
}//end drop
PropertyAnimation { //抖动
id:id_dropAnimation
target: id_dragRect
properties: "rotation"
from:-1.5
to:1.5
duration: 300
easing.type: Easing.InOutExpo
loops: 300 //循环次数
onStopped:
{
target["rotation"] = 0 //显示归位
}
}
Timer{
interval: Math.random()*500; running: true; repeat: true
onTriggered:
{
if(id_gridView.dragActive)
{
if(id_dropAnimation.stopped)
{
id_dropAnimation.start()
id_moveMouseArea.drag.target=id_dragRect
}
}
else
{
if(id_dropAnimation.started)
{
id_dropAnimation.stop()
id_moveMouseArea.drag.target=null
select_state=false
id_gridView.selectCnts=0
}
}
}
}
}//end delegate
}//end GridView
Rectangle //设置栏
{
id:id_setRect
width: parent.parent.width
height: 60
anchors
{
horizontalCenter:parent.horizontalCenter
bottom:id_gridView.bottom
}
MouseArea{
anchors.fill: parent //接收鼠标事件,避免选择背后的单元格
}
color: "#606060"
// opacity: 0.5
visible: id_gridView.dragActive
Row
{
height: id_setRect.height
width: id_setRect.width*0.9
anchors
{
top:parent.top
topMargin:5
horizontalCenter:id_setRect.horizontalCenter
}
spacing: (width-30*4)/3
Repeater
{
model: ListModel{
id:id_setModel
}
Rectangle{
property var maskFlag: index===0 && id_gridView.selectCnts!==1
height: 30
width: height
radius: width/2
color: maskFlag ? "#808080" : "white"
ImageButton01 {
anchors.centerIn: parent
width: parent.width*0.7
height: width
mipmap: true
source: img_src
onSiqClickedLeft:
{
// console.log("clicked index=", index)
switch(index)
{
case 0: //修改名称
if(id_gridView.selectCnts===1)
{
for(var i=0; i<id_cellModel.count; i++)
{
if(id_cellModel.get(i).select_state)//选中
{
id_devRenameDialog.devSn=id_cellModel.get(i).dev_sn
id_devRenameDialog.oldName=theCenterMan.takeWorkDeviceName(id_devRenameDialog.devSn)//旧名称
id_devRenameDialog.funOpen(id_devRenameDialog.oldName)
break
}
}
}
break
case 1: //移动设备
id_devMoveDialog.open()
var dev_list=[]
var ptr=0
for(i=0; i<id_cellModel.count; i++)
{
if(id_cellModel.get(i).select_state)//选中
{
dev_list[ptr++]=id_cellModel.get(i).dev_sn
}
}
id_devMoveDialog.srcGroupName=groupName
id_devMoveDialog.moveDevList=dev_list
break
case 2: //删除设备
id_devDelDialog.funOpen()
break
case 3: //完成
id_gridView.dragActive=false;
if(id_gridView.sortFlag==true)//有排序动作
{
id_gridView.sortFlag=false
dev_list=[]
for(i=0; i<id_cellModel.count; i++)
{
dev_list[i]=id_cellModel.get(i).dev_sn
}
theCenterMan.requestSortDevice(groupName, dev_list)//排序
}
break
}
}
}
Text{
height: 25
anchors
{
horizontalCenter:parent.horizontalCenter
top:parent.bottom
topMargin:3
}
text: name
font.pointSize: 10
font.family: "宋体"
color: maskFlag ? "#808080" : "white"
}
}
}
}
}
Connections
{
target: theCenterMan
onSiqAddDevice2Group:
{
if(group_name===groupName)
{
id_cellModel.append({"dev_sn":dev_sn, "select_state":false})
}
}
onSiqDelDevice:
{
for(var i=0; i<id_cellModel.count; i++)
{
if(dev_sn===id_cellModel.get(i).dev_sn)
{
id_cellModel.remove(i)
break
}
}
if(flag>0) //删除完成
{
var dev_list=[]
var ptr=0
for(i=0; i<id_cellModel.count; i++)
{
dev_list[ptr++]=id_cellModel.get(i).dev_sn
}
theCenterMan.requestSortDevice(groupName, dev_list)//重新排序
}
}
onSiqClearDevice:
{
if(group_name===groupName)
{
id_cellModel.clear()
}
}
}
Component.onCompleted:
{
var img_src="qrc:/imagesRC/mainImages/home/rename.png"
var name="修改名称"
id_setModel.append({"img_src":img_src, "name":name})
img_src="qrc:/imagesRC/mainImages/home/move.png"
name="移动设备"
id_setModel.append({"img_src":img_src, "name":name})
img_src="qrc:/imagesRC/mainImages/home/del02.png"
name="删除设备"
id_setModel.append({"img_src":img_src, "name":name})
img_src="qrc:/imagesRC/mainImages/home/finish.png"
name="完成"
id_setModel.append({"img_src":img_src, "name":name})
}
}
在GridView里的重点其实就是拖拽排序功能了,跟之前的分组排序类似的,只不过网格拖拽复杂点,首先方向是X和Y两个方向;然后是抖动效果,在长按某个设备单元格后整体就会抖动起来,让用户直观地感受到是在编辑状态;最后是选中效果,用户选中后就会增加一个打勾按钮,本质就是图片显示条件设置了。
三、抖动单元格
抖动使用的是动画组件,其中需要改变的对象是拖拽矩形,改变的属性是角度,范围从-1.5~1.5角度,如果停止后角度重置为0。在这里我们加个定时器用来管理所有单元格的抖动状态,在有设备被长按激活拖拽后,网格里所有的设备都要抖动起来,在定时器里启动;如果退出编辑去激活后,同样是在定时器内检测停止。
PropertyAnimation { //抖动
id:id_dropAnimation
target: id_dragRect
properties: "rotation"
from:-1.5
to:1.5
duration: 300
easing.type: Easing.InOutExpo
loops: 300 //循环次数
onStopped:
{
target["rotation"] = 0 //显示归位
}
}
Timer{
interval: Math.random()*500; running: true; repeat: true
onTriggered:
{
if(id_gridView.dragActive)
{
if(id_dropAnimation.stopped)
{
id_dropAnimation.start()
id_moveMouseArea.drag.target=id_dragRect
}
}
else
{
if(id_dropAnimation.started)
{
id_dropAnimation.stop()
id_moveMouseArea.drag.target=null
select_state=false
id_gridView.selectCnts=0
}
}
}
}
四、设备模型
设备模型是我们后续开发新硬件产品所需要对应增加的内容,在其它平台上,很多称之为物模型,一般用json语句来描述设备的属性、状态和功能,这种模式的特点是通用性比较强,缺点是性能损耗大,深度定制使用体验不佳,所以只能定义一些较为简单的设备模型,复杂的调试起来很麻烦。那么,我们这边的思想还是以代码驱动为核心,物模型后端逻辑直接用C/C++实现,前端界面用QML实现,配套使用,下面举例说明。
如上图所示,modelCpp存放的是物模型后端代码,modelQml存放的是物模型前端代码,由于各个物模型有一定相似性,所以都会定义一个基本的模型,具体设备模型再继承于这个基础模型,在这里我们以后面要实现的净化器为例,定义型号为AP01,那么他的ModelAp01,继承于BaseModel,具体的后面完善净化器项目的时候再说明。
同样的,前端代码也是这种模式,SimpleAp01.qml继承于BaseSimpleView,带Simple说明是简易模型,就是每个单元格内显示的内容,对于每个物模型有两个界面,一个是这里所说的简易界面,另一个是详情界面,就是之前净化器项目那个可以具体控制操作的界面。
对于模型的显示也是一个需要技巧的地方,首先要知道,每个界面都需要有一个父组件/窗口,这样才能显示,对于设备模型前端来讲,它的父窗口其实就是那个可以被拖拽的单元格了,所以我们要显示模型的时候,就是将这个单元格地址传到模型里去,然后模型内部自己主动显示在单元格上,下面通过代码了解这一流程。
首先在网格单元格内,当单元格创建完成后,会触发onParentChanged信号,在这里就可以根据设备sn将单元格矩形最为父窗口,把指针传入模型后端。
然后我们来看看控制中心的showSimpleView函数的内容,如下所示,就是根据dev_sn找到这个模型实例对象,然后调用模型的showSimple进行显示,注意,这里继续将父窗口指针继续传递。
接下来是重头戏,如下图所示,是不是感觉似曾相识,跟主程序显示是一样的,每个模型内部都有一个QML显示引擎,通过这里的配置,就可以实现模型内容的前后端交互了,在这里有两个接口,theModelAp01和theCenterMan,其中通过theCenterMan可以调用CenterMan类内的相关功能。函数末尾就是加载SimpleAp01这个模型文件。
到此为止,物模型并不能显示,因为上图中的父窗口指针只是保存在C++后端了,并没有跟前端的QML有什么关联,这就引出了最后一步,就是在具体的QML模型文件内调用takeModelParent(),将刚才保存在后端的父窗口指针赋值到当前模型的parent属性,这样就形成了闭环,完成了模型显示的功能。
五、设备编辑
当用户长按某个单元格后,除了单元格会抖动起来以外,底部还会出现一个设置栏,目前定义的功能是修改名称、移动设备和删除设备,这里比较复杂的还是上一篇所写的后台管理,特别是移动设备功能;那么对于前端代码主要做个展示,具体内容自己阅读理解应该问题不大。
Rectangle //设置栏
{
id:id_setRect
width: parent.parent.width
height: 60
anchors
{
horizontalCenter:parent.horizontalCenter
bottom:id_gridView.bottom
}
MouseArea{
anchors.fill: parent //接收鼠标事件,避免选择背后的单元格
}
color: "#606060"
// opacity: 0.5
visible: id_gridView.dragActive
Row
{
height: id_setRect.height
width: id_setRect.width*0.9
anchors
{
top:parent.top
topMargin:5
horizontalCenter:id_setRect.horizontalCenter
}
spacing: (width-30*4)/3
Repeater
{
model: ListModel{
id:id_setModel
}
Rectangle{
property var maskFlag: index===0 && id_gridView.selectCnts!==1
height: 30
width: height
radius: width/2
color: maskFlag ? "#808080" : "white"
ImageButton01 {
anchors.centerIn: parent
width: parent.width*0.7
height: width
mipmap: true
source: img_src
onSiqClickedLeft:
{
// console.log("clicked index=", index)
switch(index)
{
case 0: //修改名称
if(id_gridView.selectCnts===1)
{
for(var i=0; i<id_cellModel.count; i++)
{
if(id_cellModel.get(i).select_state)//选中
{
id_devRenameDialog.devSn=id_cellModel.get(i).dev_sn
id_devRenameDialog.oldName=theCenterMan.takeWorkDeviceName(id_devRenameDialog.devSn)//旧名称
id_devRenameDialog.funOpen(id_devRenameDialog.oldName)
break
}
}
}
break
case 1: //移动设备
id_devMoveDialog.open()
var dev_list=[]
var ptr=0
for(i=0; i<id_cellModel.count; i++)
{
if(id_cellModel.get(i).select_state)//选中
{
dev_list[ptr++]=id_cellModel.get(i).dev_sn
}
}
id_devMoveDialog.srcGroupName=groupName
id_devMoveDialog.moveDevList=dev_list
break
case 2: //删除设备
id_devDelDialog.funOpen()
break
case 3: //完成
id_gridView.dragActive=false;
if(id_gridView.sortFlag==true)//有排序动作
{
id_gridView.sortFlag=false
dev_list=[]
for(i=0; i<id_cellModel.count; i++)
{
dev_list[i]=id_cellModel.get(i).dev_sn
}
theCenterMan.requestSortDevice(groupName, dev_list)//排序
}
break
}
}
}
Text{
height: 25
anchors
{
horizontalCenter:parent.horizontalCenter
top:parent.bottom
topMargin:3
}
text: name
font.pointSize: 10
font.family: "宋体"
color: maskFlag ? "#808080" : "white"
}
}
}
}
}