
Integrating Qt Applications into Mac OS X (Using Cocoa and Objective-C ++)
- Tutorial
Good day to all!
I recently wrote about customizing the window title in Mac OS X and got requests to write more in detail about the interaction of Qt and Cocoa. I think you can expand the topic a bit and write about the integration of applications written using Qt into the Mac OS X environment. I will mention that Qt for Cocoa is used in this case, if you take Qt for Carbon, you will only have to work with carbon. But it is outdated, and it is worth using it only in extreme cases.
A regular Qt program has a number of inconsistencies with the Apple HIG. More precisely, it may have, since not all programs need additional functionality. For example, not every program needs to have a badge on top of the icon in the dock, expand the dock menu or remove / duplicate some functions in the poppy menu.
But what if this functionality is needed? If you need to display the number of notifications in the dock (a la Skype), process a click on the icon in the dock, add your menu items to the dock, and even have a normal menu, in general, make the program look like native on Mac OS? Some of this can be done using regular or semi-documented Qt functions, and some can only be done using Cocoa and, accordingly, Objective-C ... What can I do? Objective-C ++
will help us ! What kind of animal is it and what does it eat with? In essence, this is the ability to combine Objective-C and C ++ classes in one source file. Moreover, the extension of the headings remains standard (.h), but for the sources you need to specify the extension .mm so that the compiler ate it and did not choke.
Now imagine that we have some (maybe even a big) project written with Qt. Initially, it was written under Windows or Linux, but now it needs to be transferred to macros, so that it is beautiful and convenient so that poppy growers do not wrinkle their nose at the sight of this monster.
It’s clear that for starters, you need to tweak the program interface for Apple HIG, without it anywhere, but this will remain beyond the scope of this article, I will only mention useful Q_WS_ * defines that allow you to compile different code for different OSes. We will talk about the means by which we can adjust our application to the new environment (or how to create a Mac application on Qt from scratch - it depends on the goals).
So let's go in order.
First, give the name and icon to the program. No, not the name that the bundle has, but the name that will be displayed in the Application Menu. To do this, we need our own Info.plist file, not the one that qmake generates. This is done in one line in .pro:
In our .plist we write something like this:
Of course, instead of “MyAppName” and “com.mycompany.myapp” we write the English name of the program and its bundle identifier. Why english? Yes, because we localize it in another file. This is indicated by the very first parameter in the list: LSHasLocalizedDisplayName. Create the directory “ru.lproj” and the file InfoPlist.strings in it. In this file we write something like this:
Here you already need to specify the localized name of the bundle and the name displayed in the Application Menu. For this to work, you need to copy this directory to AppName.app/Contents/Resources when installing the program, it is better to do this by specifying INSTALLS in the .pro file, please refer to the qmake documentation for details. The screenshot shows that the Application Menu has a Russian name, despite the fact that the bundle itself has a name in Latin.

To set the program icon (see myicon.icns in the MyInfo.plist file), we need a .icns file that you can create yourself in a graphical editor, or convert from .ico or a bunch of .png using programs or online services. To make the icon look good, it’s better to make it several sizes: 512x512, 256x256, 128x128, 64x64, 48x48, 32x32, 16x16. The system itself will choose what size in which situation to display. The file with the icon must also be installed in Resources. The easiest way to get it installed is to write the following in the .pro file:
Objective-C ++ has many limitations. For example, you cannot include a header with an Objective-C class interface declaration in the .cpp source file, because the compiler will choke. For new projects, designed only for Mac OS X, this will not be a limitation, because you can keep all C ++ code in .mm files and enjoy life. But the meaning of Qt is cross-platform, so we will assume that our source code is still in .cpp files.
The way out is simple. You need to create a wrapper for Objective-C calls and classes in C ++. I chose this structure: there is a Qt / C ++ class that provides integration with Mac OS X. It does some work by itself, and some delegates to the "delegate", a private class that actively uses Cocoa / Objective-C. At the same time, we get the following files:
* myclass.h - the header of the main class
* myclass.cpp - implementation
* myclass_p.h - private class header (without Objective-C interfaces)
* myclass_p.mm - private class sources, can include interfaces and implementation of Objective-C classes, include any headers, etc. .
Thus, we clearly distinguish between C ++ and Objective-C ++.
By the way, in order for Objective-C ++ to work, in the .pro file all headers / sources using it must be placed in the OBJECTIVE_HEADERS and OBJECTIVE_SORCES sections. And, of course, do it inside the macx block: {}. And also, if we want to use Cocoa, then we need to add this framework to the project. We write in .pro:
And now the fun will come.
Consider the five main functions of working with the dock: adding a badge, adding an overlay, processing a click on the icon in the dock, “tossing” the program icon in the dock and adding your own menu. Using Qt, you can solve only the last problem, and even that with the poorly documented function qt_mac_set_dock_menu (QMenu *). Moreover, it must be declared by ourselves as external:
On the basis of personal experience, some restrictions are imposed in comparison with the "native" Mac menu:
* inactive (disabled) menu items become active for some reason
* QMenu does not emit aboutToHide and aboutToShow signals
* you cannot indent any elements
* if the first QAction in the menu is a separator, it will be visible (unlike all other QMenu manifestations)
So you have to adapt to it. You can, of course, do everything on Objective-C / Cocoa, but then you have to make your own mechanism for mapping QActions and native menu items. But it makes sense to do this only if there is a really great need to eliminate these restrictions.

