普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月15日首页

QML 最佳实践写出高质量、可维护、高性能的代码(十二)

作者 HelloReader
2026年4月15日 00:59

适合人群: 已能独立写 QML 应用,想提升代码质量和性能的开发者

前言

会写 QML 和写好 QML 之间,有一段不小的距离。本文覆盖 Qt 官方推荐的 QML 最佳实践,涉及类型安全、属性绑定、JavaScript 使用边界、组件封装、可维护性和性能优化六大主题,每条都配有"反例 vs 正例"的对比代码。


一、使用强类型属性声明

问题:var 类型丢失所有静态检查

// 不推荐:var 类型
property var name        // 是字符串?整数?对象?
property var count       // 无法做类型检查
property var config      // 工具无法推断类型

var 属性:

  • 无法被 qmllint 静态分析
  • 无法被 Qt Quick Compiler 编译优化
  • 赋值类型错误时,报错指向声明处而非赋值处,难以定位

解决:始终使用具体类型

// 推荐:强类型声明
property string  userName: ""
property int     itemCount: 0
property real    progress: 0.0
property bool    isLoading: false
property color   accentColor: "#4A90E2"
property url     avatarSource: ""
property date    createdAt
property var     rawData       // 只有真正需要动态类型时才用 var

强类型的好处:

强类型属性
    ├── qmllint 可静态分析       → 编码阶段发现错误
    ├── Qt Quick Compiler 可编译  → 绑定表达式运行更快
    ├── 错误信息指向赋值处        → 调试更容易
    └── 代码即文档               → 阅读者一眼知道期望类型

二、避免非限定访问(Unqualified Access)

问题:直接访问父级属性,不带 id 前缀

// 不推荐:非限定访问
Item {
    property int fontSize: 16

    Item {
        Text {
            font.pixelSize: fontSize    // 非限定访问!
            // qmllint 警告:[unqualified]
            // Qt Quick Compiler 无法编译此绑定
        }
    }
}

非限定访问的问题:

  • 运行时动态查找,性能差
  • 工具链(qmllint、编译器)无法静态确认访问是否合法
  • 当嵌套层级复杂时,fontSize 到底来自哪里?——代码难以阅读

解决:始终通过 id 限定访问

// 推荐:限定访问
Item {
    id: root
    property int fontSize: 16

    Item {
        Text {
            font.pixelSize: root.fontSize    // 限定访问,清晰明确
        }
    }
}

在 Delegate 中用 required property 替代非限定访问:

// 不推荐:Delegate 直接访问 model 角色(非限定)
ListView {
    delegate: Text {
        text: name       // 非限定访问 model 角色
        color: isActive ? "green" : "gray"
    }
}

// 推荐:required property 显式声明
ListView {
    delegate: Text {
        required property string name
        required property bool   isActive

        text: name
        color: isActive ? "green" : "gray"
    }
}

三、理解并正确使用属性绑定

3.1 声明式绑定 vs 命令式赋值

// 不推荐:在 Component.onCompleted 中命令式设置初始值
Rectangle {
    id: box
    color: "blue"

    Component.onCompleted: {
        box.width = parent.width / 2    // 命令式赋值
        box.height = parent.height / 2  // 这会破坏任何后续绑定
    }
}

// 推荐:声明式绑定,始终保持响应式
Rectangle {
    id: box
    width: parent.width / 2     // 声明式绑定:parent 宽度变化时自动更新
    height: parent.height / 2
    color: "blue"
}

3.2 在 JS 代码块中赋值会打断绑定

Rectangle {
    id: box
    width: parent.width    // 绑定

    MouseArea {
        anchors.fill: parent
        onClicked: {
            box.width = 200    // 赋值后,上面的绑定被永久打断!
                               // 之后 parent.width 变化,box.width 不再跟随
        }
    }
}

如果必须在事件中重新建立绑定,使用 Qt.binding()

onClicked: {
    box.width = Qt.binding(function() { return parent.width })
}

3.3 避免绑定循环

// 错误:绑定循环,会产生运行时警告
Item {
    property int a: b + 1    // a 依赖 b
    property int b: a + 1    // b 依赖 a → 循环!
}

// 正确:其中一个属性改为普通赋值或由外部驱动
Item {
    property int a: 0
    property int b: a + 1    // 单向依赖,安全
}

3.4 保持绑定表达式简单

