Traduction par Jean-luc Biord, du Site de la communauté Qt francophone.
English TOC.

2. Le modèle Objet de Qt

Qt est basé autour du modèle d'objet de Qt. Cette architecture est ce qui rend Qt puissant et facile à  employer. Elle est entièrement basée autour de la classe QObject et de l'outil moc.

En dérivant des classes de QObject un certain nombre d'avantages sont hérités. Ils sont énumérés ci-dessous :

Chacun de ces dispositifs est expliqué ci-dessous. Avant de continuer il est important de se rappeler que Qt est du C++ standard avec quelques macros, juste comme n'importe quelle autre application de C/C++. Il n'y a rien de non standard avec lui, ce qui est le mieux prouvé par le fait que le code est très portable.

Gestion Facile de la Mémoire

En créant une instance d'une classe dérivée de QObject il est possible de passer un pointeur vers un objet parent au constructeur. C'est la base de la gestion simplifiée de la mémoire. Quand un parent est supprimé, ses enfants sont supprimés aussi. Ceci signifie qu'une classe dérivée de QObject peut créer des instances d'enfants-QObject passant this comme parent sans s'inquiéter de leur destruction.

Pour une meilleure compréhension, voici un exemple. L'exemple 2-1 montre notre classe d'exemple. Elle est dérivée de QObject et affiche tout ce qui lui arrive sur la console. Ceci mettra en évidence ce qui se passe et à quel moment.

// A verbose object tells us what it is doing all the time
class VerboseObject : public QObject
{
public:
  VerboseObject( QObject *parent=0, char *name=0 ) : QObject( parent, name )
  {
    std::cout << "Created: " << QObject::name() << std::endl;
  }
  
  ~VerboseObject()
  {
    std::cout << "Deleted: " << name() << std::endl;
  }
  
  void doStuff()
  {
    std::cout << "Do stuff: " << name() << std::endl;
  }
};

Exemple 2-1

Pour faire quelque chose d'utile avec la classe, une routine main est exigée. Elle est montrée dans l'exemple 2-2. Notez que la première chose qui se produit est qu'une instance de QApplication est créée. Ceci est exigé pour presque toutes les applications de Qt et c'est une bonne façon d'éviter des problèmes inutiles. Le code crée une hiérarchie de mémoire représentée sur le schéma 2-1.

int main( int argc, char **argv )
{
  // Create an application
  QApplication a( argc, argv );
  
  // Create instances
  VerboseObject top( 0, "top" );
  VerboseObject *x = new VerboseObject( &top, "x" );
  VerboseObject *y = new VerboseObject( &top, "y" );
  VerboseObject *z = new VerboseObject( x, "z" );
  
  // Do stuff to stop the optimizer
  top.doStuff();
  x->doStuff();
  y->doStuff();
  z->doStuff();
  
  return 0;
}

Exemple 2-2

Soyez certain de comprendre comment l'arbre est traduit en code de l'exemple 2-2 et vice versa.

la hiérarchie de mémoire

Le schéma 2-1 la hiérarchie de mémoire

L'exemple 2-3 montre un exemple de fonctionnement du code d'exemple. Comme on peut le voir tous les objets sont supprimés, les parents d'abord et les enfants ensuite. Notez que chaque branche est supprimée complètement avant que la prochaine soit commencée. Comme on peut le voir z est supprimé avant y.

$ ./mem
Created: top
Created: x
Created: y
Created: z
Do stuff: top
Do stuff: x
Do stuff: y
Do stuff: z
Deleted: top
Deleted: x
Deleted: z
Deleted: y
$

Exemple 2-3

Si la référence du parent est enlevée de x et de y comme montré dans l'exemple 2-4, une fuite de mémoire se produit. C'est comme dans le test documenté de l'exemple 2-5.

  ...
  VerboseObject *x = new VerboseObject( 0, "x" );
  VerboseObject *y = new VerboseObject( 0, "y" );
  ...

Exemple 2-4

$ ./mem
Created: top
Created: x
Created: y
Created: z
Do stuff: top
Do stuff: x
Do stuff: y
Do stuff: z
Deleted: top
$

Exemple 2-5

