第二十五章:QT Quick/QML——现代Qt的声明式UI革命
说实话,我第一次接触QML时,心里是有点抵触的。毕竟C++写得好好的,突然冒出个类似JSON的玩意儿来画界面,总觉得不靠谱。但后来在一个移动端项目里,客户要求一周内出三个不同风格的原型——用传统Widget做,光布局就得折腾两天。结果我用QML,一天一个,还带交互动画。嗯,从那以后,我再也不敢小看这门"脚本语言"了。
25.1 QML语法:比你想的更简单
QML的语法,说白了就是"声明式"的。你不需要告诉程序"先创建窗口,再添加按钮,然后设置坐标"——你只需要描述"这里有个窗口,窗口里有个按钮,按钮在右下角"。剩下的,Qt引擎帮你搞定。
// 一个最简单的QML文件
import QtQuick 2.15
Rectangle {
width: 400
height: 300
color: "lightblue"
Text {
anchors.centerIn: parent
text: "Hello, QML!"
font.pixelSize: 24
color: "navy"
}
}
看到没?每个元素都是一个对象,花括号里是它的属性。我习惯把这种写法叫做"JSON on steroids"——你只要记住:类型 { 属性: 值 },基本语法就掌握了。
25.2 基本元素:积木块
QML里的基本元素不多,但每个都很有用。我列几个最常用的:
| 元素 | 作用 | 我常用的场景 |
|---|---|---|
| Rectangle | 矩形区域,可设颜色、圆角、边框 | 几乎所有自定义控件的背景 |
| Text | 显示文本,支持富文本 | 标签、标题、动态数据展示 |
| Image | 显示图片 | 图标、产品图、背景图 |
| Item | 不可见的容器,用于组合 | 作为自定义组件的根元素 |
| MouseArea | 鼠标交互区域 | 点击、拖拽、悬停效果 |
举个例子,一个带点击效果的卡片:
Rectangle {
width: 200; height: 100
radius: 8
color: "white"
border.color: "#ddd"
Text {
anchors.centerIn: parent
text: "点击我"
}
MouseArea {
anchors.fill: parent
onClicked: {
parent.color = parent.color === "white" ? "#e0e0e0" : "white"
}
}
}
这里要注意MouseArea的anchors.fill: parent——我见过不少新手忘了写这句,结果点半天没反应。你想想看,鼠标区域都没覆盖整个矩形,当然点不到啊。
25.3 信号与属性绑定:QML的灵魂
传统C++里,你要监听一个按钮点击,得写connect、写槽函数。QML里呢?直接写onClicked: { ... }完事。这就是信号处理器——每个信号对应一个on+信号名的属性。
但真正让我觉得"哇塞"的,是属性绑定。看这段:
Rectangle {
width: parent.width * 0.8 // 绑定:宽度始终是父对象的80%
height: width * 0.6 // 绑定:高度始终是宽度的60%
color: slider.value > 50 ? "green" : "red" // 绑定:颜色随滑块变化
}
这就是绑定——不是"赋值一次就完事",而是建立了一个关系。只要parent.width变了,width自动更新;slider.value变了,color自动更新。我在项目中用这个特性做过一个实时仪表盘,数据源一变,整个界面像水波一样层层刷新,那感觉,真爽。
width: parent.width * 0.8是单向的——parent变了,width跟着变;但如果你在JavaScript里写width = 100,绑定就断了!我曾经调试一个界面,明明设了绑定,但某个操作后宽度就不动了,查了半天才发现是某处不小心用赋值覆盖了绑定。记住:用冒号(:)建立绑定,用等号(=)会破坏绑定。
25.4 与C++交互:两全其美
QML做界面快,但复杂逻辑、高性能计算、底层系统调用,还是得靠C++。怎么结合?Qt提供了几种方式:
方式一:注册C++类到QML
// C++ 端
class DataProvider : public QObject {
Q_OBJECT
Q_PROPERTY(int value READ value NOTIFY valueChanged)
public:
int value() const { return m_value; }
signals:
void valueChanged();
public slots:
void updateValue(int v) {
m_value = v;
emit valueChanged();
}
private:
int m_value = 0;
};
// 注册
qmlRegisterType<DataProvider>("com.myapp.data", 1, 0, "DataProvider");
// QML 端
import com.myapp.data 1.0
DataProvider {
id: provider
onValueChanged: console.log("新值:", value)
}
Button {
onClicked: provider.updateValue(42)
}
我个人习惯把数据模型和业务逻辑放在C++里,界面层全部用QML。这样分工明确——C++工程师不用管UI,QML工程师不用碰底层。
方式二:上下文属性注入
// C++ 端
QQmlApplicationEngine engine;
DataProvider provider;
engine.rootContext()->setContextProperty("dataProvider", &provider);
// QML 端直接用
Text {
text: dataProvider.value // 直接访问C++对象的属性
}
这种方式更轻量,适合快速原型。但要注意:上下文属性是全局的,命名冲突了很难查。我建议只在主窗口级别用,别到处乱塞。
25.5 Qt Quick Controls:开箱即用的组件库
如果你不想从零画按钮、滑块、菜单,Qt Quick Controls就是你的救星。它提供了完整的桌面风格控件,而且支持主题切换。
import QtQuick.Controls 2.15
ApplicationWindow {
visible: true
width: 400; height: 300
title: "Qt Quick Controls 示例"
Column {
anchors.centerIn: parent
spacing: 16
Button {
text: "普通按钮"
onClicked: console.log("点击了")
}
Slider {
from: 0; to: 100
value: 50
}
ProgressBar {
value: slider.value / 100
}
Switch {
text: "开关"
}
}
}
这些控件的外观和交互,跟操作系统原生风格很接近。但如果你想要更炫酷的效果——比如圆角滑块、渐变按钮——那就得用自定义组件了。我一般这样搭配:
- 标准界面:直接用Controls,省时省力
- 品牌定制:用Controls做基础,通过
background、contentItem等属性自定义外观 - 完全自定义:用Rectangle + MouseArea + 动画,从零打造
background属性加了个渐变矩形,两行代码搞定,设计师都惊了。
25.6 知识体系总览
下面这张图,是我对本章知识结构的理解。你可以把它当作一张"地图"——学完本章后,你应该能看清QML世界的全貌:
这张图里,语法基础是地基,基本元素是砖块,信号与绑定是水泥——把它们组合起来,你就能搭建出任何界面。而C++交互和Qt Quick Controls,则是让你的房子通电通水的"基础设施"。
好了,这一章的内容就到这里。QML的世界其实比想象中要小——核心概念就那么几个,但组合起来却能千变万化。我建议你打开Qt Creator,新建一个Qt Quick项目,把上面的代码挨个敲一遍。亲手试过,比看十遍都管用。