// 不推荐:绑定中包含复杂逻辑
Text {
    text: {
        var result = ""
        for (var i = 0; i < model.count; i++) {
            result += model.get(i).name + ", "
        }
        return result.slice(0, -2)
    }
}

// 推荐:复杂逻辑提取到函数,绑定只调用函数
Item {
    function buildNameList() {
        var names = []
        for (var i = 0; i < model.count; i++) {
            names.push(model.get(i).name)
        }
        return names.join(", ")
    }

    Text {
        text: buildNameList()    // 绑定表达式简洁
    }
}

四、JavaScript 的使用边界

QML 中的 JavaScript 是把双刃剑,用好了事半功倍,滥用了则带来维护噩梦。

4.1 适合用 JavaScript 的场景

// ✅ 简单的条件表达式(三元运算符)
color: isActive ? "#4A90E2" : "#CCCCCC"

// ✅ 简单计算
width: parent.width * 0.8

// ✅ 事件处理(onClicked 等)
onClicked: {
    model.remove(index)
    showToast("已删除")
}

// ✅ 辅助函数(封装复杂逻辑,供绑定调用)
function formatDate(dateStr) {
    var d = new Date(dateStr)
    return d.getFullYear() + "-" + (d.getMonth()+1) + "-" + d.getDate()
}

4.2 不适合用 JavaScript 的场景

// ❌ 在绑定中做大量数据处理(每次绑定求值都会执行)
ListView {
    model: {
        var filtered = []
        for (var i = 0; i < sourceModel.count; i++) {
            if (sourceModel.get(i).price > 100)
                filtered.push(sourceModel.get(i))
        }
        return filtered    // 每次 sourceModel 变化都重新过滤,性能差
    }
}

// ✅ 用 C++ 代理模型或专门的过滤函数,不放在绑定里
// ❌ 用 JS 模拟属性绑定(既不响应式,也不可读)
Component.onCompleted: {
    labelText.text = "Hello " + userName    // 只执行一次,userName 变化后不更新
}

// ✅ 直接用绑定
Text {
    id: labelText
    text: "Hello " + userName    // 声明式,自动响应
}

4.3 复杂逻辑放到 C++ 或独立 .js 文件

// utils.js — 独立的工具函数库
.pragma library    // 共享模式,只加载一次

function formatCurrency(amount, symbol) {
    return symbol + amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}

function timeAgo(dateStr) {
    var diff = (Date.now() - new Date(dateStr)) / 1000
    if (diff < 60)    return "刚刚"
    if (diff < 3600)  return Math.floor(diff / 60) + " 分钟前"
    if (diff < 86400) return Math.floor(diff / 3600) + " 小时前"
    return Math.floor(diff / 86400) + " 天前"
}
import "utils.js" as Utils

Text { text: Utils.formatCurrency(price, "¥") }
Text { text: Utils.timeAgo(createdAt) }

五、属性遮蔽(Property Shadowing)陷阱

问题:子组件定义了与父组件同名的属性

// 危险:属性遮蔽
Rectangle {
    property color color: "blue"    // 遮蔽了 Rectangle 自带的 color 属性!
    // 此时 color 既指自定义属性,又指 Rectangle.color
    // 绑定行为变得不可预测
}
// 危险:在 Delegate 中声明与 model 角色同名的属性
ListView {
    delegate: Rectangle {
        property string name: "默认"    // 遮蔽了 model 的 name 角色!
        Text { text: name }             // 显示的是 "默认",而不是 model 数据
    }
}

解决:使用不会冲突的命名,或改用 required property

// 推荐:使用不冲突的命名
Rectangle {
    property color backgroundColor: "blue"    // 不与内置属性冲突
    color: backgroundColor
}

// 推荐:Delegate 用 required property 而不是声明同名属性
ListView {
    delegate: Rectangle {
        required property string name    // 明确声明来自 model
        Text { text: name }
    }
}

六、组件封装原则

6.1 单一职责:一个组件做一件事

// 不推荐:一个组件承担太多职责
// UserCard.qml — 包含数据获取、显示、编辑、删除...

// 推荐:拆分为职责单一的小组件
// UserAvatar.qml  — 只负责头像显示
// UserInfo.qml    — 只负责用户信息文本
// UserCard.qml    — 组合 Avatar + Info,加入卡片样式
// UserActions.qml — 只负责操作按钮区域

6.2 明确暴露的接口:property + signal

