基于 Qt Quick Plugin 快速构建桌面端跨平台组件

桌面端的 UI 开发框架对比移动端、Web 端的成熟方案,一直处于不温不火的状态。随着疫情掀起的风波,桌面端在线教育、视频会议等需求不断涌现。传统平台下的开发框架难以满足需求,而类 DirectUI 的框架因跨平台、可拓展性差、门槛高等问题并不能得到一些企业的认可。桌面端 Electron、Flutter 类框架出于性能、原生平台支持等个性化需求考虑,往往得不到最好的解决方案。

Qt Quick 可以较好得解决上述提到的问题。本文将从两个方面介绍通过 Qt Quick 是如何快速实现桌面端跨平台业务组件构建的,首先我们聊一下 Qt Quick 在桌面端开发的优势,再详细如何创建一个 C++ 拓展插件给 Qt Quick 应用来使用。

Qt Quick 优势

跨平台特性

Qt Quick Plugin 机制可以满足上面提到的诸多需求。首先 Qt 对跨平台支持非常友好,仅需要对特殊平台做一些简单适配就可以使用一套代码可跑在不同终端。官方以“One framework. One codebase. Any platform” 作为标题也突显了其在跨平台的方面所做的工作。

基于 Qt Quick Plugin 快速构建桌面端跨平台组件

易分发组件

使用 Qt 编写的 Qt Quick 组件容易分发,它最终导出可以是源码形式也可以是发布的二进制文件夹,内部包含了对数据模型和 UI 基础组件的包装。

UI 组件高度复用

使用 Qt Quick 可以很容易的创建一个可复用组件,官方也提供了一些基础组件如 Google Material 风格的控件等。基于这些基础组件,我们就可以拓展出不同形式的 UI 组件,在不破坏内部结构的情况下提供外部使用。

前端 QML 学习门槛低

Qt Quick 用来描述前端的 QML 语言语法简练,非常容易理解,可以与 JavaScript 混编,实现几乎所有我们能想到的能力。并且新版本 Qt Quick 对 C++ 和 QML 交互做了进一步增强,使用简单的脚本即可实现丰富的能力。

适合封装业务模块

得力于 Qt Quick 的 Model-View-Delegate 设计思想,我们可以对业务数据和 UI 基础展示能力的封装完全分离,通过 Model 提供完整的数据链条,通过 View 和 Delegate 来对不同场景做数据展示。

通过 Qt Quick Plugin 机制创建一个完整的应用,可以采取类似下图这种方式:

基于 Qt Quick Plugin 快速构建桌面端跨平台组件

以音视频场景举例,无论上层应用最终最终以什么形态呈现,底层都是一些固定的数据,比如成员和成员的状态管理、设备列表和设备的检测选择,用户视觉上看到的无非是视频画面。通过封装,我们看到的是这样一种形式:

基于 Qt Quick Plugin 快速构建桌面端跨平台组件

类似 MemberList 的设计,不要给其设置固定的视觉样式,通过全局预定义样式表来控制可以让其 UI 跟随使用者的风格变化。在会议场景它可能叫做“与会成员”,在在线教育场景它可能叫做“学生列表”。这样我们可以随意搭配组成各式类型的业务场景:

基于 Qt Quick Plugin 快速构建桌面端跨平台组件

构建一个 Qt Quick C++ Plugin

一个原生的 Qt Quick 应用允许我们直接基于其能力实现业务功能,像上面提到的场景,当不同产品线需要使用同样的功能组件或需要拓展 Qt Quick 能力时,我们就可以借助 [Qt Quick 2 Extension Plugin](http://Creating C++ Plugins for QML) 来对这些组件进行封装了。通过简单的几个步骤,我们就可以创建一个属于自己的 Qt Quick 插件。

创建插件

首先通过 Qt Creator 创建一个 Qt Quick 2 Extension Plugin 工程。创建好的基础插件工程中,会默认创建一个派生于 QQmlExtensionPlugin 的子类,用来让我们注册自己的自定义模块提供外部使用:

#include <QQmlExtensionPlugin>

class NEMeetingPlugin : public QQmlExtensionPlugin { 
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)

public:
    void registerTypes(const char* uri) override;
};

