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

0 阅读8分钟

适合人群: 已掌握基础 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 视觉语言一致,适合企业桌面工具。

各平台默认样式

操作系统默认样式
AndroidMaterial
iOSiOS Style
macOSmacOS Style
WindowsWindows 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自定义控件外观替换任意一个,保留交互行为