第23章 QML与Qt Quick:从入门到实战

QML和Qt Quick,说白了就是Qt家族里专门做界面的一把快刀。我最早接触Qt时用的是QWidget,后来转到QML,说实话,刚开始有点不适应——写界面居然不用C++?但用顺手之后,真香。

这一章我们聊聊QML的语法基础、常用控件、怎么和C++交互、动画状态,还有Canvas绘图。嗯,内容不少,但都是干货。

第23章 知识体系 QML与Qt Quick QML语法基础 Qt Quick控件 QML与C++交互 动画与状态 属性绑定 信号与槽 JavaScript集成 Button/Text ListView/Grid Dialog/Popup 注册C++类型 属性暴露 信号连接 PropertyAnimation 状态机 Canvas绘图 声明式UI + 逻辑分离 = 高效开发

23.1 QML语法基础——声明式UI的骨架

QML的语法,其实特别像JSON和JavaScript的混血儿。一个QML文件就是一个对象树,每个对象都有属性和信号。我刚开始学的时候,觉得这东西不就是写配置嘛,后来才发现,它的属性绑定机制才是精髓。

核心要点:QML是声明式语言,你描述界面长什么样,而不是一步步怎么画。这跟QWidget那种命令式写法完全不同。

来看一个最简单的例子:

// main.qml
import QtQuick 2.15

Rectangle {
    width: 400
    height: 300
    color: "lightblue"
    
    Text {
        anchors.centerIn: parent
        text: "Hello, QML!"
        font.pixelSize: 24
        color: "navy"
    }
}

这里有个坑,我刚开始踩过——anchors.centerIn: parent 这个写法,parent必须是一个Item或者有几何属性的对象。如果你把Text直接放在Window下面,Window本身不是Item,会报错。嗯,记住就好。

属性绑定是QML最强大的特性之一。你可以把一个属性绑定到另一个属性上,当源属性变化时,目标属性自动更新:

Rectangle {
    width: parent.width * 0.8  // 自动跟随父容器宽度变化
    height: width * 0.6        // 保持宽高比
}

我个人习惯把复杂的绑定逻辑写在JavaScript函数里,这样代码更清晰。比如:

function calculateWidth(parentWidth) {
    return parentWidth > 600 ? parentWidth * 0.7 : parentWidth * 0.9;
}

23.2 Qt Quick控件——开箱即用的UI组件

Qt Quick Controls 2 提供了一套现代化的控件集。我做过一个项目,用纯QML写了一个配置面板,Button、ComboBox、Slider这些控件直接拿来用,开发效率比QWidget高了一倍不止。

控件 用途 常用属性
Button 按钮触发操作 text, onClicked, enabled
TextField 单行文本输入 text, placeholderText, validator
Slider 数值范围选择 from, to, value, onValueChanged
ComboBox 下拉选择 model, currentIndex, currentText
ListView 列表展示 model, delegate, spacing

举个例子,一个带验证的输入框:

TextField {
    id: ipInput
    placeholderText: "请输入IP地址"
    validator: RegExpValidator { regExp: /^(\d{1,3}\.){3}\d{1,3}$/ }
    
    onAccepted: {
        console.log("用户输入了:", text)
    }
}

小技巧:ListView的delegate可以用Loader动态加载不同的组件,实现列表项多样化。我在做聊天软件时就用过这招,不同类型的消息显示不同的样式。

23.3 QML与C++交互——打通任督二脉

QML做界面快,但复杂业务逻辑还是C++拿手。怎么让它们配合?说白了就三种方式:注册C++类型、暴露属性、连接信号。

先看C++端怎么暴露一个对象给QML:

// MyDataModel.h
class MyDataModel : 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;
};

然后在main.cpp里注册:

int main(int argc, char *argv[]) {
    QGuiApplication app(argc, argv);
    
    MyDataModel model;
    model.setUserName("张三");
    
    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("dataModel", &model);
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    
    return app.exec();
}

QML端直接使用:

Text {
    text: dataModel.userName  // 自动绑定,C++那边变了这里跟着变
}