// 好的组件接口设计
// SearchBar.qml
Rectangle {
    id: root

    // 对外暴露的属性(接口)
    property string placeholder: "搜索..."
    property alias  searchText: field.text     // alias 透传内部属性
    property int    maxLength: 100

    // 对外发出的信号(接口)
    signal searchSubmitted(string query)
    signal cleared()

    // 内部实现细节(不对外暴露)
    TextField {
        id: field
        placeholderText: root.placeholder
        maximumLength: root.maxLength
        onAccepted: root.searchSubmitted(text)
    }

    Button {
        text: "清除"
        onClicked: {
            field.clear()
            root.cleared()
        }
    }
}

6.3 不要在组件内部直接访问外部 id

// 不推荐:组件直接引用外部 id(强耦合,组件无法复用)
// MyButton.qml
Button {
    onClicked: mainWindow.showDialog()    // 直接访问外部 id!
}

// 推荐:通过信号解耦
// MyButton.qml
Button {
    signal buttonClicked()
    onClicked: buttonClicked()           // 发出信号,由外部决定做什么
}

// main.qml
MyButton {
    onButtonClicked: mainWindow.showDialog()    // 外部连接信号
}

七、代码组织:QML 文件内部的书写顺序

Qt 官方推荐的 QML 文件内部属性书写顺序:

Rectangle {
    // 1. id(第一行,方便快速定位)
    id: root

    // 2. 属性声明(property / required property / readonly property)
    property string title: ""
    required property int index
    readonly property int maxCount: 10

    // 3. 信号声明
    signal itemSelected(int idx)

    // 4. JavaScript 函数
    function doSomething() { }

    // 5. 对象属性赋值(x, y, width, height, color…)
    x: 0; y: 0
    width: 200; height: 100
    color: "#f5f5f5"

    // 6. 子对象
    Text {
        anchors.centerIn: parent
        text: root.title
    }

    // 7. 状态和过渡
    states: [ State { name: "active" } ]
    transitions: [ Transition { } ]
}

八、性能最佳实践

8.1 使用 Loader 延迟加载非关键内容

ApplicationWindow {
    // 主内容立即加载
    MainContent { anchors.fill: parent }

    // 设置页面、帮助面板等用 Loader 延迟加载
    Loader {
        id: settingsLoader
        active: false    // 默认不加载
        sourceComponent: SettingsPanel {}
    }

    Button {
        text: "设置"
        onClicked: settingsLoader.active = true    // 第一次点击时才加载
    }
}

8.2 避免在 Delegate 中使用 Layouts 和 Anchors

// 不推荐:Delegate 中使用 ColumnLayout(创建和销毁开销大)
delegate: ColumnLayout {
    Text { text: name }
    Text { text: description }
}

// 推荐:Delegate 中用简单的 x/y/width/height 定位
delegate: Item {
    width: ListView.view.width; height: 60
    Text {
        x: 16; y: 8
        text: name
        font.pixelSize: 15; font.bold: true
    }
    Text {
        x: 16; y: 32
        text: description
        font.pixelSize: 13; color: "#888"
    }
}

8.3 使用 qmllint 进行静态检查

在 Qt Creator 终端运行:

# 检查单个文件
qmllint Main.qml

# 检查整个项目(编译警告级别)
qmllint --compiler warning *.qml

qmllint 能发现:

  • 非限定访问 [unqualified]
  • 未声明的属性
  • 废弃的 API 用法
  • 信号处理器参数未命名

8.4 使用 QML Profiler 定位性能瓶颈

在 Qt Creator 中:Analyze → QML Profiler

QML Profiler 时间线视图:
┌─────────────────────────────────────────────────────┐
│ Animations    ████░░░████░░░████░░░                 │
│ Compiling     █░░░░░░░░░░░░░░░░░░░░░░░             │
│ Creating      ██░░░░░░░░░░░░░░░░░░░░░░             │
│ Binding       ░░░██░░░██░░░██░░░                   │
│ Handling Sig  ░░░░░█░░░░░█░░░░░█░░░                │
│ JavaScript    ░░░░░░█████░░░░░░████                │
│                                                     │
│  ← 帧时间不应超过 16ms(60fps)→                   │
└─────────────────────────────────────────────────────┘

重点关注:JavaScript 函数执行时间是否超过 16ms,Binding 是否被频繁触发。


九、可维护性:做好国际化准备

从第一行代码起就养成用 qsTr() 包裹用户可见字符串的习惯:

// 不推荐:硬编码字符串(之后国际化要改遍全部文件)
Button { text: "确认" }
Label  { text: "请输入用户名" }

