Qt 与 Objective-C 的混合编程

redmeercat 发布于2年前
0 条问题

最近有好几个使用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,这就是全部了。

需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。