一、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 提供了几种布局方式。最常用的是 RowColumnGridAnchor

Column {
    spacing: 10
    anchors.centerIn: parent

    Button { text: "确定" }
    Button { text: "取消" }
}

这里有个坑:anchors.centerIn 要求父项有明确的宽高。我曾经在父项尺寸没确定时就用了居中,结果按钮跑到了左上角。排查了半天,最后发现是父项的 widthheight 没设置。

注意:使用 anchors 时,确保父项尺寸已确定。否则布局会乱。

2.3 信号与槽——QML 的沟通方式

QML 里信号用 signal 关键字声明,槽用 onXxxChangedonXxx 响应。

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

最常用的方式是通过 setContextPropertyqmlRegisterType

// 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)
    }
}

你想想看,这种双向通信机制,让界面和逻辑彻底解耦。界面只管显示,逻辑只管计算。各干各的,互不干扰。

五、知识体系总览

下面这张图,是我梳理的本章知识结构。建议你保存下来,学完本章后再回来看一遍,会有更深的理解。

Qt Quick 与 QML 知识体系 QML 应用 QML 语法基础 • 对象与属性声明 • 布局:Row, Column, Anchor • 信号与槽机制 • 属性绑定与动画 Qt Quick Controls • Button, TextField, Slider • ComboBox, TableView • 样式定制与主题 • 自定义控件封装 C++ 与 QML 交互 • setContextProperty • qmlRegisterType • Q_INVOKABLE 函数 • 信号与槽跨语言 核心原则:QML 做界面,C++ 做逻辑 通过信号与槽桥接,实现界面与逻辑解耦

六、避坑指南——我踩过的那些雷

最后分享几个实战中容易踩的坑,都是我亲身经历过的。

  • 属性绑定循环:我曾经写过 width: parent.width - 10,然后父项宽度又依赖子项。结果界面卡死。解决方案:用 Layout.fillWidth 或显式设置尺寸。
  • QML 缓存问题:修改了 QML 文件但运行没变化?清一下构建目录。Qt 的 QML 缓存有时候不会自动刷新。
  • C++ 对象生命周期:setContextProperty 注册的对象,必须在引擎销毁前保持有效。我曾经在函数里创建了局部对象注册给 QML,函数结束对象销毁,QML 端直接崩溃。
  • 性能陷阱:不要在 QML 里做大量循环计算。把计算放到 C++ 端,QML 只负责显示结果。

我的建议:刚开始学 QML,先从小项目入手。做一个简单的计算器或者待办事项列表。把语法、控件、交互都走一遍,比看十遍文档都管用。

好了,这一章的内容就到这里。QML 的世界很大,但入门并不难。记住核心原则:界面用 QML,逻辑用 C++,两者通过信号与槽沟通。把这个思路理清了,后面的学习会顺畅很多。

公众号:蓝海资料掘金营,微信deep3321