Интеграция приложений Qt в среду Mac OS X (с использованием Cocoa и Objective-C++)

  • Tutorial
Доброго всем дня!

Недавно я писал о кастомизации заголовка окна в Mac OS X и получил реквесты написать поподробнее о взаимодействии Qt и Cocoa. Думаю, тему можно немного развернуть и написать об интеграции приложений, написанных с помощью Qt, в среду Mac OS X. Оговорюсь, что используется в данном случае Qt for Cocoa, если возьмёте Qt for Carbon, то и работать придётся только с карбоном. Но он морально устарел, и использовать его стоит только в крайних случаях.

Обычная Qt-программа имеет ряд несостыковок с Apple HIG. Точнее, может иметь, так как не всем программам нужен дополнительный функционал. Например, не любой программе надо иметь бэдж поверх значка в доке, расширять меню дока или выносить/дублировать некоторые функции в маковское меню.

Но что делать, если такой функционал нужен? Если нужно отображать в доке количество уведомлений (а-ля скайп), обрабатывать клик по иконке в доке, добавлять свои пункты меню в док, да ещё и иметь нормальное меню, в общем, сделать так, чтобы программа смотрелась как родная в Mac OS? Что-то из этого можно сделать с помощью штатных или полудокументированных функций Qt, а что-то — только с использованием Cocoa и, соответственно, Objective-C… Что же делать?

Нам поможет Objective-C++!

Что это за зверь и с чем его едят? По сути, это возможность комбинировать Objective-C и C++ классы в одном файле исходников. Причём, расширение заголовочников остаётся стандартным (.h), а вот для исходников нужно указывать расширение .mm, чтобы компилятор его съел и не поперхнулся.

Теперь представим себе, что у нас есть некий (может быть даже большой) проект, написанный с помощью Qt. Изначально он писался под винду или линукс, а вот теперь его надо перенести в макось, да так, чтобы было красиво и удобно, чтобы маководы не морщили нос при виде этого чудища.

Понятно, что для начала надо подстроить интерфейс программы под Apple HIG, без этого никуда, но это останется за рамками данной статьи, упомяну лишь полезные дефайны Q_WS_*, которые позволяют компилить разный код для разных ОСей. Мы же будем говорить о том, какими средствами можно подстроить своё приложение под новое окружение (или же как создать Mac-приложение на Qt с нуля — это зависит от поставленных целей).

Итак, пойдём по порядку.

Общая интеграция



Для начала дадим имя и значок программе. Нет, не то имя, которое имеет бандл, а имя, которое будет отображаться в Application Menu. Для этого нам потребуется свой файл Info.plist, а не тот, что генерирует qmake. Это делается одной строчкой в .pro:

macx: QMAKE_INFO_PLIST = MyInfo.plist

В наш .plist пишем что-то такое:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>LSHasLocalizedDisplayName</key>
	<true/>
	<key>CFBundleIconFile</key>
	<string>myicon.icns</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleGetInfoString</key>
	<string>Created by Qt/QMake</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleExecutable</key>
	<string>MyAppName</string>
	<key>CFBundleIdentifier</key>
	<string>com.mycompany.myapp</string>
	<key>NOTE</key>
	<string>This file was generated by Qt/QMake.</string>
</dict>
</plist>

Разумеется, вместо «MyAppName» и «com.mycompany.myapp» пишем английское название программы и свой идентификатор бандла. Почему английское? Да потому что локализуем мы его в другом файле. На это указывает самый первый параметр в плисте: LSHasLocalizedDisplayName. Создаём директорию «ru.lproj», а в ней файл InfoPlist.strings. В этот файл пишем что-то такое:

/* Localized versions of Info.plist keys */

CFBundleName = "Моя хорошая программа";
CFBundleDisplayName = "Программа";

Здесь уже нужно указывать локализованное название бандла и имя, отображаемое в Application Menu. Чтобы это заработало, нужно при установке программы скопировать эту дерикторию в AppName.app/Contents/Resources, делать это лучше через указание INSTALLS в .pro файле, за подробностями прошу обращаться к документации к qmake. На скриншоте видно, что Application Menu имеет русское название, несмотря на то, что сам бандл имеет название на латинице.


Чтобы задать иконку программы (см. myicon.icns в файле MyInfo.plist), нам нужен файл .icns, который можно создать самому в графическом редакторе, либо сконвертировать из .ico или кучки .png с помощью программ или онлайн сервисов. Чтобы смотрелась иконка хорошо, лучше сделать в ней несколько размеров: 512x512, 256x256, 128x128, 64x64, 48x48, 32x32, 16x16. Система сама выберет какой размер в какой ситуации отображать. Файл с иконкой надо так же устанавливать в Resources. Самый простой способ заставить его устанавливаться — это прописать следующее в .pro файле:

macx: ICON = myicon.icns

Разделение кода



У Objective-C++ есть множество ограничений. Нельзя, к примеру, включать хедер с объявлением интерфейса Objective-C класса в файл исходников .cpp, ибо компилятор подавится. Для новых проектов, расчитанных только на Mac OS X, это не будет ограничением, ибо можно весь C++ код держать в .mm файлах и радоваться жизни. Но смысл Qt в кроссплатформенности, так что будем считать, что наши исходники всё-таки в .cpp файлах.

Выход тут прост. Надо создать «обёртку» для Objective-C вызовов и классов на C++. Я выбрал такую структуру: есть Qt/C++ класс, который обеспечивает интеграцию с Mac OS X. Какую-то работу он выполняет сам, а какую-то перепоручает «делегату», приватному классу, активно использующему Cocoa/Objective-C. При этом, получаем следующие файлы:

* myclass.h — заголовок основного класса
* myclass.cpp — реализация
* myclass_p.h — заголовок приватного класса (без Objective-C-интерфейсов)
* myclass_p.mm — исходники приватного класса, могут включать в себя интерфейсы и имплементацию классов Objective-C, включать любые хедеры и т.п.

Таким образом, мы чётко разграничиваем C++ и Objective-C++.

Кстати, чтобы Objective-C++ заработал, в .pro файле надо все хедеры/исходники, использующие его, помещать в секции OBJECTIVE_HEADERS и OBJECTIVE_SORCES. И, разумеется, делать это внутри блока macx: {}. А ещё, если мы хотим использовать Cocoa, то надо добавить этот фреймворк к проекту. Пишем в .pro:

macx: QMAKE_LFLAGS += -framework Cocoa

А теперь пойдёт интересное.

Работа с Dock



Рассмотрим пять основных функций работы с доком: добавление бэджа, добавление оверлея, обработка клика на иконке в доке, «подбрасывание» иконки программы в доке и добавление своего меню. Средствами Qt решить можно только последнюю проблему, да и то слабо документированной функцией qt_mac_set_dock_menu(QMenu*). Причём объявить её надо самим, как внешнюю:

extern void qt_mac_set_dock_menu(QMenu *); // Qt internal function

На меню, исходя из личного опыта, накладываются некоторые ограничения в сравнении с «родным» маковским меню:

* неактивные (disabled) пункты меню становятся зачем-то активными
* QMenu не эмиттит сигналы aboutToHide и aboutToShow
* нельзя сделать отступ каких-либо элементов
* если первый QAction в меню — разделитель, то он будет виден (в отличие от всех остальных проявлений QMenu)

Так что под это придётся подстраиваться. Можно, конечно, сделать всё на Objective-C/Cocoa, но тогда придётся делать свой механизм маппинга QAction'ов и родных пунктов меню. Но это имеет смысл делать только при действительно большой необходимости в устранении указанных ограничений.


Рассмотрим теперь клик на доке. Если бы мы писали на Cocoa, то проблем бы не было, достаточно было бы в AppDelegate реализовать метод applicationShouldHandleReopen:hasVisibleWindows:. Но мы не имеем прямого доступа к делегату программы, созданному в недрах Qt. Поэтому воспользуемся магией рантайма для добавления реализации этого метода. Для начала объявим функцию, которая будет реализовывать то, что нам нужно. Для этого превратим наш приватный класс в синглтон (всё равно нам не нужно более одного объекта этого класса) и напишем такую функцию:

void dockClickHandler(id self, SEL _cmd)
{
	Q_UNUSED(self)
	Q_UNUSED(_cmd)
	MyPrivate::instance()->emitClick();
}

Функция мертва без внедрения её в делегата. Не будем же с этим медлить!

MyPrivate::MyPrivate() :
	QObject(NULL)
{
	Class cls = [[[NSApplication sharedApplication] delegate] class];
	if (!class_addMethod(cls, @selector(applicationShouldHandleReopen:hasVisibleWindows:), (IMP) dockClickHandler, "v@:"))
		NSLog(@"MyPrivate::MyPrivate() : class_addMethod failed!");
}

void MyPrivate::emitClick()
{
	emit dockClicked();
}

Здесь мы берём класс делегата нашего приложения и добавляем в него метод на лету. И заодно реализуем метод emitClick(), эмиттящий Qt-сигнал о клике. Собственно, вот и всё, теперь по клику в доке мы можем показывать, к примеру, главное окно программы.