Now consider the click on the dock. If we wrote in Cocoa, then there would be no problems, it would be enough to implement the applicationShouldHandleReopen: hasVisibleWindows: method in AppDelegate. But we do not have direct access to the program delegate created in the bowels of Qt. Therefore, we will use magic of runtime to add an implementation of this method. First, we’ll declare a function that will implement what we need. To do this, we turn our private class into a singleton (anyway, we do not need more than one object of this class) and write this function:
A function is dead without embedding it in a delegate. Let's not hesitate with this!
Here we take the delegate class of our application and add the method to it on the fly. And at the same time, we implement the emitClick () method, which emits a Qt signal about a click. Actually, that’s all, now by clicking on the dock we can show, for example, the main program window.
Next, you can try toss the program icon in the dock. My first thought: “QApplication :: alert (QWidget *) is able to do the same!” The thought is true, but prematurely optimistic. The thing is that in Mac OS X 10.6. * This function works as it should, but in 10.7. * It doesn’t want for some reason (maybe this is due to the fact that Qt 4.7.x does not officially support Lion, but in 4.8 it will be fixed). I did not understand why this happens, and just wrote a toss on Cocoa, since this is done in one line:
And let this piece of code duplicate Qt-shy alert (), but it will work in all versions of Mac OS X. And for other systems, you can still use alert ().
Now let's deal with the badge. If someone does not know, then this is a red circle with text displayed on top of the program icon in the dock. For example, it is used to display the number of unread messages in Mail.app. Apple's documentation says this should be done like this:
Here badgeString is of type NSString *. Yeah, this is the first inconvenience! In Qt, QString is usually manipulated, which means that you need to write a certain “converter” of strings:
As you can see from the code (and from the commentary to it), the returned string will not be released, so you have to do it in the calling code (Qt does not use ARC, so we will monitor the memory ourselves).
Now we can write a function of our private class that will display the line we need in the badge of the dock:

Now the last aspect of working with the dock is to add an arbitrary overlay to the dock. This is done using the following code:
Here view is NSView *. But we are working with Qt! So, we need to put QWidget * in the overlay. How to get its NSView from QWidget? We write a simple function:
It's simple, the creators of Qt have done almost all the work for us.

But, alas, a bummer awaits us: the NSView received from QWidget is unsuitable for installation in the dock. That is, it is put, NSDockTile eats it, but instead of the contents of the widget in the dock, an empty space is formed. I don’t know why. But even in Qt Creator, the progress bar in the dock is hung precisely through its pure NSView, created specifically for this. So, if you need your own overlay, then you are welcome to write your View in Cocoa. Alas.
Let's move on to the Mack menu (the one on the top panel). By default, the Qt program does not create it (well, except for the standard Application Menu with system functions). The first thing that comes to mind for a Qt developer is QMenuBar. Indeed, the documentation for it says that it can perform the functions of a poppy menu. But there are two options: either make a separate QMenuBar for each program window, or make one global. We will choose the second option due to its undeniable advantages.
Again, based on the documentation, we need to create a QMenuBar with a zero parent, i.e., QMenuBar * macMenuBar = new QMenuBar (NULL), then it will be global. More precisely, the first of the menu bars thus created will become global.
And now - a lot of monotonous work with hands. We create the “Edit”, “File”, “Help” menus and so on. This is a rather tedious job, but without it the program will not look good. In addition, the standard shortcuts ⌘W and ⌘M will not close and minimize windows, respectively. They will also have to be done on their own (well, at least ⌘Q works right away).

