X. Préparer le champ de bataille▲
Dans cet exemple, on introduit le premier widget personnalisé qui se dessine de lui-même. On va aussi ajouter quelques interactions avec le clavier (en deux lignes de code).
X-A. Analyse du code ligne par ligne ▲
X-A-1. t8/lcdrange.h▲
Ce fichier est très proche du fichier lcdrange.h du Chapitre IX. Nous n'avons ajouté qu'un seul slot setRange().
void
setRange(int
minValue, int
maxValue);
On a maintenant la possibilité de régler l'intervalle LCDRange. Jusqu'à présent, on fixait ce dernier de 0 à 99.
X-A-2. t8/lcdrange.cpp▲
Il y a du changement dans le constructeur (on en discutera plus tard).
void
LCDRange::
setRange(int
minValue, int
maxValue)
{
if
(minValue <
0
||
maxValue >
99
||
minValue >
maxValue) {
qWarning
("LCDRange::setRange(%d, %d)
\n
"
"
\t
Range must be 0..99
\n
"
"
\t
and minValue must not be greater than maxValue"
,
minValue, maxValue);
return
;
}
slider->
setRange(minValue, maxValue);
}
Le slot setRange() fixe l'intervalle du curseur dans LCDRange. Puisqu'on a paramétré QLCDNumber pour qu'il puisse afficher deux chiffres, on voudrait limiter les valeurs de minValue et maxValue pour empêcher le débordement sur QLCDNumber (on aurait pu autoriser des valeurs allant jusqu'à -9, mais on a choisi de ne pas le faire). Si les arguments sont illégaux, on utilise la fonction qWarning() pour afficher un avertissement à l'utilisateur et retourner immédiatement. qWarning() est une fonction semblable à printf qui, par défaut, envoie sa sortie vers stderr. Si vous le souhaitez, vous pouvez aussi utiliser votre propre gestionnaire d'erreur en utilisant la fonction qInstallMsgHandler().
X-A-3. t8/cannonfield.h▲
CannonField est un nouveau widget personnalisé qui s'affiche de lui-même.
class
CannonField : public
QWidget
{
Q_OBJECT
public
:
CannonField(QWidget
*
parent =
0
);
CannonField hérite de QWidget. On utilise la même syntaxe que pour LCDRange.
int
angle() const
{
return
currentAngle; }
public
slots
:
void
setAngle(int
angle);
signals
:
void
angleChanged(int
newAngle);
Pour le moment, CannonField ne contient que la valeur d'un angle pour laquelle on fournit une interface avec la même syntaxe que pour la valeur de LCDRange.
protected
:
void
paintEvent(QPaintEvent
*
event);
On trouve ici un second exemple de gestionnaire d'événement : il fait partie de la multitude de gestionnaires qu'on peut rencontrer dans QWidget. Cette fonction virtuelle est appelée quand un widget a besoin de se redessiner (c'est-à-dire de repeindre sa propre surface).
X-A-4. t8/cannonfield.cpp▲
CannonField::
CannonField(QWidget
*
parent)
:
QWidget
(parent)
{
Ici encore, on utilise la même syntaxe que dans le LCDRange du chapitre précédent.
currentAngle =
45
;
setPalette(QPalette
(QColor
(250
, 250
, 200
)));
setAutoFillBackground(true
);
}
Le constructeur initialise l'angle à 45 degrés et crée une palette personnalisée pour ce widget.
Cette palette utilise la couleur indiquée comme couleur d'arrière plan et choisie selon le besoin (pour ce widget, seules les couleurs d'arrière plan et de texte sont utilisées). Ensuite, on appelle setAutoFillBackground(true) pour demander à Qt de remplir automatiquement l'arrière plan.
QColor est spécifié comme un triplet RVB (rouge-vert-bleu), où chaque composante (R, V et B) est comprise entre 0 (luminosité minimale) et 255 (luminosité maximale). Au lieu de spécifier une valeur RVB, on aurait pu aussi utiliser une couleur prédéfinie (par exemple Qt::yellow).
void
CannonField::
setAngle(int
angle)
{
if
(angle <
5
)
angle =
5
;
if
(angle >
70
)
angle =
70
;
if
(currentAngle ==
angle)
return
;
currentAngle =
angle;
update();
emit
angleChanged(currentAngle);
}
Cette fonction définit la valeur de l'angle. On a choisi un intervalle compris entre 5 et 70 et on ajuste l'amplitude de l'angle spécifié en conséquence. On a choisi de ne pas afficher d'avertissement dans le cas où l'angle serait en dehors de cet intervalle.
Si l'angle actuel est égal au précédent, on retourne immédiatement. Il est important d'émettre le signal angleChanged() dans le cas où l'angle change vraiment.
Ensuite, on définit le nouvel angle et on redessine le widget. La fonction QWidget::update() efface le widget (généralement, en le remplissant avec la couleur de l'arrière plan) et envoie un événement de dessin au widget. Le résultat est un appel à la fonction de l'événement de dessin de widget.
Pour terminer, on émet le signal angleChanged() pour signaler au monde extérieur que l'angle a changé. Le mot clé emit est un élément syntaxique propre à Qt qui n'appartient donc pas à la syntaxe C++ normalisée. En fait, c'est une macro.
void
CannonField::
paintEvent(QPaintEvent
*
/* event */
)
{
QPainter
painter(this
);
painter.drawText(200
, 200
,
tr("Angle = "
) +
QString
::
number(currentAngle));
}
Ceci est notre première tentative de création d'un gestionnaire d'événement de dessin. L'argument contient des informations concernant cet événement, comme, la région du widget qui doit être rafraîchie. Pour le moment, soyons paresseux et redessinons tout le widget.
Notre code affiche la valeur de l'angle à une certaine position dans le widget. Pour réaliser cela, on crée un QPainter, qui opère sur le widget CannonField et que l'on utilise pour dessiner une représentation littérale de la valeur de currentAngle. Nous reviendrons plus tard sur QPainter, et nous verrons qu'on peut faire des choses merveilleuses avec lui.
X-A-5. t8/main.cpp▲
#include
"cannonfield.h"
On inclut la définition de notre nouvelle classe.
class
MyWidget : public
QWidget
{
public
:
MyWidget(QWidget
*
parent =
0
);
}
;
La classe MyWidget va inclure un seul LCDRange et un CannonField.
LCDRange *
angle =
new
LCDRange;
Dans le constructeur, on crée et initialise un widget LCDRange.
angle->
setRange(5
, 70
);
On initialise LCDRange pour qu'il accepte des angles compris entre 5 et 70.
CannonField *
cannonField =
new
CannonField;
On crée notre widget CannonField.
connect
(angle, SIGNAL
(valueChanged(int
)),
cannonField, SLOT
(setAngle(int
)));
connect
(cannonField, SIGNAL
(angleChanged(int
)),
angle, SLOT
(setValue(int
)));
Ici, on connecte le signal valueChanged() de LCDRange au slot setAngle() de cannonField. Ceci va permettre de mettre à jour l'angle de cannonField quand l'utilisateur manipule LCDRange. On rajoute aussi une connexion inversée, pour que le changement de l'angle de cannonField engendre réciproquement une mise à jour de LCDRange. Dans notre exemple, nous n'aurons jamais à modifier l'angle de cannonField directement. En rajoutant le dernier connect(), on s'assure que, dans le cas d'un changement futur, aucune modification ne perturbera la synchronisation entre les deux valeurs.
Ceci illustre la puissance de la programmation par composants quand elle met en oeuvre une encapsulation appropriée.
Noter qu'il est important de n'émettre le signal valueChanged() que dans le cas où l'angle change vraiment. Si les deux widgets LCDRange et cannonField avaient omis cette précaution, le programme entrerait dans une boucle infinie
QGridLayout
*
gridLayout =
new
QGridLayout
;
Jusqu'à présent, on a utilisé QVBoxLayout pour gérer la géométrie mais on voudrait à présent un meilleur contrôle de la disposition des widgets : pour cela, on va passer à une disposition plus puissante grâce à la classe QGridLayout. QGridLayout n'est pas un widget : c'est une classe différente qui va permettre de gérer les enfants de n'importe quel widget.
On n'a pas besoin de spécifier les dimensions dans le constructeur de QGridLayout, car il va déterminer lui-même le nombre de lignes et de colonnes en se basant sur les cellules de la grille qu'on définit.
Le diagramme ci-dessus montre la disposition à laquelle on veut aboutir : le coté gauche montre un schéma de la disposition et le coté droit montre une capture d'écran actuelle du programme.
gridLayout->
addWidget(quit, 0
, 0
);
On ajoute le bouton Quit à la cellule située dans le coin supérieur gauche de la grille, c'est-à-dire la cellule avec les coordonnées (0,0).
gridLayout->
addWidget(angle, 1
, 0
);
On ajoute l'angle LCDRange dans la cellule (1,0).
gridLayout->
addWidget(cannonField, 1
, 1
, 2
, 1
);
On laisse cannonField occuper les cellules (1,1) et (2,1).
gridLayout->
setColumnStretch(1
, 10
);
On ordonne à QGridLayout de rendre la colonne de droite (2) extensible, avec un facteur de 10. Puisque la colonne de gauche ne l'est pas (son facteur d'extensibilité est 0, valeur par défaut), QGridLayout va essayer de garder inchangée la taille des widgets à gauche et va redimensionner le CannonField quand la taille de MyWidget changera.
Dans cet exemple particulier, tout facteur d'extensibilité plus grand que 0 pour la colonne 1 aurait le même effet. Dans des dispositions géométriques plus complexes, vous pouvez, en choisissant les facteurs d'extensibilité adéquats, utiliser ces facteurs pour spécifier qu'une colonne ou une ligne s'étend plus rapidement qu'une autre.
angle->
setValue(60
);
On définit la valeur initiale de l'angle. Noter que ceci va déclencher la connexion entre LCDRange et CannonField.
angle->
setFocus();
Notre dernière intervention consiste à donner le focus au widget angle, de sorte que toute action sur le clavier ira vers le widget LCDRange par défaut.
LCDRange ne contient aucun keyPressEvent() : il semblerait que ceci n'ait pas été jugé vraiment intéressant ! Cependant, son constructeur possède une nouvelle ligne.
setFocusProxy(slider);
Le LCDRange va définir le curseur comme son « focus proxy », ce qui signifie que, quand quelqu'un donne le contrôle du clavier au LCDRange, le curseur doit le prendre en charge. QSlider possède une interface clavier décente, qui a permis avec une seule ligne de code d'en donner une à LCDRange.
X-B. Lancer l'application ▲
Maintenant, le clavier peut être utilisé : les flèches Home, End, PageUp et PageDown sont vraiment opérationnelles.
Quand le curseur est manipulé, CannonField affiche un nouvel angle. Au redimensionnement, CannonField prend tout l'espace disponible.
X-C. Exercices ▲
Essayer de redimensionner la fenêtre. Que se passe-t-il quand la fenêtre devient trop étroite ou bien trop large ?
Quand vous donnez à la colonne de gauche un facteur d'extensibilité supérieur à zéro, que se passe-t-il quand vous redimensionnez la fenêtre ?
Enlevez l'appel à QWidget::setFocus(). Quel est le comportement que vous préférez ?
Essayez de remplacer « Quit » par « &Quit », quel changement cela produit-il sur le look du bouton ? Que se passe-t-il si on appuye sur Alt+Q pendant l'exécution du programme ?
Centrez le texte sur CannonField.