QML MouseArea 事件传递、自定义按钮

这篇介绍两个 demo:

  • 事件传递 --- 当多个鼠标区域叠加时,控制点击事件要不要传给下层
  • 自定义按钮 --- 用基础组件做一个带交互动效的按钮

事件传递

当两个 MouseArea 叠在一起(比如按钮盖在背景上),你点在上层,这个点击到底是"上层收到"还是"两层都收到"?默认情况下只给最上层。但有些场景希望点击能"穿过"上层传给下面,QML 提供了开关来控制。这个 demo 用一个开关现场演示开/关两种行为的差别。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        TitleSeparator {
            title: "事件传递"
            description: "演示 propagateComposedEvents 与 mouse.accepted 控制事件在重叠 MouseArea 间的传递"
        }

        Rectangle {
            id: parentBox
            Layout.fillWidth: true
            Layout.fillHeight: true
            radius: 8
            color: "#EAF4FF"
            border.color: "#1296FF"
            border.width: 2

            property int parentCount: 0

            // 外层 MouseArea 事件日志
            Text {
                anchors.top: parent.top
                anchors.horizontalCenter: parent.horizontalCenter
                anchors.topMargin: 12
                text: "外层 MouseArea\n点击次数: " + parentBox.parentCount
                font.pointSize: 12
                color: "#1296FF"
                horizontalAlignment: Text.AlignHCenter
            }

            MouseArea {
                id: parentMouse
                anchors.fill: parent
                onClicked: (mouse) => {
                    parentBox.parentCount++
                }
            }

            // 内层方块(z 更高,覆盖在父 MouseArea 之上)
            Rectangle {
                id: childBox
                width: 180
                height: 80
                radius: 8
                anchors.centerIn: parent
                color: childMouse.pressed ? "#FF6B6B" : (childMouse.containsMouse ? "#FF8787" : "#FFD4D4")
                border.color: "#FF6B6B"
                border.width: 2
                z: 1

                property int childCount: 0

                Text {
                    anchors.centerIn: parent
                    text: "内层 MouseArea\n点击次数: " + childBox.childCount
                    font.pointSize: 12
                    color: "#C0392B"
                    horizontalAlignment: Text.AlignHCenter
                }

                MouseArea {
                    id: childMouse
                    anchors.fill: parent
                    hoverEnabled: true
                    propagateComposedEvents: propagateSwitch.checked
                    onClicked: (mouse) => {
                        childBox.childCount++

                        // 若 propagate 为 true,放行事件给下层
                        if (propagateComposedEvents) {
                            mouse.accepted = false
                        }
                    }
                }
            }
        }

        RowLayout {
            Layout.alignment: Qt.AlignHCenter
            spacing: 20

            Switch {
                id: propagateSwitch
                text: "放行事件给下层"
                checked: false
            }

            Button {
                text: "重置计数"
                onClicked: {
                    parentBox.parentCount = 0
                    childBox.childCount = 0
                }
            }
        }
    }
}

关键逻辑解析

两层的叠加关系

最外层是一个铺满内容区的 MouseAreaparentMouse),中间盖着一个方块 childBox,方块里又有自己的 MouseAreachildMouse)。因为 childBox 设了 z: 1,它比外层高,所以点中间那个方块时,事件首先落在内层 childMouse 上,而不直接掉到外层。

默认行为:点到哪层算哪层

先看开关默认处于"关"的状态。这时 propagateComposedEvents 是 false,点内层方块,只有内层计数加一,外层纹丝不动。这就是 QML 的默认:事件被最上层 MouseArea "吃掉"了,不会再往下传。很多场景下这是我们要的行为------比如点一个按钮,不希望它还点到了按钮后面的背景。

打开开关:让事件穿下去

把"放行事件给下层"打开,propagateComposedEvents 变 true。但光开这一个还不够,因为开了它只代表"允许传递",真正把事件放行还得在 onClicked 里把这件事件标记为"未接受":

qml 复制代码
mouse.accepted = false

mouse.accepted 就是"这事件我接不接"。设为 false 表示"我不收这件事件",事件就会继续往下层走,外层 parentMouse 也能收到,于是内层、外层计数一起加。这串组合起来的效果是:内层自己处理了(自己的计数照加),同时把事件漏给了下层。

开关的两态对比

一个开关、一次点击,就能直观地对比出两套行为------开的时候点中间,两层都计数;关的时候点中间,只有内层计数。理解这个对排查"点击没反应"类问题很有用:很多时候是它被某个上层 MouseArea 悄悄拦截了。

自定义按钮