La fuite de mémoire montrée ci-dessus ne fait aucun mal dans la situation actuelle, parce que l'application se termine juste après qu'elle soit lancée, mais dans une autre situation ceci pourrait être une menace potentielle à  la stabilité de système global.

Signaux et Slots

Les signaux et les slots sont ce qui rend les différents composants de Qt aussi réutilisables qu'ils le sont. Ils fournissent un mécanisme par lequel il est possible de librement relier ensemble les interfaces. Par exemple, une entrée de menu, un bouton poussoir, un bouton de barre d'outils et n'importe quel autre élément peuvent fournir le signal correspondant à  l'événement approprié "activé", "cliqué" ou n'importe quel autre. En reliant un tel signal aux slots de tous autres éléments et l'événement appelle automatiquement les slots.

Un signal peut également inclure des valeurs, de ce fait permettant de relier une réglette, un spinbox, un bouton ou n'importe quelle autre valeur produite par l'élément à  n'importe quel élément acceptant des valeurs, par exemple réglette, bouton ou un spinbox, ou une quelque chose de complètement différent comme un affichage à  cristaux liquides.

L'avantage principal des signaux et des slots est que l'émetteur n'a rien à  savoir au sujet du receveur et vice versa. Ceci permet d'intégrer beaucoup de composants facilement sans que le concepteur de composant est à  penser réellement à  la configuration utilisée.

Fonction ou slot ?
Pour rendre les choses un peu plus compliquées, l'environnement de développement avec Qt 3.1.x et supérieur se rapporte parfois à  des slots comme fonctions. Car ils se comportent de la même manière dans la plupart des cas donc ce n'est pas un problème, mais dans tout ce texte le terme "slot" sera employé.

Afin de pouvoir employer les signaux et les slots chaque classe doit être déclarée dans un fichier d'en-tête. L'implémentation est mieux placée dans un fichier cpp séparé. Le fichier d'en-tête est alors passé par un outil de Qt connu sous le nom de moc. Le moc produit un cpp contenant le code qui permet la prise en compte des signaux et des slots (et plus). Le schéma 2-2 illustre ce déroulement. Notez la convention d'appellation utilisée ( le préfixe de moc_ ) dans la figure.

The moc flow

Le schéma 2-2 déroulement de moc

Cette étape additionnelle de compilation peut sembler compliquer le processus de construction, mais il y a encore un autre outil de Qt, qmake. Il le rend aussi simple que qmake -project && qmake && make pour construire n'importe quelle application Qt. Ce sera décrit en détail plus loin.

Que sont les signaux et des slots - en réalité ?
Comme cité précédemment, une application de Qt est 100% C++. Ainsi, que sont les signaux et les slots, réellement ? Une part est composée des mots-clés réels, ils sont simplement remplacés par du C++ approprié par le préprocesseur. Les slots sont alors implémentés comme n'importe quelle méthode membre de classe tandis que les signaux sont implémentés par moc. Chaque objet tient alors une liste de ses connexions (quels slots sont activés par quel signal) et de ses slots (qui sont employés pour construire la table de connexions dans la méthode connect). Les déclarations de ces tables sont cachées dans la macro Q_OBJECT macro. Naturellement il y a plus que ceci, mais tout peut être vu en regardant dans un fichier cpp produit par  moc.

Une démonstration de base de la connexion entre les signaux et les slots est démontrée dans l'exemple ci-dessous. D'abord, la classe receveuse est montrée dans l'exemple 2-6. Notez que rien n'empêche une classe receveuse d'envoyer, c.-à -d. une classe simple peut avoir des signaux et des slots.

class Reciever : public QObject
{
  Q_OBJECT
  
public:
  Reciever( QObject *parent=0, char *name=0 ) : QObject( parent, name )
  {
  }
  
public slots:
  void get( int x )
  {
    std::cout << "Recieved: " << x << std::endl;
  }
};

Exemple 2-6

Cette classe commence par la macro Q_OBJECT qui est nécessaire si d'autres dispositifs que la gestion simplifiée de mémoire doivent être employés. Cette macro inclut quelques déclarations principales et sert de marqueur au moc qui indique que la classe doit être analysée. Notez en outre qu'une nouvelle section appelée public slots a été ajoutée à  la syntaxe.

