头图

基于 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 编写的 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 机制创建一个完整的应用,可以采取类似下图这种方式:

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

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

构建一个 Qt Quick C++ Plugin

一个原生的 Qt Quick 应用允许我们直接基于其能力实现业务功能,像上面提到的场景,当不同产品线需要使用同样的功能组件或需要拓展 Qt Quick 能力时,我们就可以借助 Qt Quick 2 Extension Plugin 来对这些组件进行封装了。通过简单的几个步骤,我们就可以创建一个属于自己的 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 框架有丰富的实战经验。

5月20日线上直播预告

直播点播已经与日常生活息息相关,这个过程中大家最关注的是什么,更低的播放成本?更高的画质?这就涉及到了窄带高清技术,低带宽高清画质离不开视频编码。

业界已有比较多窄带高清技术方案,如淘宝直播、阿里窄带高清、腾讯极速高清等。NE264 是网易自研 H264 编码器,已成功应用于视频通话、直播点播中,带来更低时延、更高画质的优质体验。

本次分享将为您揭开窄带高清技术以及 JND 感知编码技术的神秘面纱。

本期主题:MCtalk Live#3 直播点播窄带高清之 JND 感知编码技术

这里,报名

更多技术干货,欢迎关注【网易智企技术+】

欢迎关注网易云信 GitHub:

600 声望
133 粉丝
0 条评论
推荐阅读
【网易易盾】网易易盾性能测试自动化建设
考虑到有些同学没有接触过性能测试,这里还是简单介绍下。性能测试是通过特定的方式对被测系统按照一定策略施加压力,获取该系统的响应时间、吞吐量等性能指标,来检测系统上线后能否满足用户需求的过程。通过性...

网易智企阅读 273

封面图
手把手教你写一份优质的前端技术简历
不知不觉一年一度的秋招又来了,你收获了哪些大厂的面试邀约,又拿了多少offer呢?你身边是不是有挺多人技术比你差,但是却拿到了很多大厂的offer呢?其实,要想面试拿offer,首先要过得了简历那一关。如果一份简...

tonychen152阅读 17.8k评论 5

封面图
正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 8.5k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy48阅读 7.1k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木75阅读 7.1k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 6.8k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木45阅读 8.6k评论 6

欢迎关注网易云信 GitHub:

600 声望
133 粉丝
宣传栏