// 推荐:从一开始就用 qsTr()
Button { text: qsTr("确认") }
Label  { text: qsTr("请输入用户名") }

lupdate 提取所有 qsTr() 字符串到 .ts 翻译文件:

lupdate MyProject.pro -ts translations/app_zh_CN.ts

总结

最佳实践 核心要点
强类型属性 int/string/bool 而不是 var
限定访问 通过 id.property 访问,避免裸用父级属性名
required property Delegate 中声明 model 角色的推荐方式
声明式绑定 能用 : 绑定就不用 = 赋值
简单绑定表达式 复杂逻辑提取为函数,不放在绑定中
避免属性遮蔽 不要用与父级或内置属性同名的属性名
单一职责组件 每个 .qml 文件只做一件事
Loader 延迟加载 非关键 UI 按需加载,减少启动时间
Delegate 简化定位 x/y 代替 Layouts,减少对象创建开销
qmllint 静态检查 每次提交前运行,发现潜在问题
qsTr() 国际化 从第一行起包裹所有用户可见字符串

Qt Quick Controls 全览控件、弹窗、导航与样式定制(十一)

作者 HelloReader
2026年4月15日 00:40

适合人群: 已掌握基础 QML 语法,想系统掌握完整控件库的开发者 > 预计耗时: 90 分钟


前言

Qt Quick Controls 提供了构建完整应用界面所需的全套控件——从最基础的按钮、输入框,到菜单、抽屉、页面导航。本文系统梳理每一类控件的完整用法,并深入讲解样式系统和自定义控件外观。


一、控件分类总览

Qt Quick Controls 的控件按功能分为六大类:

QtQuick.Controls
├── 按钮类     Button · CheckBox · RadioButton · Switch · RoundButton
├── 输入类     TextField · TextArea · Slider · Dial · SpinBox · ComboBox · Tumbler
├── 显示类     Label · ProgressBar · BusyIndicator · DelayButton
├── 容器类     Frame · GroupBox · ScrollView · Pane · Page · TabBar · ToolBar
├── 弹窗类     Dialog · Drawer · Menu · Popup · ToolTip
└── 导航类     StackView · SwipeView · PageIndicator

二、内置样式一览

Qt Quick Controls 内置多套样式,一行代码即可切换全局外观。

Basic 样式(默认,跨平台)

Basic 样式控件展示

图片来源:Qt 官方文档 — Basic Style

轻量极简,性能最佳,适合作为自定义样式的起点。

Material 样式(Google Material Design)

Material 样式浅色主题

图片来源:Qt 官方文档 — Material Style

适合移动端和现代桌面应用,视觉效果丰富。

Fusion 样式(桌面风格)

传统桌面应用外观,与 Qt Widgets 视觉语言一致,适合企业桌面工具。

各平台默认样式

操作系统 默认样式
Android Material
iOS iOS Style
macOS macOS Style
Windows Windows Style
Linux / 其他 Fusion

设置样式的三种方式

方式一:编译时导入(推荐,性能最优)

// 必须在所有其他 QtQuick.Controls 导入之前
import QtQuick.Controls.Material

ApplicationWindow {
    Material.theme: Material.Light
    Material.accent: Material.Blue
}

方式二:运行时 C++ 设置

#include <QQuickStyle>
QQuickStyle::setStyle("Material");

方式三:配置文件 qtquickcontrols2.conf

[Controls]
Style=Material

[Material]
Theme=Light
Accent=Blue

三、按钮类控件完整用法

3.1 Button 的状态属性

Button {
    text: "操作按钮"

    // 核心状态属性(只读,反映当前交互状态)
    // pressed    — 正在按下
    // hovered    — 鼠标悬停
    // checked    — 已选中(checkable 时有效)
    // enabled    — 是否可用
    // highlighted — 强调样式(Material 下显示 accent 色)
    // flat       — 扁平样式(无背景边框)

    highlighted: true
    flat: false
    checkable: true     // 允许切换选中状态
    icon.source: "images/send.svg"
    icon.width: 18
    icon.height: 18
}

3.2 DelayButton — 长按确认按钮

需要长按才能触发,适合危险操作(删除、格式化):

DelayButton {
    text: "长按删除"
    delay: 1500     // 需要按住 1.5 秒

    onActivated: console.log("确认删除!")

    // 进度条自动显示按压进度
}

3.3 RoundButton — 圆形按钮

RoundButton {
    text: "+"
    font.pixelSize: 20
    highlighted: true

    // 或使用图标
    icon.source: "images/add.svg"
}

