目录
- 引言
- 层级结构
- 默认层级结构
- z值
- 作用范围
- 遮罩实现
- -1的作用
- 热区嵌套
- 与普通元素
- 与其他热区
- 与Flickable
- 事件透传
- 总结
引言
热区有很多种,诸如MouseArea、DropArea、PinchArea等等,基本都是拦截对应的事件,允许开发者在事件函数对事件进行响应。
本文则以MouseArea为例,对热区的嵌套进行讲解。热区嵌套是开发中经常会遇到的常见,如下图所示,需要为信息卡片的右下角增加额外的按钮,在鼠标悬浮时该按钮才显示,而按钮本身也会对鼠标事件进行响应,这就是所谓的热区嵌套。
出现热区嵌套后,会出现事件无法正常传递的情况,导致两个组件单独使用都是好的,嵌套在一起就会出现问题,这也是后面将要讨论的问题。
层级结构
在解决热区嵌套所带来的问题前,需要先理解各个元素的层级结构,因为热区的父类就是Item,只是在Item的基础上进行特化。
默认层级结构
默认层级与生成的顺序有关,也就是QML中的顺序,如下所示:
Rectangle {x: 50y: 50width: 100height: widthcolor: Qt.rgba(241 / 255, 148 / 255, 138 / 255)}Rectangle {x: 100y: 100width: 100height: widthcolor: Qt.rgba(72 / 255, 201 / 255, 176 / 255)}Rectangle {x: 150y: 150width: 100height: widthcolor: Qt.rgba(95 / 255, 174 / 255, 227 / 255)}Rectangle {x: 200y: 200width: 100height: widthcolor: Qt.rgba(247 / 255, 220 / 255, 111 / 255)}
z值
如果不希望按照生成顺序去对图层进行排布,可以通过修改z值完成,代码如下:
Rectangle {x: 100y: 100width: 100height: widthcolor: Qt.rgba(72 / 255, 201 / 255, 176 / 255)z: 1}
作用范围
需要注意的是,z值只对兄弟节点有效,如下所示:
Item {id:rootRectangle {id: rect1x: 50y: 50width: 100height: widthcolor: Qt.rgba(241 / 255, 148 / 255, 138 / 255)}Rectangle {id: rect2x: 100y: 100width: 100height: widthcolor: Qt.rgba(72 / 255, 201 / 255, 176 / 255)z: 999}Rectangle {id: rect3x: 150y: 150width: 100height: widthcolor: Qt.rgba(95 / 255, 174 / 255, 227 / 255)}}Rectangle {id: rect4x: 50y: 150width: 100height: widthcolor: Qt.rgba(247 / 255, 220 / 255, 111 / 255)z: 0}
可以看到尽管rect2的z值设置为999,但并没有显示在rect4之上,也就是绿色矩形依然在黄色矩形之下。这是因为z并不是全局有效的数值,它只能决定兄弟节点之前的排序,并不能超过根节点的所在位置,也就是rect2可以显示在rect3之上,当时不能超过root的显示层级。
遮罩实现
所谓遮罩,就是显示在组件的最上层的一层半透明图层,一般是用在组件禁用时表现置灰。遮罩的实现会使用到z值,组件内堆叠顺序先按照z值进行排序,z值相同则按照生成顺序。
如果需要为组件添加遮罩,且组件内没有对z进行特殊处理,将z值设置为1即可,并不需要设置为999或者是99,这些数值多大都是没有意义的。
如果组件内对z进行了特殊的排序,用于各个图层之间的顺序规划,那么应该有一个属性用来确定当前显示层级的最大值,也就是maximumLevel,遮罩的z值设置成该属性即可,并不需要设置成999这一个令人迷惑的数字。
-1的作用
具有负z值的项绘制在其父项的内容下面,如下所示:
Rectangle {id: rect2x: 50y: 150width: 100height: widthcolor: Qt.rgba(247 / 255, 220 / 255, 111 / 255)z: 0}Rectangle {id: rootcolor: Qt.rgba(241 / 255, 148 / 255, 138 / 255)width: 150height: widthRectangle {id: rect1x: 100y: 100width: 100height: widthcolor: Qt.rgba(72 / 255, 201 / 255, 176 / 255)z: -1}}
可以看到rect1的z值设置为-1之后,可以显示在root的下方,也就是绿色的矩形会显示在红色的矩形的下方,这是一个可以使用在特殊常见的技巧。当时和之前的作用范围相同,其作用范围不会干扰到其他元素,如上图所示,绿色并没有显示在黄色的底下,即便是rect1的z值为-1、rect2的z值为0。
热区嵌套
与普通元素
首先第一个的问题就是热区和普通像素会不会发生事件争抢,也就是当热区被普通元素覆盖时,事件是否能够继续传递至热区中,如下所示:
Rectangle {id: rect1x: 100y: 100width: 100height: widthcolor: mouseArea.containsMouse ? "green" : Qt.rgba(72 / 255, 201 / 255, 176 / 255)MouseArea {id: mouseAreaanchors.fill: parenthoverEnabled: true}}Rectangle {id: rect2color: Qt.rgba(241 / 255, 148 / 255, 138 / 255)width: 150height: width}
动图中显示绿色矩形与红色矩形重叠且红色矩形覆盖在绿色矩形上方,当时鼠标在进入到的重叠部分时,底部的绿色矩形依旧变色,这说明附着在绿色矩形上的MouseArea尽管被遮挡,但依然能够生效。但为红色矩形同样附着一个相同的MouseArea,重叠区域将不能再触发。
上述的两个现象说明,普通元素并不会截断事件的传递,MouseArea之间才会相互影响。这一点非常重要,让Qt Quick在界面实现时有非常大的灵活性,可以轻松实现,热区与显示区域的完全分离,例如使用MouseArea和Rectangle实现按钮,MouseArea获取点击事件,Rectangle显示三态,Rectangle可以在MouseArea之上的很多个图层。
这一点与传统的Qt Widget非常不同,任意的QWidget都会截断事件往下方传递,即便是没有事件过滤器、事件函数,如下所示:
auto btn = new QPushButton("Test", this);connect(btn, &QPushButton::clicked, this, [=]{btn->setText("Trigger");});auto widget = new QWidget(this);widget->setFixedSize(200, 100);widget->setAttribute(Qt::WA_StyledBackground);widget->setStyleSheet("background: rgba(255, 0, 0, 0.5)");
Qt Widget透传需要额外设置才能实现,如下所示:
setAttribute(Qt::WA_TransparentForMouseEvents, true);
与其他热区
回到开篇的问题,为信息卡片添加按钮,鼠标悬浮至卡片上才显示,很快就可以写出下面的代码实现:
Rectangle {color: Qt.rgba(30 / 255, 33 / 255, 35 / 255)radius: 12Label {anchors.fill: parenttext: "Info"color: "white"font.family: "Microsoft YaHei UI"font.pixelSize: 16horizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenter}MouseArea {id: mouseAreaanchors.fill: parenthoverEnabled: true}Button {anchors.right: parent.rightanchors.bottom: parent.bottomanchors.margins: 8width: 16height: widthpadding: 0text: "+"visible: mouseArea.containsMouse}
}
如上述动图所示,会出现明明鼠标只是在按钮上方移动,却会出现按钮时而隐藏时而消失的情况,但是按钮的显示隐藏代码中是通过MouseArea的containsMouse属性去控制的,鼠标在显示层面并没有离开底部MouseArea的范围。
此处出现的问题就是热区嵌套时可能遇到错误,MouseArea和Button的出现事件的争取,问题的原因时因为上述代码中MouseArea和Button是兄弟节点,如果改为父子节点则可以解决此问题,如下所示:
MouseArea {id: mouseAreaanchors.fill: parenthoverEnabled: trueButton {anchors.right: parent.rightanchors.bottom: parent.bottomanchors.margins: 8width: 16height: widthpadding: 0text: "+"visible: mouseArea.containsMouse}}
这里可以参考Qt Widget中事件的传递方向,事件由OS产生,通过QApplication分发,先在子组件(比如说一个QButton)中发生的事件,调用了子组件的event函数之后,还会调用父组件(比如说QWidget)的 event 函数。
与Flickable
Flickable这个组件可能对于很多人比较陌生,它是ListView、GridView等的父类,顾名思义控制着滑动的功能,在视口移动时有回弹的动画,和MouseArea同样的会对鼠标事件进行处理。假设有一个信息卡片的容器,而这些卡片同样需要支持拖拽,很自然就想到如下代码(组件FootageCard代码与前文类似,此处不赘述):
GridView {id: gridViewanchors.fill: parentmodel: 50delegate: FootageCard {id: infoCardwidth: 96height: widthtext: "Info" + indexcolor: Qt.rgba(Math.random(), Math.random(), Math.random())MouseArea {anchors.fill: parentonPositionChanged: {dragCard.visible = truelet global = mapToGlobal(mouse.x, mouse.y)let local = gridView.mapFromGlobal(global)dragCard.x = local.xdragCard.y = local.y}onReleased: {dragCard.visible = false}onCanceled: {console.log("onCanceled")}}}FootageCard {id: dragCardwidth: 96height: 96text: "Drag"visible: false}}
如动图所示,出现了明显不符合预期的效果,卡片在拖拽到一半的时候被卡在中间,也就是说鼠标移动事件onPositionChanged一开始时生效的,在移动了一定距离之后事件就被中断,鼠标松开事件onReleased也不再执行,反而是取消事件onCanceled打印了出来。
这里同样会遇到事件被争抢的问题,只需要为MouseArea开启preventStealing属性即可,如下:
MouseArea {anchors.fill: parentpreventStealing: true//...
}
正常效果如下所示:
事件透传
上述常见是热区嵌套的场景,实际开发时也会遇到两个热区重叠的场景,一般是希望前一个MouseArea处理一些事件,后一个MouseArea又处理另一些事件,这里就需要使用到事件的透传。代码如下:
MouseArea {anchors.fill: parentonClicked: {console.log("onClicked MouseArea_1")}}MouseArea {anchors.fill: parentpropagateComposedEvents: trueonClicked: {console.log("onClicked MouseArea_2")mouse.accepted = false}}
不仅需要设置属性propagateComposedEvents为true,还需要重新事件函数,将事件的accepted设置为false,才能进行事件透传,这里依旧是Qt的事件传播机制。
总结
热区嵌套问题,本质上就是事件传递的问题,通常就是上层组件截断相关事件,导致下层无法接受对应的信号,抑或是接受的信号无法正确成对,如接收到鼠标按下事件缺无法接收到鼠标松开事件。排查时需要重点关注事件的传递。