Qt 與 Objective-C 的混合程式設計

foruok的部落格發表於2015-02-02

最近有好幾個使用Qt的朋友問起 Qt for iOS 的事情,因為我在這方面的經驗特別少,寫不出系統的文章來,非常抱歉,不能給出令人滿意的答覆,推薦大家去看 Jason’s Home ,在我部落格左側邊欄的友情連結裡也有,他提供了 Qt for iOS 的一些非常有意義的文章,而且是基於實踐的,他的 App 已經在 App Store 中上線。

至於我呢,在這篇文章裡,簡單介紹一些如何混合 Qt 與 OC 程式設計。

我要說的內容呢,大部分在 Qt 幫助裡都有,請大家到索引模式下,鍵入”Qt for iOS”,找到 Qt for iOS 這篇文章來看。它介紹了搭建開發環境編譯應用混合OC程式設計這三個方面,已經非常詳細了。

如果你不想啃英文,那可以接著我的文章往下看。

專案設定

既然要聊 Qt 混合 OC 程式設計,首先要簡單介紹一下 Objective-C 。我只有一句話:Go,問搜尋引擎去。因為我所知實在有限,怕誤導了您。當然如果您不怕,往下看吧。

Objective-C原始檔介紹

首先我要說一下 Objective-C 的原始檔,字尾是.m 或 .mm ,在 .mm 檔案裡,可以直接使用 C++ 程式碼。所以,我們要混合 Qt 程式碼與 Objective-C 程式碼,就需要在 Qt 專案里加入 mm 檔案。

pro 檔案配置

Qt SDK for Mac ,安裝之後, Qt Creator 會使用 XCode 提供的編譯工具鏈來編譯程式碼,能夠正確編譯 mm 檔案,也可以連結 iOS 的庫檔案。

而要混合 Objective-C 程式碼,需要更改一下 pro 檔案。一個是新增 mm 檔案,一個是連線針對 iOS 的庫檔案。

新增原始檔,使用 OBJECTIVE_SOURCES 這個變數,比如這樣子:

OBJECTIVE_SOURCES += ocview.mm

連結庫 XCode 提供的庫,則需要使用 QMAKE_LFLAGS ,類似這樣子:

ios {
    QMAKE_LFLAGS    += -framework OpenGLES
    QMAKE_LFLAGS    += -framework GLKit
    QMAKE_LFLAGS    += -framework QuartzCore
    QMAKE_LFLAGS    += -framework CoreVideo
    QMAKE_LFLAGS    += -framework CoreAudio
    QMAKE_LFLAGS    += -framework CoreImage
    QMAKE_LFLAGS    += -framework CoreMedia
    QMAKE_LFLAGS    += -framework AVFoundation
    QMAKE_LFLAGS    += -framework AudioToolbox
    QMAKE_LFLAGS    += -framework CoreGraphics
    QMAKE_LFLAGS    += -framework UIKit
}

上面是我使用 Qt 針對 iOS 程式設計的配置。我使用了很多針對 iOS 的庫,所以新增了很多 framework 。

“ -framework UIKit ”這種引數,是經由 Makefile 傳遞給 Clang 的引數,-framework 是用來指示要連結某個框架(或者說庫)的關鍵字,它後面跟的是框架(庫)名。

需要注意的是,我們使用針對 iOS 的庫,不是通過“ LIBS += ”這種方式來引入哦。當然,你自己通過 Qt 實現的 .a 庫,依然需要使用“ LIBS += ”這種方式。

指定plist檔案

有時你需要為你的專案指定 plist 檔案, plist 檔案全名是 Property List ,字尾是 .plist 。它用來定義 iOS 應用的屬性,比如 Bundle(iOS上的一個應用被稱為一個 Bundle ) 的顯示名字、可執行檔名字、簽名、證書等等,當然也可以儲存一些配置資料。具體的介紹參考 iOS 開發的文件吧。

要在 pro 檔案裡新增 plist ,要使用 QMAKE_INFO_PLIST 關鍵字。如下面這樣子:

QMAKE_INFO_PLIST += MultiWindow.plist

好啦,關於 pro 檔案中與混合使用Objective-C 相關的配置項,大體就這些了。接下來我們看如何寫 Objective C 程式碼啦。

混合使用Objective C 程式碼

乖乖,很惶恐啊,這是我的弱項,沒寫過多少 Objective-C 程式碼。所以,請不要問我 OC 有關的問題,我真不知道……

背景

我的示例,是在 QML 的介面上疊加iOS原生的介面,即 UIView、UIWindow之類的。因為 OC 是 C 的近親,和 C++ 有著天然的血緣,混合起來特別方便哈,比 Android 上使用 JNI 程式設計好用多了。

不過有一點, OC 都適用 [] 這種語法來呼叫函式,使用 XCode 的話,語法提示和自動完成功能非常強大,基本不用思考的就能找到你要用的函式。而 Qt Creator 麼,嘿嘿,就沒這麼好相與了,純粹要手寫哦。我當時都是開著 XCode 看 API 文件找的,比較痛苦。

QQuickView 是什麼

我測試時的示例,用的是 Qt Quick App 專案模板,使用 QQuickView 來載入 QML 文件。這裡也以此為例來說明。

首先要說 QQuickView 到底是什麼。

QQuickView 呢,其實是一個 UIView 。UIView 則是 iOS 開發框架裡很多介面元素的根兒,比如 UIWindow 就是 UIView 的子類。

