Mac OS X: Handling Apple Events
Материал из Wiki.crossplatform.ru
Qt Quarterly | Выпуск 12 | Документация |
by Trenton Schulz
Apple events are a high-level interprocess communication mechanism on Mac OS X.They're also the backbone of AppleScript, the scripting language ofMac OS X. Here we take a look at adding support for thisfeature to a Qt/Mac application.
When a user double clicks on a file in the Finder, Finder sendsan Apple event to the application associated with the file andasks it to open the file. If the application is notrunning, it is launched and then the request is made. The advantageof this approach is that there is only one instance of the applicationrunning.
On Windows, this usually is done by launching the application andpassing the name of the file as a command line argument. Looking atmany files results in launching many instances of the sameapplication. (The QtSingleApplication component, available as aQt Solution, addresses this issue.)
Qt 3 does not provide an abstraction for handling Apple events, butit is straightforward to add support for them in your application usingMac's Carbon API. Let's assume we have an application calledXpmViewer that displays XPM files (an X11 image format). The mainwindow class declaration follows:
class XpmViewer : public QMainWindow { Q_OBJECT public: XpmViewer(QWidget *parent = 0); void loadXpmFile(const QString &fileName); ... };
The loadXpmFile() function takes a the name of an XPM file anddisplays the image to the user.
In order to process Apple events, we need to install an Apple event handlerand pass the contents of the event to the XpmViewer instance. One wayto achieve this is to make our own custom event and have the QApplication deal with it. Here's the definition of the event type:
const int OpenEventID = QEvent::User + 100; class OpenEvent : public QCustomEvent { public: OpenEvent(const QString &fileName) : QCustomEvent(OpenEventID), file(fileName) {} QString fileName() const { return file; } private: QString file; };
Let's also add the logic of adding and removing the Apple event handler and thehandler itself to a QApplication subclass:
class XpmApplication : public QApplication { public: XpmApplication(int argc, char *argv[]); XpmApplication(); void setXpmViewer(XpmViewer *viewer); protected: void customEvent(QCustomEvent *event); private: static OSStatus appleEventHandler( const AppleEvent *event, AppleEvent *, long); ... };
In addition to the Apple event handler and our custom event handler,we have a setXpmViewer() convenience function that we'll needlater on to call XpmViewer::loadXpmFile().
XpmApplication::XpmApplication(int argc, char *argv[]) : QApplication(argc, argv), viewer(0) { AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments, appleEventHandler, 0, false); } XpmApplication:: XpmApplication() { AERemoveEventHandler(kCoreEventClass, kAEOpenDocuments, appleEventHandler, 0, false); }
In the constructor and the destructor, we set up an Appleevent handler. We first pass the class of events we are interestedin, and then the event ID of the Apple event. In this case, we areonly interested in the kAEOpenDocuments event which is partof the AECoreEvent class.
Then we pass our static Apple event function. Like many callback schemes,there is an opportunity to pass some extra data. Here, we just passzero. Finally we pass false, telling Carbon that we only wantthis to be an application-specific handler (as opposed to system-wide). Ifwe wanted to look at more events, we could make more calls toAEInstallEventHandler(), each with a different function to handle theevent. Alternatively, we could accept all the events and demultiplex themin the handler.
Before we review the function that will give us the OpenEvents, let'stake a look at our custom event handler:
void XpmApplication::customEvent(QCustomEvent *event) { if (event->type() == OpenEventID) viewer->loadXpmFile( static_cast<OpenEvent *>(event)->fileName()); }
The event handler makes sure the the custom event is the right type.Then we call XpmViewer::loadXpmFile() with the file namespecified by the event.
OSStatus XpmApplication::appleEventHandler( const AppleEvent *event, AppleEvent *, long) { AEDescList docs; if (AEGetParamDesc(event, keyDirectObject, typeAEList, &docs) == noErr) { long n = 0; AECountItems(&docs, &n); UInt8 strBuffer[256]; for (int i = 0; i < n; i++) { FSRef ref; if (AEGetNthPtr(&docs, i + 1, typeFSRef, 0, 0, &ref, sizeof(ref), 0) != noErr) continue; if (FSRefMakePath(&ref, strBuffer, 256) == noErr) { OpenEvent event(QString::fromUtf8( reinterpret_cast<char *>(strBuffer))); QApplication::sendEvent(qApp, &event); } } } return noErr; }
The Apple event handler has the event passed in as its first argument.The second argument is the reply and the last argument is the extra data wecould have passed in. Here, we can ignore the last two arguments.Also, since we stated we were only interested in Open Documents events, we cansafely assume the only type of event we have here is Open Documents.
The event contains a list of file names. We first geta copy of this list with AEGetParamDesc(). Then we iterate overthe list and retrieve a file system reference (FSRef) out of thelist. We then call FSRefMakePath() to get the file name as aUTF-8 string. Once we have the file name, we create an OpenEventand then send it to our XpmApplication, which handles it incustomEvent().
When building the application, we must explicitly link against Carbonby adding this line to the .pro file:
LIBS += -framework Carbon
If we use our XpmApplication in main() and build our program, we should beable to drag an XPM file from the Finder onto the icon in the Dock representingour application.
We now have an application that can open XPM files. Usually you alsowant to associate your program with the file type. This requires us toadd some information to the application's property list, which ispart of the application's bundle.
A property list is an XML file of key--value pairs that Mac OS X uses for some runtime information about theapplication. qmake provides a default property list when it puts theapplication into the bundle as part of the make process; in our case we need acustom one. Here's our complete property list:
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDocumentTypes</key> <array> <dict> <key>CFBundleTypeExtensions</key> <array> <string>xpm</string> </array> <key>CFBundleTypeIconFile</key> <string>application.icns</string> <key>CFBundleTypeMIMETypes</key> <array> <string>image/x-xpm</string> </array> <key>CFBundleTypeName</key> <string>X PixMap File</string> <key>CFBundleTypeRole</key> <string>Viewer</string> <key>LSIsAppleDefaultForType</key> <true/> </dict> </array> <key>CFBundleIconFile</key> <string>application.icns</string> <key>CFBundleExecutable</key> <string>appleevents</string> <key>CFBundleIdentifier</key> <string>com.trolltech.XpmViewer</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>1.0</string> <key>CSResourcesFileMapped</key> <true/> </dict> </plist>
The CFBundleDocumentTypes property (shown in bold) is anarray of dictionaries that describe supported file types. In our case, there isonly one file type (XPM).
The following table gives a quick explanation of what each key means inthe CFBundleDocumentTypes array.
Key | Description |
---|---|
CFBundleTypeExtensions | The file name extension for the file |
<tt>CFBundleTypeIconFile | The icon in your bundle that Finder should associate with the file type |
CFBundleTypeMIMETypes | The MIME type for the file, this can be an array of multiple strings |
CFBundleTypeName | The text that will be shown in Finder |
CFBundleTypeRole | Specifies whether the program can open (Viewer), open and save (Editor), or is simply a shell to another program |
LSIsAppleDefaultForType | Tells Finder (or more specifically Launch Services) that we want to be the default association for the file type |
More information on these and other keys isprovided on Apple's web site.
We've also added an icon to the bundle using the CFBundleIconFileproperty. The icon will be used for the application.
We now need to make sure that property list and the icon are copiedinto the bundle. Thankfully, we can let qmake do the dirty workfor us by adding these lines to the .pro file:
RC_FILE = xpmviewer.icns QMAKE_INFO_PLIST = Info.plist
The RC_FILE entry specifies the name of the icon file in thesource tree. In the bundle, this file is always renamedapplication.icns.
After building our application with this new information and movingthe resulting application to the Application directory, we shouldhave a fully functioning Qt application that responds to the Appleopen event. The full source code for the application can bedownloaded here.
The good news is that Qt 4.0 is expected to includea QFileOpenEvent class for Mac OS X that will remove the need toget involved with Carbon directly.
As a final note, if you are interested in more information about Apple events,you might want to consult Apple's Event Manager page.
Naturally, no mention of Qt and scripting would be complete withouta plug for Trolltech's own scripting toolkit, Qt Script forApplications (QSA). See theQSA Overview for details.