在一个带圆角的 Rectangle 上放置一个 MouseArea,当点击圆角外区域时,依旧能触发点击事件。用 OpacityMask 遮罩裁出来的图形也一样。
Rectangle {
width: 100
height: width
radius: width / 2
color: "green"
MouseArea {
anchors.fill: parent
onClicked: console.log('click',mouseX,mouseY)
}
}
因为 Item 对应的 QQuickItem 类是用 contains 接口来判断是否处在区域内的,但是其默认只判断了矩形范围,并没有对圆角或者图形裁剪等做判断。
bool QQuickItem::contains(const QPointF &point) const
{
Q_D(const QQuickItem);
if (d->mask) { //mask 是 Qt5.11 的 containmentMask 属性新增的
bool res = false;
d->extra->maskContains.invoke(d->mask,
Qt::DirectConnection,
Q_RETURN_ARG(bool, res),
Q_ARG(QPointF, point));
return res;
} else {
qreal x = point.x();
qreal y = point.y();
return x >= 0 && y >= 0 && x <= d->width && y <= d->height;
}
}
QML 制作异形点击区域主要有两种方式:
- 继承 QQuickItem 或其子类,重写 contains 接口,并处理点击相关事件。
- 从 Qt5.11 开始,Item 提供了 containmentMask 属性,可以用 Shape 来构造一个路径供 Item 的 contions 接口进行判断。此属性也可以和自定义 QQuickItem 配合,这样我们只需要处理 contains 接口,而鼠标事件仍旧由 MouseArea 处理。
此方式可以参照 Qt 示例,路径如:
D:\QtOnline\Examples\Qt-5.15.2\quick\customitems\maskedmousearea
示例的逻辑就是在 Image 上贴一个自定义 QQuickItem,然后在 contains 接口中根据图片透明度来区分是否处在区域内。
#ifndef MASKEDMOUSEAREA_H
#define MASKEDMOUSEAREA_H
#include
#include
class MaskedMouseArea : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(bool pressed READ isPressed NOTIFY pressedChanged)
Q_PROPERTY(bool containsMouse READ containsMouse NOTIFY containsMouseChanged)
Q_PROPERTY(QUrl maskSource READ maskSource WRITE setMaskSource NOTIFY maskSourceChanged)
Q_PROPERTY(qreal alphaThreshold READ alphaThreshold WRITE setAlphaThreshold NOTIFY alphaThresholdChanged)
QML_ELEMENT
public:
MaskedMouseArea(QQuickItem *parent = 0);
bool contains(const QPointF &point) const;
bool isPressed() const { return m_pressed; }
bool containsMouse() const { return m_containsMouse; }
QUrl maskSource() const { return m_maskSource; }
void setMaskSource(const QUrl &source);
qreal alphaThreshold() const { return m_alphaThreshold; }
void setAlphaThreshold(qreal threshold);
signals:
void pressed();
void released();
void clicked();
void canceled();
void pressedChanged();
void maskSourceChanged();
void containsMouseChanged();
void alphaThresholdChanged();
protected:
void setPressed(bool pressed);
void setContainsMouse(bool containsMouse);
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void hoverEnterEvent(QHoverEvent *event);
void hoverLeaveEvent(QHoverEvent *event);
void mouseUngrabEvent();
private:
bool m_pressed;
QUrl m_maskSource;
QImage m_maskImage;
QPointF m_pressPoint;
qreal m_alphaThreshold;
bool m_containsMouse;
};
#endif
#include "maskedmousearea.h"
#include
#include
#include
MaskedMouseArea::MaskedMouseArea(QQuickItem *parent)
: QQuickItem(parent),
m_pressed(false),
m_alphaThreshold(0.0),
m_containsMouse(false)
{
setAcceptHoverEvents(true);
setAcceptedMouseButtons(Qt::LeftButton);
}
void MaskedMouseArea::setPressed(bool pressed)
{
if (m_pressed != pressed) {
m_pressed = pressed;
emit pressedChanged();
}
}
void MaskedMouseArea::setContainsMouse(bool containsMouse)
{
if (m_containsMouse != containsMouse) {
m_containsMouse = containsMouse;
emit containsMouseChanged();
}
}
void MaskedMouseArea::setMaskSource(const QUrl &source)
{
if (m_maskSource != source) {
m_maskSource = source;
m_maskImage = QImage(QQmlFile::urlToLocalFileOrQrc(source));
emit maskSourceChanged();
}
}
void MaskedMouseArea::setAlphaThreshold(qreal threshold)
{
if (m_alphaThreshold != threshold) {
m_alphaThreshold = threshold;
emit alphaThresholdChanged();
}
}
bool MaskedMouseArea::contains(const QPointF &point) const
{
if (!QQuickItem::contains(point) || m_maskImage.isNull())
return false;
QPoint p = point.toPoint();
if (p.x() < 0 || p.x() >= m_maskImage.width() ||
p.y() < 0 || p.y() >= m_maskImage.height())
return false;
qreal r = qBound(0, m_alphaThreshold * 255, 255);
return qAlpha(m_maskImage.pixel(p)) > r;
}
void MaskedMouseArea::mousePressEvent(QMouseEvent *event)
{
setPressed(true);
m_pressPoint = event->pos();
emit pressed();
}
void MaskedMouseArea::mouseReleaseEvent(QMouseEvent *event)
{
setPressed(false);
emit released();
const int threshold = qApp->styleHints()->startDragDistance();
const bool isClick = (threshold >= qAbs(event->x() - m_pressPoint.x()) &&
threshold >= qAbs(event->y() - m_pressPoint.y()));
if (isClick)
emit clicked();
}
void MaskedMouseArea::mouseUngrabEvent()
{
setPressed(false);
emit canceled();
}
void MaskedMouseArea::hoverEnterEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(true);
}
void MaskedMouseArea::hoverLeaveEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(false);
}
Image {
id: moon
scale: moonArea.pressed ? 1.1 : 1.0
opacity: moonArea.containsMouse ? 1.0 : 0.7
source: Qt.resolvedUrl("images/moon.png")
MaskedMouseArea {
id: moonArea
anchors.fill: parent
alphaThreshold: 0.4
maskSource: moon.source
}
}
鼠标事件处理可以用 MouseArea 或者 TapHandler 等。为什么用 Shape 来做 mask 呢?当你在源码中搜 contains 接口的时候你会发现只有 Shape 重写了该接口用于判断路径,这就很离谱,要不怎么说 QML 只是个半成品。
bool QQuickShape::contains(const QPointF &point) const
{
Q_D(const QQuickShape);
switch (d->containsMode) {
case BoundingRectContains:
return QQuickItem::contains(point);
case FillContains: //从源码来看需要设置为fill
for (QQuickShapePath *path : d->sp) {
if (path->path().contains(point))
return true;
}
}
return false;
}
一个简单的实现:
Item {
anchors.centerIn: parent
width: 120
height: 90
Shape {
id: ctr
anchors.fill: parent
//只有FillContains才会逐个路径判断
//默认是BoundingRectContains根据矩形区域判断
containsMode: Shape.FillContains
layer.enabled: true
layer.samples: 16
ShapePath {
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
strokeWidth: 4
strokeColor: mouse_area.containsMouse ? "red" : "blue"
fillColor: "gray"
startX: ctr.width / 2; startY: 4
PathLine { x: 4; y: ctr.height - 4 }
PathLine { x: ctr.width - 4; y: ctr.height - 4 }
PathLine { x: ctr.width / 2; y: 4 }
}
}
MouseArea {
id: mouse_area
anchors.fill: parent
containmentMask: ctr
hoverEnabled: true
onClicked: {
console.log('click',mouseX,mouseY)
}
}
}
Shape 做点简单的几何样式还好,复杂的还是直接自定义 QQuickItem 重写 contains 接口方便。在支持 containmentMask 属性的版本,QQuickItem 只重写 contains 接口即可,可以不用写鼠标事件处理。
Image {
id: img
anchors.centerIn: parent
source: "qrc:/img.png"
MouseArea {
anchors.fill: parent
containmentMask: mask
ImageMask {
id: mask
anchors.fill: parent
maskSource: img.source
alphaThreshold: 0.1
}
onClicked: console.log('click')
}
}
#pragma once
#include
#include
#include
//修改自示例 maskedmouseaarea
class ImageMask : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QUrl maskSource READ maskSource WRITE setMaskSource NOTIFY maskSourceChanged)
Q_PROPERTY(qreal alphaThreshold READ alphaThreshold WRITE setAlphaThreshold NOTIFY alphaThresholdChanged)
public:
using QQuickItem::QQuickItem;
bool contains(const QPointF &point) const override {
//判断了QQuickItem::contains的话还得设置宽高
if (!QQuickItem::contains(point) || m_maskImage.isNull())
return false;
QPoint p = point.toPoint();
//这里用的image的实际宽高,没有考虑缩放
if (p.x() < 0 || p.x() >= m_maskImage.width() ||
p.y() < 0 || p.y() >= m_maskImage.height())
return false;
qreal r = qBound(0, m_alphaThreshold * 255, 255);
return qAlpha(m_maskImage.pixel(p)) > r;
}
QUrl maskSource() const { return m_maskSource; }
void setMaskSource(const QUrl &source) {
if (m_maskSource != source) {
m_maskSource = source;
m_maskImage = QImage(QQmlFile::urlToLocalFileOrQrc(source));
emit maskSourceChanged();
}
}
qreal alphaThreshold() const { return m_alphaThreshold; }
void setAlphaThreshold(qreal threshold) {
if (m_alphaThreshold != threshold) {
m_alphaThreshold = threshold;
emit alphaThresholdChanged();
}
}
signals:
void maskSourceChanged();
void alphaThresholdChanged();
private:
QUrl m_maskSource;
QImage m_maskImage;
qreal m_alphaThreshold{0.0f};
};