Button {
    text: "修改名字"
    onClicked: {
        dataModel.userName = "李四"  // 直接调用C++的setter
    }
}

注意:我曾经犯过一个错误——在C++的setter里忘记发射信号,结果QML界面死活不更新。记住,Q_PROPERTY的NOTIFY信号必须发射,否则绑定不会生效。

还有一种方式是把C++类注册为QML类型:

qmlRegisterType<MyDataModel>("com.myapp.models", 1, 0, "MyDataModel");

这样在QML里可以直接 import com.myapp.models 1.0 然后 MyDataModel { id: model } 来创建实例。

23.4 动画与状态——让界面活起来

QML的动画系统,说实话,比QWidget的动画框架好用太多了。你想想看,在QWidget里做个渐变动画得写多少代码?在QML里几行就搞定。

最简单的属性动画:

Rectangle {
    id: box
    width: 100; height: 100
    color: "red"
    
    PropertyAnimation on x {
        from: 0
        to: 300
        duration: 1000
        loops: Animation.Infinite
    }
}

状态机是QML另一个杀手锏。我做过一个登录界面,用状态管理不同UI状态:

Item {
    state: "idle"
    
    states: [
        State {
            name: "idle"
            PropertyChanges { target: loginBtn; enabled: true }
            PropertyChanges { target: spinner; visible: false }
        },
        State {
            name: "loading"
            PropertyChanges { target: loginBtn; enabled: false; text: "登录中..." }
            PropertyChanges { target: spinner; visible: true }
        },
        State {
            name: "success"
            PropertyChanges { target: loginBtn; text: "登录成功" }
            PropertyChanges { target: spinner; visible: false }
        }
    ]
    
    transitions: [
        Transition {
            from: "*"; to: "*"
            PropertyAnimation { properties: "opacity"; duration: 300 }
        }
    ]
}

为什么用状态机?因为当界面有多个控件需要同时变化时,状态机比手动设置每个控件的属性要清晰得多。你想想看,如果不用状态机,你得写一堆if-else来切换控件的可见性、文本、颜色……维护起来简直是噩梦。

23.5 Canvas绘图——自由发挥的画布

QML的Canvas元素提供了2D绘图能力,API跟HTML5 Canvas几乎一样。如果你做过Web前端,上手零成本。

画一个简单的饼图:

Canvas {
    id: canvas
    width: 300; height: 300
    
    onPaint: {
        var ctx = getContext("2d");
        ctx.reset();
        
        var data = [30, 20, 50];  // 三个扇区
        var colors = ["#FF6384", "#36A2EB", "#FFCE56"];
        var total = 0;
        data.forEach(function(v) { total += v; });
        
        var startAngle = 0;
        for (var i = 0; i < data.length; i++) {
            var sliceAngle = (data[i] / total) * Math.PI * 2;
            ctx.beginPath();
            ctx.moveTo(150, 150);
            ctx.arc(150, 150, 120, startAngle, startAngle + sliceAngle);
            ctx.closePath();
            ctx.fillStyle = colors[i];
            ctx.fill();
            startAngle += sliceAngle;
        }
    }
}

经验之谈:Canvas的onPaint在初始化时只会调用一次。如果你需要重绘,记得调用 canvas.requestPaint()。我在做实时波形图时,就是靠定时器不断调用requestPaint来刷新画面的。

Canvas还支持渐变、阴影、图像绘制等高级功能。不过要注意,Canvas是像素级操作,如果界面需要频繁重绘,性能可能会成为瓶颈。这时候可以考虑用ShaderEffect或者直接上OpenGL。

小结

这一章我们聊了QML的语法基础、常用控件、C++交互、动画状态和Canvas绘图。QML这套东西,说白了就是让你用更少的代码写出更漂亮的界面。我个人觉得,QML+Qt Quick的组合,在嵌入式设备和桌面应用的快速原型开发上,优势非常明显。

如果你之前一直用QWidget,不妨试试QML。刚开始可能有点不习惯,但用上几个项目之后,你会爱上这种声明式写界面的感觉。


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