I’ll note some features of QActions in Mac OS X. If you specify shortcuts to them, then you need to pass “Ctrl + M” to the QKeySequence constructor to create a "M shortcut. And in general, the ⌘ key in Qt under the poppy goes everywhere like Ctrl, and the Ctrl key like Meta. For easier portability of programs, presumably.
Well, about the limitations of this approach to creating a menu.
* you cannot indent menu items
* QMenu does not emit a signal aboutToHide (only aboutToShow)
* the following bug occurs: if the "Help" menu is called "Help", a system block for searching menu items will be automatically created in it, but if it is named in Russian, this is not will happen even if the current locale in the system is Russian. How to get rid of this glitch, I have not yet found.
To apply everything that I wrote in a previous article to a Qt program, you need to do the following. First, we set where we need retain / release, because not included ARC. Secondly, in Objective-C ++ it is allowed what could not be done in pure Objective-C: taking a class if it is declared only as a forward:
This saves us from having to have at least one window in the program before calling this function. Otherwise, everything is the same, so I won’t copy-paste the code here. You can see this in the example (link below).
So, we examined Objective-C ++ as applied to the Qt + Cocoa bundle for integrating a Qt program into Mac OS X. By and large, there is nothing complicated in this if you have basic knowledge of Objective-C and Cocoa. You just need to know some of the features that I tried to focus on in this article.
If anyone is interested in the full source code of the test project, then welcome to GitHub !
PS: one could also consider embedding into the program notifications via Growl, but the reader can do this himself if he has learned the material.
I recently wrote about customizing the window title in Mac OS X and got requests to write more in detail about the interaction of Qt and Cocoa. I think you can expand the topic a bit and write about the integration of applications written using Qt into the Mac OS X environment. I will mention that Qt for Cocoa is used in this case, if you take Qt for Carbon, you will only have to work with carbon. But it is outdated, and it is worth using it only in extreme cases.

But what if this functionality is needed? If you need to display the number of notifications in the dock (a la Skype), process a click on the icon in the dock, add your menu items to the dock, and even have a normal menu, in general, make the program look like native on Mac OS? Some of this can be done using regular or semi-documented Qt functions, and some can only be done using Cocoa and, accordingly, Objective-C ... What can I do? Objective-C ++
will help us ! What kind of animal is it and what does it eat with? In essence, this is the ability to combine Objective-C and C ++ classes in one source file. Moreover, the extension of the headings remains standard (.h), but for the sources you need to specify the extension .mm so that the compiler ate it and did not choke.
Now imagine that we have some (maybe even a big) project written with Qt. Initially, it was written under Windows or Linux, but now it needs to be transferred to macros, so that it is beautiful and convenient so that poppy growers do not wrinkle their nose at the sight of this monster.
It’s clear that for starters, you need to tweak the program interface for Apple HIG, without it anywhere, but this will remain beyond the scope of this article, I will only mention useful Q_WS_ * defines that allow you to compile different code for different OSes. We will talk about the means by which we can adjust our application to the new environment (or how to create a Mac application on Qt from scratch - it depends on the goals).
So let's go in order.
General integration
First, give the name and icon to the program. No, not the name that the bundle has, but the name that will be displayed in the Application Menu. To do this, we need our own Info.plist file, not the one that qmake generates. This is done in one line in .pro:
macx: QMAKE_INFO_PLIST = MyInfo.plist
In our .plist we write something like this:
LSHasLocalizedDisplayName CFBundleIconFile myicon.icns CFBundlePackageType APPL CFBundleGetInfoString Created by Qt/QMake CFBundleSignature ???? CFBundleExecutable MyAppName CFBundleIdentifier com.mycompany.myapp NOTE This file was generated by Qt/QMake.
Of course, instead of “MyAppName” and “com.mycompany.myapp” we write the English name of the program and its bundle identifier. Why english? Yes, because we localize it in another file. This is indicated by the very first parameter in the list: LSHasLocalizedDisplayName. Create the directory “ru.lproj” and the file InfoPlist.strings in it. In this file we write something like this:
/* Localized versions of Info.plist keys */
CFBundleName = "Моя хорошая программа";
CFBundleDisplayName = "Программа";
Here you already need to specify the localized name of the bundle and the name displayed in the Application Menu. For this to work, you need to copy this directory to AppName.app/Contents/Resources when installing the program, it is better to do this by specifying INSTALLS in the .pro file, please refer to the qmake documentation for details. The screenshot shows that the Application Menu has a Russian name, despite the fact that the bundle itself has a name in Latin.

