I. L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Digia à 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 Johan Thelin paru dans la Qt Quarterly Issue 27.

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

Le panneau de contrôle typique ne fonctionne pas comme une interface fenêtrée. Il n'exécute pas d'application utilisateurs multiples. Il n'utilise pas non plus un clavier grand format. Seuls un écran tactile et un petit groupe de boutons basiques sont utilisés.

Dans cet article, on présente un patron basique qui sera utilisé pour implémenter ce type d'application. On parle de patron, car chacun de ces systèmes a ses propres particularités et il est donc difficile de faire un moule pour tous.

III. La pile

Tous ces systèmes sont construits autour de fenêtres plein écran - des « panneaux ». La plupart de ces panneaux sont assez statiques et sont créés au lancement de l'application. Pour les instancier une seule fois, le patron « singleton » pourra être utilisé. Ceci reporte la création des panneaux à leur première utilisation mais évite de les créer plusieurs fois.

La construction importante des blocs forme la base du patron : la pile de panneaux. Tous les panneaux sont sauvegardés dans une QStackWidget. Cette pile de panneaux est sauvegardée dans un widget qui agit comme une application à fenêtre principale. Comme on le peut faire dans une application principale, il y a une classe « singleton » et elle s'appelle PanelStack. Cette pile de panneaux contient tous les panneaux et laisse le choix d'afficher un des panneaux en référant à son index qui lui aura été donné lors de son ajout dans la pile.

 
Sélectionnez
class PanelStack : public QWidget
{
   Q_OBJECT
    
public:
   static PanelStack *instance();
    
   int addPanel(AbstractPanel *);
   void showPanel(int);
    
private:
   ...
};


Ainsi, chaque panneau est actuellement une implémentation de AbstractPanel et tous les panneaux sont des singletons. L'idée est que, lors du premier appel d'une instance d'un panneau, il soit lui-même ajouté à la pile. Ensuite, chaque panneau fournit une méthode appelée showPanel(). Elle initialise le panneau et lui indique la pile de panneaux où il doit s'afficher. Ceci veut dire que chaque panneau garde son propre index ; tout ce qu'il suffit de savoir lors de l'implémentation est qu'il faut appeler la méthode showPanel() du panneau à afficher.

 
Sélectionnez
class AbstractPanel : public QWidget
{
protected:
    AbstractPanel(QWidget *parent = 0);
    
    void addPanelToStack();
    int panelIndex() const;
    
private:
    ...
};


L'implémentation actuelle du singleton est faite dans chaque sous-classe de AbstractPanel. Ceci est également le cas lors de l'ajout de la méthode showPanel(). On a laissé de côté la méthode virtuelle showPanel(), si différents panneaux ont besoin de différents arguments lors de leur initialisation.

IV. Un exemple

Continuons avec un exemple, un panneau de contrôle de VCR (magnétoscope). Il consiste en quatre panneaux : le menu principal, le panneau de lecture (avec les boutons lecture, pause et arrêt), le panneau d'enregistrement (qui permet à l'utilisateur de gérer les programmations d'enregistrements) et, enfin, le panneau des détails d'un enregistrement (laissant l'utilisateur configurer une programmation d'enregistrement).

Chacun de ces panneaux peut être construit en utilisant Qt Designer et leurs interfaces seront globalement les mêmes. Dans cet article, on s'arrête sur le widget RecordingPanel. L'exemple entier est disponible.

Image non disponible

Figure 1 - Panneau de programmation des enregistrements

Avant de continuer, un petit mot à propos de la sauvegarde des données depuis le code de l'interface utilisateur. On utilise une classe d'enregistrement en parallèle des classes de panneaux affichées ici et de la pile de panneaux. Ainsi, tous les panneaux peuvent accéder aux données indépendamment les uns des autres.

Dans cet exemple trivial, on utilise une QStringList pour garder tous les enregistrements, mais dans une application réelle il faudra probablement utiliser un singleton, avec une possibilité de couplage avec une interface QAbstractModel. On commence par déclarer la classe comme un singleton et fournir la méthode showPanel(). Dans ce cas, le panneau affiche toujours tous les enregistrements, on n'a donc pas besoin d'argument.

 
Sélectionnez
class RecordingsPanel : public AbstractPanel
{
public:
    static RecordingsPanel *instance();
    void showPanel();
    ...
private:
    RecordingsPanel();
    static RecordingsPanel *m_instance;
    ...
};

Voici maintenant lL'implémentation suit. Noter que les méthodes d'implémentation ajoutent le panneau à la pile. Ceci est fait pour autoriser la méthode addPanel() à utiliser les méthodes, signaux et slots de la classe actuelle en cours d'implémentation et non la classe de base. Si l'appel de la méthode addPanelToStack() est effectué depuis le constructeur de AbstractPanel, les méthodes virtuelles, signaux et slots devront l'être depuis AbstractPanel et non depuis RecordingPanel.

 
Sélectionnez
RecordingsPanel *RecordingsPanel::m_instance = 0;
    RecordingsPanel *RecordingsPanel::instance()
{
    if (!m_instance)
    {
        m_instance = new RecordingsPanel();
        m_instance->addPanelToStack();
    }
    
    return m_instance;
}
    
