• 第十八章:QML扩展


    第十八章:QML扩展

    用C++ 扩展QML

    仅用QML来创建应用在某些场景下会受到限制。QML的 运行时(环境)是使用C++ 来开发的,而运行时 是可以扩展的,以使其可以自由和充份地利用相关系统环境的性能。

    理解QML运行时

    当运行QML应用时,QML是在运行时环境中被执行的。运行时是在C++ 的QtQml模块中实现的。引擎-负责执行QML,上下文-为每个组件提供全局级的属性访问,以及组件-可以从QML中实例化的QML元素,以上几部分组成了QML的运行时。

    #include <QtGui>
    #include <QtQml>
    
    int main(int argc, char **argv)
    {
        QGuiApplication app(argc, argv);
        QUrl source(QStringLiteral("qrc:/main.qml"));
        QQmlApplicationEngine engine;
        engine.load(source);
        return app.exec();
    }

    本例中,QGuiApplication封装与应用程序实例相关的所有内容(如,程序名、命令行参数、事件循环的管理等)。QQmlApplicationEngine管理了组件和上下文层次顺序。它需要加载一个标准的QML文件作为应用程序的起点。这时,QML文件一般是包括窗体和文本类型的main.qml

    注意
    QmlApplicationEngine如果加载仅以Item作为根元素的main.qml,那么显示器上将不会有任何内容,因为它需要一个窗体来管理渲染层。
    引擎是可以加载不包括任何用户界面(如,平面对象)的QML代码的。因此,它并不会为你创建默认可视窗体。qml运行时首先会尝试检查main QML的根元素中是否存在一个窗体,如果没有,则创建一个窗体,并将原来的根元素作为新创建窗体的子元素。因此,你的main.qml必须要主动创建窗体,并以其作为根元素。

    import QtQuick 2.5
    import QtQuick.Window 2.2
    
    Window {
        visible: true
        width: 512
        height: 300
    
        Text {
            anchors.centerIn: parent
            text: "Hello World!"
        }
    }

    在QML中,我们声明了依赖关系,这里是QtQuickQtQuick.Window。这些声明将会触发从导入路径对这些模块的查找,如果查找成功,则会由引擎加载所需要插件。然后,新加载的类型将通过代表报告的 qmldir 文件中的声明提供给 QML 环境。点此了解qmldir
    也可以通过在main.cpp中直接向引擎添加类型的方式来简化插件创建。这里我们假定有一个从QObject基类继承的子类CurrentTime

    QQmlApplicationEngine engine();
    
    qmlRegisterType<CurrentTime>("org.example", 1, 0, "CurrentTime");
    
    engine.load(source);

    现在就可以在QML文件中使用CurrentTime了。

    import org.example 1.0
    
    CurrentTime {
        // access properties, functions, signals
    }

    如果不需要在QML里新建实例,也可以使用引擎的上下文属性来将C++ 对象暴露给QML,如:

    QScopedPointer<CurrentTime> current(new CurrentTime());
    
    QQmlApplicationEngine engine();
    
    engine.rootContext().setContextProperty("current", current.value())
    
    engine.load(source);

    注意
    不要混淆了setContextProperty()setProperty()。第一个为qml的上下文设置了上下文属性,而setProperty() 就为QObject设置一个动态属性,这不是用在当下的这个场景里的。

    现在就可以在应用中的任何地方使用current属性了。由于上下文继承,它在 QML 代码中随处可用。current对象注册在最外层的根上下文中,该上下文随处继承。

    import QtQuick
    import QtQuick.Window
    
    Window {
        visible: true
        width: 512
        height: 300
    
        Component.onCompleted: {
            console.log('current: ' + current)
        }
    }

    以下是常用的几种扩展QML的方法:

    • Context 属性 - setContextProperty()
    • 通过引擎注册类型 - 在main.cpp来调用qmlRegisterType
    • QML 扩展插件 - 灵活性最大,后面会讨论
      Context 属性 对于较小的程序来说比较容易使用。你只需要将全局对象暴露给系统API,而不需要其它操作。确保没有名称冲突很重要(如,为对象使用特殊字符$-本例中是$.currentTime)。$是有效的JS变量。
      注册QML类型允许用户从QML来控制C++ 对象的生命周期。这是contxt属性做不到的。而且,它也不污染全局命名空间。所有类型仍然需要先注册,因此,应用程序启动时要链接所有库,对多数程序来说这不是什么问题。
      QML扩展插件是最为灵活的。它允许QML文件在首次调用导入模块时加载插件并注册类型。通过使用QML单例,也不会污染全局命名空间了。可以在不同的工程之间重用插件,这在使用Qt开发多个项目时,很方便。
      回到最简单的main.qml文件:
    import QtQuick 2.5
    import QtQuick.Window 2.2
    
    Window {
        visible: true
        width: 512
        height: 300
    
        Text {
            anchors.centerIn: parent
            text: "Hello World!"
        }
    }

    当导入QtQuickQtQuick.Window时,是告诉QML运行时来找相应的扩展插件并加载它们。这些是QML引擎在QML导入路径里查找模块来实现的。这些新加载的类型将在QML环境中可用。
    本章的剩余部分将关注 QML 扩展插件。因为它提供了最大的灵活性和可重用性。

    插件内容

    插件是有着确定接口,可根据需要进行加载的库。它不同于单纯的库文件,因为库文件是应用程序启动时加载的。在QML里,接口被称为QQmlExtensionPlugin。有两个我们感兴趣的方法initializeEngine()registerTypes()。当插件首次被加载时会调用initializeEngine(),这会使引擎将插件对象暴露给顶层上下文context。多数时候,只会用到registerTypes()方法。这允许在提供的 URL 上向引擎注册自定义 QML 类型。
    通过创建一个小的FileIO工具类来了解一下。它允许从QML中读取文本文件。在模拟的 QML 实现中,第一次迭代版本代码可能看起来像这样:

    // FileIO.qml (good)
    QtObject {
        function write(path, text) {};
        function read(path) { return "TEXT" }
    }

    这是一个纯QML实现,它可能基于C++ 的QML API接口。我们用它来实现API。这里需要一个readwrite函数。我们也看到write函数接收pathtext参数,而read函数接收path参数并返回文本。如你所见,pathtext是常用参数,或许可以将其提取出来作为属性,来简化声明式上下文环境下的API的易用性。

    // FileIO.qml (better)
    QtObject {
        property url source
        property string text
        function write() {} // open file and write text 
        function read() {} // read file and assign to text 
    }

    是的,这样看起来更象是QML API了。使用属性以允许应用环境来绑定这些属性,并对属性变化做出响应。
    为了在C++ 创建API,我们应该创建一个象这样的Qt C++ 接口:

    class FileIO : public QObject {
        ...
        Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
        Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
        ...
    public:
        Q_INVOKABLE void read();
        Q_INVOKABLE void write();
        ...
    }

    FileIO类型需要使用QML 引擎来注册。我们想要在“org.example.io”模块中使用,qmlRegisterType<FileIO>("org.example.io", 1, 0, "FileIO")

    import org.example.io 1.0
    
    FileIO {
    }

    一个插件可以以同样的模块名暴露多个类型。但不能从一个插件暴露多个模块。所以模块与插件之间有一对一的关系。这个关系通过模块标识符来表达。

    插件的创建

    Qt Creator包括一个向导来创建 QtQuick 2 QML Extension Plugin ,可以在新建工程向导的 Library 下找到。我们用它来创建一个名为 fileio 的插件,插件从org.example.io模块启动一个FileIO对象。

    注意
    向导生成一个基于QMake的项目。请从本章的例子开始,将其更改为基于CMake的工程。

    工程应该包括fileio.hfileio.cpp,它们声明和实现了FileIO类型,还有一个允许 QML 引擎发现扩展的实际插件类的fileio_plugin.cpp
    插件类是从QQmlEngineExtensionPlugin类继承的,并包含Q_OBJECTQ_PLUGIN_METADATA宏。整个文件如下:

    #include <QQmlEngineExtensionPlugin>
    
    class FileioPlugin : public QQmlEngineExtensionPlugin
    {
        Q_OBJECT
        Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid)
    };
    
    #include "fileio_plugin.moc"

    扩展会自动发现并注册所有以QML_ELEMENTQML_NAMED_ELEMENT标记的类型。我们将在FileIO的实现部分看到这是如何做到的。
    为了能让模块顺利导入,用户需要指定一个URI。比如 import org.example.io。有趣的是,我们在任何地方都看不到模块 URI。这是使用 qmldir 文件从外部设置的,或者在项目的 CMakeLists.txt 文件中设置。
    qmldir文件指定了QML插件内容,让插件在QML端更好地使用。为插件手写的qmldir文件看起来应该象这样:

    module org.example.io
    plugin fileio

    这就是用户要导入模块的URI,以及之后指定的要加载的URI中插件的名字。插件行必须与插件文件名相同(mac系统里,文件系统中的名字应该是libfileio_debug.dylib,而在qmldir中应该是fileio;对于Linux系统, 对应的文件系统中的文件名应该是libfileio.so)。这些文件是由Qt Creator根据给定的信息来创建的。
    创建正确的qmldir最简章的方式是在项目的CMakeLists.txt里,在qt_add_qml_module宏里。这里的URI参数用于指定插件的URI,如,org.example.io。这种方式下,qmldir文件就会在项目构建时生成。

    如何安装模块?

    当要导入名为‘org.example.io’的模块,QML引擎会查找某个导入路径,并根据qmldir尝试锁定 “org/example/io” 路径。qmldir接着告诉引擎加载哪个库作为模块URI指定的QML扩展插件。有相同URI名称的两个模块将会相互覆盖。

    FileIO的实现

    记得我们要创建的FileIO API应该象这样:

    class FileIO : public QObject {
        ...
        Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
        Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
        ...
    public:
        Q_INVOKABLE void read();
        Q_INVOKABLE void write();
        ...
    }

    我们先省略属性,因为它们是简单的 setter 和 getter。
    read方法以reading模式打开文件并使用文本流读取数据。

    void FileIO::read()
    {
        if(m_source.isEmpty()) {
            return;
        }
        QFile file(m_source.toLocalFile());
        if(!file.exists()) {
            qWarning() << "Does not exist: " << m_source.toLocalFile();
            return;
        }
        if(file.open(QIODevice::ReadOnly)) {
            QTextStream stream(&file);
            m_text = stream.readAll();
            emit textChanged(m_text);
        }
    }

    当文本变更了,要使用emit textChanged(m_text)来发出变化信息。否则,属性绑定将无效。
    write方法做了同样的事情,但以write模式打开文件,并使用流来将text属性的内容写入文件。

    void FileIO::write()
    {
        if(m_source.isEmpty()) {
            return;
        }
        QFile file(m_source.toLocalFile());
        if(!file.exists()) {
            qWarning() << "Does not exist: " << m_source.toLocalFile();
            return;
        }
        if(file.open(QIODevice::WriteOnly)) {
            QTextStream stream(&file);
            stream << m_text;
        }
    }

    为了能让类型对QML可见,我们在Q_PROPERTY那几行下面添加了QML_ELEMENT宏。这告诉Qt这个类对QML是可见的。如果你想提供与C++ 类不同的名字,可以使用QML_NAMED_ELEMENT宏。
    别忘了最后要调用make install。否则,插件文件不会被拷贝到qml文件夹,而qml引擎也无法锁定模块。

    注意
    因为读和写是阻塞型的函数调用,你应该在小型文本文件中使用FileIO,否则将可能阻塞Qt的UI线程。小心使用!

    使用FileIO

    现在就可以使用我们最近创建的文件来访问数据了。本例中,我们将会用到JSON格式的城市数据并将其显示在表格中。我们为些创建两个工程:一个是扩展插件(工程名为fileio),可以提供从文件中读取文本的方法;另一个是在表格中展示数据,(工程名:CityUI)。CityUI使用fileio扩展来读写文件。

    JSON数据其实是可以方便地转化为JS对象/数组的格式化的文本,它也能方便地转化为普通文本。我们使用FileIO来读取JSON格式数据并使用内置的Javascript函数JSON.parse()将其转化为JS对象。数据后续被用于表格视图的模型。这是在读文档和写文档函数中实现的,如下。

    FileIO {
        id: io
    }
    
    function readDocument() {
        io.source = openDialog.fileUrl
        io.read()
        view.model = JSON.parse(io.text)
    }
    
    function saveDocument() {
        var data = view.model
        io.text = JSON.stringify(data, null, 4)
        io.write()
    }

    本例中用到的JSON数据是cities.json文件。它包含了城市数据条目列表,每个条目包含了关于城市的相关数据,如下:

    [
        {
            "area": "1928",
            "city": "Shanghai",
            "country": "China",
            "flag": "22px-Flag_of_the_People's_Republic_of_China.svg.png",
            "population": "13831900"
        },
        ...
    ]

    应用程序窗体

    使用Qt Creator的QtQuick Application向导来创建基于Qt Quick Controls 2的应用 。虽然使用 ui.qml 文件的新的窗体方式比以前的版本更具可用性,但我们将不会使用新的QML窗体,因为这在本书中难于解释。所以现在你可以移除窗体文件。
    基本的窗体配置应该是一个ApplicationWindow,包含一个工具栏,菜单栏和一个状态栏。我们仅使用菜单栏来创建一些标准的菜单条目,如打开和保存文档。基本的配置将仅显示空窗体。

    import QtQuick 2.5
    import QtQuick.Controls 1.3
    import QtQuick.Window 2.2
    import QtQuick.Dialogs 1.2
    
    ApplicationWindow {
        id: root
        title: qsTr("City UI")
        width: 640
        height: 480
        visible: true
    }

    使用Actions

    为更好的使用/重用命令,这里使用QML Action类型。这将使我们在可以在后续使用可能的工具栏动作。打开、保存和退出动作是基本的。打开和保存动作目前还不包含任何逻辑,后面会有。菜单栏是使用一个文件菜单和三个动作条目来创建的。而且我们已经准备了一个文件对话框,用于后面打开城市文件。对话框在声明时还不可见,需要使用open()方法来显示。

    Action {
        id: save
        text: qsTr("&Save")
        shortcut: StandardKey.Save
        onTriggered: {
            saveDocument()
        }
    }
    
    Action {
        id: open
        text: qsTr("&Open")
        shortcut: StandardKey.Open
        onTriggered: openDialog.open()
    }
    
    Action {
        id: exit
        text: qsTr("E&xit")
        onTriggered: Qt.quit();
    }
    
    menuBar: MenuBar {
        Menu {
            title: qsTr("&File")
            MenuItem { action: open }
            MenuItem { action: save }
            MenuSeparator {}
            MenuItem { action: exit }
        }
    }
    
    FileDialog {
        id: openDialog
        onAccepted: {
            root.readDocument()
        }
    }

    格式化表格

    各城市的数据将会显示在表格中。为此我们使用了表格视图TableView控件,并声明4个列:城市,国家,区域,人口。每列是标准的TableViewColumn。后面我们将会添加国旗列以及删除操作列(需要自定义列委托)。

    TableView {
        id: view
        anchors.fill: parent
        TableViewColumn {
            role: 'city'
            title: "City"
            width: 120
        }
        TableViewColumn {
            role: 'country'
            title: "Country"
            width: 120
        }
        TableViewColumn {
            role: 'area'
            title: "Area"
            width: 80
        }
        TableViewColumn {
            role: 'population'
            title: "Population"
            width: 80
        }
    }

    现在应用会展示一个有文件菜单的菜单栏,以有一个有4个列的空表。下一步将会用FileIO扩展来为表填充有用的数据。

    cities.json文档是城市条目数组。以下是一个条目例子:

    [
        {
            "area": "1928",
            "city": "Shanghai",
            "country": "China",
            "flag": "22px-Flag_of_the_People's_Republic_of_China.svg.png",
            "population": "13831900"
        },
        ...
    ]

    我们要做的就是,让用户选择文件、读取、转化并将数据展示在表格视图。

    数据读取

    我们让打开动作来打开一个对话框。当用户选择了一个文件后,文件对话框的onAccepted函数会被调用。应该在那里调用readDocument()函数。readDocument()函数从文件对话框获取URL并将其赋予FileIO对象,然后调用read()方法。从FileIO加载的文本接着被使用JSON.parse()解析,解析结果对象真接被赋给表格视图的模型。真是太方便了。

    Action {
        id: open
        ...
        onTriggered: {
            openDialog.open()
        }
    }
    
    ...
    
    FileDialog {
        id: openDialog
        onAccepted: {
            root.readDocument()
        }
    }
    
    function readDocument() {
        io.source = openDialog.fileUrl
        io.read()
        view.model = JSON.parse(io.text)
    }
    
    
    FileIO {
        id: io
    }

    数据写入

    对于保存文档,我们将‘save’动作与saveDocument()函数绑定。保存文档函数从视图接收模型,该模型是JS对象,需要使用JSON.stringify()函数来将其转化为文本。结果文本被赋予FileIO对象的text属性,并调用write()将数据保存到硬盘。stringify函数的null4参数将对结果JSON数据进行4个空格缩进的格式化。这仅是为使保存的文档有更好地可读性。

    Action {
        id: save
        ...
        onTriggered: {
            saveDocument()
        }
    }
    
    function saveDocument() {
        var data = view.model
        io.text = JSON.stringify(data, null, 4)
        io.write()
    }
    
    FileIO {
        id: io
    }

    这就是有着读、写、展示JSON文档基本功能的应用了。想想编写 XML 读取器和写入器所花费的所有时间。使用 JSON,您只需要一种读取和写入文本文件或发送接收文本缓冲区的方法。

    画龙点睛

    程序目前还不完备。我们将为其添加国旗列,并允许用户从模型中移除城市条目以修改文档。
    本例中,旗图标文件存放在与main.qml文档同级的flags文件夹下。为了在列表中显示他们,需要定义一个委托来渲染国旗图片。

    TableViewColumn {
        delegate: Item {
            Image {
                anchors.centerIn: parent
                source: 'flags/' + styleData.value
            }
        }
        role: 'flag'
        title: "Flag"
        width: 40
    }

    这就是显示国旗所要做的所有工作。它将JS模型的flag属性以styleData.value暴露给委托。委托接着为图片地址加上 'flags/'前辍,并显示为Image元素。
    对于移除数据功能,我们用类似的技术来显示一个移除按钮。

    TableViewColumn {
        delegate: Button {
            iconSource: "remove.png"
            onClicked: {
                var data = view.model
                data.splice(styleData.row, 1)
                view.model = data
            }
        }
        width: 40
    }

    对于数据移除操作,先获得对视图模型的的引用 ,然后使用JS的splice方法来移除一个条目。可以用这个方法是因为模型是来源于 JS 数据。splice方法通过移除已存在元素或添加新元素的方式来改变数组内容。
    不幸的是,JS数组并不象类似QAbstractItemModel的Qt模型那样聪明,它(JS数组)的行或数据变化并不通知视图。目前视图将不会变化因为它未被通知有变化。仅当将数据设置回视图时,视图才会识别有新数据并刷新视图内容。使用view.model = data的方式重新为模型赋值,是使得视图知道有数据变化的一种方式。

    总结

    本章创建的插件是非常简单的插件。但它是可以在其它不同类型的应用中被重用和扩展的。使用插件可以创建非常灵活的方案。比如,你可以只使用qml来启动UI。打开 CityUI 项目所在的文件夹,使用 qml main.qml 启动 UI。QML 引擎可以从任何项目中轻松使用该扩展,并且可以在任何地方导入该扩展。
    我们鼓励您以某种使用qml的方式编写应用程序。这大大节省了开发人员的时间,而且在应用中保持逻辑和界面呈现的清晰分离也是一种好习惯。
    使用插件的唯一坏处是它让部署变得更复杂,越简单的程序越明显(因为创建和部署插件的工作量不变)。现在你需要部署你的应用和插件。如果这对你来说比较困难,你仍然可以使用FileIO类并在main.cpp文件里qmlRegisterType来直接注册。QML代码不变。
    在大型项目里, 你不会这样来使用应用。您有一个简单的qml运行时,类似于Qt提供的qml命令,并且需要所有本机功能作为插件提供。您的项目是使用这些 qml 扩展插件的简单纯 qml 项目。这提供了更大的灵活性并省去了 UI 更改后的编译步骤。在编辑UI文件后需要运行UI。这使得用户界面编写人员能够保持灵活性和敏捷性,以便进行所有这些像素级的小修改。
    插件在 C++ 后端开发和 QML 前端开发之间提供了良好而清晰的分离。在开发 QML 插件时,始终牢记 QML 方面,并毫不犹豫地首先使用仅 QML 的模型来验证您的 API,然后再用 C++ 实现它。如果 API 是用 C++ 编写的,人们通常会犹豫是否要更改它或害怕重写。在 QML 中模拟 API 提供了更大的灵活性和更少的初始工作量。使用插件时,模拟 API 和真实 API 之间的切换只是更改 qml 运行时的导入路径。

  • 相关阅读:
    获取成本
    销售订单跟踪成本
    装机
    这就是用战术上的勤奋掩盖战略上的懒惰
    CPA-计划(参考)
    大帝名言
    BZOJ 2100: [Usaco2010 Dec]Apple Delivery spfa
    BZOJ 2834: 回家的路 Dijkstra
    BZOJ 4070: [Apio2015]雅加达的摩天楼 根号分治+spfa
    BZOJ 4152: [AMPPZ2014]The Captain Dijkstra+贪心
  • 原文地址:https://www.cnblogs.com/sammy621/p/16148757.html
Copyright © 2020-2023  润新知