To set the program icon (see myicon.icns in the MyInfo.plist file), we need a .icns file that you can create yourself in a graphical editor, or convert from .ico or a bunch of .png using programs or online services. To make the icon look good, it’s better to make it several sizes: 512x512, 256x256, 128x128, 64x64, 48x48, 32x32, 16x16. The system itself will choose what size in which situation to display. The file with the icon must also be installed in Resources. The easiest way to get it installed is to write the following in the .pro file:
macx: ICON = myicon.icns
Code separation
Objective-C ++ has many limitations. For example, you cannot include a header with an Objective-C class interface declaration in the .cpp source file, because the compiler will choke. For new projects, designed only for Mac OS X, this will not be a limitation, because you can keep all C ++ code in .mm files and enjoy life. But the meaning of Qt is cross-platform, so we will assume that our source code is still in .cpp files.
The way out is simple. You need to create a wrapper for Objective-C calls and classes in C ++. I chose this structure: there is a Qt / C ++ class that provides integration with Mac OS X. It does some work by itself, and some delegates to the "delegate", a private class that actively uses Cocoa / Objective-C. At the same time, we get the following files:
* myclass.h - the header of the main class
* myclass.cpp - implementation
* myclass_p.h - private class header (without Objective-C interfaces)
* myclass_p.mm - private class sources, can include interfaces and implementation of Objective-C classes, include any headers, etc. .
Thus, we clearly distinguish between C ++ and Objective-C ++.
By the way, in order for Objective-C ++ to work, in the .pro file all headers / sources using it must be placed in the OBJECTIVE_HEADERS and OBJECTIVE_SORCES sections. And, of course, do it inside the macx block: {}. And also, if we want to use Cocoa, then we need to add this framework to the project. We write in .pro:
macx: QMAKE_LFLAGS += -framework Cocoa
And now the fun will come.
Work with Dock
Consider the five main functions of working with the dock: adding a badge, adding an overlay, processing a click on the icon in the dock, “tossing” the program icon in the dock and adding your own menu. Using Qt, you can solve only the last problem, and even that with the poorly documented function qt_mac_set_dock_menu (QMenu *). Moreover, it must be declared by ourselves as external:
extern void qt_mac_set_dock_menu(QMenu *); // Qt internal function
On the basis of personal experience, some restrictions are imposed in comparison with the "native" Mac menu:
* inactive (disabled) menu items become active for some reason
* QMenu does not emit aboutToHide and aboutToShow signals
* you cannot indent any elements
* if the first QAction in the menu is a separator, it will be visible (unlike all other QMenu manifestations)
So you have to adapt to it. You can, of course, do everything on Objective-C / Cocoa, but then you have to make your own mechanism for mapping QActions and native menu items. But it makes sense to do this only if there is a really great need to eliminate these restrictions.

Now consider the click on the dock. If we wrote in Cocoa, then there would be no problems, it would be enough to implement the applicationShouldHandleReopen: hasVisibleWindows: method in AppDelegate. But we do not have direct access to the program delegate created in the bowels of Qt. Therefore, we will use magic of runtime to add an implementation of this method. First, we’ll declare a function that will implement what we need. To do this, we turn our private class into a singleton (anyway, we do not need more than one object of this class) and write this function:
void dockClickHandler(id self, SEL _cmd)
{
Q_UNUSED(self)
Q_UNUSED(_cmd)
MyPrivate::instance()->emitClick();
}
A function is dead without embedding it in a delegate. Let's not hesitate with this!
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();
}
Here we take the delegate class of our application and add the method to it on the fly. And at the same time, we implement the emitClick () method, which emits a Qt signal about a click. Actually, that’s all, now by clicking on the dock we can show, for example, the main program window.
Next, you can try toss the program icon in the dock. My first thought: “QApplication :: alert (QWidget *) is able to do the same!” The thought is true, but prematurely optimistic. The thing is that in Mac OS X 10.6. * This function works as it should, but in 10.7. * It doesn’t want for some reason (maybe this is due to the fact that Qt 4.7.x does not officially support Lion, but in 4.8 it will be fixed). I did not understand why this happens, and just wrote a toss on Cocoa, since this is done in one line:
void MyPrivate::requestAttention()
{
[NSApp requestUserAttention: NSInformationalRequest];
}
And let this piece of code duplicate Qt-shy alert (), but it will work in all versions of Mac OS X. And for other systems, you can still use alert ().
Now let's deal with the badge. If someone does not know, then this is a red circle with text displayed on top of the program icon in the dock. For example, it is used to display the number of unread messages in Mail.app. Apple's documentation says this should be done like this:
[[NSApp dockTile] setBadgeLabel: badgeString];
Here badgeString is of type NSString *. Yeah, this is the first inconvenience! In Qt, QString is usually manipulated, which means that you need to write a certain “converter” of strings:
// warning! nsstring isn't released!
NSString * nsStringFromQString(const QString & s)
{
const char * utf8String = s.toUtf8().constData();
return [[NSString alloc] initWithUTF8String: utf8String];
}
As you can see from the code (and from the commentary to it), the returned string will not be released, so you have to do it in the calling code (Qt does not use ARC, so we will monitor the memory ourselves).
Now we can write a function of our private class that will display the line we need in the badge of the dock:
void MyPrivate::setDockBadge(const QString & badgeText)
{
NSString * badgeString = nsStringFromQString(badgeText);
[[NSApp dockTile] setBadgeLabel: badgeString];
[badgeString release];
}