void RecordingsPanel::showPanel()
{
    PanelStack::instance()->showPanel(panelIndex());
}


Le patron singleton, qui est montré ci-dessus, est utilisé pour tous les panneaux.

Maintenant, le panneau en lui-même (voir son image ci-dessus) : on peut voir que ce panneau consiste en une liste d'enregistrements et quatre boutons. Chacun de ses boutons dispose d'un slot correspondant.

 
Sélectionnez
class RecordingsPanel : public AbstractPanel
{
public:
    static RecordingsPanel *instance();
    void showPanel();
    
private slots:
    void addClicked();
    void editClicked();
    void removeClicked();
    void backClicked();
};

Les slots permettant la navigation entre les panneaux sont add(), edit() et back(). Le bouton Remove Recording supprime simplement un enregistrement dans la liste, mais laisse la liste affichée.

Le bouton Back demande au menu principal de s'afficher en utilisant la méthode addPanel(), tandis que les boutons Add Recording et Edit Recording passent l'index de l'enregistrement au panneau de détails de l'enregistrement. Pour les nouveaux enregistrements, un index de -1 est passé pour indiquer qu'un nouvel enregistrement vient d'être créé.

 
Sélectionnez
void RecordingsPanel::addClicked()
{
    RecordingDetailsPanel::instance()->showPanel(-1);
}
    
void RecordingsPanel::editClicked()
{
    RecordingDetailsPanel::instance()->showPanel(
            ui.recordingsList->currentRow());
}
    
void RecordingsPanel::backClicked()
{
    MainMenu::instance()->showPanel();
}

Dans la classe RecordingDetailsPanel, la méthode showPanel() contrôle que l'interface utilisateur est bien initialisée.

 
Sélectionnez
void RecordingDetailsPanel::showPanel(int recordIndex)
{
    m_currentIndex = recordIndex;
    
    if (m_currentIndex == -1)
        ui.lineEdit->setText("");
    else
        ui.lineEdit->setText(recordings[m_currentIndex]);
    
    PanelStack::instance()->showPanel(panelIndex());
}

L'implémentation de la pile de panneaux pour gérer ce cas est très simple. Le widget gère un QStackWidgetQStackWidget. Quand on ajoute un panneau à l'objet PanelStack, on ajoute ce panneau au et retourne son index.

 
Sélectionnez
int PanelStack::addPanel(AbstractPanel *panel)
{
    return m_panelStack->addWidget(panel);
}

D'une façon similaire, la méthode showPanel() modifie simplement le widget affiché actuellement dans la pile de widgets.

 
Sélectionnez
void PanelStack::showPanel(int index)
{
   m_panelStack->setCurrentIndex(index);
}

Le fait d'ajouter un widget sur un autre widget n'est pas une bonne solution pour revenir en arrière. Cependant, si on doit ajouter de nouvelles fonctionnalités à la pile de panneaux, on doit commencer par faire quelques adaptations. On examinera une solution au point suivant.

V. Garder l'historique dans la pile

Dans l'exemple précédent, on a construit un ensemble de panneaux qui permettait de naviguer suivant un patron statique. Le fait de cliquer sur le bouton Back quand le panneau des enregistrements passe au menu principal et inversement. Il s'agit d'une bonne solution pour les petits systèmes, mais ceci veut dire qu'on ne peut pas réutiliser les panneaux dans différents emplacements dans l'arbre de navigation.

Pour résoudre ce problème, on peut garder l'historique de navigation dans la pile de panneaux. À chaque fois qu'un panneau est affiché, son index est ajouté dans une pile d'historique. Plutôt que de se déplacer vers le panneau parent quand on quitte le panneau, une méthode back() est ajoutée à la pile de panneaux. Ceci déplace d'un pas dans la pile d'historique jusqu'à ce que le premier panneau soit atteint. Dans l'esprit des navigateurs web, une méthode home() est aussi ajoutée. Celle-ci permet d'afficher le premier panneau de l'historique sur un simple appel.

La réutilisation des panneaux est un des avantages de l'utilisation des panneaux. Un autre avantage est que les dépendances au niveau du code source sont réduites : chaque panneau a seulement besoin de connaitre la pile de panneaux et son emplacement dans la profondeur de l'arbre de navigation. Les panneaux parents ne sont pas importants. Finalement, on crée les slots back() et home(), en supprimant le besoin de créer des slots dans les panneaux.

 
Sélectionnez
class PanelStack : public QWidget
{
    ...
    int addPanel(AbstractPanel *);
    void showPanel(int);
    
public slots:
    void back();
    void home();
    
private:
    ...
};

