I. L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Nokia à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est une traduction de l'article original écrit par Trenton Schulz paru dans la Qt Quarterly Issue 18.

Cet article est une traduction de l'un des tutoriels en anglais écrits par Nokia Corporation and/or its subsidiary(--ies), inclus dans la documentation de Qt. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II. Introduction

Sur Mac OS X, les utilisateurs s'attendent à ce que les applications Qt profitent des types spécifiques de fenêtres, des styles et évènements disponibles sur leur plateforme. On passe en revue les fonctionnalités spécifiques de ce système d'exploitation de façon qu'elles soient compréhensibles des développeurs sur les autres plateformes.

III. Nouveaux types de fenêtres

Mac OS X fournit deux types de fenêtres qui sont uniques sur cette plateforme : les feuilles et les tiroirs.

Image non disponible

Les tiroirs sont des fenêtres qui glissent sur les autres fenêtres et sont typiquement utilisées pour les informations qui sont liées à la fenêtre parente. On peut créer un tiroir en passant Qt::Drawer comme option au constructeur de QWidget. Ceci va juste créer une fenêtre sur les autres plateformes. Dans Qt, les tiroirs donnent une bonne expérience utilisateur si on les utilise avec QDockWidget.

Image non disponible

Les feuilles sont des boîtes de dialogue spéciales qui arrivent au-dessus des autres fenêtres. Chaque feuille est liée à une fenêtre et aide les utilisateurs à lier une boîte de dialogue à un certain widget. En général, on réalise cette liaison avec les boîtes de dialogue de type modal. Ainsi, chaque boîte de dialogue peut interrompre les traitements d'une fenêtre.

Avec Qt, on peut créer une feuille en passant Qt::Sheet comme option dans le constructeur de la fenêtre ; ceci n'a pas d'effet sur les autres plateformes.

Grâce au mécanisme de signaux et slots de Qt, il est également possible de créer des boîtes de dialogue modales avec Qt, avec juste un petit peu de rigueur. Au lieu d'utiliser QDialog::exec() pour arrêter l'exécution dans une méthode, les traitements peuvent être terminés dans un slot. Ceci se modifie dans la classe MainWindow. Voici un exemple SDI Qt pour montrer les avantages de cette fonctionnalité.

 
Sélectionnez
    ...
    private slots:
        void finishSheet(int sheetValue);
    
    private:
        void maybeSave();
        bool reallyQuit;
        QMessageBox *messageBox;
    ...


On aura besoin d'un nouveau slot et d'une variable booléenne. Il est possible de réutiliser la boîte de message. La méthode maybeSave() ne doit pas forcément renvoyer quelque chose, puisqu'on ne lui donne pas de valeur.

La variable reallyQuit est initialisée à true dans la méthode setCurrentFile() (voir plus bas), indiquant qu'on peut quitter quand il n'y a pas de changement à sauvegarder. La variable reallyQuit est initialisée à false dans la méthode documentWasModified() si le document est modifié :

 
Sélectionnez
    void MainWindow::documentWasModified()
    {
        reallyQuit = !textEdit->document()->isModified();
        setWindowModified(!reallyQuit);
    }


On pourra légèrement modifier la méthode closeEvent() pour faire fonctionner la fenêtre dans un style de document modal :

 
Sélectionnez
    void MainWindow::closeEvent(QCloseEvent *event)
    {
        if (reallyQuit) {
            writeSettings();
            event->accept();
        } else {
            maybeSave();
            event->ignore();
        }
    }


Ici, on contrôle la variable reallyQuit. Si elle vaut true, on peut sans danger accepter l'évènement et fermer la fenêtre. Sinon, on ignore l'évènement de fermeture et on appelle la méthode maybeSave().

Dans la méthode maybeSave(), un message d'avertissement est créé comme une feuille si une boîte de message n'existe pas déjà ; le texte du bouton est modifié pour être plus utile, on connecte son signal finished() au slot finishClose() :

 
Sélectionnez
    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();  
    }


Finalement, on affiche le message et on continue le traitement. Le message pourra bloquer tout accès utilisateur à la fenêtre jusqu'à ce que l'utilisateur décide quoi faire. Arrivé là, le signal finished() pourra être émis avec l'information indiquant quel bouton a été pressé.

Dans le slot finishClose(), on finit de fermer la fenêtre si l'utilisateur a sélectionné « Save » en sauvegardant le document ou s'il a sélectionné « Don't Save ».

 
Sélectionnez
    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();
    }


Si le bouton « Cancel » est pressé, il n'y a pas grand-chose à faire, on quitte donc immédiatement le slot. Sinon, si l'utilisateur a choisi de sauvegarder, on teste alors si la sauvegarde a réussi. En cas d'échec, on quitte immédiatement. Si la sauvegarde a réussi ou que l'utilisateur n'a pas choisi de sauvegarder, alors on appelle encore la méthode close() qui ferme finalement la fenêtre.

