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 项目中复用。
二、自定义滑动区域 SwipeArea
1. 为什么要自定义 SwipeArea
Qt Quick 里有很多原生控件,但实际项目中经常会遇到"原生控件不完全满足需求"的情况。
比如手机系统常见操作:
- 从顶部下滑,唤出控制中心。
- 从底部上滑,返回桌面或查看后台。
- 左右滑动,切换页面。
这些都可以用 MouseArea 的按下、移动、释放事件来模拟。
SwipeArea 的本质是:
qml
MouseArea + 坐标记录 + 阈值判断 + 自定义信号
2. 实现思路
一次滑动事件可以拆成 3 步:
onPressed:记录鼠标或手指按下时的原点坐标。onPositionChanged:判断当前移动更偏向 X 方向还是 Y 方向。onReleased:根据释放点和原点的差值,判断是上滑、下滑、左滑还是右滑。
为了避免轻微抖动也触发滑动,需要设置一个阈值:
qml
property int threshold: 50
只有移动距离超过 threshold,才认为这是一次有效滑动。
三、Demo 1:封装 SwipeArea.qml
新建文件:SwipeArea.qml
qml
import 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.qml
qml
import 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 负责识别动作,业务界面负责响应动作。这样组件就能复用,不会和某一个具体页面绑死。
五、自定义按钮 QuickButton
1. 为什么要自定义按钮
Qt Quick Controls 里有原生 Button,但实际项目里经常需要高度定制:
- 自定义背景色。
- 自定义按下颜色。
- 自定义圆角。
- 自定义字体。
- 自定义图标。
- 自定义点击信号。
所以可以用 Rectangle 做外观,用 MouseArea 做交互,用 Label 或 Image 显示内容。
六、Demo 3:封装 QuickButton.qml
新建文件:QuickButton.qml
qml
import 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 实现计数器
qml
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 组件,常用在界面切换。
核心属性:
qml
source: "Home.qml"
当 source 改变时,Loader 会卸载旧界面并加载新界面。
常见用途:
- 首页切换到设置页。
- 设置页返回首页。
- 点击应用图标进入详情页。
- 根据状态动态加载不同页面。
九、Demo 5:Loader 页面切换完整示例
1. Main.qml
qml
import 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.qml
qml
import 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.qml
qml
import 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.qml
qml
import 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 管加载,子页面管发信号,主页面管切换。
也就是:
text
子页面点击按钮 -> emit signal -> Main.qml Connections 接收 -> 修改 Loader.source
十、HarmonyUI 综合示例
1. 综合示例用到了什么
HarmonyUI 示例的重点不是完全复刻鸿蒙系统,而是把前面知识串起来:
Image做背景和图标。Label显示时间、日期、文字。Timer每秒更新时间。Grid排列应用图标。QuickButton封装应用入口。SwipeArea处理上下滑动。Loader切换页面。Connections接收页面信号。
2. 项目建议文件结构
text
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.qml
qml
import 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.qml
qml
import 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.qml
qml
import 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. 为什么自定义组件里的信号外部收不到
检查组件里是否定义了信号:
qml
signal openPage(string pageName)
然后确认外部是否用 Connections 正确连接:
qml
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 滑动太灵敏
调大阈值:
qml
threshold: 80
阈值越大,需要滑动更远才触发,误触更少。
4. 为什么 QuickButton 点击后颜色不恢复
检查 onReleased 里有没有恢复颜色:
qml
onReleased: {
root.color = root.normalColor
}
如果鼠标按下后移出按钮再释放,可能需要进一步处理 onCanceled。
十三、复习路线
建议按这个顺序复习:
- 先敲
QuickButton.qml,理解属性封装和信号暴露。 - 再敲
SwipeArea.qml,理解按下、移动、释放的事件流程。 - 然后敲
Loader三页面切换示例,理解source + Connections。 - 最后敲简化版 HarmonyUI,把前面组件组合起来。
这节课最重要的一句话是:Qt Quick 的基础控件只是积木,真正做项目时要学会把积木封装成自己的组件。