Qt/Mac Special Features
Материал из Wiki.crossplatform.ru
Qt Quarterly | Выпуск 18 | Документация |
by Trenton Schulz |
Although Qt is designed to make cross-platform application development easierfor our users on all supported platforms, this doesn't mean that we onlyprovide features that are common to all platforms. Where possible, we exposeplatform-specific functionality in ways that fit in with our cross-platformAPI, and this is particularly true for Qt/Mac.
Содержание
On Mac OS X, users expect Qt applications to take advantage of the specialwindow types, styles and events available on that platform. We pass on thatfunctionality to developers in ways that are understandable to developers onother platforms.
New Window Types
Mac OS X provides two types of windows that are unique to the platform: sheetsand drawers.
Drawers are windows that slide out from other windows and are typically usedfor information that is closely linked with the parent window. You can create adrawer by passing Qt::Drawer as one of the flags in the QWidget constructor.This will just create a window on other platforms. In Qt, you typically get agood user experience if you use this with QDockWidgets.
Sheets are special dialogs that come down on top of application windows.Each sheet is tied to a window and helps the user to tie a dialog to acertain widget. Typically this is done with "document modal" dialogs.This means that each dialog only interrupts event processing for a singlewindow.
In Qt, you can create a sheet by passing Qt::Sheet as part of your windowflags; it has no effect on other platforms.
Thanks to Qt's signals and slots mechanism, it has always been possible tocreate document modal dialogs with Qt. It just requires a little bit ofdiscipline. Instead of using QDialog::exec() to halt further execution ina function, processing should be done in a slot.Let's modify the MainWindow class from Qt's SDI example to takeadvantage of this feature.
... private slots: void finishSheet(int sheetValue); private: void maybeSave(); bool reallyQuit; QMessageBox *messageBox; ...
We will need an extra slot and an extra boolean variable. It's alsouseful to reuse our message box. There's no need for themaybeSave() function to return a value since we won't be gettinga value right away.
The reallyQuit variable is set to true in setCurrentFile()(shown later), indicating that we can quit when there are no pending changesto be saved. The reallyQuit variable is set to false in thedocumentWasModified() function if the document is modified:
void MainWindow::documentWasModified() { reallyQuit = !textEdit->document()->isModified(); setWindowModified(!reallyQuit); }
We must modify the closeEvent() slightly to make it work in the documentmodal style:
void MainWindow::closeEvent(QCloseEvent *event) { if (reallyQuit) { writeSettings(); event->accept(); } else { maybeSave(); event->ignore(); } }
Here we check our reallyQuit variable. If it's true, we can safelyaccept the event and close the window. Otherwise, we ignore the close eventand call our new maybeSave() function.
In maybeSave(), we create a warning message box as a sheet if we don'thave one already, set its button text to be more helpful, connect itsfinished() signal to our finishClose() slot:
void MainWindow::maybeSave() { if (!messageBox) { messageBox = new QMessageBox(tr("SDI"), tr("The document has been modified.\n" "Do you want to save your changes?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::Default, QMessageBox::No, QMessageBox::Cancel | QMessageBox::Escape, this, Qt::Sheet); messageBox->setButtonText(QMessageBox::Yes, isUntitled ? tr("Save...") : tr("Save")); messageBox->setButtonText(QMessageBox::No, tr("Don't Save")); connect(messageBox, SIGNAL(finished(int)), this, SLOT(finishClose(int))); } messageBox->show(); }
Finally, we show the message box and continue on our way. The message boxwill block any further access on the window until the user decides what to do.At that point, the finished() signal will be emitted with informationabout the button that was pressed.
In the finishClose() slot, we can finish closing the window if the userselects "Save..." and saves the document, or selects "Don't Save" instead.
void MainWindow::finishClose(int sheetValue) { switch (sheetValue) { case QMessageBox::Yes: reallyQuit = save(); if (!reallyQuit) return; break; case QMessageBox::No: reallyQuit = true; break; case QMessageBox::Cancel: default: return; } close(); }
If the cancel button was pressed, we don't want to do anything, so we leave theslot immediately. Otherwise, if the user chose to save, we test if the savesucceeds. If it doesn't save successfully, we also leave immediately. If wesucceed or the user doesn't want to save, we call close() again, which willfinally close the window.
New Looks
Qt 4's support for the Mac's native interfaces makes it possible to giveapplications even more of a native look and feel.
Brushed Metal
Along with the standard windows, you can also use windows with a brushed metalappearance by adding the WA_MacMetalStyle attribute to your window.
Custom Dock Menus
It has always been possible to change the icon on the dock by usingQApplication::setIcon(). It is now also possible to set a QMenu for thedock icon as well. This is available through a C++ function, but is not ina header, so you need to extern it yourself.
QMenu *menu = new QMenu; // Add actions to the menu // Connect them to slots ... extern void qt_mac_set_dock_menu(QMenu *); qt_mac_set_dock_menu(menu);
Under the Hood
Qt 4 uses the latest technologies from Apple's Human InterfaceToolbox. This means that each QWidget is an HIView and we useQuartz2D for the underlying paint engine. It also means that we getcomposited windows "for free". However, it also means that youcan't paint on widgets outside of a paint event.
This is different from Qt 3 on Mac OS X, where all the widgets weresimply regions in the window. This also makes it possible to placeother HIViews inside a Qt window, or Qt widgets inside other Carbonwindows.
New Events
Qt 4 also introduces some new events that are currently only sent on Mac OS X,but help keep code platform-independent.
QFileOpenEvent
Avid readers of Qt Quarterly will remember an article in issue 12 about dealingwith the Apple Open Event. This has been simplified greatly with theintroduction of the FileOpen event type. Now, all that's needed is to subclass QApplication, reimplement event(), and handle the FileOpen event.Here's a sample implementation:
class AwesomeApplication : public QApplication { Q_OBJECT public: ... protected: bool event(QEvent *); ... private: void loadFile(const QString &fileName); }; bool AwesomeApplication::event(QEvent *event) { switch (event->type()) { case QEvent::FileOpen: loadFile(static_cast<QFileOpenEvent *>( event)->file()); return true; default: return QApplication::event(event); } }
You still have to create a custom Info.plist that registers yourapplication with the proper file types. The information is included back in issue 12.
QIconDragEvent
You can set the icon on the window with QWidget::setIcon(). On Mac OS X,this is called a proxy icon because it can also act as a "proxy" for thefile you are working on. This means you can drag the icon and use it as a filereference. You can use Command+Click on the icon to reveal the file'slocation.
The QIconDragEvent is sent whenever someone clickson the proxy icon. With a little bit of code, you can create your own proxyicon. We'll transform Qt's Application example to work with this.
class MainWindow : public QMainWindow { Q_OBJECT ... protected: bool event(QEvent *event); ... private slots: void openAt(QAction *where); ... private: QIcon fileIcon; ... };
We have added some new functions to our MainWindow class. Theobvious one is the event() function. But we will need the openAt()helper slot later. We keep a QIcon around for the file icon which weobtain in the constructor by calling the style's standardIcon() functionto get the icon from QStyle's repository:
fileIcon = style()->standardIcon(QStyle::SP_FileIcon, 0, this);
The real action happens in our event() function:
bool MainWindow::event(QEvent *event) { if (!isActiveWindow()) return QMainWindow::event(event);
First, we only really care about processing the IconDrag event if ourwindow is active (if it isn't, we just call our super class's function).We check the type and if it's the IconDrag event, we acceptthe event and check the current keyboard modifiers:
switch (event->type()) { case QEvent::IconDrag: { event->accept(); Qt::KeyboardModifiers currentModifiers = qApp->keyboardModifiers(); if (currentModifiers == Qt::NoModifier) { QDrag *drag = new QDrag(this); QMimeData *data = new QMimeData(); data->setUrls(QUrl::fromLocalFile(curFile)); drag->setMimeData(data);
If we have no modifiers then we want to drag the file. So, we first createa QDrag object and create some QMimeData that contains the QUrl ofthe file that we are working on.
QPixmap cursorPixmap = style()->standardPixmap(QStyle::SP_FileIcon, 0, this); drag->setPixmap(cursorPixmap); QPoint hotspot(cursorPixmap.width() - 5, 5); drag->setHotSpot(hotspot); drag->start(Qt::LinkAction | Qt::CopyAction);
Since we want to create the illusion of dragging an icon, we use the iconitself as the drag's pixmap, set the hotspot of the cursor to be at thetop-right corner of the pixmap. We then start the drag, allowing only copyand link actions.
When users Command+Click on the icon, we want to show a popup menushowing where the file is located. The Command modifier is represented bythe generic Qt::ControlModifier flag:
} else if (currentModifiers==Qt::ControlModifier) { QMenu menu(this); connect(&menu, SIGNAL(triggered(QAction *))); QFileInfo info(curFile); QAction *action = menu.addAction(info.fileName()); action->setIcon(fileIcon);
We start by creating a menu and connecting itstriggered signal to the openAt() slot. We then split up our path withthe file name and create an action for each part of the path.
QStringList folders = info.absolutePath().split('/'); QStringListIterator it(folders); it.toBack(); while (it.hasPrevious()) { QString string = it.previous(); QIcon icon; if (!string.isEmpty()) { icon = style()->standardIcon(QStyle::SP_DirClosedIcon, 0, this); } else { // At the root string = "/"; icon = style()->standardIcon(QStyle::SP_DriveHDIcon, 0, this); } action = menu.addAction(string); action->setIcon(icon); }
We also make sure we pick an appropriate icon for that part of the path.
QPoint pos(QCursor::pos().x() - 20, frameGeometry().y()); menu.exec(pos);
Finally, we place the menu in a nice place and call exec() on it.
We ignore icon drags using other combinations of modifiers; even so, we havehandled the event so we return true:
} else { event->ignore(); } return true; } default: return QMainWindow::event(event); } }
Now we need to take a look at the openAt() slot:
void MainWindow::openAt(QAction *action) { QString path = curFile.left( curFile.indexOf(action->text())) + action->text(); if (path == curFile) return; QProcess process; process.start("/usr/bin/open", QStringList() << path, QIODevice::ReadOnly); process.waitForFinished(); }
This might not be the most comprehensible code that ever was written, but itserves our purpose. We first take the text of the QAction passed in andtry to construct a path from it. If the path is the current file, there is noneed to do anything. Otherwise we create a QProcess,call /usr/bin/open on the path, and wait for the process to finish.The open command will query Launch Services for what it should do with thepath and send the appropriate open event to the correct program. For directories,Finder will open a window at that location. While there is certainly more thanone way to deal with paths, this one requires the least amount of effort fromus.
This completes our implementation to deal with the proxy icon, but there's abit more work we need to do to add the finishing touches that give richfeedback to our users. Let's take a look at what other things we should do:
void MainWindow::setCurrentFile(const QString &fileName) { curFile = fileName; textEdit->document()->setModified(false); setWindowModified(false); QString shownName; QIcon icon; if (curFile.isEmpty()) { shownName = "untitled.txt"; } else { shownName = strippedName(curFile); icon = fileIcon; } setWindowTitle(tr("%1[*] - %2").arg(shownName).arg(tr("Application"))); setWindowIcon(icon); }
The first thing we need to do is make sure that we don't show an icon when westart with a brand new document. The main reason is that there is no fileto save yet, so it is impossible to drag or show where it is in the filesystem. If we are opening a file that does exist, we definitely want the icon,so we set it to the file icon we created at start up.
If the document is modified, we of course mark it in the title bar withsetWindowModified(), but we can also darken the icon as well (with afunction not shown here) to make it a little bit more obvious.
void MainWindow::documentWasModified() { bool modified = textEdit->document()->isModified(); if (!curFile.isEmpty()) { if (!modified) { setWindowIcon(fileIcon); } else { static QIcon darkIcon; if (darkIcon.isNull()) darkIcon = QIcon(darkenPixmap(fileIcon.pixmap(16, 16))); setWindowIcon(darkIcon); } } setWindowModified(modified); }
Here we simply create a "darkened" icon from our normal one by converting thepixmap to an image and darkening each pixel. We keep a static QIcon aroundso we only have to do this darkening once. When the file is no longer modified,as a result of either saving or undoing, we simply reset our file icon.
And with that we have enabled our application to make use of proxy icons.
Conclusions
This was only a whirlwind tour of various features that wereintroduced in Qt/Mac for Qt 4. More information about Mac-specificissues, such as configurationand deployment (including qmake'ssupport for Universal Binaries), is available in the Qt documentationand on the Trolltech website.
Copyright © 2006 Trolltech | Trademarks |