IV. Nouveaux looks

Le support de Qt 4 pour les interfaces Mac natives permet de donner aux applications un look and feel encore plus natif.

IV-A. Métal brossé

Avec les fenêtres standards, on peut aussi utiliser les fenêtres avec une apparence de métal brossé en ajoutant l'attribut WA_MacMetalStyle à la construction de la fenêtre.

Image non disponible

IV-B. Menus de dock personnalisés

Il est également possible de changer les icônes sur le dock en utilisant QApplication::setWindowIcon() et de paramétrer une classe QMenu depuis l'icône du dock. C'est possible grâce à une méthode C++, mais comme elle n'est pas présente dans un fichier d'en-tête, il faut l'externaliser.

 
Sélectionnez

            QMenu *menu = new QMenu;
            // Ajout des actions au menu
            // puis connexions aux slots
            ...
            extern void qt_mac_set_dock_menu(QMenu *);
            qt_mac_set_dock_menu(menu);
        

IV-C. Sous le capot

Qt 4 utilise les dernières technologies venant des barres d'outils d'Apple. Chaque objet QWidget est une HIView et utilise une Quartz2D pour le moteur de rendu sous-jacent. On peut aussi créer des composants librement. Cependant, on peut enfin ne pas dessiner sur les widgets en dehors d'un évènement de dessin.

Dans Qt 3 sous Mac OS X, la situation se présentait de manière totalement différente : tous les widgets étaient simplement des zones dans la fenêtre.

V. Nouveaux évènements

Qt 4 introduit aussi quelques nouveaux évènements qui sont actuellement seulement envoyés sous Mac OS X, mais permettant de garder le code indépendant de la plateforme.

V-A. QFileOpenEvent

Les lecteurs avides de Qt Quarterly se souviennent d'un article du numéro 12 à propos du traitement des évènements d'ouverture Apple. Ceci a été considérablement simplifié avec l'introduction du type d'évènement FileOpen. Maintenant, il suffit de de sous classer QApplication, ré implémenter event() et traiter l'évènement FileOpen. Voici un exemple simple d'implémentation :

 
Sélectionnez
    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);
        }
    }

Il faut encore créer un fichier Info.plist personnalisé qui enregistrera l'application avec le type de fichier approprié. Cette information est présente dans le numéro 12 de Qt Quarterly.

V-B. QIconDragEvent

On peut paramétrer l'icône de la fenêtre avec la méthode QWidget::setIcon(). Sur Mac OS X, c'est ce qu'on appelle une icône proxy, car elle peut aussi agir comme un « proxy » pour le fichier sur lequel on travaille. Ceci veut dire qu'on peut déplacer l'icône et l'utiliser dans un dossier de référence. On peut utiliser la combinaison Command + clic sur l'icône pour donner l'emplacement du fichier.

L'évènement QIconDragEvent est envoyé quand on clique sur l'icône proxy. À l'aide d'un petit bout de code, on peut créer une telle icône. Voici l'application d'exemple Qt modifiée à cette fin :

 
Sélectionnez
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
        ...
    protected:
        bool event(QEvent *event);
        ...
    private slots:
        void openAt(QAction *where);
        ...
    private:
        QIcon fileIcon;
        ...
    };


On ajoute des nouvelles méthodes à la classe MainWindow. La méthode event() est évidente. On aura cependant besoin du slot openAt() plus tard. On garde un objet QIcon pour le fichier correspondant à l'icône qu'on obtiendra dans le constructeur en appelant la méthode de style standardIcon() pour récupérer l'icône du dépôt de QStyle.

 
Sélectionnez
    fileIcon = style()->standardIcon(QStyle::SP_FileIcon, 0, this);


