跳到主要内容

QML 与 C++ 交互

在实际的 Qt Quick 应用程序中,QML 负责界面展示,而 C++ 负责业务逻辑和数据处理。QML 与 C++ 的交互是 Qt Quick 开发的核心技能之一。本章将详细介绍如何在 QML 和 C++ 之间进行数据传递、方法调用和信号通信。

交互方式概述

QML 与 C++ 的交互主要有以下几种方式:

方式用途特点
属性绑定数据传递自动同步,支持属性变化通知
信号槽连接事件通信松耦合,支持跨线程
Q_INVOKABLE方法调用从 QML 调用 C++ 方法
注册类型实例化 C++ 对象在 QML 中创建 C++ 对象
上下文属性全局对象访问简单直接,适合单例

暴露 C++ 属性到 QML

使用 Q_PROPERTY

Q_PROPERTY 宏是暴露 C++ 属性到 QML 的核心机制:

// Person.h
#ifndef PERSON_H
#define PERSON_H

#include <QObject>
#include <QString>

class Person : public QObject
{
Q_OBJECT
// QML_ELEMENT 用于 Qt 6 的类型注册
QML_ELEMENT

// 定义属性:名称、读取方法、写入方法、变化信号
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
Q_PROPERTY(QString email READ email CONSTANT) // 只读属性

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

// Getter 方法
QString name() const;
int age() const;
QString email() const;

// Setter 方法
void setName(const QString &name);
void setAge(int age);

signals:
// 属性变化信号(命名规范:<属性名>Changed)
void nameChanged();
void ageChanged();

private:
QString m_name;
int m_age = 0;
QString m_email;
};

#endif // PERSON_H
// Person.cpp
#include "Person.h"

Person::Person(QObject *parent)
: QObject(parent)
, m_email("[email protected]")
{
}

QString Person::name() const
{
return m_name;
}

void Person::setName(const QString &name)
{
if (m_name != name) {
m_name = name;
emit nameChanged(); // 发出变化信号
}
}

int Person::age() const
{
return m_age;
}

void Person::setAge(int age)
{
if (m_age != age) {
m_age = age;
emit ageChanged();
}
}

QString Person::email() const
{
return m_email; // CONSTANT 属性没有 setter
}

在 QML 中使用

import QtQuick
import com.example // 自定义模块

Item {
Person {
id: person
name: "张三"
age: 25

// 监听属性变化
onNameChanged: console.log("姓名变化:", name)
onAgeChanged: console.log("年龄变化:", age)
}

Text {
text: person.name + ", " + person.age + "岁"
}

MouseArea {
onClicked: {
person.name = "李四" // 调用 setter
person.age = 30
}
}
}

属性类型说明

QML 支持的属性类型包括:

C++ 类型QML 类型说明
boolbool布尔值
int, uintint整数
double, floatreal浮点数
QStringstring字符串
QUrlurlURL
QColorcolor颜色
QDate, QTime, QDateTimedate, time, datetime日期时间
QRect, QRectFrect矩形
QPoint, QPointFpoint
QSize, QSizeFsize尺寸
QVariantListlist列表
QVariantMapvar对象/映射
QObject*var 或具体类型对象引用

组属性(Grouped Properties)

将相关属性组织在一起:

class Address : public QObject
{
Q_OBJECT
Q_PROPERTY(QString street READ street WRITE setStreet)
Q_PROPERTY(QString city READ city WRITE setCity)
Q_PROPERTY(QString zipCode READ zipCode WRITE setZipCode)
// ...
};

class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(Address* address READ address CONSTANT)
// ...
};
Person {
address.street: "长安街1号"
address.city: "北京"
address.zipCode: "100000"
}

暴露 C++ 方法到 QML

使用 Q_INVOKABLE

标记可从 QML 调用的方法:

// Calculator.h
class Calculator : public QObject
{
Q_OBJECT
QML_ELEMENT

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

// 使用 Q_INVOKABLE 标记可调用方法
Q_INVOKABLE int add(int a, int b);
Q_INVOKABLE int subtract(int a, int b);
Q_INVOKABLE double multiply(double a, double b);
Q_INVOKABLE double divide(double a, double b);

// 有返回值的方法
Q_INVOKABLE QString formatResult(double value);

// 无返回值的方法
Q_INVOKABLE void clear();

// 重载方法
Q_INVOKABLE int calculate(int a, int b, const QString &op);
Q_INVOKABLE double calculate(double a, double b, const QString &op);
};
// Calculator.cpp
int Calculator::add(int a, int b)
{
return a + b;
}