Далее можно попробовать подбросить иконку программы в доке. Первая же мысль: «так это же умеет делать QApplication::alert(QWidget*)!» Мысль верная, но преждевременно оптимистичная. Всё дело в том, что в Mac OS X 10.6.* эта функция работает как надо, а вот в 10.7.* почему-то не хочет (может быть, это связано с тем, что Qt 4.7.x официально не поддерживает Lion, а в 4.8 это пофиксят). Я не стал разбираться почему так происходит, и просто написал подбрасывание на Cocoa, благо это делается одной строкой:

void MyPrivate::requestAttention()
{
	[NSApp requestUserAttention: NSInformationalRequest];
}

И пусть этот кусок кода дублирует Qt-шный alert(), зато работать будет во всех версиях Mac OS X. А для других систем можно по-прежнему использовать alert().

Теперь разберёмся с бэджем. Если кто не в курсе, то это крсный кружок с текстом, отображаемый поверх значка программы в доке. Например, он используется для отображения числа непрочитанных писем в Mail.app. Документация от Apple говорит, что делать это надо так:

[[NSApp dockTile] setBadgeLabel: badgeString];

Здесь badgeString имеет тип NSString*. Ага, вот и первое неудобство! В Qt обычно идёт манипулирование QString, а значит, надо написать некий «конвертер» строк:

// warning! nsstring isn't released!
NSString * nsStringFromQString(const QString & s)
{
	const char * utf8String = s.toUtf8().constData();
	return [[NSString alloc] initWithUTF8String: utf8String];
}

Как видно из кода (и из комментария к нему), возвращаемая строка не релизится, так что придётся делать это в вызывающем коде (Qt не использует ARC, так что за памятью следить будем сами).

Теперь можем написать функцию нашего приватного класса, которая будет выводить нужную нам строку в бэдже дока:

void MyPrivate::setDockBadge(const QString & badgeText)
{
	NSString * badgeString = nsStringFromQString(badgeText);
	[[NSApp dockTile] setBadgeLabel: badgeString];
	[badgeString release];
}


Теперь последний аспект работы с доком — добавить произвольный оверлей в док. Это делается с помощью следующего кода:

[[NSApp dockTile] setContentView: view];

Здесь view это NSView*. Но мы-то работаем с Qt! А значит, нам надо в оверлей поместить QWidget*. Как же получить из QWidget'а его NSView? Пишем простую функцию:

NSView * nsViewFromWidget(QWidget * w)
{
	return (NSView *)w->winId();
}

Всё просто, создатели Qt сделали почти всю работу за нас.

Но, увы, ждёт нас облом: NSView, полученный из QWidget'а непригоден для установки в док. То есть, он ставится, NSDockTile его съедает, но вместо содержимого виджета в доке образуется пустое место. Не знаю уж, почему. Но даже в Qt Creator'е прогресс-бар в док вешается именно через свой чистый NSView, созданный специально для этого. Так что, если нужен свой оверлей, то милости просим написать свой View на Cocoa. Увы.

Работа с меню



Перейдём к маковскому меню (тому, что на верхней панели). По умолчанию, Qt-программа его не создаёт (ну, если не считать стандартного Application Menu с системными функциями). Первое, что приходит на ум Qt-разработчику, это QMenuBar. И действительно, в документации к нему написано, что он может выполнять функции маковского меню. Но тут два варианта: либо сделать отдельный QMenuBar для каждого окна программы, либо сделать один глобальный. Выберем второй вариант в силу его неоспоримых преимуществ.

Опять же, исходя из документации, нам нужно создать QMenuBar с нулевым родителем, т.е., QMenuBar * macMenuBar = new QMenuBar(NULL), тогда он будет глобальным. Точнее, первый, из соданных таким образом менюбаров, станет глобальным.

А теперь — куча монотонной работы руками. Создаём сами меню «Правка», «Файл», «Справка» и так далее. Это достаточно нудная работа, но без неё программа хорошо выглядеть не будет. Кроме того, стандартные сочетания клавиш ⌘W и ⌘M не будут соответственно закрывать и сворачивать окна. Их придётся так же делать самостоятельно (хорошо хоть ⌘Q работает сразу).

Отмечу некоторые особенности QAction'ов в Mac OS X. Если им задавать шорткаты, то в конструктор QKeySequence надо передавать «Ctrl+M» для создания шортката "⌘M". И вообще, клавиша "⌘" в Qt под мак везде проходит как Ctrl, а клавиша «Ctrl» — как Meta. Для более лёгкой портируемости программ, надо полагать.