通过该接口注册我们的自定义类型提供引入插件的 QML 前端使用:

void NEMeetingPlugin::registerTypes(const char* uri) {
    // @uri NEMeeting
    qmlRegisterType<NEMEngine>(uri, 1, 0, "NEMEngine");
    qmlRegisterType<NEMAuthenticate>(uri, 1, 0, "NEMAuthenticate");
    qmlRegisterType<NEMAccount>(uri, 1, 0, "NEMAccount");
    //......
    // Devices
    qmlRegisterType<NEMDevices>(uri, 1, 0, "NEMDevices");
    qmlRegisterType<NEMDevicesModel>(uri, 1, 0, "NEMDeviceModel");
    //......
    // Schedules
    qmlRegisterType<NEMSchedule>(uri, 1, 0, "NEMSchedule");
    qmlRegisterType<NEMScheduleModel>(uri, 1, 0, "NEMScheduleModel");
    //......
    // Meeting
    qmlRegisterType<NEMSession>(uri, 1, 0, "NEMSession");
    qmlRegisterType<NEMMine>(uri, 1, 0, "NEMMine");
    qmlRegisterType<NEMAudioController>(uri, 1, 0, "NEMAudioController");
    //......
    // Providers
    qmlRegisterType<NEMFrameProvider>(uri, 1, 0, "NEMFrameProvider");
    //......
}

这些组件有些是前端不可见组件,他们将作为一个前端可实例化的对象来创建具体的实例,例如 NEMEngine是整个组件的唯一引擎,这些对象要继承自 QObject。

class NEMEngine : public QObject {}

而数据相关的封装则不同,他们需要继承自 QAbstract*Model,以设备相关的数据模型举例,以下为示例代码:

class NEMDevicesModel : public QAbstractListModel {
    Q_OBJECT

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

    enum { DeviceName, DevicePath, DeviceProperty };

    Q_PROPERTY(NEMDevices* deviceController READ deviceController WRITE setDeviceController NOTIFY deviceControllerChanged)
    Q_PROPERTY(NEMDevices::DeviceType deviceType READ deviceType WRITE setDeviceType NOTIFY deviceTypeChanged)

    int rowCount(const QModelIndex& parent = QModelIndex()) const override;
    QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
    QHash<int, QByteArray> roleNames() const override;

    NEMDevices* deviceController() const;
    void setDeviceController(NEMDevices* deviceController);

    NEMDevices::DeviceType deviceType() const;
    void setDeviceType(const NEMDevices::DeviceType& deviceType);

Q_SIGNALS:
    void deviceControllerChanged();
    void deviceTypeChanged();

private:
    NEMDevices* m_deviceController = nullptr;
    NEMDevices::DeviceType m_deviceType = NEMDevices::DEVICE_TYPE_UNKNOWN;
};

对数据模型的封装秉持完整、可定制、参数化的原则,尽量不要在组件的封装过程中掺杂细节的业务需求,以 NeRTC 2.0 SDK 设备枚举顺序举例,SDK 提供了两种枚举设备的方式。

  • 一种是 SDK 推荐设备,当你有内置设备、外接、蓝牙等不同设备时,SDK 会选择一个最适合的作为第一个设备使用。
  • 另外一种是系统默认设备,跟随系统变更来选择设备使用。

两种方案从某些业务场景角度考虑只需要一种,但作为一个可以二次开发的组件来说,应该都可以提供上层配置,所以在设备相关的管理器中,提供了 AutoSelectMode 参数提供外部引入插件的开发者来控制使用哪种模式。