QString Calculator::formatResult(double value)
{
return QString::number(value, 'f', 2);
}

在 QML 中调用

import QtQuick
import com.example

Item {
Calculator {
id: calc
}

function doCalculation() {
// 调用 C++ 方法
var sum = calc.add(10, 20)
console.log("结果:", sum)

var formatted = calc.formatResult(3.14159)
console.log("格式化:", formatted)

// 调用重载方法
var result1 = calc.calculate(10, 5, "add")
var result2 = calc.calculate(10.5, 2.5, "multiply")
}
}

使用 Slots

槽函数也可以从 QML 调用:

class NetworkManager : public QObject
{
Q_OBJECT
QML_ELEMENT

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

public slots:
// public slots 可以从 QML 直接调用
void sendRequest(const QString &url);
void cancelAll();
QString getLastResponse();

// 带返回值的槽
bool isConnected() const;
};
NetworkManager {
id: network

onSendRequest: (url) => {
console.log("发送请求:", url)
}
}

Button {
text: "发送请求"
onClicked: network.sendRequest("https://api.example.com/data")
}

Q_INVOKABLE vs Slots

特性Q_INVOKABLESlots
返回值支持支持支持
信号连接不支持支持
语义明确明确"可调用"信号槽语义
推荐场景纯方法调用需要信号连接

信号与槽连接

C++ 信号到 QML 处理器

C++ 中定义的信号会自动在 QML 中生成对应的处理器:

// DataFetcher.h
class DataFetcher : public QObject
{
Q_OBJECT
QML_ELEMENT

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

Q_INVOKABLE void fetchData(const QString &query);

signals:
// 信号会自动生成 onDataReceived 处理器
void dataReceived(const QString &data);
void errorOccurred(const QString &errorMessage);
void progressChanged(int percentage);

private:
void processData(const QByteArray &rawData);
};

在 QML 中处理信号

DataFetcher {
id: fetcher

// 自动生成的信号处理器:on + 信号名(首字母大写)
onDataReceived: (data) => {
console.log("收到数据:", data)
resultText.text = data
}

onErrorOccurred: (errorMessage) => {
console.log("错误:", errorMessage)
errorDialog.text = errorMessage
errorDialog.open()
}

onProgressChanged: (percentage) => {
progressBar.value = percentage
}
}

Button {
text: "获取数据"
onClicked: fetcher.fetchData("users")
}

QML 信号到 C++ 槽

从 QML 发出信号连接到 C++ 槽:

// ButtonController.h
class ButtonController : public QObject
{
Q_OBJECT
QML_ELEMENT

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

public slots:
// 槽函数,接收来自 QML 的信号
void onButtonClicked();
void onValueChanged(int newValue);
void onTextInputChanged(const QString &text);
};

方式一:QML 中连接

ButtonController {
id: controller
}

Button {
text: "点击"
onClicked: controller.onButtonClicked()
}

Slider {
onValueChanged: controller.onValueChanged(value)
}

方式二:C++ 中连接

// 在 C++ 代码中连接 QML 对象的信号
QQuickItem *qmlButton = view.rootObject()->findChild<QQuickItem*>("myButton");

if (qmlButton) {
QObject::connect(qmlButton, SIGNAL(clicked()),
controller, SLOT(onButtonClicked()));
}

方式三:使用 Connections

import QtQuick

Item {
id: root

signal buttonClicked(int buttonId)

ButtonController {
id: controller
}

Connections {
target: root
function onButtonClicked(buttonId) {
controller.handleButtonClick(buttonId)
}
}

Button {
text: "点击"
onClicked: root.buttonClicked(1)
}
}

注册 C++ 类型到 QML

使用 QML_ELEMENT(Qt 6 推荐)

Qt 6 使用 QML_ELEMENT 宏和 CMake 进行类型注册:

// MyType.h
#include <QObject>
#include <QtQml>

class MyType : public QObject
{
Q_OBJECT
QML_ELEMENT // 标记为 QML 元素

Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)

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

int value() const;
void setValue(int value);

Q_INVOKABLE void doSomething();

signals:
void valueChanged();

private:
int m_value = 0;
};
# CMakeLists.txt
find_package(Qt6 REQUIRED COMPONENTS Quick)

