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 类型 | 说明 |
|---|---|---|
bool | bool | 布尔值 |
int, uint | int | 整数 |
double, float | real | 浮点数 |
QString | string | 字符串 |
QUrl | url | URL |
QColor | color | 颜色 |
QDate, QTime, QDateTime | date, time, datetime | 日期时间 |
QRect, QRectF | rect | 矩形 |
QPoint, QPointF | point | 点 |
QSize, QSizeF | size | 尺寸 |
QVariantList | list | 列表 |
QVariantMap | var | 对象/映射 |
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_INVOKABLE | Slots |
|---|---|---|
| 返回值支持 | 支持 | 支持 |
| 信号连接 | 不支持 | 支持 |
| 语义明确 | 明确"可调用" | 信号槽语义 |
| 推荐场景 | 纯方法调用 | 需要信号连接 |
信号与槽连接
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++ 交互的核心技术:
- 属性暴露:使用 Q_PROPERTY 将 C++ 属性暴露给 QML
- 方法调用:使用 Q_INVOKABLE 和 slots 从 QML 调用 C++ 方法
- 信号通信:C++ 信号到 QML 处理器,QML 信号到 C++ 槽
- 类型注册:QML_ELEMENT、QML_SINGLETON、上下文属性
- 数据模型:自定义 ListModel 供 QML 视图使用
掌握这些技术,可以充分发挥 QML 在界面开发和 C++ 在业务逻辑方面的各自优势,构建高效、可维护的 Qt Quick 应用程序。
练习
- 创建一个 Person 类,包含 name、age 属性,并在 QML 中显示和编辑
- 实现一个 Calculator 类,提供加减乘除方法,从 QML 调用
- 创建一个自定义 ListModel,在 ListView 中显示
- 实现一个设置管理器,支持保存和加载设置
- 实现一个简单的网络请求类,发出请求并通过信号返回结果