0.前言
有时候,我们的MouseArea区域重叠了,这时候默认是最上层的MouseArea接收鼠标事件,而我们可能需要底层也能接收这个事件。虽然可以关联两个MouseArea的信号,但是这种操作局限性比较大,耦合性太强,某个MouseArea并不能被外部所见等等。在Qt Widgets中可以通过setAttribute(Qt::WA_TransparentForMouseEvents,true)来进行鼠标穿透,而在QML中我们可以通过propagateComposedEvents属性或者使用mouse.accepted=false来完成部分鼠标事件的传递。
Qt官方文档如是说:
propagateComposedEvents保存组合的鼠标事件是否将自动传播到与它重叠但在视觉堆叠顺序中较低的其他MouseArea 。默认情况下,此属性为false。这里面包含几个组成事件:clicked,doubleClicked和pressAndHold。它们由基本的鼠标事件组成(比如pressed),并且与基本事件相比可以不同地传播。
如果propagateComposedEvents设置为true,则组合事件将自动传播到场景中相同位置的其他MouseArea。每个事件将按照堆栈顺序传播到其下方的下一个启用的 MouseArea,并向下传播此可视层次结构,直到MouseArea接受该事件为止。与pressed事件不同,如果没有处理程序,则组合事件将不会被自动接受。
也就是说,类似clicked的组合事件,只要把propagateComposedEvents设置为true,在没有写处理程序的时候会自动的传递事件,有处理程序则加上mouse.accepted=false也会传递。而pressed等基本事件,只需要在处理程序中写上mouse.accepted=false就会传递下去,不用设置propagateComposedEvents。
1.组合事件的传递
直接上代码:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
MouseArea{
anchors.fill: parent
onClicked: console.log("~root clicked")
onDoubleClicked: console.log("~root double")
onPressAndHold: console.log("~root hold")
}
//item的层级在root之上(因为他写在mousearea的后面,又是同级的)
Rectangle{
width: 200
height: 200
color: "green"
MouseArea{
anchors.fill: parent
//是否传递到被覆盖的MouseArea
propagateComposedEvents: true
onClicked: {
console.log("item clicked")
//组合事件可以不写处理程序,或者accepted为false
mouse.accepted = false
}
onDoubleClicked: console.log("item double")
//onPressAndHold: console.log("item hold")
}
}
}
代码中,我们设置propagateComposedEvents为true,开启组合事件的传递,通过打印我们可以看到,没有写处理程序的hold和写了mouse.accepted = false的click传递到了被遮挡的MouseArea,而doubleClick则没有传递。
2.基本事件的传递
一般我们主要关注press和release事件,而release又是跟随press的,如果press设置mouse.accepted=false,那么relase只会在接收press的MouseArea里响应,即便它设置为true。如果press没有设置,那么即便release设置mouse.accepted=false,也是没有用的。代码如下:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
MouseArea{
anchors.fill: parent
onPressed: console.log("~root pressed")
onReleased: console.log("~root released")
}
//item的层级在root之上(因为他写在mousearea的后面,又是同级的)
Rectangle{
width: 200
height: 200
color: "green"
MouseArea{
anchors.fill: parent
//是否传递到被覆盖的MouseArea
//propagateComposedEvents: true
onPressed: {
console.log("item pressed")
mouse.accepted=false
}
onReleased: {
console.log("item released")
mouse.accepted=true
}
}
}
}
通过打印可以看到,release被接收press的MouseArea直接处理了,也就没有打印 “item released” 。
除了点击还有hover相关事件我们也比较常用,相关的主要有entered、exited、positionChanged等。默认鼠标hover是没有开启的,这些事件是由我们按下鼠标时才会激活,我们把MouseArea的hoverEnabled设置为true就能处理鼠标的移动了。
如果上层的MouseArea没有设置hoverEnabled,那么hover相关的事件自然就会传递下去,要是鼠标按下时的hover操作也传递的话,可以写上onPressed: mouse.accepted=false。但是如果上层MouseArea处理了entered或者exited就尴尬了,这两个信号里没有mouse参数,没法设置mouse.accepted,而postionChanged就算有mouse参数,设置了也没效果。
测试的结论就是hover如果没有设置hoverEnabled,那么会传递下去;如果设置为了true,那么就只能自己享用hover相关的事件了。
(2024-04-24补充)
最近又遇到了类似的问题,想到了一个方案就是事件转发,上层的MouseArea将hover事件转发给底层的MouseArea。如果是两个MouseArea部分重叠的情况,还需要自己进行区域判断来创建enter和leave事件;如果只是上下重合,直接转发即可。
#pragma once
#include <QCoreApplication>
#include <QHoverEvent>
// quick-private
#include <QtQuick/private/qquickmousearea_p.h>
// 转发hover事件
// 这里继承QQuickMouseArea重写接口转发,也可以用事件过滤转发
class MyMouseArea : public QQuickMouseArea
{
Q_OBJECT
Q_PROPERTY(QQuickMouseArea *forwardTo READ getForwardTo WRITE setForwardTo NOTIFY forwardToChanged)
public:
using QQuickMouseArea::QQuickMouseArea;
QQuickMouseArea *getForwardTo() const {
return forwardTo;
}
void setForwardTo(QQuickMouseArea *forward) {
if (forwardTo != forward) {
forwardTo = forward;
emit forwardToChanged();
}
}
protected:
void hoverEnterEvent(QHoverEvent *event) override {
QQuickMouseArea::hoverEnterEvent(event);
doForward(event);
}
void hoverMoveEvent(QHoverEvent *event) override {
QQuickMouseArea::hoverMoveEvent(event);
doForward(event);
}
void hoverLeaveEvent(QHoverEvent *event) override {
QQuickMouseArea::hoverLeaveEvent(event);
doForward(event);
}
void doForward(QHoverEvent *event) {
if (!forwardTo) {
return;
}
// TODO 这里没有处理交叠部分,而是直接转发了
QHoverEvent hover{event->type(),
mapToItem(forwardTo, event->posF()),
mapToItem(forwardTo, event->oldPosF()),
event->modifiers()};
// 使用send多个MouseArea之间移动时保持一定的处理顺序
QCoreApplication::sendEvent(forwardTo, &hover);
// QCoreApplication::sendEvent(forwardTo, event);
}
signals:
void forwardToChanged();
private:
// 事件转发到对应的MouseArea
QQuickMouseArea *forwardTo{nullptr};
};
3.混合情况
有时候混合事件(如pressed)和基本事件(如clicked)可能都在使用,这就要注意其中的关联关系,比如clicked是和press相关的。如果press设置了mouse.accepted=false,那么即便不把propagateComposedEvents设置为true,pressed和clicked也会传递下去:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
MouseArea{
anchors.fill: parent
onPressed: console.log("~root pressed")
onClicked: console.log("~root clicked")
}
//item的层级在root之上(因为他写在mousearea的后面,又是同级的)
Rectangle{
width: 200
height: 200
color: "green"
MouseArea{
anchors.fill: parent
//是否传递到被覆盖的MouseArea
//propagateComposedEvents: true
onPressed: mouse.accepted=false
}
}
}
可以看到,因为pressed设置mouse.accepted=false,clicked事件也在pressed接受的MouseArea触发了。
让我们来看个更复杂点的例子:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
MouseArea{
anchors.fill: parent
onPressed: console.log("~root pressed")
onClicked: console.log("~root clicked")
}
Rectangle{
width: 200
height: 200
color: "green"
MouseArea{
anchors.fill: parent
//是否传递到被覆盖的MouseArea
propagateComposedEvents: true
onPressed: {
console.log("item pressed")
mouse.accepted=true
}
/*onClicked: {
console.log("item clicked")
mouse.accepted=false
}*/
}
}
MouseArea{
anchors.fill: parent
onPressed: {
console.log("surface pressed")
mouse.accepted=false
}
onClicked: {
console.log("surface clicked")
mouse.accepted=false
}
}
}
最上层的surface把pressed设置了mouse.accepted=false,所以即便没有设置propagateComposedEvents,组合事件clicked也传递下去了。而中间层的item只处理了press,而又把propagateComposedEvents设置为了true,所以clicked最后传递到了~root上去了。
最后的总结就是:组合事件如clicked主要由propagateComposedEvents设置,基本事件如pressed主要由mouse.accepted设置,组合事件会被基本事件影响。
(2019-11-29 补充)
覆盖在类似Control2里的PushButton、TextField这些控件上时,需要用 "onPressed: mouse.accepted=false; " 来传递下去。
(2024-01-15 补充)
MouseArea 默认是有 cursorShape 的,没有属性来关闭 MouseArea 的 cursorShape,甚至 enable:false 都不行,如果只是想过滤点击,但是鼠标样式还是用下层组件的,可以自己继承一个 QQuickItem,如下:
#pragma once
#include <QQuickItem>
#include <QEvent>
#include <QCursor>
#include <QDebug>
// 过滤点击事件
class MouseItem : public QQuickItem
{
Q_OBJECT
public:
explicit MouseItem(QQuickItem *parent = nullptr)
: QQuickItem(parent) {
setAcceptedMouseButtons(Qt::AllButtons);
// 只过滤点击事件就别设置 cursorShape
// setCursor(QCursor(Qt::PointingHandCursor));
// unsetCursor();
}
void mousePressEvent(QMouseEvent *event) override {
qDebug()<<"press"<<event->pos();
event->ignore();
}
void mouseReleaseEvent(QMouseEvent *event) override {
// press ignore 后不会进入该 release 了
qDebug()<<"release"<<event->pos();
event->ignore();
}
};
4.参考
Qt文档:MouseArea QML Type | Qt Quick 5.15.16