QML学习笔记(十五)QML的信号处理器(MouseArea)
前言
从本节开始,我们将详细学习有关QML的信号与槽机制。因为我已经对QWidget中的信号槽机制有比较全面的了解,所以在接下来的学习中可能会进行一些对比。
本节中介绍的信号处理器,其实可以理解为所谓的槽函数。比如之前已经使用过的MouseArea,我们实现了它的onClicked,这就是一个信号处理器。
一、MouseArea的信号处理器
在实际开发中,如何监听和处理鼠标相关事件是一个重要的事情。除了此前所用过的单击事件,还有一系列常用的行为,例如双击、滚轮,甚至还有进入、离开等等。我们将逐个进行学习和演示。
先创建一个新工程,命名为QmlSignalHandlers,并创建一个测试矩形,绑定一个MouseArea:
Window {visible: truewidth: 640height: 480title: qsTr("QmlSignalHandlers")Rectangle{id: rectIdwidth: 150height: 150color: "red"MouseArea{anchors.fill: parent}}
}
然后我希望实现一系列鼠标相关的处理器。我们可以先在帮助文档搜索一下:
往下划,它有相当多的属性:
然后,它还有那么多的信号:
而这些信号,我们都可以实现它们对应的信号处理器,也就是信号对应的槽函数。
二、鼠标单击和双击
我们先从最简单的开始,我们之前已经使用过onClicked,我们直接模仿一下,实现onDoubleClicked:
MouseArea{anchors.fill: parentonClicked: {console.log("click...");}onDoubleClicked: {console.log("double click...");}
}
我们手动敲代码的时候应该能感受到,编辑器可以给我们进行快速补全,这非常方便我们的开发。
运行后,我们先单击矩形:
后双击矩形:
这意味着什么?意味着双击的行为也会触发一次单击的行为,这一点一定要注意,不然会导致一些逻辑代码的混乱。不过好在我们默认情况下,实现了双击就不会实现单击。
三、鼠标事件进入、离开、取消
首先介绍一个MouseArea的属性,那就是hoverEnabled,你必须把它设置成true,才可以监听实现鼠标覆盖的事件。比方说我鼠标移动上去,但并没有点击,此时我希望能触发相关事件,那我们就必须要设置这个标志。
hoverEnabled: true
你可能一下子没想到这个东西的应用,实际上是非常常见的需求。比如我们需要自定义实现鼠标三态控件样式,那我们就需要分别实现鼠标覆盖和鼠标点击的不同样式。再来我们应该都用过鼠标放到窗口边缘,然后通过拖拽改变窗口尺寸的功能,这里需要监听鼠标覆盖事件,改变鼠标图标的样式。
然后,我设置三个处理器:
onEntered: {console.log("enter...");}onExited: {console.log("exit...");}onCanceled: {console.log("cancel...");}
运行程序,观察现象:
鼠标刚放上去,就打印了enter;
鼠标左键单击按下,打印了click;
鼠标从矩形的范围移出,打印了exit。
这个逻辑大家应该很好理解吧?我就不多说了。
如果我们没有设置hoverEnabled,那估计鼠标点击的那一刻,才会打印出enter了。
至于onCancel其实是个不常用的处理器,我也没办法触发它。网上的解释是这样的:onCanceled 只有在 拖拽(drag)场景 且 被系统/用户取消 时才会触发,日常点击根本不会进这个处理器。
四、onWheel鼠标滚轮
wheel是鼠标滚轮的意思,我们也可以对其进行监听哦:
onWheel:{console.log("wheel:"+wheel.angleDelta);
}
运行:
angleDelta的值要不是120,要不是-120。实际上,120在qt当中被处理成15°,angleDelta代表滚动了一格的意思,有兴趣可以自行了解。
五、鼠标的按下、移动和释放
我们已经学会了鼠标点击onClicked使用,但实际上鼠标点击这个行为还可以被拆解为按下、移动和释放三种行为。这个在QWidget中亦有实现:
void mousePressEvent(QMouseEvent* event)override;void mouseMoveEvent(QMouseEvent* event)override;void mouseReleaseEvent(QMouseEvent* event)override;
在QML的MouseArea中,我们使用onPressed、onPositionChanged和onReleased来实现。下面我们详细实现一下:
1.onPressed点击
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: {console.log("onPressed", mouse.x, mouse.y)if (mouse.button === Qt.LeftButton)console.log("left press", mouse.x, mouse.y)
}
我们先说一下这句代码:acceptedButtons: Qt.LeftButton | Qt.RightButton。
事实上鼠标点击是可以监听左键和右键的,但默认只监听左键。如果你想要实现右键的点击,需要给MouseArea设置接收的按钮,也就是acceptedButtons。
既然都可以接收多种按钮了,那自然onPressed里面也要有对应的区分。这里用到了全局对象当中的Qt.LeftButton。
以上功能都很常见,甚至都是标准写法了,在QWidget中也有对应的实现。
2.onPositionChanged移动
它这里写的比mouseMoveEvent稍微复杂一点,但意思都是鼠标移动了。我们可以打印实时坐标。
onPositionChanged: {console.log("onPositionChanged:", mouse.x, mouse.y)
}
这个处理器触发得比较频繁,如果影响查看打印信息的话,可以暂时屏蔽掉它。
3.onReleased松开
鼠标按下后松开的时刻会触发:
onReleased: {console.log("onReleased:", mouse.x, mouse.y)
}
以上三个处理器基本上都是搭配一起使用的,以实现一些复杂的业务逻辑,最典型的就是鼠标拖拽。
我们可以尝试实现它!
六、简单实现鼠标拖拽控件移动
我尝试了自行实现,思路是设置几个自定义的属性,记录鼠标按下的那一刻的坐标,然后在移动时实时计算偏移坐标并进行计算和设置,但效果不是很好,存在跳闪情况,这是我的完整代码:
Window {visible: truewidth: 640height: 480title: qsTr("QmlSignalHandlers")Rectangle{id: rectIdwidth: 150height: 150color: "red"MouseArea{property bool isPressed: falseproperty int anchorX: 0 // 按下时鼠标相对矩形的xproperty int anchorY: 0 // 按下时鼠标相对矩形的yproperty int rectX: 0 // 按下时鼠标相对矩形的xproperty int rectY: 0 // 按下时鼠标相对矩形的yanchors.fill: parenthoverEnabled: trueacceptedButtons: Qt.LeftButton | Qt.RightButtononPressed: {console.log("onPressed", mouse.x, mouse.y)if (mouse.button === Qt.LeftButton) {console.log("left press", mouse.x, mouse.y)isPressed = true;// 记录按下瞬间鼠标在矩形内的本地坐标anchorX = mouse.x;anchorY = mouse.y;rectX = rectId.x;rectY = rectId.y;}}onPositionChanged: {console.log("onPositionChanged:", mouse.x, mouse.y)if (isPressed) {// 鼠标全局坐标 - 锚点 = 矩形新左上角rectId.x = rectX + mouse.x - anchorX;rectId.y = rectY + mouse.y - anchorY;}}onReleased: {console.log("onReleased:", mouse.x, mouse.y)isPressed = false;}}}
}
我觉得稍微复杂的点是,MouseArea的坐标是相对于Rectangle的,但实际移动的却是矩形。
我让ai帮我重新设计了一个,这段代码效果更好,拖拽起来效果很正常:
Window {visible: truewidth: 640height: 480title: qsTr("DragRect")Rectangle {id: rectIdwidth: 150; height: 150; color: "red"MouseArea {anchors.fill: parentacceptedButtons: Qt.LeftButtonhoverEnabled: trueproperty bool pressed: falseproperty real startSceneX: 0 // 按下时全局坐标property real startSceneY: 0property real startRectX: 0 // 按下时矩形左上角property real startRectY: 0onPressed: {pressed = truevar scenePos = mapToItem(null, mouse.x, mouse.y) // 映射到窗口坐标startSceneX = scenePos.xstartSceneY = scenePos.ystartRectX = rectId.xstartRectY = rectId.y}onPositionChanged: {if (pressed) {var curScene = mapToItem(null, mouse.x, mouse.y)rectId.x = startRectX + (curScene.x - startSceneX)rectId.y = startRectY + (curScene.y - startSceneY)}}onReleased: pressed = false}}
}
实现思路大差不差,但它将鼠标事件的坐标转化为顶层窗口的相对坐标了。
mapToItem 这个接口就是实现了这个功能。
target 填谁,就返回相对于谁的坐标
target == null 时特指顶层窗口(rootItem)坐标,俗称“全局坐标”
返回值是 point{x: …, y: …}
我认为这个处理还是比较聪明的,就是如果相对的0点坐标不是顶层窗口的话,要记得处理一下,把null改掉。
七、onPressAndHold鼠标按住
onPressAndHold的触发场景是鼠标按下,短时间没有松开的时候,将会被触发。
onPressAndHold:{console.log("onPressAndHold...");
}
我们可以利用这个处理器,对刚才的拖拽效果稍作改造,就可以实现一个长按控件并移动的这样一个功能。这个功能也很常见,不是吗?
当然,如果觉得长按太久,或者长按不够久,我完全可以沿用pressed事件,自己去做时间方面的判断即可。onPressAndHold只是给你提供了一个快速的使用入口。
八、总结
本章算是对MouseArea有一个全面的了解和应用了,但本意其实是借着MouseArea来学习信号处理器的使用。事实上所有常用组件都会有类似的功能,我们需要通过帮助文档或百度来进行学习。这些在后面对每一个组件进行深入学习时,也会有所提及的。