Qt 的 QQuickView 是一個 UIView ,建立了 QQuickView 例項後,就有了一個 UIView ,然後 Qt 玩了一些魔法,拿到了 UIView 的 OpenGL Context ,跑起了 Qt 的事件迴圈,在這個 OpenGL Context 上從零開始繪製了自己的場景和 UI 系統。

就這麼簡單,你可以查閱 Qt 原始碼來進一步瞭解。

需要注意的是, QML 介面元素的渲染,與 UIView 這種原生介面的渲染,不在一個執行緒中。而且 iOS 對 OpenGL ES 的支援很好,你可以同時使用多個 OpenGL Context 。更好的是,你可以視窗模式來建立一個 OpenGL Context 。不像 Android 版本哦, Qt 使用 OpenGL 的時候都是全屏模式,區域性更新不支援,所以我們在 Android 上使用 QML 裡的 Camera 和 VideoOutput 來開發拍照應用時, VideoOutput 必須是全屏模式(必須fill_parent)。而在 iOS 上,則沒有這個限制了。看來 iOS 還是很美好的啦。

我靠,扯得有點兒遠,最近寫技術文章少了,越來越羅嗦了。言歸正傳吧。

因為 QQuickView 實際上就是一個 UIView ,所以可以強制轉換為 UIView ,然後使用 OC 的方法來建立新的 UIView 或者 UIWindow ,這樣就有了原生的 UI 元件了,你可以在這個原生的 UI 元件上使用 OpenGL 繪製自己的東西或者新增其它原生的控制元件,非常美好。

不過要說明的是,通過這種方法建立出來的 iOS 原生介面元素,會始終在 QML 介面之上,把 QML 場景裡的介面元素給蓋住。

混合程式碼

要使用 OC 的類庫,需要在 mm 檔案內包含相關的標頭檔案,又有幾部分工作要做。一個是在 pro 檔案里加入 SDK 路徑,使用 INCLUDEPATH 變數即可,不多說了。另外一點是在 mm 檔案內包含 Objective-C 的標頭檔案,與 C++ 標頭檔案一個理兒,不過要使用 #import 哦。類似醬紫:

#import <UIKit/UIKit.h>
#import <GLKit/GLKit.h>

包含了標頭檔案,就可以使用 Objective-C 類庫了。比如我要在 QQuickView 上面建立一個新的 iOS 原生的 UIView ,.mm 檔案裡可以這樣:

void addOCView(QQuickWindow *w)
{
    UIView *view = reinterpret_cast<UIView *>(w->winId());

    CGRect  viewRect = CGRectMake(10, 10, 100, 100);
    UIView* myView = [[UIView alloc] initWithFrame:viewRect];
    [myView setBackgroundColor:[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0]];
    [view addSubview: myView];
}

如你所見,我寫了一個 addOCView 方法,它的引數是 QQuickView 。在 addOCView 方法裡,我把 QQuickView 強制轉換為 UIView 來使用。

我建立了一個新的 UIView ,設定了它的背景顏色,然後把它新增為 QQuickView 的子視窗。諾,就這麼簡單了。

說下 main.cpp ,看它如何使用 addOCView() 方法。程式碼如下:

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView viewer;
    viewer.setResizeMode(QQuickView::SizeRootObjectToView);
    viewer.setSource(QUrl("qrc:/main.qml"));
    viewer.show();

    addOCView(&viewer);

    return app.exec();
}

一點兒都不驚喜是吧,就是直接呼叫了 addOCView 哦。哈哈,確實如此了。

iOS 原生介面與 QML 元素的位置對映

混合使用 iOS 原生介面時,也可以達到原生介面與 QML 介面的無縫整合。關鍵就在於計算 QML 介面元素的位置,然後根據 QML 介面元素的位置來設定原生介面的位置。

QML元素位置換算

QML 提供了換算元素位置的方法,Item 有個方法,叫作 mapToItem() ,它可以把一個相對於 Qt Quick Item 的區域對映到另一個 Item 上,得到座標了。比如你的 qml 檔案是下面的樣子:

Rectangle {

    ...

    Rectangle {
        id: videoLayer;
        anchors.margins: 8;
        anchors.left: parent.left;
        anchors.right: parent.right;
        anchors.top: parent.top;
        anchors.bottom: actionBar.top;
        color: "green";
    }

    ...

}

你想把原生的 UIView 定位到 id 為 videoLayer 的元素內部,可以類似下面換算一個座標並使用它:

var coordinate = videoLayer.mapToItem(null, 8, 8, videoLayer.width - 16, videoLayer.height - 16);
winUtil.addUIView(coordinate.x, coordinate.y, coordinate.width, coordinate.height);

換算出的coordinate有 x 、 y 、 width 、 height 屬性。當 mapToItem 的第一個引數為 null 時,換算的結果就是相對於 QQuickView 的。那我們在新增 UIView 時,就可以用這個換算結果來構造一個 CGRect 物件,用這個 CGRect 來初始化 UIView 的位置。

設定 UIView 的位置

前面示例程式碼中的 winUtil 是我在 C++ 內實現的一個輔助類,匯出到了 QML 環境中。它的 addUIView 方法根據傳入的座標來設定原生 UIView 的位置。參考程式碼如下:

UIView *v = reinterpret_cast<UIView*>(view->winId());
    uiw = [[UIWindow alloc] initWithFrame:CGRectMake(x, y, width, height)];
    [v addSubview: uiw];

上面程式碼中,view 是 QQuickView 。這次我們建立 UIView 時使用了傳入的 x 、 y 、 width 、 height,這樣新建的 UIView 就和 QML 元素整合在一起了,看起來好像是一體的。

OK,這就是全部了。

相關文章