四、输入类控件完整用法

4.1 Dial — 旋钮控件

适合音量、亮度等环形调节:

import QtQuick
import QtQuick.Controls

Column {
    spacing: 8

    Dial {
        id: volumeDial
        from: 0; to: 100; value: 50
        stepSize: 1

        // 旋转模式
        inputMode: Dial.Circular      // 圆形拖动(默认)
        // inputMode: Dial.Horizontal // 水平拖动
        // inputMode: Dial.Vertical   // 垂直拖动
    }

    Label {
        anchors.horizontalCenter: parent.horizontalCenter
        text: "音量:" + Math.round(volumeDial.value)
    }
}

4.2 Tumbler — 滚筒选择器

适合时间、日期选择:

Row {
    spacing: 0

    Tumbler {
        id: hourTumbler
        model: 24
        delegate: Label {
            required property int index
            text: index.toString().padStart(2, "0")
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
            opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2)
            font.pixelSize: Math.abs(Tumbler.displacement) < 0.5 ? 18 : 14
        }
    }

    Label {
        anchors.verticalCenter: parent.verticalCenter
        text: ":"
        font.pixelSize: 18
        font.bold: true
    }

    Tumbler {
        id: minuteTumbler
        model: 60
        delegate: Label {
            required property int index
            text: index.toString().padStart(2, "0")
            horizontalAlignment: Text.AlignHCenter
            verticalAlignment: Text.AlignVCenter
            opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2)
            font.pixelSize: Math.abs(Tumbler.displacement) < 0.5 ? 18 : 14
        }
    }
}

五、容器类控件

5.1 Frame 与 GroupBox

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Column {
    spacing: 16
    width: 300

    // Frame:带边框的容器
    Frame {
        width: parent.width
        ColumnLayout {
            width: parent.width
            Label { text: "账号信息"; font.bold: true }
            TextField { Layout.fillWidth: true; placeholderText: "用户名" }
            TextField { Layout.fillWidth: true; placeholderText: "邮箱" }
        }
    }

    // GroupBox:带标题的 Frame
    GroupBox {
        width: parent.width
        title: "通知设置"
        ColumnLayout {
            width: parent.width
            CheckBox { text: "邮件通知" }
            CheckBox { text: "短信通知" }
            CheckBox { text: "推送通知"; checked: true }
        }
    }
}

5.2 TabBar + StackLayout — 选项卡导航

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Column {
    width: 400

    TabBar {
        id: tabBar
        width: parent.width

        TabButton { text: "首页" }
        TabButton { text: "发现" }
        TabButton { text: "消息" }
        TabButton { text: "我的" }
    }

    StackLayout {
        width: parent.width
        height: 300
        currentIndex: tabBar.currentIndex    // 与 TabBar 绑定

        Rectangle { color: "#E6F1FB"; Label { anchors.centerIn: parent; text: "首页内容" } }
        Rectangle { color: "#E1F5EE"; Label { anchors.centerIn: parent; text: "发现内容" } }
        Rectangle { color: "#FAEEDA"; Label { anchors.centerIn: parent; text: "消息内容" } }
        Rectangle { color: "#FAECE7"; Label { anchors.centerIn: parent; text: "我的内容" } }
    }
}

5.3 ToolBar — 工具栏

ApplicationWindow {
    width: 500; height: 400
    visible: true

    header: ToolBar {
        RowLayout {
            anchors.fill: parent

            ToolButton {
                icon.source: "images/menu.svg"
                onClicked: drawer.open()
            }

            Label {
                text: "应用标题"
                font.pixelSize: 16
                font.bold: true
                Layout.fillWidth: true
                horizontalAlignment: Text.AlignHCenter
            }

            ToolButton {
                icon.source: "images/search.svg"
            }

            ToolButton {
                icon.source: "images/more.svg"
            }
        }
    }
}

六、弹窗类控件

6.1 Dialog — 标准对话框

Dialog {
    id: confirmDialog
    anchors.centerIn: parent
    title: "确认操作"
    modal: true
    width: 280

    // 内容区
    contentItem: Label {
        text: "确定要删除这条记录吗?此操作不可撤销。"
        wrapMode: Text.Wrap
        padding: 8
    }

    // 标准按钮
    standardButtons: Dialog.Ok | Dialog.Cancel

    onAccepted: console.log("用户点击了确定")
    onRejected: console.log("用户取消了操作")
}