Les exemples 2-7 et 2-8 contiennent les réalisations des classes émettrices. La première classe, SenderA. met en application le signal send qui est émis de la fonction membre doEmit. La deuxième classe émettrice, SenderB émet le signal transmit depuis le fonction membre doStuff. Notez que ces classes ont en commun d'hériter de QObject.

class SenderA : public QObject
{
  Q_OBJECT
  
public:
  SenderA( QObject *parent=0, char *name=0 ) : QObject( parent, name )
  {
  }
  
  void doSend()
  {
    emit( send( 7 ) );
  }
  
signals:
  void send( int );
};

Exemple 2-7

class SenderB : public QObject
{
  Q_OBJECT
  
public:
  SenderB( QObject *parent=0, char *name=0 ) : QObject( parent, name )
  {
  }
  
  void doStuff()
  {
    emit( transmit( 5 ) );
  }
  
signals:
  void transmit( int );
};

Exemple 2-8

Pour démontrer comment le code de moc est fusionné avec le code écrit par les programmeurs humains, cet exemple n'emploie pas la méthode qmake classique, mais inclut à la place le code explicitement. Pour invoquer moc, la ligne de commande suivante est employée : $QTDIR/bin/moc sisl.cpp -o moc_sisl.h. La première ligne de l'exemple 2-9 inclut le fichier résultant, moc_sisl.h. Le code d'exemple contient également le code créant les différents objets et reliant les différents signaux et slots. Ce morceau de code est le seul endroit approprié de la classe de réception et des classes d'envoi. Les classes elles-mêmes connaissent seulement la signature, également connue sous le nom d'interface, des signaux à  émettre ou recevoir.

#include "moc_sisl.h"

int main( int argc, char **argv )
{
  QApplication a( argc, argv );
  
  Reciever r;
  SenderA sa;
  SenderB sb;
  
  QObject::connect( &sa, SIGNAL(send(int)), &r, SLOT(get(int)) );
  QObject::connect( &sb, SIGNAL(transmit(int)), &r, SLOT(get(int)) );
  
  sa.doSend();
  sb.doStuff();
  
  return 0;
}        

Exemple 2-9

En conclusion, l'exemple 2-10 montre le résultat d'un essai. Les signaux sont émis et les receveurs affichent les valeurs.

$ ./sisl
Recieved: 7
Recieved: 5
$

Exemple 2-10

Valeurs dans le connect ?
Une fausse idée répandue est qu'il est possible de définir les valeurs à  envoyer avec un signal lors de la connexion, par exemple. connect( &a, SIGNAL(signal(5)), &b, SLOT(slot(int)) );. Ce n'est pas possible. Seulement les signatures des signaux sont utilisées dans l'appel de connexion. En émettant le signal un paramètre peut être fourni, et seulement alors. La version correcte du code précédent serait connect( &a, SIGNAL(signal(int)), &b, SLOT(slot(int)) ); et la valeur (5) serait indiquée en émettant le signal.

Propriétés

Un objet Qt peut avoir des propriétés. Ce sont simplement des valeurs qui ont un type et, au moins une fonction de lecture, mais probablement aussi une fonction d'écriture. Celles-ci, par exemple, sont employées par Qt Designer pour montrer les propriétés de tous les widgets. La documentation officielle de propriétés peut être trouvée ici.

Les propriétés sont non seulement une bonne manière d'organiser le code et de définir quelle fonction affecte quelle propriété. Elles peuvent également être employées comme forme primitive de réflexion. N'importe quel pointeur de QObject peut accéder aux propriétés de l'objet qu'il pointe. Même si c'est une classe dérivée, plus complexe.

Le code de l'exemple 2-11 démontre comment une classe avec des propriétés est déclarée. Le code montré appartient au fichier propobject.h. La macro Q_PROPERTY en combinaison avec le macro Q_ENUMS macro fait tout.

#ifndef PROPOBJECT_H
#define PROPOBJECT_H

#include <qobject.h>
#include <qvariant.h>

class PropObject : public QObject
{
  Q_OBJECT

  Q_PROPERTY( TestProperty testProperty READ testProperty WRITE setTestProperty )
  Q_ENUMS( TestProperty )

