4. QML语言基础:QML语法入门、属性与信号绑定、JavaScript在QML中的使用、QML与C++交互

好,咱们进入正题。这一章是QML的根基,说白了就是你要学会怎么用QML“说话”。很多新手一上来就急着写动画、画界面,结果连属性绑定都搞反了,调试起来一头雾水。我个人习惯是,先把语法骨架搭稳,再往上添肉。

4.1 QML语法入门:从“Hello World”开始

QML的语法,其实很像搭积木。每个RectangleTextButton都是一块积木,你通过属性来描述它长什么样,通过嵌套来组合它们。

先看一个最简单的例子:

import QtQuick 2.15

Rectangle {
    width: 400
    height: 300
    color: "lightblue"

    Text {
        anchors.centerIn: parent
        text: "Hello, HMI!"
        font.pixelSize: 24
        color: "navy"
    }
}

这里有几个关键点:

  • 导入语句import QtQuick 2.15 是必须的,它告诉QML引擎我要用哪个版本的Qt Quick模块。版本号要和你用的Qt版本匹配,我遇到过有人把2.15写成2.5,结果一堆属性不识别。
  • 对象声明Rectangle { ... } 就是一个对象。大括号里是它的属性和子对象。
  • 属性赋值width: 400,冒号左边是属性名,右边是值。注意,这里不是等号,是冒号。
  • 锚点布局anchors.centerIn: parent 意思是“把我放在父对象的正中间”。这是QML里最常用的布局方式之一。
我的小习惯:写QML时,我习惯把属性按“尺寸→位置→外观→行为”的顺序排列。比如先写width/height,再写anchors,然后color,最后写信号处理。这样别人看你的代码,一眼就能找到重点。

QML的类型系统也很简单。基本类型包括intrealstringboolcolorfont等。你不需要像C++那样声明类型,QML会自动推断。但要注意,color类型可以用名字("red")、十六进制("#FF0000")或HSL方式指定。

4.2 属性与信号绑定:QML的灵魂

属性绑定是QML最强大的特性之一。说白了,就是让一个属性的值“自动跟随”另一个属性的变化。不用写任何更新代码,QML引擎帮你搞定。

举个例子:

Rectangle {
    width: 400; height: 300
    color: "lightgray"

    Rectangle {
        id: slider
        width: 50; height: 200
        color: "steelblue"
        x: 20; y: 50

        MouseArea {
            anchors.fill: parent
            drag.target: parent
            drag.axis: Drag.XAxis
            drag.minimumX: 20
            drag.maximumX: 330
        }
    }

    Text {
        text: "滑块位置: " + slider.x.toFixed(0)
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        font.pixelSize: 18
    }
}

看到text: "滑块位置: " + slider.x.toFixed(0)这行了吗?这就是属性绑定。当slider.x变化时,text会自动更新。你不需要写任何onXChanged信号处理函数。

避坑指南:我曾经在项目里犯过一个低级错误——在JavaScript代码块里用等号给属性赋值,比如 slider.x = 100。这会破坏绑定!一旦你用JavaScript直接赋值,原来的绑定关系就断了,属性不再自动更新。正确的做法是使用 slider.x = Qt.binding(function() { return ... }) 来重新建立绑定。

信号绑定是另一个核心概念。QML里几乎所有用户交互都会产生信号,比如点击、拖拽、键盘输入。你可以用onSignalName的语法来响应信号。

Button {
    text: "点击我"
    onClicked: {
        console.log("按钮被点击了")
        // 这里可以写更多逻辑
    }
}

信号也可以带参数。比如MouseAreaclicked信号会传递鼠标坐标:

MouseArea {
    onClicked: (mouse) => {
        console.log("点击位置: " + mouse.x + ", " + mouse.y)
    }
}

4.3 JavaScript在QML中的使用

QML本身不是一门完整的编程语言,它主要做界面描述。真正的逻辑处理,需要靠JavaScript。你可以在QML文件里直接写JS代码,也可以把JS函数放到独立的.js文件中。

在QML里写JS,主要有三种方式:

  1. 属性值里写表达式:比如 text: "总数: " + (count + 1)
  2. 信号处理器里写代码块:比如 onClicked: { ... }
  3. 自定义函数:用 function 关键字定义