Button {
    text: "删除"
    onClicked: confirmDialog.open()
}

6.2 自定义 Dialog 内容

Dialog {
    id: inputDialog
    anchors.centerIn: parent
    title: "重命名"
    modal: true
    width: 300

    contentItem: ColumnLayout {
        spacing: 12
        width: parent.width

        Label { text: "请输入新名称:" }

        TextField {
            id: nameField
            Layout.fillWidth: true
            placeholderText: "名称"
            focus: true    // 对话框打开时自动聚焦
        }
    }

    footer: DialogButtonBox {
        Button {
            text: "取消"
            DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
        }
        Button {
            text: "确认"
            enabled: nameField.text.length > 0
            highlighted: true
            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
        }
    }

    onAccepted: console.log("新名称:" + nameField.text)
}

6.3 Drawer — 侧滑抽屉

ApplicationWindow {
    id: window
    width: 400; height: 600
    visible: true

    Drawer {
        id: drawer
        width: 260
        height: window.height
        edge: Qt.LeftEdge    // Qt.RightEdge / Qt.TopEdge / Qt.BottomEdge

        // 抽屉内容
        ColumnLayout {
            anchors.fill: parent
            anchors.margins: 0
            spacing: 0

            // 用户信息头部
            Rectangle {
                Layout.fillWidth: true
                height: 120
                color: "#4A90E2"

                Column {
                    anchors.centerIn: parent
                    spacing: 6
                    Rectangle {
                        width: 56; height: 56; radius: 28
                        color: "white"
                        anchors.horizontalCenter: parent.horizontalCenter
                        Label {
                            anchors.centerIn: parent
                            text: "用"
                            font.pixelSize: 22
                            font.bold: true
                            color: "#4A90E2"
                        }
                    }
                    Label {
                        text: "用户名"
                        color: "white"
                        font.pixelSize: 14
                        anchors.horizontalCenter: parent.horizontalCenter
                    }
                }
            }

            // 菜单列表
            Repeater {
                model: ["首页", "收藏", "历史记录", "设置", "帮助"]
                delegate: ItemDelegate {
                    required property string modelData
                    Layout.fillWidth: true
                    text: modelData
                    onClicked: {
                        console.log("导航到:" + modelData)
                        drawer.close()
                    }
                }
            }

            Item { Layout.fillHeight: true }

            ItemDelegate {
                Layout.fillWidth: true
                text: "退出登录"
            }
        }
    }

    Button {
        text: "打开抽屉"
        anchors.centerIn: parent
        onClicked: drawer.open()
    }
}

6.4 Menu — 上下文菜单

Menu {
    id: contextMenu

    MenuItem {
        text: "复制"
        shortcut: "Ctrl+C"
        onTriggered: console.log("复制")
    }

    MenuItem {
        text: "粘贴"
        shortcut: "Ctrl+V"
        onTriggered: console.log("粘贴")
    }

    MenuSeparator {}    // 分割线

    Menu {
        title: "导出为"
        MenuItem { text: "PDF" }
        MenuItem { text: "PNG" }
        MenuItem { text: "SVG" }
    }

    MenuSeparator {}

    MenuItem {
        text: "删除"
        enabled: false    // 禁用状态
    }
}

// 右键触发
MouseArea {
    anchors.fill: parent
    acceptedButtons: Qt.RightButton
    onClicked: contextMenu.popup()    // 在鼠标位置弹出
}

6.5 ToolTip — 悬停提示

Button {
    text: "保存"
    icon.source: "images/save.svg"

    // 方式一:附加属性(最简单)
    ToolTip.visible: hovered
    ToolTip.text: "保存文件 (Ctrl+S)"
    ToolTip.delay: 800    // 悬停 800ms 后显示

    // 方式二:独立 ToolTip 组件(可自定义外观)
}

七、导航类控件

7.1 StackView — 页面栈导航

StackView 实现类似移动端的前进/后退页面导航:

// 页面切换时间线:
// push()  → 新页面从右侧滑入
// pop()   → 当前页面向右滑出
// replace() → 替换当前页面(无返回)

StackView {
    id: stackView
    anchors.fill: parent

    // 初始页面
    initialItem: homePage
}

Component {
    id: homePage
    Rectangle {
        color: "#f5f5f5"
        Column {
            anchors.centerIn: parent
            spacing: 12

            Label { text: "首页"; font.pixelSize: 24; font.bold: true }

            Button {
                text: "进入详情页"
                onClicked: stackView.push(detailPage, { title: "详情内容" })
            }
        }
    }
}