Ну и об ограничениях данного подхода к созданию меню.

* нельзя сделать отступ пунктов меню
* QMenu не эмиттит сигнал aboutToHide (только aboutToShow)
* имеет место следующий баг: если меню «Справка» называется «Help», то в нём будет автоматически создан системный блок поиска по пунктам меню, но если он будет назван по-русски, этого не произойдёт, даже если в системе текущая локаль — русская. Как избавиться от этого глюка, я пока не нашёл.

Почти финал — кастомный заголовок окна в Qt



Чтобы применить всё, написанное мной в прошлой статье, к Qt-программе, нужно сделать следующее. Во-первых, ставим где надо retain/release, т.к. не включен ARC. Во-вторых, в Objective-C++ разрешено то, что не удалось сделать в чистом Objective-C: взятие класса, если он объявлен только форвардом:

id _class = [NSThemeFrame class];

Это избавляет нас от необходимости иметь хотя бы одно окно в программе перед вызовом данной функции. В остальном всё то же самое, так что копипастить код сюда не буду. Посмотреть это можно в примере (ссылка ниже).

Заключение



Итак, мы рассмотрели Objective-C++ в применении к связке Qt+Cocoa для интеграции программы, написанной на Qt, в среду Mac OS X. По большому счёту, ничего сложного в этом нет, если имеются базовые знания Objective-C и Cocoa. Надо только знать некоторые особенности, на которых я постарался заострить внимание в данной статье.

Если кого интересуют полные исходники тестового проекта, то добро пожаловать на GitHub!