看个综合例子:

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 400; height: 300
    title: "JS示例"

    property int counter: 0

    function formatTime(seconds) {
        var min = Math.floor(seconds / 60)
        var sec = seconds % 60
        return min + "分" + sec + "秒"
    }

    Column {
        anchors.centerIn: parent
        spacing: 20

        Text {
            id: display
            text: "计时: " + formatTime(counter)
            font.pixelSize: 28
        }

        Button {
            text: "加1秒"
            onClicked: {
                counter++
                if (counter > 3600) {
                    console.warn("超过1小时了,注意!")
                }
            }
        }

        Button {
            text: "重置"
            onClicked: {
                counter = 0
                display.text = "计时: 0分0秒"  // 注意:这里直接赋值会破坏绑定
            }
        }
    }
}
注意:上面例子中,重置按钮的 onClicked 里直接写了 display.text = "..."。这会破坏 display.text 原本的绑定关系。如果后面 counter 再变化,display 不会更新。正确的做法是只修改 counter = 0,让绑定自动更新。嗯,这里要特别小心。

对于复杂的JS逻辑,我建议放到独立的.js文件中:

// utils.js
function calculateSpeed(distance, time) {
    if (time === 0) return 0
    return (distance / time) * 3.6  // 转换为km/h
}

function formatSpeed(speed) {
    return speed.toFixed(1) + " km/h"
}

然后在QML里导入:

import "utils.js" as Utils

Text {
    text: Utils.formatSpeed(currentSpeed)
}

这样做的好处是逻辑复用,而且代码更清晰。我在做车载仪表盘项目时,所有单位换算、数据格式化都放在独立的JS文件里,界面文件只负责布局和绑定。

4.4 QML与C++交互:打通任督二脉

QML做界面确实快,但复杂的业务逻辑、硬件通信、高性能计算,还是得靠C++。两者怎么配合?主要有两种方式:

方式 适用场景 实现方法
注册C++类到QML 需要创建多个C++对象实例 qmlRegisterType<MyClass>()
暴露C++对象给QML 单例对象,如数据模型、后端服务 engine.rootContext()->setContextProperty()

先说第一种,注册C++类。假设你有一个SpeedModel类:

// speedmodel.h
class SpeedModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int speed READ speed WRITE setSpeed NOTIFY speedChanged)

public:
    explicit SpeedModel(QObject *parent = nullptr);

    int speed() const;
    void setSpeed(int newSpeed);

signals:
    void speedChanged();

private:
    int m_speed = 0;
};

在main.cpp里注册:

qmlRegisterType<SpeedModel>("com.hmi.models", 1, 0, "SpeedModel");

然后在QML里直接用:

import com.hmi.models 1.0

SpeedModel {
    id: carSpeed
    speed: 80
}

Text {
    text: "当前车速: " + carSpeed.speed + " km/h"
}

第二种方式,暴露对象。适合那种全局唯一的对象,比如车辆状态管理器:

// main.cpp
VehicleManager manager;
engine.rootContext()->setContextProperty("vehicle", &manager);

QML里直接使用vehicle这个变量名:

Text {
    text: "电池电量: " + vehicle.batteryLevel + "%"
}
我的经验:在车载项目中,我通常把硬件抽象层(HAL)用C++实现,通过 setContextProperty 暴露给QML。这样QML只负责显示和动画,C++负责和CAN总线、传感器打交道。分工明确,调试起来也方便。有一次CAN总线数据频率太高,QML界面卡顿,我就在C++层做了数据降采样,问题立刻解决。

C++和QML之间还可以通过信号互相通信。C++发射信号,QML用onSignalName接收:

// C++
emit speedChanged(newSpeed);

// QML
onSpeedChanged: {
    console.log("速度变化了: " + speed)
}

反过来,QML也可以调用C++的Q_INVOKABLE方法:

// C++
Q_INVOKABLE void setTargetTemperature(int temp);

// QML
Button {
    onClicked: vehicle.setTargetTemperature(22)
}

嗯,到这里,QML语言基础的核心内容就差不多了。记住一句话:QML管界面,C++管逻辑,JS管粘合。下一章我们会深入QML的布局系统,到时候你会看到这些基础语法怎么组合成真正的车载界面。