L'action réelle se passe dans la méthode event() :

 
Sélectionnez
    bool MainWindow::event(QEvent *event)
    {
        if (!isActiveWindow())
            return QMainWindow::event(event);


Tout d'abord, on peut traiter l'évènement IconDrag si la fenêtre est active (si elle ne l'est pas, on appelle juste la méthode de la classe mère). On contrôle le type d'évènement et, s'il s'agit d'un évènement IconDrag, on accepte l'évènement et on contrôle les modificateurs claviers courants :

 
Sélectionnez
  switch (event->type()) {
     caseQEvent::IconDrag: {
         event->accept();
         Qt::KeyboardModifiers currentModifiers = qApp-keyboardModifiers();

          if (currentModifiers == Qt::NoModifier) {
              QDrag *drag = new QDrag(this);
              QMimeData *data = new QMimeData();
              data->setUrls(QList<QUrl>() << QUrl::fromLocalFile(curFile));
              drag->setMimeData(data);


Si aucun modificateur n'est présent, l'utilisateur souhaite déplacer le fichier. On crée donc un objet QDrag et un objet QMimeData qui contient l'URL QUrl du fichier avec lequel on doit travailler.

 
Sélectionnez
      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);


Puisqu'on veut créer l'illusion de faire glisser une icône, on utilise l'icône elle-même comme le déplacement d'un pixmap ; on paramètre l'emplacement du curseur pour qu'il se trouve au coin en haut à droite du pixmap. Ensuite, on lance le déplacement en autorisant seulement la copie et le lien des actions.

Lorsqu'un utilisateur effectue la combinaison de touche Command + clic sur l'icône, on veut afficher un menu affichant l'emplacement du fichier. Le modificateur Command est représenté par l'option générique Qt::ControlModifier :

 
Sélectionnez
    } else if (currentModifiers==Qt::ControlModifier) {
            QMenu menu(this);
            connect(&menu, SIGNAL(triggered(QAction *)), this, SLOT(openAt(QAction *)));

            QFileInfo info(curFile);
            QAction *action = menu.addAction(info.fileName());
            action->setIcon(fileIcon);


On commence par créer un menu et on connecte son signal de déclenchement au slot openAt(). Ensuite, on découpe le chemin avec le nom de fichier et on crée une action pour chaque partie du chemin :

 
Sélectionnez
      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 { // A la racine
              string = "/";
              icon = style()->standardIcon(QStyle::SP_DriveHDIcon, 0, this);
          }
          action = menu.addAction(string);
          action->setIcon(icon);
      }


On peut également s'assurer qu'on choisit une icône appropriée pour cette partie du chemin.

 
Sélectionnez
    QPoint pos(QCursor::pos().x() - 20, frameGeometry().y());
    menu.exec(pos);


Finalement, on place le menu dans une bonne place, puis on appelle la méthode exec() sur le menu.

On ignore les déplacements d'icônes utilisant une autre combinaison des modificateurs ; malgré tout, on a traité l'évènement, on retourne donc true :

 
Sélectionnez
   } else {
                event->ignore();
            }
            return true;
          }
          default:
            return QMainWindow::event(event);
        }
    }


Voici la définition du slot openAt() :

 
Sélectionnez
    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();
    }


Ceci n'est peut-être pas le code le plus compréhensible que l'on vient d'écrire, mais il illustre bien le propos. Premièrement, on prend le texte passé en paramètre de l'action QAction et on essaie de construire un chemin à partir de celui-ci. Si le chemin est le fichier courant, nous n'avons plus rien d'autre à faire. Sinon, on crée un nouvel objet QProcess, on appelle « /usr/bin/open » sur ce chemin et on attend la fin du traitement. La commande open effectue une requête sur les services démarrés pour savoir ce qu'il y a lieu de faire avec ce chemin et envoie alors l'évènement d'ouverture approprié au bon programme. Pour les répertoires, l'application Finder ouvrira une fenêtre à l'emplacement désigné. Même s'il y a certainement plus d'un moyen de traiter les chemins, ce traitement nécessite le moins d'efforts.

Image non disponible

On complète l'implémentation pour terminer le traitement lié à l'icône proxy, mais ceci réclame un peu plus de travail pour l'ajout de la touche finale qui donnera un meilleur rendu aux utilisateurs. Jetons un coup d'œil aux éléments à ajouter :

 
Sélectionnez
    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);
    }


La première chose à faire est de s'assurer qu'on n'affiche pas d'icône quand on ouvre un nouveau document. La raison principale est qu'il n'y a pas de fichier à enregistrer, il est donc impossible de déplacer ou d'afficher quelque chose sur le système de fichiers. Si on ouvre un fichier qui existe, on veut afficher son icône, on donne donc en paramètre l'icône du fichier lors de la création à l'ouverture.

Si le document est modifié, on doit bien sûr l'indiquer dans la barre de titre avec la méthode setWindowModified(), mais cela peut aussi assombrir l'icône (avec une fonction non montrée ici) pour faire plus évident.

 
Sélectionnez
    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);
    }
Image non disponible

Ici, on crée simplement une icône assombrie depuis une icône normale en convertissant le dessin depuis une image et en assombrissant chaque pixel. On garde une QIcon statique afin que l'on construise l'icône une seule fois. Quand le fichier n'est pas modifié, à la suite d'un enregistrement ou non, on remet en place l'icône normale.

Ensuite, on active l'application pour utiliser l'icône proxy.

VI. Conclusion

Ceci est seulement un ensemble de fonctionnalités variées qui ont été introduites dans Qt pour Mac OS X avec Qt 4. Pour plus d'informations à propos de question spécifiques sur Mac, comme la configuration et le déploiement (incluant le support de qmake pour les binaires universels), voir la documentation.

VII. Remerciements

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisés à traduire cet article !

Je tiens à remercier Thibaut Cuvelier pour ses conseils ainsi que Maxime Gault et Jacques Thery pour leur relecture.