PS: ещё можно было бы рассмотреть встраивание в программу уведомлений через Growl, но это читатель может сделать и сам, если он усвоил материал.
Метки:
Поделиться публикацией
Похожие публикации
Комментарии 25
  • +4
    Офигенная статья. А что касается дока, то все думаю доделать кроссплатформенный класс для работы с ним ибо сейчас док есть и в Макоси и в Убунте и в выходящих кедах и даже в винде по сути дела новый таскбар являет собой док.
    • 0
      Упс, промахнулся мимо Вашего комментария. См. ниже.
  • 0
    Спасибо за высокую оценку!
    С доком дела обстоят достаточно сложно, в разных системах он имеет разную функциональность, собственно, по этой причине в Qt нет встроенных функций для работы с ним.
    Кроме того, в винде версии ниже 7 таскбар несравним с доком. И Unity есть пока только в убунту.
    Так что, как ни крути, придётся делать разные классы для каждой системы. Унифицировать тут вряд ли можно.
    • +1
      Все доки имеют менюшку, во всех доках в принципе можно встраивать бейджы, везде можно иконку дока менять, обрабатывать нажатие на неё, делать подпрыгивание или другую сигнализацию и во всех доках можно делать индикатор прогресса. Где-то нативными методами где-то тупо рисуя его.
      То бишь общий интерфейс как ни странно есть.
      • +1
        Ну, разве что действительно вручную рисовать.
        В таскбаре вин7 есть, к примеру, фича, которой я больше нигде не видел — это выдача попапа с заданными виджетами внутри при наведении на значок мышки.
        Кроме того, остаются ещё системы без поддержки «доковости» в принципе.
        • +1
          Есть системы и без трея вовсе. Но QSystemTray таки существует.
    • +1
      Насёт Unity — не совсем точно. Для KDE SC 4, например, есть icon-tasks, который реализует Unity API — такой себе Unity без оного. И может быть где угодно. Скорее проблема в том, что нет гарантии, что у пользователя это есть. Особенно в мире Linux. Но тут можно пойти простым путём — нет, ну и ладно.
      • 0
        Там dbus интерфейсы, спросили у него есть апи круто, нету, ну и ваши проблемы.
        • 0
          Ну да, я о том, же, что если нет функционоала дока, то и ладно. И так можно было бы в любой ОС сделать. Под XP/Vista — не будет работать, под 7- будет.
  • 0
    Несмотря на то, что сам являюсь сторонником переносимого кода, всё-так считаю что Qt как ни прикручивай к MacOS X, всё равно:
    1) интерфейс смотрится чужеродно, некрасиво
    2) в аппстор таким программам путь заказан
    3) маководы в большинстве своем перфекционисты и педанты, поэтому из-за п.1 такой софт не будет у них в фаворе. Это значит следующее: допустим есть офигенная прога, которая суперски делает свое дело, но использует Qt GUI. Если нет ничего нативного, маководы скачают ее и будут пользоваться. Как только выйдет кривая поделка, полная багов, но с красивым нативным «лицом», — маководы выбросят QT-хорошую-прогу и поставят нативный костыль. У меня была возможность наблюдать это не раз.

    На основании вышесказанного, если конечная цель проекта — успешное приложение для MacOS X, — лучше разобраться с Objective-C и Cocoa, а Qt оставить в покое.

    • 0
      В начале поста я оговорился, что рассчитываю в основном на тех, кому надо достаточно быстро свою программу впилить в макось. Так что конечная цель — более-менее успешное портирование. Далее по пунктам.

      1. Интерфейс очень сильно приближен к родному, но его всё равно можно кастомизировать, используя Style Sheets. Единственное, чего не хватает — это родного скролла с резиновыми краями (и исчезающего скроллбара).
      2. Не согласен, на хабре даже проскакивали ссылки на такие программы, да и я такие видел не раз.
      3. Я и сам в некотором роде маковод. Но у меня стоят программы, написанные на Java и GTK+, которые смотрятся достаточно не-нативно. И ничего. И многие их ставят.

      Вообще, если создатели Qt допилят нативный скроллинг, то пользователь может даже не узнать о том, что твоя программа написана не на Cocoa. Но с одним я согласен: если хочешь делать изначально под мак и только под него, то делай это на Cocoa (иногда с добавлением Carbon'а).
      • 0
        1. ни разу еще не видел хотя бы приблизительно похожего.
        2. можно ссылочку хоть на одну? любопытно.
        3. я написал «в большинстве своем», не имел в виду вас. я, к примеру, не ахти какой педант, но стараюсь не ставить на мой мак чужеродного софта.

        В целом я с вами согласен — для портинга нормально. Но если это не портинг, а лень учиться чему-то новому, то Qt не прокатит.
        • +2
          Уже не надо, сам нашел: itunes.apple.com/us/app/togmeg/id445287955

          image

          И все равно выглядит чужеродно. Мне кажется объём работы по мимикрированию GUI в этом случае сопоставим с объёмом работы по переносу GUI на Сocoa, это конечно при условии что логику программы оставить написанной на C++.
        • 0
          Лично мне не лень учиться новому. Обычно начальству «лень» ждать конца обучения, да ещё и оплачивать его. =)

          По пункту 2 — есть ещё скробблер Last.FM, в аппсторе его правда нет, но популярностью пользуется.
          • 0
            Если начальство адекватное, можете попробовать объяснить ему, что

            1. Конечный результат будет заведомо чуть хуже
            2. Время, потраченное на шаманство со стилями и прочие танцы с бубном, примерно равно портингу GUI на Cocoa. А для действительно хорошего программиста оно будет еще меньше.
            • 0
              1. Это да
              2. Для сложного проекта (где GUI достаточно много, причём кастомного) это не так.
              • 0
                Для кастомного гуя вообще лучше юзать Qt Quick. Впрочем, на QtQuick вполне можно будет юзать и кокоа виджеты.
                • 0
                  QtQuick можно юзать разве что для новых проектов, если есть уже большая софтина с кучей наворотов, то придётся весь GUI переписывать с нуля.
                  Ну, и куча ограничений всё же стоит на этот Quick.
    • 0
      Ну а если же конечная цель — работа на многих платформах, включая MacOS X (иногда портинги делают «для галочки», чтоб было), тогда конечно, Qt — самое оно.
      • +1
        Ну, я как раз и хочу, чтобы портинг был не «для галочки», а достаточно серьёзным с минимальными затратами. Можно, конечно, как скайп — для каждой системы родной GUI, но это дольше и дороже.
    • 0
      Я вот на маке использую PhpStorm, хотя он даже не на Qt, а на (обожемой) Java. Ибо в остальном это очень удобная IDE, несмотря на её «некрасивость». Так что, ИМХО, не совсем родную программу на Qt для Mac OS X люди легко потерпят, если сама программа при этом будет действительно хороша и не будет нативных аналогов.

      А в целом я с вами согласен
  • +1
    Спасибо за статью. Было интересно почитать. Даже не смотря на то, что в основном сижу под седьмым виндовозом.
  • 0
    Мне, как iOS/Mac разработчику с большим стажем программировани на Qt за спиной, статья ничего нового не принесла. Но в общем очень полезная, особенно учитывая, что Mac набирает популярности в мире.
    • 0
      А ну еще, Qt не очень спешать внедрять новые фишки по совместимости, чего только стоит надпись ворнинг, о том что Qt не своместим с 10.7
      • +1
        В Qt 4.8 Такого ворнинга нету

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.