Component {
    id: detailPage
    Rectangle {
        property string title: ""
        color: "#E6F1FB"
        Column {
            anchors.centerIn: parent
            spacing: 12

            Label { text: title; font.pixelSize: 20 }

            Button {
                text: "← 返回"
                onClicked: stackView.pop()
            }
        }
    }
}

StackView 页面切换动画流程:

┌─────────────┐  push()   ┌─────────────┬─────────────┐
│   首页      │ ────────▶ │   首页      │   详情页    │
│  (当前)   │           │  (历史)   │  (当前)   │
└─────────────┘           └─────────────┴─────────────┘

                 pop()    ┌─────────────┐
                ────────▶ │   首页      │
                          │  (当前)   │
                          └─────────────┘

7.2 SwipeView + PageIndicator — 横划导航

适合引导页、图片轮播、多步骤表单:

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Column {
    width: 360
    spacing: 0

    SwipeView {
        id: swipeView
        width: parent.width
        height: 280

        // 第一页
        Rectangle {
            color: "#4A90E2"
            Label {
                anchors.centerIn: parent
                text: "欢迎使用"
                font.pixelSize: 24
                font.bold: true
                color: "white"
            }
        }

        // 第二页
        Rectangle {
            color: "#1D9E75"
            Label {
                anchors.centerIn: parent
                text: "功能介绍"
                font.pixelSize: 24
                font.bold: true
                color: "white"
            }
        }

        // 第三页
        Rectangle {
            color: "#E2934A"
            Label {
                anchors.centerIn: parent
                text: "开始使用"
                font.pixelSize: 24
                font.bold: true
                color: "white"
            }
        }
    }

    // 页面指示点
    PageIndicator {
        anchors.horizontalCenter: parent.horizontalCenter
        count: swipeView.count
        currentIndex: swipeView.currentIndex    // 双向绑定
        interactive: true    // 点击圆点可跳转
    }
}

八、自定义控件外观

8.1 替换 background 和 contentItem

每个控件的外观由 background(背景)和 contentItem(内容)组成,单独替换其中任意一个即可改变外观:

// 自定义圆角按钮,保留所有交互行为
Button {
    id: btn
    text: "自定义按钮"
    width: 140; height: 44

    background: Rectangle {
        radius: btn.height / 2      // 完全圆角
        color: btn.pressed   ? "#2C72C7" :
               btn.hovered   ? "#5BA3E8" :
               btn.enabled   ? "#4A90E2" : "#AAAAAA"

        Behavior on color {
            ColorAnimation { duration: 120 }
        }

        border.width: 0
    }

    contentItem: Text {
        text: btn.text
        color: "white"
        font.pixelSize: 14
        font.bold: true
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
    }
}

8.2 自定义 ProgressBar

ProgressBar {
    id: bar
    width: 300
    value: 0.65

    background: Rectangle {
        implicitWidth: 200; implicitHeight: 8
        color: "#e0e0e0"
        radius: 4
    }

    contentItem: Item {
        implicitWidth: 200; implicitHeight: 8

        Rectangle {
            width: bar.visualPosition * parent.width
            height: parent.height
            radius: 4

            // 渐变进度条
            gradient: Gradient {
                orientation: Gradient.Horizontal
                GradientStop { position: 0.0; color: "#4A90E2" }
                GradientStop { position: 1.0; color: "#1D9E75" }
            }

            Behavior on width {
                NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
            }
        }
    }
}

8.3 封装统一风格的自定义控件

把自定义样式封装到独立的组件文件,在整个项目复用:

// PrimaryButton.qml
import QtQuick
import QtQuick.Controls

Button {
    id: root
    height: 44

    property color primaryColor: "#4A90E2"

    background: Rectangle {
        radius: 8
        color: root.pressed ? Qt.darker(root.primaryColor, 1.2)
             : root.hovered ? Qt.lighter(root.primaryColor, 1.1)
             : root.enabled ? root.primaryColor
             :                "#cccccc"
        Behavior on color { ColorAnimation { duration: 100 } }
    }

    contentItem: Text {
        text: root.text
        color: root.enabled ? "white" : "#888888"
        font.pixelSize: 14
        font.bold: true
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
    }
}

使用:

PrimaryButton { text: "确认"; width: 120 }
PrimaryButton { text: "危险操作"; width: 120; primaryColor: "#E24A4A" }
PrimaryButton { text: "成功"; width: 120; primaryColor: "#1D9E75" }

