一、QML 是什么?为什么 Qt 要搞这么个东西?
说实话,我第一次接触 QML 时也挺困惑的。C++ 写得好好的,干嘛要学一门新语言?
后来我明白了。Qt Widgets 做界面,就像用砖头砌墙——稳当,但慢。QML 呢?它像搭积木,快,而且漂亮。
QML 全称 Qt Meta-Object Language,是一种声明式语言。你不需要写「怎么画一个按钮」,只需要说「这里要一个按钮」。底层渲染由 Qt Quick 引擎搞定,性能不差。
我在项目中遇到过这样一个场景:客户要求一周内出一个移动端原型。用 Widgets 写?光布局调整就得三天。换成 QML,一天搞定界面,两天联调逻辑。嗯,这就是 QML 的价值。
核心认知:QML 负责界面描述,C++ 负责业务逻辑。两者各司其职,通过信号与槽桥接。
二、QML 语法基础——别怕,它比 JavaScript 还简单
2.1 对象与属性
QML 里万物皆对象。创建一个矩形,就这么写:
import QtQuick 2.15
Rectangle {
width: 200
height: 100
color: "lightblue"
border.color: "gray"
border.width: 2
radius: 10
}
你看,没有分号,没有大括号包裹的类定义。属性名: 属性值,完事。
我个人习惯把属性按逻辑分组:尺寸放前面,外观放中间,行为放后面。这样别人看你的代码,一眼就能找到关键信息。
2.2 布局与定位
QML 提供了几种布局方式。最常用的是 Row、Column、Grid 和 Anchor。
Column {
spacing: 10
anchors.centerIn: parent
Button { text: "确定" }
Button { text: "取消" }
}
这里有个坑:anchors.centerIn 要求父项有明确的宽高。我曾经在父项尺寸没确定时就用了居中,结果按钮跑到了左上角。排查了半天,最后发现是父项的 width 和 height 没设置。
注意:使用 anchors 时,确保父项尺寸已确定。否则布局会乱。
2.3 信号与槽——QML 的沟通方式
QML 里信号用 signal 关键字声明,槽用 onXxxChanged 或 onXxx 响应。
Button {
text: "点击我"
onClicked: {
console.log("按钮被点击了")
label.text = "已点击"
}
}
Text {
id: label
text: "未点击"
}
为什么会这样设计?因为 QML 是声明式的,你不需要手动连接信号和槽。框架自动帮你做了。说白了,你只管写「点击后做什么」,不用管「怎么连接」。
三、Qt Quick Controls——开箱即用的组件库
Qt Quick Controls 提供了按钮、输入框、滑块、表格等常用控件。版本 2 以后,控件支持样式定制,非常灵活。
3.1 常用控件一览
| 控件 | 用途 | 常用属性 |
|---|---|---|
| Button | 按钮 | text, onClicked, enabled |
| TextField | 单行输入 | text, placeholderText, onAccepted |
| Slider | 滑块 | from, to, value, onValueChanged |
| ComboBox | 下拉框 | model, currentIndex, onCurrentIndexChanged |
| TableView | 表格 | model, delegate, columnWidthProvider |
3.2 样式定制——让界面不再千篇一律
Qt Quick Controls 2 支持通过 qml 文件自定义样式。你想想看,一个按钮在不同应用里长得不一样,这才是正常的。
// MyButton.qml
Button {
background: Rectangle {
color: parent.pressed ? "#3a3a3a" : "#5a5a5a"
radius: 5
border.color: "#888"
}
contentItem: Text {
text: parent.text
color: "white"
font.bold: true
}
}
我在项目中做过一个工业控制面板,所有按钮都要符合客户的企业色。用这种自定义方式,我写了一套基础样式文件,所有按钮继承自它。改一个地方,全局生效。嗯,维护起来很舒服。
小技巧:把样式文件放在单独的目录,用 qmlRegisterType 注册。这样 C++ 端也能使用这些自定义控件。
四、C++ 与 QML 交互——打通任督二脉
纯 QML 做不了复杂计算,纯 C++ 做界面又太慢。两者结合才是王道。
4.1 从 C++ 暴露对象给 QML
最常用的方式是通过 setContextProperty 或 qmlRegisterType。
// C++ 端
class DataProvider : public QObject {
Q_OBJECT
Q_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged)
public:
QString userName() const { return m_userName; }
void setUserName(const QString &name) {
if (m_userName != name) {
m_userName = name;
emit userNameChanged();
}
}
signals:
void userNameChanged();
private:
QString m_userName;
};
// 注册到 QML 上下文
DataProvider provider;
engine.rootContext()->setContextProperty("dataProvider", &provider);
// QML 端使用
Text {
text: dataProvider.userName
}
Button {
text: "修改名字"
onClicked: dataProvider.userName = "新用户"
}
这里要注意:Q_PROPERTY 里的 NOTIFY 信号必须写。不写的话,QML 无法感知属性变化,界面不会刷新。我曾经踩过这个坑,调试了一下午才发现少了 NOTIFY。
4.2 从 QML 调用 C++ 函数
用 Q_INVOKABLE 标记的函数,可以直接在 QML 里调用。
// C++ 端
class Calculator : public QObject {
Q_OBJECT
public:
Q_INVOKABLE int add(int a, int b) {
return a + b;
}
};
// QML 端
Button {
text: "计算"
onClicked: {
var result = calculator.add(3, 4)
console.log("结果:", result)
}
}
我个人习惯把所有业务逻辑封装在 C++ 类里,QML 只做界面展示和用户交互。这样后期换界面框架,业务代码不用动。
4.3 信号与槽的跨语言通信
C++ 发射信号,QML 接收;反过来也行。
// C++ 发射信号
emit dataProvider.userNameChanged();
// QML 接收
Connections {
target: dataProvider
onUserNameChanged: {
console.log("用户名变了:", dataProvider.userName)
}
}
你想想看,这种双向通信机制,让界面和逻辑彻底解耦。界面只管显示,逻辑只管计算。各干各的,互不干扰。
五、知识体系总览
下面这张图,是我梳理的本章知识结构。建议你保存下来,学完本章后再回来看一遍,会有更深的理解。
六、避坑指南——我踩过的那些雷
最后分享几个实战中容易踩的坑,都是我亲身经历过的。
- 属性绑定循环:我曾经写过
width: parent.width - 10,然后父项宽度又依赖子项。结果界面卡死。解决方案:用Layout.fillWidth或显式设置尺寸。 - QML 缓存问题:修改了 QML 文件但运行没变化?清一下构建目录。Qt 的 QML 缓存有时候不会自动刷新。
- C++ 对象生命周期:用
setContextProperty注册的对象,必须在引擎销毁前保持有效。我曾经在函数里创建了局部对象注册给 QML,函数结束对象销毁,QML 端直接崩溃。 - 性能陷阱:不要在 QML 里做大量循环计算。把计算放到 C++ 端,QML 只负责显示结果。
我的建议:刚开始学 QML,先从小项目入手。做一个简单的计算器或者待办事项列表。把语法、控件、交互都走一遍,比看十遍文档都管用。
好了,这一章的内容就到这里。QML 的世界很大,但入门并不难。记住核心原则:界面用 QML,逻辑用 C++,两者通过信号与槽沟通。把这个思路理清了,后面的学习会顺畅很多。