基於 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](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 框架有豐富的實戰經驗。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM