用 C++ 编写 QML 扩展
该 Qt Qml模块为通过 C++ 扩展 QML 提供了一套 API。您可以编写扩展来添加自己的 QML 类型、扩展现有的 Qt 类型,或调用普通 QML 代码无法访问的 C/C++ 函数。
本教程介绍如何使用 C++ 编写 QML 扩展,其中包括 QML 的核心功能,如属性、信号和绑定。它还展示了如何通过插件部署扩展。
本教程中涉及的许多主题在Overview - QML and C++ Integration(概述 - QML 与 C++ 集成)及其文档子主题中有更详细的说明。特别是,您可能会对 "将 C++ 类的属性公开给 QML "和 "从 C++ 定义 QML 类型"这两个子课题感兴趣。
打开教程源代码
本教程中的代码是 Qt 源代码的一部分。如果您用Qt Online Installer 安装了 Qt,您可以在 Examples/Qt-6.9.0/qml/tutorials/extending-qml/ 下的 Qt 安装目录中找到源代码。
从零开始创建项目
或者,你也可以按照教程从头开始创建源代码:按照Qt Creator 中的说明,使用 Qt Creator 中的Qt Quick Application模板为每一章创建一个新项目:创建Qt Quick 应用程序。然后根据生成的骨架代码进行调整和扩展。
第 1 章:创建新类型
extending-qml/chapter1-basics
扩展 QML 时的一个常见任务是提供一个新的 QML 类型,以支持内置的 Qt Quick types.例如,这可以用来实现特定的数据模型,或提供具有自定义绘画和绘图功能的类型,或访问内置 QML 功能无法访问的系统功能(如网络编程)。
在本教程中,我们将展示如何使用Qt Quick 模块中的 C++ 类来扩展 QML。最终结果将是一个简单的饼图显示,由几个自定义 QML 类型通过绑定和信号等 QML 功能连接在一起实现,并通过插件提供给 QML Runtime。
首先,让我们创建一个名为 "PieChart "的新 QML 类型,它有两个属性:名称和颜色。我们将在名为 "Charts"(图表)的可导入类型命名空间中提供该类型,其版本为 1.0。
我们希望这个PieChart
类型能像这样从 QML 中使用:
import Charts PieChart { width: 100; height: 100 name: "A simple pie chart" color: "red" }
为此,我们需要一个 C++ 类来封装PieChart
类型及其属性。由于 QML 广泛使用 Qt Qml 的元对象系统,这个新类必须:
- 继承自QObject
- 使用Q_PROPERTY 宏声明其属性
类声明
下面是我们的PieChart
类,定义在piechart.h
中:
#include <QtQuick/QQuickPaintedItem> #include <QColor> class PieChart : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QString name READ name WRITE setName FINAL) Q_PROPERTY(QColor color READ color WRITE setColor FINAL) QML_ELEMENT public: PieChart(QQuickItem *parent = nullptr); QString name() const; void setName(const QString &name); QColor color() const; void setColor(const QColor &color); void paint(QPainter *painter) override; private: QString m_name; QColor m_color; };
该类继承自QQuickPaintedItem ,因为我们希望覆盖QQuickPaintedItem::paint() 以使用QPainter API 执行绘图操作。如果该类只是表示某种数据类型,而不是实际需要显示的项目,那么它可以简单地继承自QObject 。或者,如果我们想扩展现有的基于QObject 的类的功能,也可以从该类继承。或者,如果我们想创建一个不需要使用QPainter API 执行绘制操作的可视化项目,我们可以直接子类化QQuickItem 。
PieChart
类使用Q_PROPERTY 宏定义了name
和color
这两个属性,并重载了QQuickPaintedItem::paint()。PieChart
类使用QML_ELEMENT 宏注册,以便在 QML 中使用。如果不注册该类,App.qml
将无法创建PieChart
。
qmake 设置
要使注册生效,需要在项目文件CONFIG
中添加qmltypes
选项,并给出QML_IMPORT_NAME
和QML_IMPORT_MAJOR_VERSION
:
CONFIG += qmltypes QML_IMPORT_NAME = Charts QML_IMPORT_MAJOR_VERSION = 1
CMake 设置
同样,要使注册在使用 CMake 时生效,请使用qt_add_qml_module命令:
qt_add_qml_module(chapter1-basics URI Charts QML_FILES App.qml DEPENDENCIES QtQuick )
类实现
piechart.cpp
中的类实现只需根据情况设置和返回m_name
和m_color
的值,并实现paint()
以绘制简单的饼图:
PieChart::PieChart(QQuickItem *parent) : QQuickPaintedItem(parent) { } ... void PieChart::paint(QPainter *painter) { QPen pen(m_color, 2); painter->setPen(pen); painter->setRenderHints(QPainter::Antialiasing, true); painter->drawPie(boundingRect().adjusted(1, 1, -1, -1), 90 * 16, 290 * 16); }
QML 使用
既然我们已经定义了PieChart
类型,就可以在 QML 中使用它了。App.qml
文件创建了一个PieChart
项目,并使用标准 QMLText 项目显示饼图的细节:
import Charts import QtQuick Item { width: 300; height: 200 PieChart { id: aPieChart anchors.centerIn: parent width: 100; height: 100 name: "A simple pie chart" color: "red" } Text { anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 } text: aPieChart.name } }
请注意,虽然颜色在 QML 中指定为字符串,但它会自动转换为 PieChartcolor
属性的QColor 对象。其他各种值类型也会自动转换。例如,"640x480 "这样的字符串可自动转换为QSize 值。
我们还将创建一个 C++ 应用程序,使用QQuickView 运行和显示App.qml
。
以下是应用程序main.cpp
:
#include "piechart.h" #include <QtQuick/QQuickView> #include <QGuiApplication> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQuickView view; view.setResizeMode(QQuickView::SizeRootObjectToView); view.loadFromModule("Charts", "App"); view.show(); return QGuiApplication::exec(); }
构建项目
要构建项目,我们需要包含文件、链接库,并定义一个名为 "Charts "的类型命名空间(版本为 1.0),用于显示 QML 的任何类型。
使用 qmake
QT += qml quick CONFIG += qmltypes QML_IMPORT_NAME = Charts QML_IMPORT_MAJOR_VERSION = 1 HEADERS += piechart.h SOURCES += piechart.cpp \ main.cpp RESOURCES += chapter1-basics.qrc DESTPATH = $$[QT_INSTALL_EXAMPLES]/qml/tutorials/extending-qml/chapter1-basics target.path = $$DESTPATH INSTALLS += target
使用 CMake
# Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause cmake_minimum_required(VERSION 3.16) project(chapter1-basics LANGUAGES CXX) find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick) qt_standard_project_setup(REQUIRES 6.8) qt_add_executable(chapter1-basics main.cpp piechart.cpp piechart.h ) set_target_properties(chapter1-basics PROPERTIES WIN32_EXECUTABLE TRUE MACOSX_BUNDLE TRUE ) target_link_libraries(chapter1-basics PUBLIC Qt6::Core Qt6::Gui Qt6::Qml Qt6::Quick ) qt_add_qml_module(chapter1-basics URI Charts QML_FILES App.qml DEPENDENCIES QtQuick ) install(TARGETS chapter1-basics BUNDLE DESTINATION . RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) qt_generate_deploy_qml_app_script( TARGET chapter1-basics OUTPUT_SCRIPT deploy_script MACOS_BUNDLE_POST_BUILD NO_UNSUPPORTED_PLATFORM_ERROR DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM ) install(SCRIPT ${deploy_script})
现在我们可以构建并运行应用程序了:
注意: 你可能会看到一个警告Expression ... depends on non-bindable properties:PieChart::name.出现这种情况是因为我们为可写name
属性添加了绑定,但尚未为其定义通知信号。因此,如果name
的值发生变化,QML 引擎就无法更新绑定。下面几章将讨论这个问题。
第 2 章:连接到 C++ 方法和信号
extending-qml/chapter2-methods
假设我们希望PieChart
有一个 "clearChart()"(清除图表)方法,清除图表后发出一个 "chartCleared"(图表清除)信号。这样,我们的App.qml
就能调用clearChart()
并接收chartCleared()
信号:
import Charts import QtQuick Item { width: 300; height: 200 PieChart { id: aPieChart anchors.centerIn: parent width: 100; height: 100 color: "red" onChartCleared: console.log("The chart has been cleared") } MouseArea { anchors.fill: parent onClicked: aPieChart.clearChart() } Text { anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 } text: "Click anywhere to clear the chart" } }
为此,我们在 C++ 类中添加了一个clearChart()
方法和一个chartCleared()
信号:
class PieChart : public QQuickPaintedItem { ... public: ... Q_INVOKABLE void clearChart(); signals: void chartCleared(); ... };
Q_INVOKABLE 的使用使clearChart()
方法可用于 Qt Meta-Object 系统,进而可用于 QML。
注意: 您也可以将该方法声明为 Qt 槽,而不是使用Q_INVOKABLE ,因为公共槽和受保护槽也可从 QML 调用(您不能调用私有槽)。
clearChart()
方法将颜色更改为Qt::transparent ,重新绘制图表,然后发出chartCleared()
信号:
现在,当我们运行应用程序并点击窗口时,饼图消失,应用程序输出:
qml: The chart has been cleared
第 3 章:添加属性绑定
extending-qml/chapter3-bindings
属性绑定是 QML 的一个强大功能,它允许不同类型的值自动同步。当属性值改变时,它使用信号来通知和更新其他类型的值。
让我们启用color
属性的属性绑定。也就是说,如果我们有这样的代码
import Charts import QtQuick Item { width: 300; height: 200 Row { anchors.centerIn: parent spacing: 20 PieChart { id: chartA width: 100; height: 100 color: "red" } PieChart { id: chartB width: 100; height: 100 color: chartA.color } } MouseArea { anchors.fill: parent onClicked: { chartA.color = "blue" } } Text { anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 } text: "Click anywhere to change the chart color" } }
color: chartA.color "语句将chartB
的color
值与chartA
的color
值绑定。只要chartA
的color
值发生变化,chartB
的color
值就会更新为相同的值。当点击窗口时,MouseArea 中的onClicked
处理程序会改变chartA
的颜色,从而将两个图表都变为蓝色。
为color
属性启用属性绑定非常简单。我们在Q_PROPERTY() 声明中添加了一个NOTIFY功能,以表示每当值发生变化时,就会发出一个 "colorChanged "信号。
class PieChart : public QQuickPaintedItem { ... Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) public: ... signals: void colorChanged(); ... };
然后,我们在setColor()
中发出该信号:
void PieChart::setColor(const QColor &color) { if (color != m_color) { m_color = color; update(); // repaint with the new color emit colorChanged(); } }
setColor()
在发出colorChanged()
之前,必须检查颜色值是否确实发生了变化。这样可以确保不会不必要地发出信号,还可以防止在其他类型响应值变化时出现循环。
绑定的使用对 QML 至关重要。如果可以实现,你应该总是为属性添加 NOTIFY 信号,这样你的属性就可以在绑定中使用。不能绑定的属性不能自动更新,也不能在 QML 中灵活使用。另外,由于绑定在 QML 使用中被频繁调用和依赖,如果没有实现绑定,你的自定义 QML 类型的用户可能会看到意想不到的行为。
第 4 章:使用自定义属性类型
extending-qml/chapter4-customPropertyTypes
PieChart
类型目前有一个字符串类型属性和一个颜色类型属性。它还可以有许多其他类型的属性。例如,它可以有一个 int-type 属性,用于存储每个图表的标识符:
// C++ class PieChart : public QQuickPaintedItem { Q_PROPERTY(int chartId READ chartId WRITE setChartId NOTIFY chartIdChanged) ... public: void setChartId(int chartId); int chartId() const; ... signals: void chartIdChanged(); }; // QML PieChart { ... chartId: 100 }
除了int
之外,我们还可以使用其他各种属性类型。许多 Qt 数据类型,如QColor,QSize 和QRect 都是 Qt XML 自动支持的。(完整列表请参阅QML 和 C++文档之间的数据类型转换)。
如果我们想创建一个 QML 默认不支持的属性,我们需要向 QML 引擎注册该类型。
例如,让我们用color
属性的 "PieSlice "类型代替property
的使用。我们不指定颜色,而是指定一个PieSlice
值,它本身包含一个color
:
import Charts import QtQuick Item { width: 300; height: 200 PieChart { id: chart anchors.centerIn: parent width: 100; height: 100 pieSlice: PieSlice { anchors.fill: parent color: "red" } } Component.onCompleted: console.log("The pie is colored " + chart.pieSlice.color) }
与PieChart
一样,这个新的PieSlice
类型继承于QQuickPaintedItem ,并用Q_PROPERTY() 声明其属性:
class PieSlice : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor color READ color WRITE setColor FINAL) QML_ELEMENT public: PieSlice(QQuickItem *parent = nullptr); QColor color() const; void setColor(const QColor &color); void paint(QPainter *painter) override; private: QColor m_color; };
要在PieChart
中使用它,我们要修改color
属性声明和相关方法签名:
class PieChart : public QQuickItem { Q_OBJECT Q_PROPERTY(PieSlice* pieSlice READ pieSlice WRITE setPieSlice FINAL) ... public: ... PieSlice *pieSlice() const; void setPieSlice(PieSlice *pieSlice); ... };
在实现setPieSlice()
时有一点需要注意。PieSlice
是一个可视化项,因此必须使用QQuickItem::setParentItem() 将其设置为PieChart
的子项,以便PieChart
知道在绘制其内容时绘制该子项:
void PieChart::setPieSlice(PieSlice *pieSlice) { m_pieSlice = pieSlice; pieSlice->setParentItem(this); }
与PieChart
类型一样,PieSlice
类型也必须使用QML_ELEMENT 表达到 QML 中。
class PieSlice : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor color READ color WRITE setColor FINAL) QML_ELEMENT public: PieSlice(QQuickItem *parent = nullptr); QColor color() const; void setColor(const QColor &color); void paint(QPainter *painter) override; private: QColor m_color; }; ...
与PieChart
一样,我们将版本 1.0 的 "Charts"(图表)类型命名空间添加到构建文件中:
使用 qmake:
QT += qml quick CONFIG += qmltypes QML_IMPORT_NAME = Charts QML_IMPORT_MAJOR_VERSION = 1 HEADERS += piechart.h \ pieslice.h SOURCES += piechart.cpp \ pieslice.cpp \ main.cpp RESOURCES += chapter4-customPropertyTypes.qrc DESTPATH = $$[QT_INSTALL_EXAMPLES]/qml/tutorials/extending-qml/chapter4-customPropertyTypes target.path = $$DESTPATH INSTALLS += target
使用 CMake:
... qt_add_executable(chapter4-customPropertyTypes main.cpp piechart.cpp piechart.h pieslice.cpp pieslice.h ) qt_add_qml_module(chapter4-customPropertyTypes URI Charts QML_FILES App.qml DEPENDENCIES QtQuick ) ...
第 5 章:使用列表属性类型
extending-qml/chapter5-listproperties
现在,一个PieChart
只能有一个PieSlice
。理想情况下,图表应该有多个切片,并有不同的颜色和大小。为此,我们可以使用slices
属性来接受PieSlice
项的列表:
import Charts import QtQuick Item { width: 300; height: 200 PieChart { anchors.centerIn: parent width: 100; height: 100 slices: [ PieSlice { anchors.fill: parent color: "red" fromAngle: 0; angleSpan: 110 }, PieSlice { anchors.fill: parent color: "black" fromAngle: 110; angleSpan: 50 }, PieSlice { anchors.fill: parent color: "blue" fromAngle: 160; angleSpan: 100 } ] } }
为此,我们将PieChart
中的pieSlice
属性替换为slices
属性,并声明为QQmlListProperty 类型。QQmlListProperty 类能在 QML 公开的类型中创建列表属性。我们用slices()
函数替换pieSlice()
函数,该函数返回一个片段列表。我们还使用QList 来存储内部切片列表,即m_slices
:
class PieChart : public QQuickItem { Q_OBJECT Q_PROPERTY(QQmlListProperty<PieSlice> slices READ slices FINAL) ... public: ... QQmlListProperty<PieSlice> slices(); private: QString m_name; QList<PieSlice *> m_slices; };
虽然slices
属性没有相关的WRITE
函数,但由于QQmlListProperty 的工作方式,它仍然是可修改的。在PieChart
的实现中,我们实现了PieChart::slices()
,以返回QQmlListProperty 的值:
QQmlListProperty<PieSlice> PieChart::slices() { return QQmlListProperty<PieSlice>(this, &m_slices); }
这合成了与 QML 列表交互的必要函数。这样得到的QQmlListProperty 就是列表的视图。您也可以手动提供列表的各个访问函数。如果您的列表不是QList ,或您想限制或定制 QML 对列表的访问,就必须这样做。但在大多数情况下,使用QList 指针的构造函数是最安全、最简单的选择。
PieSlice
类也被修改为包含fromAngle
和angleSpan
属性,并根据这些值绘制切片。如果您已阅读过本教程前面几页的内容,这只是一个简单的修改,因此这里不显示代码。
第 6 章:编写扩展插件
extending-qml/chapter6-plugins
目前,PieChart
和PieSlice
类型由App.qml
使用,在 C++ 应用程序中使用QQuickView 显示。使用 QML 扩展的另一种方法是创建一个插件库,将其作为一个新的 QML 导入模块提供给 QML 引擎。这样,PieChart
和PieSlice
类型就可以注册到一个类型命名空间,任何 QML 应用程序都可以导入,而不是限制这些类型只能由一个应用程序使用。
为 QML 创建 C++ 插件》(Creating C++ Plugins for QML)中描述了创建插件的步骤。首先,我们创建一个名为ChartsPlugin
的插件类。它是QQmlEngineExtensionPlugin 的子类,并使用Q_PLUGIN_METADATA() 宏向 Qt XML 元对象系统注册插件。
下面是chartsplugin.h
中的ChartsPlugin
定义:
#include <QQmlEngineExtensionPlugin> class ChartsPlugin : public QQmlEngineExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid) };
然后,我们配置构建文件,将项目定义为插件库。
使用 qmake:
TEMPLATE = lib CONFIG += plugin qmltypes QT += qml quick QML_IMPORT_NAME = Charts QML_IMPORT_MAJOR_VERSION = 1 TARGET = $$qtLibraryTarget(chartsplugin) HEADERS += piechart.h \ pieslice.h \ chartsplugin.h SOURCES += piechart.cpp \ pieslice.cpp DESTPATH=$$[QT_INSTALL_EXAMPLES]/qml/tutorials/extending-qml/chapter6-plugins/$$QML_IMPORT_NAME target.path=$$DESTPATH qmldir.files=$$PWD/qmldir qmldir.path=$$DESTPATH INSTALLS += target qmldir CONFIG += install_ok # Do not cargo-cult this! OTHER_FILES += qmldir # Copy the qmldir file to the same folder as the plugin binary cpqmldir.files = qmldir cpqmldir.path = . COPIES += cpqmldir
使用 CMake
# Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause qt6_policy(SET QTP0001 NEW) qt6_add_qml_module(chartsplugin URI "Charts" PLUGIN_TARGET chartsplugin DEPENDENCIES QtQuick ) target_sources(chartsplugin PRIVATE piechart.cpp piechart.h pieslice.cpp pieslice.h ) target_link_libraries(chartsplugin PRIVATE Qt6::Core Qt6::Gui Qt6::Qml Qt6::Quick ) install(TARGETS chartsplugin RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}/Charts" LIBRARY DESTINATION "${CMAKE_INSTALL_BINDIR}/Charts" ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/qmldir DESTINATION "${CMAKE_INSTALL_BINDIR}/Charts" )
在 Windows 或 Linux 上构建此示例时,Charts
目录将与使用我们新导入模块的应用程序位于同一级别。这样,QML 引擎就能找到我们的模块,因为 QML 导入的默认搜索路径包括应用程序可执行文件的目录。在 macOS 上,插件二进制文件被复制到应用程序捆绑包中的Contents/PlugIns
。在 qmake 中,这个路径被设置为chapter6-plugins/app.pro
:
macos:!qtConfig(static) { charts.files = $$OUT_PWD/Charts charts.path = Contents/PlugIns QMAKE_BUNDLE_DATA += charts }
为了考虑到这一点,我们还需要在main.cpp
中添加这个位置作为QML 导入路径:
QQuickView view; #ifdef Q_OS_MACOS view.engine()->addImportPath(app.applicationDirPath() + "/../PlugIns"); #endif ...
当多个应用程序使用相同的 QML 导入时,定义自定义导入路径也很有用。
.pro
文件还包含额外的魔法,以确保模块定义 qmldir 文件总是复制到与插件二进制文件相同的位置。
qmldir
文件声明了模块名称和模块可用的插件:
module Charts optional plugin chartsplugin typeinfo plugins.qmltypes depends QtQuick prefer :/qt/qml/Charts/
现在我们有了一个 QML 模块,只要 QML 引擎知道在哪里能找到它,它就能被导入到任何应用程序中。示例包含一个加载App.qml
的可执行文件,它使用import Charts 1.0
语句。另外,也可以使用qml 工具加载 QML 文件,将导入路径设置为当前目录,以便找到qmldir
文件:
qml -I . App.qml
QML 引擎将加载 "Charts"(图表)模块,该模块提供的类型可用于任何导入该模块的 QML 文档。
第 7 章:总结
在本教程中,我们展示了创建 QML 扩展的基本步骤:
- 通过子类化QObject 定义新的 QML 类型,并用QML_ELEMENT 或QML_NAMED_ELEMENT() 注册它们。
- 使用Q_INVOKABLE 或 Qt slots 添加可调用方法,并使用
onSignal
语法连接 Qt 信号 - 通过定义NOTIFY信号添加属性绑定
- 如果内置类型不够用,可定义自定义属性类型
- 定义列表属性类型QQmlListProperty
- 通过定义 Qt 插件和编写qmldir文件创建插件库
QML 和 C++ 集成概述文档显示了可添加到 QML 扩展中的其他有用功能。例如,我们可以使用默认属性来添加切片,而无需使用slices
属性:
PieChart { PieSlice { ... } PieSlice { ... } PieSlice { ... } }
或使用属性值源不时随机添加和删除切片:
PieChart { PieSliceRandomizer on slices {} }
注: 要继续学习 QML 扩展和功能,请参阅《用 C++ 编写高级 QML 扩展》教程。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.