QML与 Qt Quick入门实战:从零基础到 C++ 双向通信详解

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 &amp;name) {
    if (m_userName == name) return;
    m_userName = name;
    emit userNameChanged();
}
Q_INVOKABLE void greet(const QString &amp;msg) {
qDebug() &lt;&lt; "来自 QML 的调用:" &lt;&lt; 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. 进阶学习路线

掌握本文内容后,可以按下面的顺序继续深入:

  1. 状态与动画 :State、Transition、Behavior、NumberAnimation;
  2. 模型视图进阶 :ListModel、DelegateModel、TableView;
  3. 异步与网络 :WorkerScript、XMLHttpRequest、C++ 侧 QNetworkAccessManager;
  4. 性能优化 :减少绑定层级、使用 Loader 懒加载、避免在 delegate 中做重计算;
  5. 工程化 :QML 模块划分、qmldir、资源打包、国际化 qsTr();
  6. Qt Quick 3D:需要 3D 场景时可继续学习。

11. 总结

QML 的学习曲线其实比想象中平缓,关键在于转变思维:从"命令式地操作控件"转向"声明式地描述状态"。把界面写成数据驱动的结构,让属性绑定替你完成刷新,让信号与槽连接交互,让 C++ 承担业务逻辑。

建议你从本文的待办清单出发,逐步加上持久化、分类筛选、动画过渡,每加一个功能就回头复习对应章节。写够三五个小项目,Qt Quick 就会成为你手里很顺手的工具。

相关推荐
霸道流氓气质2 小时前
Lingua 语言检测库:n-gram 统计模型、置信度阈值调优与 Spring Boot 分层检测管道的生产级实战
java·spring boot·后端
文丑颜不良啊2 小时前
LeetCode1281~1300题目解析
java·leetcode
missile1112 小时前
# HDR 算法 ISP 通路客制化方案(G100 / mt6789)
java·前端·javascript
扶尔魔ocy2 小时前
【QT+FFmpeg】知识点编目及传送门
qt·ffmpeg
qq_401700412 小时前
Qt Modbus 工程化封装:超时、重试、取消与资源释放,别让异步请求变成隐患
数据库·qt
全栈陈序员2 小时前
【Python】基础语法入门(三十三)—— 日志系统详解:用 logging 记录程序运行
java·开发语言·python
她的男孩2 小时前
低代码加字段后表结构怎么同步:预览安全 DDL,高风险变更再强制执行
java·spring boot·低代码
简简单单就是我_hehe3 小时前
一次 ClickHouse 服务器磁盘告警的排查与修复实录
java·服务器·前端
Thomas.Sir3 小时前
第11课:文件上传下载、跨域CORS配置,SpringBoot4.x跨域新方案
java·spring boot