QML实现异形按钮(不规则点击区域)

前言

在一个带圆角的 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 处理。

方式一:自定义 QQuickItem

此方式可以参照 Qt 示例,路径如:

D:QtOnlineExamplesQt-5.15.2quickcustomitemsmaskedmousearea

示例的逻辑就是在 Image 上贴一个自定义 QQuickItem,然后在 contains 接口中根据图片透明度来区分是否处在区域内。

#ifndef MASKEDMOUSEAREA_H
#define MASKEDMOUSEAREA_H

#include <QImage>
#include <QQuickItem>


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 <QStyleHints>
#include <QGuiApplication>
#include <qqmlfile.h>

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<int>(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
        }
    }

方式二:containmentMask属性赋值Shape

鼠标事件处理可以用 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)
            }   
        }
    }

方式三:containmentMask属性赋值自定义QQuickItem

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 <QQuickItem>
#include <QImage>
#include <qqmlfile.h>

//修改自示例 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<int>(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};
};

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>