除了对数据模型、自定义类型等进行封装外,还可以提供一些前端组件让使用插件的开发者更快捷的创建应用。以视频渲染的容器举例,以下是借助 C++ 注册到前端的 NEMFrameProvider 来实现一个简单的视频渲染的 Delegate。

import QtQuick 2.0
import QtMultimedia 5.14
import NEMeeting 1.0

Rectangle {
    id: root

    property bool mirrored: false
    property alias frameProvider: frameProvider

    color: '#000000'

    VideoOutput {
        anchors.fill: parent
        source: frameProvider
        transform: Rotation {
            origin.x: root.width / 2
            origin.y: root.height / 2
            axis { x: 0; y: 1; z: 0 }
            angle: mirrored ? 180 : 0
        }
    }

    NEMFrameProvider {
        id: frameProvider
    }
}

通过工程配置,我们让其导出插件时同时将这些 .qml UI 文件也同时导出:

pluginfiles.files += \
    imports/$$QML_IMPORT_NAME/qmldir \
    imports/$$QML_IMPORT_NAME/components/NEMVideoOutput.qml
    .......

引入插件

使用一个创建好的插件更为方便,一般插件编译完成后最终是一个文件夹的形式分发,我们只需要在引入的功能中配置我们要引入的插件及路径即可:

# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH = $$PWD/../bin

在 QML 中使用时,我们首先需要 import 相应的插件:

import NEMeeting 1.0

这样你就可以使用插件中注册进来的类型了:

// 创建引擎实例
NEMEngine { 
    id: nemEngine
    appKey: "092dcd94d2c2566d1ed66061891*****"
}

对设备列表做展示仅需要创建一个列表,并指定插件注册进来的设备数据模型即可。

ComboBox {
    Layout.fillWidth: true
    textRole: "deviceName"
    valueRole: "deviceId"
    currentIndex: {
        return nemDevices.currentPlayoutIndex
    }
    // 使用 C++ 注册进来的数据模型
    model: NEMDeviceModel {
        id: listModel
        deviceController: nemDevices
        deviceType: NEMDevices.DEVICE_TYPE_PLAYOUT
    }
    onActivated: {
        nemDevices.selectDevice(NEMDevices.DEVICE_TYPE_PLAYOUT, currentValue)
    }
}

设备对象类型创建时我们可以通过预设的参数来指定设备的选择方式为 SDK 推荐模式
NEMDevices.RECOMMENDED_MODE :

NEMDevices {
    id: nemDevices
    engine: nemEngine
    autoSelectMode: NEMDevices.RECOMMENDED_MODE
}

程序在发布时,你只需要将插件目录与程序同时分发即可,无需多余的配置即可完成应用的打包发布流程。

总结

对于 Qt Quick 2 Extension Plugin 的开发和使用,官方提供了非常详细的文档。通过这种机制,我们不仅可以创建一个封装了某底层能力 SDK 完整功能的开发组件,还可以让使用者高度自定义交互行为。这是以往桌面端 UI 开发框架很难甚至无法做到的事情。

QML 语言的低门槛也可以让从事过前端、C++ 或一些脚本类语言的开发者迅速切换到 Qt Quick 开发环境。他们不需要关注某个插件的具体实现细节,仅需要将这些组件做一些简单拼装就可以组成一个完整的应用。同时这也是网易云信团队一直以来努力的方向,我们通过解决方案及易用体系等方式,让音视频以及即时通信等技术能够快速、高效接入相应的服务中。

以上就是本文的全部分享,关于 Qt Quick 更多技术干货,也欢迎持续锁定我们。

作者介绍

邓佳佳,网易智企云信高级开发工程师,负责维护网易云信跨平台 NIM SDK 和上层解决方案预研开发,包括基于 NIM SDK 和 NERTC SDK 构建的在线教育、互动直播、IM 即时通讯、网易会议解决方案的维护,对 Duilib、Qt Quick、CEF 框架有丰富的实战经验。

上一篇:上云数据分析首选产品Quick BI的可视化之路


下一篇:基础算法之快速排序(quick sorting)