在有些场景下需要特殊定制的按钮,现成的 Button 不容易满足,不如用最基础的元素造一个。原理很简单:一个 Rectangle 当外观,一个 MouseArea 铺上去接收鼠标,再用鼠标状态去驱动外观变化。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        TitleSeparator {
            title: "自定义按钮"
            description: "使用 MouseArea 实现完整的按钮交互:悬停变色、按下缩放、释放回弹"
        }

        // 自定义按钮
        Rectangle {
            id: btn
            Layout.alignment: Qt.AlignHCenter
            width: 140
            height: 50
            radius: 8
            color: mouseArea.pressed ? "#0E7BD9" : (mouseArea.containsMouse ? "#3FA9F5" : "#1296FF")
            scale: mouseArea.pressed ? 0.95 : 1.0

            Behavior on scale {
                NumberAnimation { duration: 100; easing.type: Easing.OutQuad }
            }
            Behavior on color {
                ColorAnimation { duration: 150 }
            }

            Text {
                anchors.centerIn: parent
                text: "自定义按钮"
                color: "white"
                font.pointSize: 13
                font.bold: true
            }

            MouseArea {
                id: mouseArea
                anchors.fill: parent
                hoverEnabled: true
                cursorShape: Qt.PointingHandCursor
                onClicked: btn.clickCount++
            }

            property int clickCount: 0
        }


        Text {
            Layout.alignment: Qt.AlignHCenter
            text: "矩形按钮点击: " + btn.clickCount + " 次"
            font.pointSize: 12
            color: "#999"
        }

        // 圆角图标按钮
        Rectangle {
            id: iconBtn
            Layout.alignment: Qt.AlignHCenter
            width: 50
            height: 50
            radius: 25
            color: iconMouse.pressed ? "#E8910C" : (iconMouse.containsMouse ? "#FFB74D" : "#FF9800")
            scale: iconMouse.pressed ? 0.9 : 1.0

            Behavior on scale {
                NumberAnimation { duration: 100; easing.type: Easing.OutQuad }
            }
            Behavior on color {
                ColorAnimation { duration: 150 }
            }

            Text {
                anchors.centerIn: parent
                text: "+"
                color: "white"
                font.pointSize: 24
                font.bold: true
            }

            MouseArea {
                id: iconMouse
                anchors.fill: parent
                hoverEnabled: true
                cursorShape: Qt.PointingHandCursor
                onClicked: iconBtn.iconClickCount++
            }

            property int iconClickCount: 0
        }

        Text {
            Layout.alignment: Qt.AlignHCenter
            text: "圆形按钮点击: " + iconBtn.iconClickCount + " 次"
            font.pointSize: 12
            color: "#999"
        }

        Item { Layout.fillHeight: true }
    }
}

关键逻辑解析

一个按钮的三态外观

自定义按钮的中枢是把鼠标状态映射到颜色上。以蓝色矩形按钮为例:

qml 复制代码
color: mouseArea.pressed ? "#0E7BD9" : (mouseArea.containsMouse ? "#3FA9F5" : "#1296FF")

三种状态:普通、悬停、按下各对应一个颜色,一眼就能看出鼠标所处的阶段。

按下缩小、松开回弹

除了变色,还让按钮在按下时轻微缩一点:

qml 复制代码
scale: mouseArea.pressed ? 0.95 : 1.0

按住时 scale 降到 0.95(比原尺寸小一点),松开回到 1.0。配合 100 毫秒的 Behavior 过渡,按下、弹起的动作就带了一点"按下感",比单纯变色更生动。

鼠标变成手型

cursorShape: Qt.PointingHandCursor 让鼠标移入按钮时从箭头变成手型,这是所有可点击控件都该有的细节,用户一看就知道"这里能点"。它同样依赖 hoverEnabled: true 才能跟随悬停生效。

Behavior 统一做平滑

无论状态怎么在三种颜色、两种尺寸之间切换,都是动画过渡而不是生硬跳变。

运行验证

  1. Qt Creator 打开示例的 CMakeLists.txt
  2. Ctrl+R 运行。

扩展复用方向

  • 事件传递的开关思路可以做"半透明遮罩不挡点击"------遮罩开着但 accepted 设为 false,点击穿透
  • 自定义按钮套模板改外形就能出很多种按钮:指针在圆角矩形上变胶囊、图标按钮、Toggle 开关按钮
  • 给按钮加 Behavior 的这套写法可以扩展到任意可点击元素上,统一手感
  • 把自定义按钮封装成可复用组件,暴露 pressedhovered 等信号,全项目统一用

已验证环境

相关推荐
小庞在加油14 小时前
WinDbg实战:QT/跨平台项目死锁与无响应问题排查指南
开发语言·qt·windbg·工具
hy.z_77716 小时前
【QT】1.QT初识(背景介绍、搭建开发环境、Qt Creator、程序、项目文件解析、编程注意事项)
qt
不会代码的小猴17 小时前
7. JSON
开发语言·c++·笔记·qt·算法·json
沫璃染墨17 小时前
《Qt从零入门系列(六):信号与槽进阶——从多种连接方式到Lambda表达式》
开发语言·c++·qt·代码规范·设计规范·qt5
橙橙笔记21 小时前
QT的安装
开发语言·qt·安装·软件
AI的探索之旅1 天前
图像格式与转换一文讲透:相机 / OpenCV / Qt
数码相机·qt·opencv
2kπ1 天前
QT开发笔记
开发语言·笔记·qt
Quz1 天前
QML MouseArea:点击、悬停与坐标获取
qt
程与留1 天前
04_Qt项目构建流程深度解析——MOC、UIC、RCC 的作用与原理
qt