  Q_PROPERTY( QString anotherProperty READ anotherProperty )
  
public:
  PropObject( QObject *parent=0, char *name=0 );
  
  enum TestProperty { InitialValue, AnotherValue };
  
  void setTestProperty( TestProperty p );
  TestProperty testProperty() const;
  
  QString anotherProperty() const { return QString( "I'm read-only!" ); }

private:
  TestProperty m_testProperty;  
};

#endif

Exemple 2-11

Notez qu'il n'y a aucune virgule entre les paramètres de la macro Q_PROPERTY. La syntaxe est que d'abord le type de la propriété est déclaré, puis le nom suit. Ensuite qu'un mot-clé, READ ou WRITE,est rencontré et est suivi de la fonction correspondante de membre.

Lire et écrire les fonctions membres
Il n'y a rien de spécial au sujet de la lecture et l'écriture des fonctions membres. La seule restriction est que le lecteur doit correspondre au type de paramètre et ne prenne aucun argument (c.-à -d. il doit être du type void) tandis que celui qui écrit doit accepter seulement un argument et il doit être du type de paramètre.

Il est courant dans les habitudes de déclarer des fonctions qui écrivent comme slots. Ceci augmente le facilité de réutilisation puisque la plupart écrivent des fonctions comme des slots normaux. Certains de ces derniers sont également des candidats pour émettre des signaux.

Exemple 2-12 montre le code de propobject.cpp. C'est l'implémentation de la classe de propriété d'objet.

#include "propobject.h"

PropObject::PropObject( QObject *parent, char *name ) : QObject( parent, name )
{
  m_testProperty = InitialValue;
}

void PropObject::setTestProperty( TestProperty p ) { m_testProperty = p; }
PropObject::TestProperty PropObject::testProperty() const { return m_testProperty; }

Exemple 2-12

Notez que le constructeur accepte les arguments de QObjectparent et name. Ceux-ci sont passés à  la classe de base. Les fonctions membres sont seulement des implémentations triviales d'une propriété lecture/écriture et d'une propriété en lecture seule.

En conclusion, l'exemple 2-13 montre le code de main.cpp. Ce code accède aux propriétés par l'interface standard de QObject au lieu de l'accès direct. L'exécution montrée dans l'exemple 2-14 prouve que le code fonctionne réellement. Notez que l'enum est montré comme il est traité par l'ordinateur en interne, c.-à -d. comme nombre entier.

#include <qapplication.h>
#include <iostream>

#include "propobject.h"

int main( int argc, char **argv )
{
  QApplication a( argc, argv );

  QObject *o = new PropObject();
  
  std::cout << o->property( "testProperty" ).toString() << std::endl;
  
  o->setProperty( "testProperty", "AnotherValue" );
  std::cout << o->property( "testProperty" ).toString() << std::endl;
  
  std::cout << o->property( "anotherProperty" ).toString() << std::endl;
  
  return 0;
}

Exemple 2-13

$ ./prop
0
1
I'm read-only!
$

Exemple 2-14

Introspection

Chaque objet de Qt a un méta-objet. Cet objet est représenté par une instance de la classe QMetaObject. Il est employé pour fournir des informations au sujet de la classe courante. Le méta-objet peut être consulté par la fonction de membre QObject::metaObject(). Le méta-objet fournit quelques fonctions utiles énumérées ci-dessous.

Donne le nom de la classe, par exemple PropObject dans l'exemple de la section précédente.

Donne le méta-objet de la classe supérieure, ou 0 (nul) s'il n'y en a aucun.

Donne les noms des noms des propriétés et les méta-données pour chaque propriété comme QMetaProperty.

Donne les noms des slots de la classe. Si le paramètre facultatif, super, est positionné à  true les slots de la classe supérieure sont inclus aussi.

Donne les noms des signaux de la classe. Un paramètre facultatif, super, est disponible quant à  la fonction membre signalNames.

Les fonctions membres sont juste énumérées plus haut. Il y a plus d'informations de méta disponible. Regardez la documentation officielle pour les détails.

Résumé

Le code source de ce chapitre peut être téléchargé ici.

Lecture recommandée

This is a part of digitalfanatics.org and is valid XHTML.