qt_add_qml_module(myapp
URI MyApp
VERSION 1.0
SOURCES
MyType.h
MyType.cpp
main.cpp
)

注册不可实例化的类型

对于只能作为属性类型,不能在 QML 中实例化的类型:

class DataModel : public QObject
{
Q_OBJECT
QML_UNCREATABLE("DataModel 只能从 C++ 创建")
QML_ELEMENT

// ...
};

注册单例类型

// GlobalSettings.h
class GlobalSettings : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON // 标记为单例

Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged)

public:
static GlobalSettings *create(QQmlEngine *engine, QJSEngine *js);

QString theme() const;
void setTheme(const QString &theme);

signals:
void themeChanged();

private:
QString m_theme;
};
// 在 QML 中直接使用单例(无需实例化)
import com.example

Text {
text: "当前主题: " + GlobalSettings.theme
}

Button {
text: "切换主题"
onClicked: GlobalSettings.theme = "dark"
}

设置上下文属性

上下文属性是一种简单的方式将 C++ 对象暴露给 QML:

// main.cpp
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "DataManager.h"
#include "UserManager.h"

int main(int argc, char *argv[])
{
QApplication app(argc, argv);

QQmlApplicationEngine engine;

// 创建 C++ 对象
DataManager dataManager;
UserManager userManager;

// 设置上下文属性
engine.rootContext()->setContextProperty("dataManager", &dataManager);
engine.rootContext()->setContextProperty("userManager", &userManager);

// 加载 QML
const QUrl url(u"qrc:/Main.qml"_qs);
engine.load(url);

return app.exec();
}
// Main.qml
import QtQuick

Window {
visible: true

Text {
// 直接使用上下文属性
text: "用户: " + userManager.currentUserName
}

Button {
text: "加载数据"
onClicked: {
// 调用上下文属性的方法
dataManager.loadData()
}
}

Connections {
target: dataManager
function onDataLoaded(data) {
console.log("数据加载完成:", data)
}
}
}

上下文属性的优缺点

优点:

  • 实现简单,代码量少
  • 适合全局单例对象

缺点:

  • 所有 QML 文件都可以访问,缺乏封装
  • 不支持属性绑定优化
  • 类型信息不完整

数据模型

自定义 ListModel

在 C++ 中创建列表模型供 QML 使用:

// User.h
class User : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString email READ email CONSTANT)
Q_PROPERTY(int age READ age CONSTANT)

public:
User(const QString &name, const QString &email, int age, QObject *parent = nullptr);

QString name() const;
QString email() const;
int age() const;

private:
QString m_name;
QString m_email;
int m_age;
};

// UserModel.h
#include <QAbstractListModel>

class UserModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT

public:
enum Roles {
NameRole = Qt::UserRole + 1,
EmailRole,
AgeRole
};

explicit UserModel(QObject *parent = nullptr);

// 必须实现的方法
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;

// 可选:支持编辑
QHash<int, QByteArray> roleNames() const override;

// 添加数据
Q_INVOKABLE void addUser(const QString &name, const QString &email, int age);
Q_INVOKABLE void clear();

private:
QList<User*> m_users;
};
// UserModel.cpp
int UserModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return m_users.count();
}

QVariant UserModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_users.count())
return QVariant();

User *user = m_users.at(index.row());

switch (role) {
case NameRole:
return user->name();
case EmailRole:
return user->email();
case AgeRole:
return user->age();
default:
return QVariant();
}
}

QHash<int, QByteArray> UserModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[EmailRole] = "email";
roles[AgeRole] = "age";
return roles;
}

void UserModel::addUser(const QString &name, const QString &email, int age)
{
beginInsertRows(QModelIndex(), m_users.count(), m_users.count());
m_users.append(new User(name, email, age, this));
endInsertRows();
}

在 QML 中使用

ListView {
width: 300
height: 400

model: userModel // 从 C++ 设置

delegate: ItemDelegate {
width: ListView.view.width
text: model.name + " (" + model.age + "岁)"
secondaryText: model.email

onClicked: console.log("选中:", model.name)
}
}

Button {
text: "添加用户"
onClicked: userModel.addUser("新用户", "[email protected]", 25)
}

完整示例

数据同步示例

// Settings.h
#pragma once

#include <QObject>
#include <QSettings>

class Settings : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON

