目录
- 样式切换
- 演示代码
- 关键逻辑解析
- 标题栏的样式名从哪来
- 默认控件
- 自定义控件外观
- 演示代码
- 关键逻辑解析
- 换个样式,内边距也会变
- 运行与源码说明
示例qml_styles主要讲 Qt Quick Controls 的样式系统:六种内置样式怎么切换、都是什么效果、以及如何自定义样式且不被系统样式影响。
三个页面各看一件事:
- 样式切换:六张卡片对应六种样式,任选一个样式即可,UI会自动重启
- 默认控件:展示对应系统样式下的控件外观
- 自定义控件外观:自定义样式,可以观察到它们脱离了系统样式的干扰
样式切换
任选一个样式,然后切换到默认控件这张页面,可以看到组件的变化,如图所示:
演示代码
delegate: Rectangle { id: card required property string modelData Layout.fillWidth: true Layout.preferredHeight: 90 radius: 8 color: hover.hovered ? "#F0F7FF" : "#FFFFFF" border.color: card.modelData === StyleHelper.currentStyle ? "#1296FF" : "#e0e0e0" border.width: card.modelData === StyleHelper.currentStyle ? 2 : 1 // ... 省略卡片内部:样式名、"当前"标记、一句话简介 ... HoverHandler { id: hover cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: StyleHelper.restartWithStyle(card.modelData) } }真正干活的是 C++ 里的这一小段:
voidStyleHelper::restartWithStyle(constQString&style){if(style.isEmpty()||style==QQuickStyle::name())return;// 重启时带上 -style,新进程里由 Qt 自己取走并应用QProcess::startDetached(QCoreApplication::applicationFilePath(),{QStringLiteral("-style"),style});QCoreApplication::quit();}关键逻辑解析
点卡片以后,程序把自己重新拉起来:新进程带着样式名启动,旧进程退出。新进程里 Qt 在注册 QML 类型之前就读到了这个样式名,界面一出来就是新的样子。
要点只有三个:卡片被点、带着样式名另起一个自己、旧进程退出。卡片上的点击调restartWithStyle,这是 C++ 侧注入给 QML 的方法;它内部用QProcess::startDetached另起一个自己,参数里带 Qt 认的-style,然后让当前进程退出。
之所以非要重启,是因为样式选择发生在 QML 类型注册那一刻,注册完就换不了。Qt 文档的原话是 “It is not possible to change the style after the QML types have been registered.”。理论上还有一条路:销毁所有 QML 引擎、清掉类型注册、再重新设样式重建引擎。等于把整个界面拆了重建,内存里的状态全丢,那个函数还要求调用前必须没有任何引擎活着。重启进程反而干净。
标题栏的样式名从哪来
标题栏里那行「Controls 样式与主题:Basic」不是自己解析命令行得来的,而是直接问 Qt:
QStringStyleHelper::currentStyle()const{// QQuickStyle::name() 就是控件实际渲染用的样式名,-style / 环境变量 / 平台默认// 这几级 Qt 已经解析完;别自己去 argv 里翻,QGuiApplication 构造时就把它取走了。returnQQuickStyle::name();}这里藏着一个坑。main.cpp里要是自己去QCoreApplication::arguments()找-style,永远拿不到:
// 错误示范:这样永远找不到 -styleQGuiApplicationapp(argc,argv);constQStringList args=QCoreApplication::arguments();intidx=args.indexOf("-style");QGuiApplication构造时会把-style <值>从 argv 里摘走,存进内部的 styleOverride,Qt Quick Controls 随后从那里读。构造函数文档里有一句:
Note:
argcandargvmight be changed as Qt removes command line arguments that it recognizes.
症状很怪:样式确实切过去了,标题栏的样式名却一直是空的。QQuickStyle::name()返回的是 Qt 按优先级解析完的结果,命令行、环境变量QT_QUICK_CONTROLS_STYLE、qtquickcontrols2.conf、平台默认,这一串它已经算过了,和控件实际渲染用的是同一份答案。
不带任何参数运行时,它返回平台默认的真实样式名,Windows 上是Windows,Linux 上是Fusion。所以标题栏里永远是个具体名字。
默认控件
同一份代码,换个样式再回来,按钮、开关、滑块的样子完全不同。这一页没有代码可讲,就是十来种常用控件摆在当前样式下:三个状态的按钮和一个圆形按钮、复选项、开关、单选项、滑块、进度条、输入框、下拉框、数字输入框。
六种样式各自的脾气:
| 样式 | 特点 |
|---|---|
| Basic | 轻量、性能最好,控件最朴素,适合完全自绘 |
| Fusion | 跨平台桌面风格,跟随系统自动切换明暗,Linux 默认样式 |
| Material | Google Material Design,带明暗主题和主色、强调色 |
| Universal | 微软通用设计规范,跨设备一致,带明暗主题和强调色 |
| Imagine | 基于图片素材,换一套图片就能整体换装 |
| Windows | Windows 原生观感,Windows 平台的默认样式 |
自定义控件外观
这一页把按钮的背景、开关的指示器、滑块的手柄全换成自己画的圆角矩形和圆点。
切换系统样式,这一页的样子不会变。前面两页的控件外观由样式决定,这一页由代码里的部件决定。
演示代码
按钮换了背景和正文:
Button { id: customButton text: "自定义按钮" implicitWidth: 130 background: Rectangle { radius: 6 color: customButton.pressed ? "#0B6FBF" : customButton.hovered ? "#2AA5FF" : "#1296FF" border.color: "#0B6FBF" } contentItem: Text { text: customButton.text color: "white" font.pointSize: 11 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } }开关换了指示器,轨道是圆角矩形,里面套一个圆点:
Switch { id: customSwitch text: "自定义开关" indicator: Rectangle { implicitWidth: 42 implicitHeight: 20 radius: 10 color: customSwitch.checked ? "#1296FF" : "#CCCCCC" Rectangle { width: 16 height: 16 radius: 8 color: "white" y: 2 x: customSwitch.checked ? parent.width - width - 2 : 2 Behavior on x { NumberAnimation { duration: 120 } } } } contentItem: Text { text: customSwitch.text color: "#333" font.pointSize: 11 leftPadding: customSwitch.indicator.width + 8 verticalAlignment: Text.AlignVCenter } }关键逻辑解析
换掉一个部件,等于把那部分渲染权收回到自己手里。控件本身还在管状态和交互,画成什么样由你决定。
这一页动了三处:按钮的background(正文一并换)、开关的indicator、滑块的handle。开关的轨道是个圆角矩形,里面的圆点跟着选中状态左右走,切换时补一段短动画。
代价是样式自带的视觉反馈不再参与。按压、悬停这些状态要在自己的部件里自己判断,示例里是拿控件的按压、悬停状态给颜色。
还有一类样式根本不接受换部件。Windows 这类原生样式受系统主题管理,换部件时 Qt 会在控制台报:
The current style does not support customization of this control (property: "background" ...) Please customize a non-native style (such as Basic, Fusion, Material, etc).想看自定义效果,就切到非原生样式下。
换个样式,内边距也会变
样式不只换外观,也会换控件内部的内边距算法。标题栏原本用的是 ToolBar,切到 Imagine 之后标题直接看不见了。
Imagine 的 ToolBar 把内边距算成 SafeArea 加背景图自带的内边距:
topPadding: SafeArea.margins.top + (background ? background.topPadding : 0)标题栏把背景换成纯色的 Rectangle 之后,Rectangle 没有 topPadding 这个属性,0 加 undefined 得到 NaN,内容区高度跟着变成 0,标题就画不出来。其余五种样式的 ToolBar 内边距是固定值,不受影响。
修法有两种:把四个方向的内边距显式写死,或者干脆别用 ToolBar。标题栏本来就是一条纯色带子,用 Rectangle 加 Text 自己画,样式爱怎么换都碰不到它。示例用的是后者。
运行与源码说明
- Qt 6.5+,构建脚本里写的是
qt_standard_project_setup(REQUIRES 6.5) - 六种样式各自是独立的 QML 模块,必须显式
find_package并链进来,否则加载不到 - 命令行直接指定样式:
qml_styles.exe -style Material - Windows 样式只在 Windows 上可用,macOS、iOS 同理
- 用 Qt Creator 打开
qml_styles/CMakeLists.txt直接运行
已验证环境:
- 我的 Qt 版本:Qt 6.12.0
- 操作系统:Windows 11
- GitCode:QML 示例合集V2.0 - qml_styles