Pull to refresh

Смешать и слегка взболтать: Qt, Carbon и Cocoa

Reading time 6 min
Views 2.4K
Qt можно считать наиболее распространенной кроссплатформенной библиотекой для разработки пользовательских интерфейсов. Carbon (С++) и Cocoa (Objective-C), в свою очередь, являются основными фреймворками, используемыми Apple, что означает гармоничный внешний вид и наилучшую интеграцию с системой при их использовании. Несмотря на попытки создания свободных клонов, в полном объеме они реализованы только в MacOS.

Уверен, что вопросом связи Qt и Cocoa задавались многие, но в условиях отсутствия внятной информации в интернете вряд-ли достигли значимых успехов.


Вступление

К версии Qt 4.5, Trolltech начала использовать Cocoa API, что позволяет собирать приложения для 64-битных платформ. Стоит заметить, что Apple постепенно переводит свои приложения с 32 в 64-битные, так что мера весьма своевременна.
В большинстве случаев, возможностей, предоставляемых этой версией, более чем достаточно.
Несмотря на то, что прогресс идет вперед, все еще остаются те, кто не хотят (или не могут) пользоваться новыми версиями библиотеки. Да и после 4.5 остаются те, у кого установлена сборка без поддержки Cocoa. В таком случае единственным решением является использование Carbon в качестве связующего звена.

Замечу, что несмотря на кажущуюся легкость, сборка такого моста является делом нетривиальным.
В документации QT практически нет упоминаний про использование в связке с Objective-C, но я то знаю, что эти файлы имеют расширения .m и .mm и подключаются директивой OBJECTIVE_SOURCES, но об этом далее.

В качестве моста у нас будет выступать файл, написанный на Objective-C++, который позволяет одновременно использовать C, C++ и Objective-C. Неприятной особенностью является то, что класс, в зависимости от реализации, может принимать либо сообщения от Objective-C, либо предоставлять слоты для QT.

Схематично это выглядит так:



Исходный код

Copy Source | Copy HTML
  1. class menuWrapperPrivate; // Forward declaration
  2.  
  3. @interface menuWrapperProxy : NSObject {
  4. @private
  5.     menuWrapperPrivate * wrap;
  6. }
  7. - (void) emitSayHelloRequested; //используется, как прокси обратного вызова для Obj-C события
  8. - (menuWrapperPrivate *) getQtProxy; //предоставляет QObject для связывания. Вызывается из Qt класса.
  9. @end


Так как наш класс — оболочка, практически никаких функций он не предоставлет. Далее идет описание menuWrapperPrivate:

Copy Source | Copy HTML
  1. class menuWrapperPrivate : public QObject
  2. {
  3.     Q_OBJECT
  4.     public:
  5.         TrayMenu * menu;
  6.     signals:
  7.         void sayHello();
  8.     public slots:
  9.         void privateWrapperSlot() { emit sayHello();};
  10. };


Тоже ничего особенного, как видите. Единственная задача класса — передать вызов из Objective-C++ в Qt (C++).

Copy Source | Copy HTML
  1. @implementation menuWrapperProxy
  2. - (id)init
  3. {
  4.     if ((self = [super init])) {
  5.         wrap = new menuWrapperPrivate(); //С++ инициализация
  6.         wrap->menu = [[TrayMenu alloc] init]; //Objective-C инициализация
  7.         [NSApp setDelegate: wrap->menu];
  8.         [wrap->menu setParent: self];
  9.     }
  10.     return self;
  11. }
  12.  
  13. //передатчик сообщения
  14. - (void) emitSayHelloRequested
  15. {
  16.     wrap->privateWrapperSlot(); //С++ вызов
  17. }
  18.  
  19. - (menuWrapperPrivate *) getQtProxy
  20. {
  21.     return wrap;
  22. }
  23. @end


Главной строкой этой части является инициализация меню. TrayMenu класс полностью написан на Objective-C (этим я хотел показать, что можно подключить многие исходники практически без модификации). Расширение нужно дать .mm, чтоб компилятор правильно определил тип файла (в принципе, и так определит, но идеологически правильнее)

Copy Source | Copy HTML
  1. @interface TrayMenu : NSObject {
  2. @private
  3.         NSStatusItem *_statusItem;
  4.         menuWrapperProxy *_parent;
  5. }
  6. - (void) setParent: (menuWrapperProxy *)parent;
  7. @end


