基于 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” 做为标题也突显了其在跨平台的方面所作的工做。app

跨平台特性

易分发组件

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

UI 组件高度复用

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

前端 QML 学习门槛低

Qt Quick 用来描述前端的 QML 语言语法简练,很是容易理解,能够与 JavaScript 混编,实现几乎全部咱们能想到的能力。而且新版本 Qt Quick 对 C++ 和 QML 交互作了进一步加强,使用简单的脚本便可实现丰富的能力。性能

适合封装业务模块

得力于 Qt Quick 的 Model-View-Delegate 设计思想,咱们能够对业务数据和 UI 基础展现能力的封装彻底分离,经过 Model 提供完整的数据链条,经过 View 和 Delegate 来对不一样场景作数据展现。学习

经过 Qt Quick Plugin 机制建立一个完整的应用,能够采起相似下图这种方式:ui

Qt Quick Plugin 机制建立完整应用

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

封装以后

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

各业务场景

构建一个 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 推荐模式<br />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 框架有丰富的实战经验。

5月20日线上技术直播预告

直播预告

明晚19点,相约聊聊【直播点播窄带高清之 JND 感知编码技术】当即报名

更多技术干货,欢迎关注【网易智企技术+】微信公众号

相关文章
相关标签/搜索