1. 引言
如果你已经会用 Qt Widgets 写窗口程序,但对 QML 和 Qt Quick 完全陌生,那么这篇文章就是为你准备的。它不假设你懂 JavaScript,也不假设你懂声明式 UI,只要求你会基本的 C++ 和 Qt 工程结构。
读完之后,你应该能够:
- 说清楚 QML、Qt Quick、Qt Quick Controls 三者之间的关系;
- 独立搭建一个 QML 工程并跑起来;
- 掌握 QML 的语法、属性绑定、信号与槽、组件化;
- 用 C++ 与 QML 双向通信,把业务逻辑放到 C++ 里;
- 完成一个可交互的小项目,并知道后续该往哪学。
阅读建议:前四节务必动手敲一遍,QML 是一门"写出来才有感觉"的语言,光看语法收益很低。
2. QML与Qt Quick
初学者最容易混淆的就是这几个名词,先把概念理清。
| 名词 | 定位 | 说明 |
|---|---|---|
| QML | 语言 | 一种声明式的描述性语言,语法类似 JSON + JavaScript,用来描述界面结构和行为 |
| Qt Quick | 框架/模块 | 基于 QML 的 UI 框架,提供 Item、Rectangle、Text 等可视化元素和动画能力 |
| Qt Quick Controls | 控件库 | 在 Qt Quick 之上提供 Button、Slider、TableView 等标准控件 |
| Qt Quick Designer | 工具 | Qt Creator 中的可视化设计器,可拖拽编辑 QML |
一句话总结:QML 是语言,Qt Quick 是用这门语言写出来的 UI 框架,Qt Quick Controls 是框架上的控件库。
和 Widgets 相比,Qt Quick 的核心优势在于:
- 声明式:描述"界面长什么样",而不是"一步步怎么画";
- 属性绑定 :数据变化自动驱动界面刷新,不用手动
setText; - 动画与特效:内置状态机、过渡动画,做流畅交互成本很低;
- 跨平台与 GPU 渲染:基于场景图(Scene Graph),适合移动端和嵌入式。
为了更直观地看清两者的差异,下面从几个关键维度做一次对比:
| 对比维度 | QML / Qt Quick | Qt Widgets |
|---|---|---|
| 编程范式 | 声明式,描述界面应该呈现的状态,由引擎负责渲染与更新。 | 命令式,通过 C++ 代码一步步创建、布局和操作控件。 |
| 界面描述方式 | 用 QML 文本描述对象树,结构清晰,可交给设计人员参与编写。 | 用 C++ 代码或 .ui 文件搭建控件层级,界面与逻辑耦合更紧。 |
| 数据刷新机制 | 属性绑定自动驱动刷新,数据一变界面就跟着变。 | 需要手动调用 setText、update 等接口主动通知界面更新。 |
| 动画能力 | 内置状态机、Transition 和各类动画元素,做流畅动效成本很低。 | 依赖 QPropertyAnimation 等类手动实现,复杂动效编写量大。 |
| 渲染方式 | 基于场景图(Scene Graph)走 GPU 渲染,适合高频刷新与复杂特效。 | 基于传统 QPainter 软件绘制,控件风格统一但特效开销较高。 |
| 典型适用场景 | 移动端、嵌入式 HMI、需要动画与触控交互的现代化界面。 | 桌面端传统业务软件、表单密集型工具、需要原生控件风格的应用。 |
| 学习成本 | 需要额外掌握 QML 语法与 JavaScript,但入门后开发效率提升明显。 | 对熟悉 C++ 的开发者上手最快,但复杂界面代码量偏大。 |
**选型建议:**如果项目面向桌面端、以表单和传统控件为主,继续用 Widgets 更稳妥;如果追求现代化交互、动画效果,或目标平台是移动端与嵌入式,优先选择 Qt Quick,并把业务逻辑放在 C++ 侧。
3. 第一个 QML 工程
3.1 安装与版本选择
建议使用 Qt 5.15 LTS 或 Qt 6.x(6.5 及以上为 LTS)。安装时在 Qt Creator 的组件选择里,务必勾选:
- Qt Quick Controls 2(控件库)
- Qt Quick Designer(可选,可视化设计)
- 对应编译器的 Desktop 套件(MSVC 或 MinGW)
3.2 创建工程
在 Qt Creator 中选择 File → New File or Project → Application (Qt) → Qt Quick Application ,构建系统推荐 CMake(Qt 6 默认)。生成后你会看到这样的结构:
text
myapp/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
└── qml.qrc
main.cpp 的作用是启动 QML 引擎并加载 Main.qml:
cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/Main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj.url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
对应的 CMakeLists.txt 关键部分:
cmake
cmake_minimum_required(VERSION 3.16)
project(myapp VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2)
qt_add_executable(myapp main.cpp)
qt_add_qml_module(myapp
URI MyApp
VERSION 1.0
QML_FILES Main.qml
)
target_link_libraries(myapp PRIVATE Qt6::Quick Qt6::QuickControls2)
3.3 第一个QML界面
把 Main.qml 改成下面这样:
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
id: window
width: 480
height: 320
visible: true
title: qsTr("我的第一个 Qt Quick 程序")
ColumnLayout {
anchors.centerIn: parent
spacing: 16
Text {
id: label
text: qsTr("你好,Qt Quick!")
font.pixelSize: 24
Layout.alignment: Qt.AlignHCenter
}
Button {
text: qsTr("点我")
Layout.alignment: Qt.AlignHCenter
onClicked: label.text = qsTr("按钮被点击了!")
}
}
}
按 Ctrl+R 运行,你会看到一个窗口,点击按钮后文字发生变化。注意这里没有一行 C++ 逻辑,界面和交互全部由 QML 描述完成。
4. QML语法核心
4.1 对象与属性
QML 文件本质上是一棵对象树。每个 { } 块是一个对象,块内是它的属性:
qml
Rectangle {
width: 200
height: 100
color: "steelblue"
radius: 8
Text {
anchors.centerIn: parent
text: "Hello"
color: "white"
}
}
属性赋值有三种常见形式:
- 字面量:
width: 200、color: "#3498db"; - 绑定表达式:
width: parent.width / 2; - 分组属性:
font.pixelSize: 20、anchors.centerIn: parent。
4.2 属性绑定
属性绑定是 QML 最强大的特性。当表达式依赖的属性变化时,结果会自动重新计算:
qml
Rectangle {
id: root
width: 400
height: 200
Rectangle {
width: root.width / 2
height: root.height / 2
color: "tomato"
}
}
拖动改变 root 的尺寸,内部矩形会自动跟着变,无需任何事件处理代码。
注意:一旦你用命令式赋值覆盖了绑定(例如在 JS 里写
rect.width = 100),绑定就会被破坏。需要恢复时用Qt.binding()。
4.3 锚点布局
anchors 是 Qt Quick 中最常用的定位方式,它把元素锚定到父元素或兄弟元素上:
qml
Rectangle {
id: parentRect
width: 300
height: 200
color: "#ecf0f1"
Rectangle {
anchors.fill: parent
anchors.margins: 20
color: "#3498db"
}
}
常用锚点:fill、centerIn、left、right、top、bottom、horizontalCenter、verticalCenter,配合 margins 系列属性使用。
4.4 布局管理器
当元素较多时,推荐使用 RowLayout、ColumnLayout、GridLayout,它们会自动处理尺寸分配:
qml
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
TextField {
Layout.fillWidth: true
placeholderText: "请输入内容"
}
Button {
text: "提交"
Layout.fillWidth: true
}
}
5. 信号与槽
5.1 内置信号
QML 中处理信号非常直观,信号名首字母大写并加 on 前缀即可:
qml
Button {
text: "保存"
onClicked: console.log("按钮被点击")
}
MouseArea {
anchors.fill: parent
onPressed: console.log("按下")
onReleased: console.log("抬起")
}
5.2 自定义信号
用 signal 关键字声明信号,用 signalName() 触发:
qml
Item {
id: root
signal submitted(string content)
function doSubmit(text) {
root.submitted(text)
}
Component.onCompleted: doSubmit("hello")
}
使用方通过 onSubmitted 接收:
qml
MyItem {
onSubmitted: console.log("收到:" + content)
}
5.3 属性变化信号
任何属性变化都会自动产生 onXxxChanged 信号:
qml
Text {
id: counter
text: "0"
onTextChanged: console.log("文本变为:" + text)
}
6. 组件化
6.1 独立QML文件
新建一个 MyButton.qml,文件名就是组件名:
qml
import QtQuick
import QtQuick.Controls
Button {
property string accentColor: "#3498db"
background: Rectangle {
color: accentColor
radius: 6
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
在其他文件中直接使用:
qml
MyButton {
text: "自定义按钮"
accentColor: "#e74c3c"
onClicked: console.log("clicked")
}
6.2 内联组件
如果组件只在当前文件使用,可以用 Component 内联定义:
qml
Component {
id: itemDelegate
Rectangle {
width: 100
height: 40
color: "lightgray"
}
}
ListView {
model: 5
delegate: itemDelegate
}
内联组件适合复用度不高、只在当前文件出现的场景;一旦需要在多个文件中使用,还是建议拆成独立的 QML 文件。
6.3 组件生命周期
组件从创建到销毁会经历若干阶段,QML 提供了两个最常用的钩子函数,用来在关键节点插入初始化或清理逻辑。
常用生命周期钩子:
Component.onCompleted:组件创建完成后调用,适合初始化;Component.onDestruction:组件销毁前调用,适合释放资源。
7. C++与QML双向通信
真实项目里,界面用 QML,业务逻辑用 C++。两者通信主要有三种方式。
7.1 注册C++对象
cpp
#include <QObject>
#include <QDebug>
class Backend : public QObject
{
Q_OBJECT
Q_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged)
public:
explicit Backend(QObject *parent = nullptr) : QObject(parent) {}
QString userName() const { return m_userName; }
void setUserName(const QString &name) {
if (m_userName == name) return;
m_userName = name;
emit userNameChanged();
}
Q_INVOKABLE void greet(const QString &msg) {
qDebug() << "来自 QML 的调用:" << msg;
}
signals:
void userNameChanged();
private:
QString m_userName;
};
在 main.cpp 中注入:
cpp
Backend backend;
engine.rootContext()->setContextProperty("backend", &backend);
QML 中即可直接使用:
qml
Text {
text: backend.userName
}
Button {
text: "调用 C++"
onClicked: backend.greet("hello from QML")
}
7.2 qmlRegisterType
cpp
#include <QQmlEngine>
qmlRegisterType<Backend>("MyApp", 1, 0, "Backend");
qml
import MyApp 1.0
Backend {
id: backend
}
7.3 Model驱动列表
C++ 侧继承 QAbstractListModel,QML 侧用 ListView 展示,这是最标准的列表数据方案:
cpp
class TodoModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles { TitleRole = Qt::UserRole + 1 };
int rowCount(const QModelIndex parent = QModelIndex()) const override {
return m_items.size();
}
QVariant data(const QModelIndex index, int role) const override {
if (!index.isValid() || role != TitleRole) return {};
return m_items.at(index.row());
}
QHash<int, QByteArray> roleNames() const override {
return { { TitleRole, "title" } };
}
Q_INVOKABLE void add(const QString title) {
beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
m_items.append(title);
endInsertRows();
}
private:
QStringList m_items;
};
qml
ListView {
anchors.fill: parent
model: todoModel
delegate: Rectangle {
width: ListView.view.width
height: 48
Text {
anchors.centerIn: parent
text: title
}
}
}
8. 实战示例
把前面知识串起来,做一个能增删的待办列表。
8.1 界面结构
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
id: window
width: 420
height: 560
visible: true
title: qsTr("待办清单")
property var todos: []
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
RowLayout {
Layout.fillWidth: true
spacing: 8
TextField {
id: input
Layout.fillWidth: true
placeholderText: qsTr("输入待办事项")
onAccepted: addBtn.clicked()
}
Button {
id: addBtn
text: qsTr("添加")
onClicked: {
if (input.text.trim() === "") return
window.todos.push({ title: input.text, done: false })
input.text = ""
}
}
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: window.todos
clip: true
spacing: 6
delegate: Rectangle {
width: ListView.view.width
height: 52
radius: 6
color: modelData.done ? "#d5f5e3" : "#f4f6f7"
RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
CheckBox {
checked: modelData.done
onToggled: modelData.done = checked
}
Text {
Layout.fillWidth: true
text: modelData.title
font.strikeout: modelData.done
elide: Text.ElideRight
}
Button {
text: qsTr("删除")
onClicked: window.todos.splice(index, 1)
}
}
}
}
Text {
Layout.fillWidth: true
text: qsTr("共 %1 项,已完成 %2 项")
.arg(window.todos.length)
.arg(window.todos.filter(t => t.done).length)
color: "#7f8c8d"
}
}
}
8.2 关键点解析
property var todos: []声明了一个数组属性,QML 会监听它的变化;ListView的model直接绑定数组,delegate中用modelData访问元素;index是 delegate 内置的索引,用于删除;- 底部统计文本通过绑定自动更新,无需手动刷新。
提示:数组的
push、splice会触发视图刷新,但直接修改元素内部字段(如modelData.done = true)在部分 Qt 版本中不会自动刷新,生产环境建议改用ListModel或 C++ 的QAbstractListModel。
9. 调试与常见坑
9.1 调试手段
console.log()/console.warn():最直接的输出方式;- Qt Creator 的 QML Debugger:可断点、查看对象树;
qmlscene(Qt 5)或qml(Qt 6)命令行工具:单独预览 QML 文件;- 运行时按
F2可打开 QML 检查器查看元素边界。
9.2 新手常见问题
| 现象 | 原因 | 解决 |
|---|---|---|
| 界面空白 | 根元素没有设置 visible: true 或尺寸为 0 |
检查 ApplicationWindow 的 visible 与 width/height |
| 绑定失效 | 被命令式赋值覆盖 | 用 Qt.binding() 恢复,或避免直接赋值 |
| 找不到模块 | 未安装对应 Qt 模块或 import 版本不匹配 | 检查安装组件与 import 语句 |
| 列表不刷新 | 直接修改数组元素内部字段 | 改用 ListModel 的 set() 或 C++ Model |
| 中文乱码 | 源文件编码非 UTF-8 | 统一使用 UTF-8 保存 |
10. 进阶学习路线
掌握本文内容后,可以按下面的顺序继续深入:
- 状态与动画 :
State、Transition、Behavior、NumberAnimation; - 模型视图进阶 :
ListModel、DelegateModel、TableView; - 异步与网络 :
WorkerScript、XMLHttpRequest、C++ 侧QNetworkAccessManager; - 性能优化 :减少绑定层级、使用
Loader懒加载、避免在 delegate 中做重计算; - 工程化 :QML 模块划分、
qmldir、资源打包、国际化qsTr(); - Qt Quick 3D:需要 3D 场景时可继续学习。
11. 总结
QML 的学习曲线其实比想象中平缓,关键在于转变思维:从"命令式地操作控件"转向"声明式地描述状态"。把界面写成数据驱动的结构,让属性绑定替你完成刷新,让信号与槽连接交互,让 C++ 承担业务逻辑。
建议你从本文的待办清单出发,逐步加上持久化、分类筛选、动画过渡,每加一个功能就回头复习对应章节。写够三五个小项目,Qt Quick 就会成为你手里很顺手的工具。