Намеренно опускаю лишние детали скрипта, для экономии места.

Copy Source | Copy HTML
  1. @implementation TrayMenu
  2. //Сам источник обратного вызова.
  3. - (void) onMyRequest:(id)sender {
  4.         [_parent emitSayHelloRequested];
  5. }
  6.  
  7. - (void) actionQuit:(id)sender {
  8.     [NSApp terminate:sender];
  9. }
  10.  
  11. - (NSMenu *) createMenu {
  12.     NSZone *menuZone = [NSMenu menuZone];
  13.     NSMenu *menu = [[NSMenu allocWithZone:menuZone] init];
  14.     NSMenuItem *menuItem;
  15.  
  16.     menuItem = [menu addItemWithTitle:@"Say hello"
  17.                                action:@selector(onMyRequest:)
  18.                         keyEquivalent:@""];
  19.     [menuItem setTarget:self];
  20.  
  21.     [menu addItem:[NSMenuItem separatorItem]];
  22.  
  23.     menuItem = [menu addItemWithTitle:@"Quit"
  24.                                action:@selector(actionQuit:)
  25.                         keyEquivalent:@""];
  26.     [menuItem setToolTip:@"Click to Quit this App"];
  27.     [menuItem setTarget:self];
  28.  
  29.     return menu;
  30. }
  31.  
  32. //Overloaded функция, вызывается после окончания загрузки ресурсов
  33. - (void) applicationDidFinishLaunching:(NSNotification *)notification {
  34.     NSMenu *menu = [self createMenu];
  35.  
  36.     _statusItem = [[[NSStatusBar systemStatusBar] //Доберемся до статусбара
  37.                     statusItemWithLength:NSVariableStatusItemLength] retain];
  38.     [_statusItem setMenu:menu];
  39.             [_statusItem setTitle: @"Menu"];
  40.     [_statusItem setHighlightMode:YES];
  41.     [_statusItem setToolTip:@"Test Tray"];
  42.  
  43.     [menu release];
  44. }
  45. - (void) setParent: (menuWrapperProxy *)parent
  46. {
  47.     _parent = parent;
  48. }
  49. @end
  50.  

Кнопка меню по нажатию генерирует событие, которое через оболочку и прокси класс попадает в Qt приложение. Схема вряд ли будет отличаться в более сложных случаях. Хорошим тоном будет дать файлу расширение .m

Copy Source | Copy HTML
  1. menuWrapperProxy * mwp = [[menuWrapperProxy alloc] init];
  2. menuWrapperPrivate * signalWrapper = [mwp getQtProxy];
  3. QMessageBox * box = new QMessageBox(0);
  4. connect(signalWrapper, SIGNAL(sayHello()), box, SLOT(exec()));


Просто, правда? Немного Obj-C++ и классы на разных языках понимают друг друга.



Теперь нужно заставить компилятор собирать весь этот ужас.
В документации сказано, что в .pro файл нужно добавлять указание (а лучше прочитать изложенное ниже):

OBJECTIVE_SOURCES += file1.m file2.mm

Проблема состоит в том, что изначально qmake не умеет обращаться с Obective-C файлами, а только с Objective-C++.
Длительное курение умных бумажек, пляски с бубном и метод тыка помогли мне прийти к решению — добавить указания:

QMAKE_CXXFLAGS = -ObjC++
QMAKE_CFLAGS = -ObjC++.

Не уверен, что 100% верно, но позволяет собирать код и писать на Objective-C++ в любом файле проекта независимо от директив QMake.

https://habr.com/images/px.gif#%3D%22http%3A%2F%2Fimg406.imageshack.us%2Fimg406%2F9150%2F18285056.jpg%22

Заключение

Надеюсь, статья окажется полезной Вам в Ваших проектах. В отличие от других примеров в интернете, код рабочий, содержит честный Objective-C класс и не требует непонятных библиотек. Нужно лишь правильно расположить файлы заголовков, что не потребует более получаса.

P.S. Вот здесь выложил весь проект, который будет достаточно просто откомпилировать.
Tags:
Hubs:
+40
Comments 24
Comments Comments 24

Articles