
Qt Quick 综合开发复习自定义组件、Loader 与 HarmonyUI 实战本文根据《Qt Quick 开发教程综合讲解 2026年6月29日》整理适合作为课后复习博客。重点包括自定义滑动区域SwipeArea、自定义按钮QuickButton、Loader动态加载界面以及一个类 HarmonyOS 的综合 UI 示例。一、这节课到底在讲什么这节课的核心不是某一个单独控件而是 Qt Quick 的一个重要能力把基础元素组合成自己的组件。前面学过的Rectangle、MouseArea、Image、Label、Timer、Grid、Loader等都可以继续组合封装成更贴近实际项目的组件。本节主要目标会基于MouseArea自定义滑动区域。会基于Rectangle MouseArea自定义按钮。会使用Loader实现页面切换。会把自定义组件放进一个综合 UI 项目中复用。二、自定义滑动区域 SwipeArea1. 为什么要自定义 SwipeAreaQt Quick 里有很多原生控件但实际项目中经常会遇到“原生控件不完全满足需求”的情况。比如手机系统常见操作从顶部下滑唤出控制中心。从底部上滑返回桌面或查看后台。左右滑动切换页面。这些都可以用MouseArea的按下、移动、释放事件来模拟。SwipeArea的本质是MouseArea 坐标记录 阈值判断 自定义信号2. 实现思路一次滑动事件可以拆成 3 步onPressed记录鼠标或手指按下时的原点坐标。onPositionChanged判断当前移动更偏向 X 方向还是 Y 方向。onReleased根据释放点和原点的差值判断是上滑、下滑、左滑还是右滑。为了避免轻微抖动也触发滑动需要设置一个阈值property int threshold: 50只有移动距离超过threshold才认为这是一次有效滑动。三、Demo 1封装 SwipeArea.qml新建文件SwipeArea.qmlimport QtQuick MouseArea { id: root // 记录按下时的起点坐标。 property point origin: Qt.point(0, 0) // 滑动阈值。移动距离超过这个值才算有效滑动。 property int threshold: 50 // 记录当前判断出来的主要拖动方向。 // 初始允许 X 和 Y 两个方向移动时再判断主方向。 property int currentAxis: Drag.XAndYAxis // 对外暴露一个滑动信号。 // direction 可取值left、right、up、down。 signal swipe(string direction) onPressed: function(mouse) { // 保存按下那一刻的位置。 origin Qt.point(mouse.x, mouse.y) // 每次重新按下时都重置方向判断。 currentAxis Drag.XAndYAxis } onPositionChanged: function(mouse) { var dx mouse.x - origin.x var dy mouse.y - origin.y // 还没超过阈值时不急着判断方向避免误触。 if (Math.abs(dx) threshold Math.abs(dy) threshold) { return } // 哪个方向变化更大就认为用户主要想往哪个方向滑。 if (Math.abs(dx) Math.abs(dy)) { currentAxis Drag.XAxis } else { currentAxis Drag.YAxis } } onReleased: function(mouse) { var dx mouse.x - origin.x var dy mouse.y - origin.y // 如果释放时依然没有超过阈值就不触发滑动信号。 if (Math.abs(dx) threshold Math.abs(dy) threshold) { return } if (currentAxis Drag.XAxis) { if (dx 0) { root.swipe(right) } else { root.swipe(left) } } else if (currentAxis Drag.YAxis) { if (dy 0) { root.swipe(down) } else { root.swipe(up) } } } }练习说明这个组件本身不显示任何东西因为它继承自MouseArea。它的作用是接收滑动动作然后发出swipe(direction)信号。四、Demo 2使用 SwipeArea 实现下滑显示控制中心新建或修改Main.qmlimport QtQuick import QtQuick.Controls Window { width: 480 height: 800 visible: true title: SwipeArea Practice Rectangle { id: page anchors.fill: parent color: #eaf1f8 Label { anchors.centerIn: parent text: 向下滑动显示控制中心\n向上滑动隐藏控制中心 horizontalAlignment: Text.AlignHCenter font.pixelSize: 24 } Rectangle { id: controlCenter width: parent.width height: 260 y: -height color: #222831 radius: 16 Label { anchors.centerIn: parent text: 控制中心 color: white font.pixelSize: 32 font.bold: true } // y 变化时加动画让下拉效果更自然。 Behavior on y { NumberAnimation { duration: 220 easing.type: Easing.OutCubic } } } SwipeArea { anchors.fill: parent threshold: 60 onSwipe: function(direction) { if (direction down) { // 下滑控制中心滑入页面。 controlCenter.y 0 } else if (direction up) { // 上滑控制中心隐藏到顶部外面。 controlCenter.y -controlCenter.height } } } } }重点记忆SwipeArea负责识别动作业务界面负责响应动作。这样组件就能复用不会和某一个具体页面绑死。五、自定义按钮 QuickButton1. 为什么要自定义按钮Qt Quick Controls 里有原生Button但实际项目里经常需要高度定制自定义背景色。自定义按下颜色。自定义圆角。自定义字体。自定义图标。自定义点击信号。所以可以用Rectangle做外观用MouseArea做交互用Label或Image显示内容。六、Demo 3封装 QuickButton.qml新建文件QuickButton.qmlimport QtQuick import QtQuick.Controls Rectangle { id: root width: 140 height: 52 radius: 10 // 正常状态颜色。 property color normalColor: #2f80ed // 按下状态颜色。 property color pressedColor: #1c5fb8 // 按钮文字。 property string buttonText: 按钮 // 文字颜色。 property color textColor: white // 字体设置。 property int fontPixelSize: 20 property bool fontBold: false property string fontFamily: // 对外暴露按钮事件。 signal pressed() signal released() signal clicked() color: normalColor Label { anchors.centerIn: parent text: root.buttonText color: root.textColor font.pixelSize: root.fontPixelSize font.bold: root.fontBold font.family: root.fontFamily } MouseArea { anchors.fill: parent onPressed: { // 按下时切换颜色并把事件发给外部。 root.color root.pressedColor root.pressed() } onReleased: { // 释放时恢复颜色。 root.color root.normalColor root.released() } onClicked: { // 点击事件交给外部处理。 root.clicked() } } }关键点QuickButton把外观属性和交互信号都封装好了外部只需要改属性和处理onClicked。七、Demo 4使用 QuickButton 实现计数器import QtQuick import QtQuick.Controls Window { width: 640 height: 420 visible: true title: QuickButton Practice property int count: 0 Label { id: countLabel anchors.centerIn: parent text: count font.pixelSize: 64 font.bold: true } Row { spacing: 20 anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 40 QuickButton { buttonText: 减一 normalColor: #eb5757 pressedColor: #b84040 fontPixelSize: 24 onClicked: { count - 1 } } QuickButton { buttonText: 清零 normalColor: #4f4f4f pressedColor: #222222 fontPixelSize: 24 onClicked: { count 0 } } QuickButton { buttonText: 加一 normalColor: #27ae60 pressedColor: #1f8a4c fontPixelSize: 24 onClicked: { count 1 } } } }复习提醒这里的countLabel.text: count是属性绑定。只要count变化界面就会自动刷新。八、Loader 动态加载界面1. Loader 是什么Loader用于动态加载 QML 组件常用在界面切换。核心属性source: Home.qml当source改变时Loader会卸载旧界面并加载新界面。常见用途首页切换到设置页。设置页返回首页。点击应用图标进入详情页。根据状态动态加载不同页面。九、Demo 5Loader 页面切换完整示例1. Main.qmlimport QtQuick Window { width: 420 height: 720 visible: true title: Loader Practice Loader { id: pageLoader anchors.fill: parent source: Home.qml } Connections { // Loader 加载出来的对象可以通过 pageLoader.item 访问。 target: pageLoader.item function onOpenPage(pageName) { if (pageName settings) { pageLoader.source SettingsPage.qml } else if (pageName network) { pageLoader.source NetworkPage.qml } else if (pageName home) { pageLoader.source Home.qml } } } }2. Home.qmlimport QtQuick import QtQuick.Controls Rectangle { id: root color: #f1f5f9 signal openPage(string pageName) Label { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: 80 text: Home font.pixelSize: 42 font.bold: true } Column { spacing: 20 anchors.centerIn: parent QuickButton { buttonText: 设置 normalColor: #2f80ed onClicked: root.openPage(settings) } QuickButton { buttonText: 网络 normalColor: #27ae60 pressedColor: #1f8a4c onClicked: root.openPage(network) } } }3. SettingsPage.qmlimport QtQuick import QtQuick.Controls Rectangle { id: root color: #e8f0ff signal openPage(string pageName) Label { anchors.centerIn: parent text: Settings Page font.pixelSize: 36 } QuickButton { anchors.left: parent.left anchors.top: parent.top anchors.margins: 24 buttonText: 返回 normalColor: #4f4f4f pressedColor: #222222 onClicked: root.openPage(home) } }4. NetworkPage.qmlimport QtQuick import QtQuick.Controls Rectangle { id: root color: #e9fff2 signal openPage(string pageName) Label { anchors.centerIn: parent text: Network Page font.pixelSize: 36 } QuickButton { anchors.left: parent.left anchors.top: parent.top anchors.margins: 24 buttonText: 返回 normalColor: #4f4f4f pressedColor: #222222 onClicked: root.openPage(home) } }Loader 的关键记忆Loader管加载子页面管发信号主页面管切换。也就是子页面点击按钮 - emit signal - Main.qml Connections 接收 - 修改 Loader.source十、HarmonyUI 综合示例1. 综合示例用到了什么HarmonyUI 示例的重点不是完全复刻鸿蒙系统而是把前面知识串起来Image做背景和图标。Label显示时间、日期、文字。Timer每秒更新时间。Grid排列应用图标。QuickButton封装应用入口。SwipeArea处理上下滑动。Loader切换页面。Connections接收页面信号。2. 项目建议文件结构HarmonyUI/ ├─ Main.qml ├─ Home.qml ├─ Wechat.qml ├─ QuickButton.qml ├─ SwipeArea.qml └─ images/ ├─ background.jpg ├─ wechat.png ├─ settings.png └─ browser.png如果暂时没有图片也可以先用纯色矩形代替先把逻辑跑通。十一、Demo 6简化版 HarmonyUI 主入口1. Main.qmlimport QtQuick Window { width: 480 height: 800 visible: true title: HarmonyUI Demo Loader { id: pageLoader anchors.fill: parent source: Home.qml } Connections { target: pageLoader.item function onOpenPage(pageName) { if (pageName wechat) { pageLoader.source Wechat.qml } else if (pageName home) { pageLoader.source Home.qml } } } }2. Home.qmlimport QtQuick import QtQuick.Controls import QtQuick.Layouts Rectangle { id: root color: #dce9f7 signal openPage(string pageName) property string currentTime: property string currentDate: function updateDateTime() { var now new Date() currentTime Qt.formatTime(now, hh:mm) currentDate Qt.formatDate(now, yyyy年MM月dd日) } Component.onCompleted: updateDateTime() Timer { interval: 1000 repeat: true running: true onTriggered: root.updateDateTime() } // 顶部状态栏。 Row { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 14 spacing: 12 Label { text: 5G font.pixelSize: 16 font.bold: true } Label { text: WiFi font.pixelSize: 16 } Item { // 占位 Item把电量文字推到右侧。 width: 1 height: 1 Layout.fillWidth: true } Label { text: 100% font.pixelSize: 16 } } // 时间日期区域。 Column { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: 90 spacing: 8 Label { text: root.currentTime font.pixelSize: 56 font.bold: true anchors.horizontalCenter: parent.horizontalCenter } Label { text: root.currentDate font.pixelSize: 18 color: #334155 anchors.horizontalCenter: parent.horizontalCenter } Label { text: 晴 26°C font.pixelSize: 18 color: #334155 anchors.horizontalCenter: parent.horizontalCenter } } // APP 图标区域。 Grid { id: appGrid columns: 4 rowSpacing: 26 columnSpacing: 28 anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 280 QuickButton { width: 76 height: 76 radius: 22 buttonText: 微信 fontPixelSize: 16 normalColor: #22c55e pressedColor: #16a34a onClicked: root.openPage(wechat) } QuickButton { width: 76 height: 76 radius: 22 buttonText: 设置 fontPixelSize: 16 normalColor: #64748b pressedColor: #475569 } QuickButton { width: 76 height: 76 radius: 22 buttonText: 浏览 fontPixelSize: 16 normalColor: #0ea5e9 pressedColor: #0284c7 } QuickButton { width: 76 height: 76 radius: 22 buttonText: 相册 fontPixelSize: 16 normalColor: #f97316 pressedColor: #ea580c } } Rectangle { id: controlPanel width: parent.width height: 280 y: -height color: #111827 radius: 24 Behavior on y { NumberAnimation { duration: 220 easing.type: Easing.OutCubic } } Label { anchors.centerIn: parent text: 控制中心 color: white font.pixelSize: 32 font.bold: true } } SwipeArea { anchors.fill: parent threshold: 70 onSwipe: function(direction) { if (direction down) { controlPanel.y 0 } else if (direction up) { controlPanel.y -controlPanel.height } } } }3. Wechat.qmlimport QtQuick import QtQuick.Controls Rectangle { id: root color: #f8fafc signal openPage(string pageName) Label { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: 110 text: 微信登录 font.pixelSize: 42 font.bold: true } Column { anchors.centerIn: parent spacing: 18 Rectangle { width: 300 height: 52 radius: 8 color: white border.color: #cbd5e1 Label { anchors.centerIn: parent text: 请输入账号 color: #94a3b8 font.pixelSize: 18 } } Rectangle { width: 300 height: 52 radius: 8 color: white border.color: #cbd5e1 Label { anchors.centerIn: parent text: 请输入密码 color: #94a3b8 font.pixelSize: 18 } } QuickButton { width: 300 height: 52 buttonText: 登录 normalColor: #22c55e pressedColor: #16a34a } } QuickButton { anchors.left: parent.left anchors.top: parent.top anchors.margins: 24 width: 92 height: 44 buttonText: 返回 normalColor: #475569 pressedColor: #334155 fontPixelSize: 18 onClicked: root.openPage(home) } }十二、常见问题整理1. 为什么自定义组件里的信号外部收不到检查组件里是否定义了信号signal openPage(string pageName)然后确认外部是否用Connections正确连接Connections { target: pageLoader.item function onOpenPage(pageName) { console.log(pageName) } }信号名是openPage处理函数就要写成onOpenPage。2. 为什么 Loader 切换页面没有反应常见原因source文件名写错。QML 文件没有加入CMakeLists.txt的QML_FILES。子页面没有发信号。Connections.target没有指向pageLoader.item。3. 为什么 SwipeArea 滑动太灵敏调大阈值threshold: 80阈值越大需要滑动更远才触发误触更少。4. 为什么 QuickButton 点击后颜色不恢复检查onReleased里有没有恢复颜色onReleased: { root.color root.normalColor }如果鼠标按下后移出按钮再释放可能需要进一步处理onCanceled。十三、复习路线建议按这个顺序复习先敲QuickButton.qml理解属性封装和信号暴露。再敲SwipeArea.qml理解按下、移动、释放的事件流程。然后敲Loader三页面切换示例理解source Connections。最后敲简化版 HarmonyUI把前面组件组合起来。这节课最重要的一句话是Qt Quick 的基础控件只是积木真正做项目时要学会把积木封装成自己的组件。