基於 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 機制創建一個完整的應用,可以採取類似下圖這種方式:

Qt Quick Plugin 機制創建完整應用

以音視頻場景舉例,無論上層應用最終最終以什麼形態呈現,底層都是一些固定的數據,比如成員和成員的狀態管理、設備列表和設備的檢測選擇,用戶視覺上看到的無非是視頻畫面。通過封裝,我們看到的是這樣一種形式:

封裝之後

類似 MemberList 的設計,不要給其設置固定的視覺樣式,通過全局預定義樣式表來控制可以讓其 UI 跟隨使用者的風格變化。在會議場景它可能叫做“與會成員”,在在線教育場景它可能叫做“學生列表”。這樣我們可以隨意搭配組成各式類型的業務場景:

各業務場景

構建一個 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 感知編碼技術】立即報名

更多技術乾貨,歡迎關注【網易智企技術+】微信公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章