Now the last aspect of working with the dock is to add an arbitrary overlay to the dock. This is done using the following code:
[[NSApp dockTile] setContentView: view];
Here view is NSView *. But we are working with Qt! So, we need to put QWidget * in the overlay. How to get its NSView from QWidget? We write a simple function:
NSView * nsViewFromWidget(QWidget * w)
{
return (NSView *)w->winId();
}
It's simple, the creators of Qt have done almost all the work for us.

But, alas, a bummer awaits us: the NSView received from QWidget is unsuitable for installation in the dock. That is, it is put, NSDockTile eats it, but instead of the contents of the widget in the dock, an empty space is formed. I don’t know why. But even in Qt Creator, the progress bar in the dock is hung precisely through its pure NSView, created specifically for this. So, if you need your own overlay, then you are welcome to write your View in Cocoa. Alas.
Work with the menu
Let's move on to the Mack menu (the one on the top panel). By default, the Qt program does not create it (well, except for the standard Application Menu with system functions). The first thing that comes to mind for a Qt developer is QMenuBar. Indeed, the documentation for it says that it can perform the functions of a poppy menu. But there are two options: either make a separate QMenuBar for each program window, or make one global. We will choose the second option due to its undeniable advantages.
Again, based on the documentation, we need to create a QMenuBar with a zero parent, i.e., QMenuBar * macMenuBar = new QMenuBar (NULL), then it will be global. More precisely, the first of the menu bars thus created will become global.
And now - a lot of monotonous work with hands. We create the “Edit”, “File”, “Help” menus and so on. This is a rather tedious job, but without it the program will not look good. In addition, the standard shortcuts ⌘W and ⌘M will not close and minimize windows, respectively. They will also have to be done on their own (well, at least ⌘Q works right away).

I’ll note some features of QActions in Mac OS X. If you specify shortcuts to them, then you need to pass “Ctrl + M” to the QKeySequence constructor to create a "M shortcut. And in general, the ⌘ key in Qt under the poppy goes everywhere like Ctrl, and the Ctrl key like Meta. For easier portability of programs, presumably.
Well, about the limitations of this approach to creating a menu.
* you cannot indent menu items
* QMenu does not emit a signal aboutToHide (only aboutToShow)
* the following bug occurs: if the "Help" menu is called "Help", a system block for searching menu items will be automatically created in it, but if it is named in Russian, this is not will happen even if the current locale in the system is Russian. How to get rid of this glitch, I have not yet found.
Almost final - custom window title in Qt
To apply everything that I wrote in a previous article to a Qt program, you need to do the following. First, we set where we need retain / release, because not included ARC. Secondly, in Objective-C ++ it is allowed what could not be done in pure Objective-C: taking a class if it is declared only as a forward:
id _class = [NSThemeFrame class];
This saves us from having to have at least one window in the program before calling this function. Otherwise, everything is the same, so I won’t copy-paste the code here. You can see this in the example (link below).
Conclusion
So, we examined Objective-C ++ as applied to the Qt + Cocoa bundle for integrating a Qt program into Mac OS X. By and large, there is nothing complicated in this if you have basic knowledge of Objective-C and Cocoa. You just need to know some of the features that I tried to focus on in this article.
If anyone is interested in the full source code of the test project, then welcome to GitHub !
PS: one could also consider embedding into the program notifications via Growl, but the reader can do this himself if he has learned the material.