九、综合示例:设置页面

整合本文所有控件,构建一个完整的应用设置页面:

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

ApplicationWindow {
    width: 400; height: 650
    visible: true
    title: "设置"

    ScrollView {
        anchors.fill: parent
        contentWidth: availableWidth

        ColumnLayout {
            width: parent.width
            spacing: 0

            // 外观设置
            GroupBox {
                Layout.fillWidth: true
                Layout.margins: 16
                title: "外观"

                ColumnLayout {
                    width: parent.width
                    spacing: 4

                    RowLayout {
                        Layout.fillWidth: true
                        Label { text: "深色模式"; Layout.fillWidth: true }
                        Switch { id: darkSwitch }
                    }

                    RowLayout {
                        Layout.fillWidth: true
                        Label { text: "主题色"; Layout.fillWidth: true }
                        ComboBox {
                            model: ["蓝色", "绿色", "橙色", "紫色"]
                            Layout.preferredWidth: 100
                        }
                    }

                    RowLayout {
                        Layout.fillWidth: true
                        Label { text: "字体大小"; Layout.fillWidth: true }
                        Slider {
                            from: 12; to: 20; value: 15
                            stepSize: 1
                            Layout.preferredWidth: 120
                        }
                    }
                }
            }

            // 通知设置
            GroupBox {
                Layout.fillWidth: true
                Layout.leftMargin: 16
                Layout.rightMargin: 16
                title: "通知"

                ColumnLayout {
                    width: parent.width
                    spacing: 4

                    Repeater {
                        model: ["接收推送通知", "邮件提醒", "声音提示", "震动反馈"]
                        delegate: RowLayout {
                            required property string modelData
                            required property int index
                            Layout.fillWidth: true
                            Label { text: modelData; Layout.fillWidth: true }
                            Switch { checked: index < 2 }
                        }
                    }
                }
            }

            // 存储设置
            GroupBox {
                Layout.fillWidth: true
                Layout.leftMargin: 16
                Layout.rightMargin: 16
                title: "存储与数据"

                ColumnLayout {
                    width: parent.width
                    spacing: 8

                    Label {
                        text: "已用空间:1.2 GB / 5 GB"
                        font.pixelSize: 13; color: "#666"
                    }

                    ProgressBar {
                        Layout.fillWidth: true
                        value: 0.24
                    }

                    Button {
                        Layout.fillWidth: true
                        text: "清理缓存"
                        onClicked: cacheDialog.open()
                    }
                }
            }

            // 账号操作
            GroupBox {
                Layout.fillWidth: true
                Layout.leftMargin: 16
                Layout.rightMargin: 16
                Layout.bottomMargin: 16
                title: "账号"

                ColumnLayout {
                    width: parent.width
                    spacing: 8

                    Button {
                        Layout.fillWidth: true
                        text: "修改密码"
                    }

                    Button {
                        Layout.fillWidth: true
                        text: "退出登录"
                        onClicked: logoutDialog.open()
                    }
                }
            }
        }
    }

    // 清理缓存确认对话框
    Dialog {
        id: cacheDialog
        anchors.centerIn: parent
        title: "清理缓存"
        modal: true
        standardButtons: Dialog.Ok | Dialog.Cancel
        contentItem: Label {
            text: "确认清理所有缓存数据?"
            padding: 8
        }
        onAccepted: console.log("缓存已清理")
    }

    // 退出登录确认对话框
    Dialog {
        id: logoutDialog
        anchors.centerIn: parent
        title: "退出登录"
        modal: true
        standardButtons: Dialog.Yes | Dialog.No
        contentItem: Label {
            text: "确认退出当前账号?"
            padding: 8
        }
        onAccepted: console.log("已退出登录")
    }
}

总结

控件 用途 关键属性
Dial 旋钮调节 inputModestepSize
Tumbler 滚筒选择 modelvisibleItemCount
TabBar + StackLayout 选项卡切换 currentIndex 双向绑定
ToolBar 应用顶部工具栏 放在 header 属性
Dialog 模态对话框 standardButtonsmodal
Drawer 侧滑抽屉导航 edgeopen() / close()
Menu 上下文菜单 popup()MenuSeparator
ToolTip 悬停提示 delayvisible: hovered
StackView 页面栈前进/后退 push() / pop()
SwipeView 横划页面切换 配合 PageIndicator 使用
background / contentItem 自定义控件外观 替换任意一个,保留交互行为
❌
❌