Traduction par Jean-luc Biord, du Site de la communauté
Qt francophone.
English TOC.
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.
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;
}
};
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;
}
Soyez certain de comprendre comment l'arbre est traduit en code de l'exemple 2-2 et vice versa.
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
$
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" );
...
$ ./mem
Created: top
Created: x
Created: y
Created: z
Do stuff: top
Do stuff: x
Do stuff: y
Do stuff: z
Deleted: top
$
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.
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.
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;
}
};
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 );
};
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 );
};
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;
}
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
$
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.
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
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; }
Notez que le constructeur accepte les arguments de QObject, parent 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;
}
$ ./prop
0
1
I'm read-only!
$
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.
Le code source de ce chapitre peut être téléchargé ici.
This is a part of digitalfanatics.org and is valid XHTML.
Copyright (c) 2002-2004 by Johan Thelin (e8johan -at- digitalfanatics.org). This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 (local copy) or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.