Les modifications à effectuer sont la création de la variable d'instance m_history et sa mise à jour quand la méthode showPanel() est appelée. Quand la méthode back() est appelée, on dépile un index ; quand la méthode home(), on dépile tous les index mais on utilise seulement le premier.

 
Sélectionnez
void PanelStack::showPanel(int index)
{
    m_history.push(index);
    showTopPanel();
}
    
void PanelStack::back()
{
    if (m_history.count() <= 1)
        return; // ne peut retourner un historique.
    
    m_history.pop();
    showTopPanel();
}
    
void PanelStack::home()
{
    if (m_history.count() <= 1) // On affiche la page principale
        return; // s'il n'y a pas de panneau à afficher.
    
    while (m_history.count() > 1)
            m_history.pop();
    
    showTopPanel();
}

Ce design force une règle pour l'utilisateur. Le premier panneau affiché est le menu et on ne peut pas dépiler l'historique avant celui-ci. Comme on a pu le voir, les méthodes home(), back() et showPanel() ne peuvent mettre à jour la pile de widgets. Il faut plutôt appeler la méthode showTopPanel(). C'est une méthode privée dont voici les détails :

 
Sélectionnez
void PanelStack::showTopPanel()
{
    m_panelStack->setCurrentIndex(m_history.top());
    dynamic_cast<AbstractPanel*>(m_panelStack->widget(
       m_history.top()))->enterPanel();
}

Elle ne se contente pas d'afficher le bon panneau, mais elle appelle la méthode enterPanel() du panneau. La méthode enterPanel() est une méthode virtuelle ajoutée à la classe AbstractPanel. L'implémentation par défaut est factice et ne fait rien.

L'idée ici est qu'un panneau peut apparaître deux fois, comme le résultat d'un appel à sa méthode showPanel() et d'un évènement de la pile de panneaux, résultant d'un appel aux méthodes home() et back(). Dans le dernier cas, le panneau doit savoir qui l'a appelé pour s'afficher afin qu'il mette à jour son contenu. Par exemple, le panneau des enregistrements met à jour sa liste d'enregistrements.

VI. Ajouter des éléments sur la pile

On a maintenant une méthode qui est appelée à chaque fois qu'un nouveau panneau doit être affiché. Pourquoi ne pas l'utiliser pour mettre à jour certaines parties de l'interface utilisateur ? Par exemple, si une barre de titre est une partie de chaque panneau, pourquoi ne pas la placer dans la pile de panneaux et ajouter une méthode virtuelle appelée titleText() pour chaque panneau abstrait ?

 
Sélectionnez
class AbstractPanel : public QWidget
{
public:
    virtual void enterPanel() {}
    virtual QString titleText() const = 0;
    ...
};

Une petite modification à la pile de panneaux met à jour la variable d'instance m_titleLabel chaque fois qu'un nouveau panneau est modifié.

 
Sélectionnez
void PanelStack::showTopPanel()
{
    m_panelStack->setCurrentIndex( m_history.top() );
    AbstractPanel *panel = dynamic_cast<AbstractPanel*>(
        m_panelStack->widget(m_history.top()));
    
    panel->enterPanel();
    m_titleLabel->setText(panel->titleText());
}

Il y a d'autres éléments qui peuvent être ajoutés à la pile de panneaux pour mettre en place une infrastructure commune. Si besoin, les boutons Back et Home et peut-être quelques raccourcis à ajouter sur des formulaires spécifiques. Ces boutons pourront être cachés et affichés en ajoutant des méthodes virtuelles comme hasBack() et hasHome() dans la classe AbstractPanel.

VII. Plus d'informations sur les piles

L'approche des piles ne tient compte que du transfert d'informations des panneaux vers la pile. On peut travailler sur d'autres solutions. Ceci apporte de l'aide quand on interagit avec des systèmes avec des méthodes de contrôle rudimentaires.

Par exemple, implémenter un plug-in de clavier avec un pavé numérique consiste en quatre boutons placés après l'écran peut être une solution exagérée. Plutôt, un panneau récupère les événements dans la pile de panneaux et, depuis la pile, passe les événements au panneau courant à travers un ensemble de méthodes virtuelles. Une solution personnalisée, oui, mais qui sera probablement utilisée avec un matériel personnalisé.

J'ai trouvé l'approche en pile de panneaux utile dans des cas bien ciblés et cela fonctionne toujours bien tant que c'est flexible et adaptatif pour des applications spécifiques. J'espère que vous avez trouvé que cette solution est pensée pour des systèmes embarqués.

Le code source des exemples décrits dans cet article est disponible.

VIII. 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 et Claude Leloup pour sa relecture.