Q_PROPERTY(QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged)
Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged)
Q_PROPERTY(int timeout READ timeout WRITE setTimeout NOTIFY timeoutChanged)

public:
static Settings *create(QQmlEngine *engine, QJSEngine *js);

QString serverUrl() const;
void setServerUrl(const QString &url);

bool darkMode() const;
void setDarkMode(bool enabled);

int timeout() const;
void setTimeout(int seconds);

Q_INVOKABLE void reset();
Q_INVOKABLE void save();

signals:
void serverUrlChanged();
void darkModeChanged();
void timeoutChanged();

private:
explicit Settings(QObject *parent = nullptr);

QSettings m_settings;
QString m_serverUrl;
bool m_darkMode = false;
int m_timeout = 30;
};
// Settings.cpp
#include "Settings.h"

Settings *Settings::create(QQmlEngine *engine, QJSEngine *js)
{
return new Settings();
}

Settings::Settings(QObject *parent)
: QObject(parent)
{
// 从存储加载设置
m_serverUrl = m_settings.value("server/url", "https://api.example.com").toString();
m_darkMode = m_settings.value("ui/darkMode", false).toBool();
m_timeout = m_settings.value("network/timeout", 30).toInt();
}

QString Settings::serverUrl() const { return m_serverUrl; }

void Settings::setServerUrl(const QString &url)
{
if (m_serverUrl != url) {
m_serverUrl = url;
emit serverUrlChanged();
}
}

bool Settings::darkMode() const { return m_darkMode; }

void Settings::setDarkMode(bool enabled)
{
if (m_darkMode != enabled) {
m_darkMode = enabled;
emit darkModeChanged();
}
}

int Settings::timeout() const { return m_timeout; }

void Settings::setTimeout(int seconds)
{
if (m_timeout != seconds) {
m_timeout = seconds;
emit timeoutChanged();
}
}

void Settings::reset()
{
setServerUrl("https://api.example.com");
setDarkMode(false);
setTimeout(30);
}

void Settings::save()
{
m_settings.setValue("server/url", m_serverUrl);
m_settings.setValue("ui/darkMode", m_darkMode);
m_settings.setValue("network/timeout", m_timeout);
m_settings.sync();
}
// SettingsPage.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import com.example

Page {
title: "设置"

ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15

// 服务器 URL
GridLayout {
Layout.fillWidth: true
columns: 2

Label { text: "服务器地址:" }
TextField {
Layout.fillWidth: true
text: Settings.serverUrl
onTextChanged: Settings.serverUrl = text
}

Label { text: "深色模式:" }
Switch {
checked: Settings.darkMode
onCheckedChanged: Settings.darkMode = checked
}

Label { text: "超时时间(秒):" }
SpinBox {
from: 5
to: 120
value: Settings.timeout
onValueChanged: Settings.timeout = value
}
}

// 按钮
RowLayout {
Layout.topMargin: 20

Button {
text: "保存"
onClicked: {
Settings.save()
saveConfirmation.open()
}
}

Button {
text: "重置"
onClicked: Settings.reset()
}
}

Item { Layout.fillHeight: true }
}

Dialog {
id: saveConfirmation
title: "提示"
standardButtons: Dialog.Ok
Label { text: "设置已保存" }
}
}

小结

本章介绍了 QML 与 C++ 交互的核心技术:

  1. 属性暴露:使用 Q_PROPERTY 将 C++ 属性暴露给 QML
  2. 方法调用:使用 Q_INVOKABLE 和 slots 从 QML 调用 C++ 方法
  3. 信号通信:C++ 信号到 QML 处理器,QML 信号到 C++ 槽
  4. 类型注册:QML_ELEMENT、QML_SINGLETON、上下文属性
  5. 数据模型:自定义 ListModel 供 QML 视图使用

掌握这些技术,可以充分发挥 QML 在界面开发和 C++ 在业务逻辑方面的各自优势,构建高效、可维护的 Qt Quick 应用程序。

练习

  1. 创建一个 Person 类,包含 name、age 属性,并在 QML 中显示和编辑
  2. 实现一个 Calculator 类,提供加减乘除方法,从 QML 调用
  3. 创建一个自定义 ListModel,在 ListView 中显示
  4. 实现一个设置管理器,支持保存和加载设置
  5. 实现一个简单的网络请求类,发出请求并通过信号返回结果

参考资源