XIV. Accrochons des briques en l'air▲
Dans cet exemple, nous étendons notre classe LCDRange pour ajouter un label. Nous fournissons également une cible sur laquelle on pourra tirer.
XIV-A. Analyse du code ligne par ligne▲
XIV-A-1. t12/lcdrange.h▲
LCDRange possède maintenant un label.
class
QLabel
;
class
QSlider
;
Nous déclarons à l'avance QLabel et QSlider car nous voulons dans la définition de la classe utiliser des pointeurs sur cette classe. Nous pourrions également utiliser #include, mais cela ralentirait inutilement la compilation.
class
LCDRange : public
QWidget
{
Q_OBJECT
public
:
LCDRange(QWidget
*
parent =
0
);
LCDRange(const
QString
&
text, QWidget
*
parent =
0
);
Nous avons ajouté un nouveau constructeur qui permet de fournir un label à cette classe en complément du parent.
QString
text() const
;
La fonction retourne le label.
void
setText(const
QString
&
text);
Cette partie configure le label.
private
:
void
init();
Comme nous avons maintenant deux constructeurs, nous avons choisi de placer l'initialisation commune dans la fonction privée init().
QLabel
*
label;
Nous avons également une nouvelle variable privée : un QLabel. QLabel est l'un des widgets standards de Qt et peut afficher un texte ou un QPixmap, avec ou sans fenêtre.
XIV-A-2. t12/lcdrange.cpp▲
LCDRange::
LCDRange(QWidget
*
parent)
:
QWidget
(parent)
{
init();
}
Ce constructeur appelle la fonction init(), qui contient le code d'initialisation commun.
LCDRange::
LCDRange(const
QString
&
text, QWidget
*
parent)
:
QWidget
(parent)
{
init();
setText(text);
}
Ce constructeur appelle d'abord init() et configure ensuite le label.
void
LCDRange::
init()
{
QLCDNumber
*
lcd =
new
QLCDNumber
(2
);
lcd->
setSegmentStyle(QLCDNumber
::
Filled);
slider =
new
QSlider
(Qt
::
Horizontal);
slider->
setRange(0
, 99
);
slider->
setValue(0
);
label =
new
QLabel
;
label->
setAlignment(Qt
::
AlignHCenter |
Qt
::
AlignTop);
connect
(slider, SIGNAL
(valueChanged(int
)),
lcd, SLOT
(display(int
)));
connect
(slider, SIGNAL
(valueChanged(int
)),
this
, SIGNAL
(valueChanged(int
)));
QVBoxLayout
*
layout =
new
QVBoxLayout
;
layout->
addWidget(lcd);
layout->
addWidget(slider);
layout->
addWidget(label);
setLayout(layout);
setFocusProxy(slider);
}
La configuration du LCD et du slider est la même que dans le chapitre précédent. Ensuite, nous créons un QLabel et lui demandons que son contenu soit aligné : au centre dans le sens horizontal, et en haut dans le sens vertical. L'appel à QObject::connect() a également été emprunté au chapitre précédent.
QString
LCDRange::
text() const
{
return
label->
text();
}
Cette fonction renvoie le texte du label.
void
LCDRange::
setText(const
QString
&
text)
{
label->
setText(text);
}
Cette fonction configure le texte du label.
XIV-A-3. t12/cannonfield.h▲
Le cannonfield a maintenant deux nouveaux signaux : hit() et missed(). De plus, il contient une cible.
void
newTarget();
Ce slot crée une cible à une nouvelle position.
signals
:
void
hit();
void
missed();
Le signal hit() est émis quand un tir touche la cible. Le signal missed() est émis quand le projectile va au-delà des bords droit et bas du widget (il est alors certain qu'il n'a pas atteint et qu'il n'atteindra pas la cible).
void
paintTarget(QPainter
&
painter);
Cette fonction privée dessine la cible.
QRect
targetRect() const
;
Cette fonction privée retourne le rectangle lié à la cible.
QPoint
target;
Cette variable privée contient le centre de la cible.
XIV-A-4. t12/cannonfield.cpp▲
#include
<stdlib.h>
Nous incluons le fichier d'en-tête stdlib.h car nous avons besoin de la fonction qrand().
newTarget();
Cette ligne a été ajoutée au constructeur. Il crée une position "aléatoire" pour la cible. En fait, la fonction newTarget() va essayer de peindre la cible. Comme nous sommes dans un constructeur, le widget CannonField est invisible. Qt garantit qu'aucun dommage n'est fait quand QWidget::update() est appelé sur un widget invisible.
void
CannonField::
newTarget()
{
static
bool
firstTime =
true
;
if
(firstTime) {
firstTime =
false
;
QTime
midnight(0
, 0
, 0
);
qsrand
(midnight.secsTo(QTime
::
currentTime()));
}
target =
QPoint
(200
+
qrand
() %
190
, 10
+
qrand
() %
255
);
update();
}
Cette fonction privée crée un point central de cible à une position aléatoire.
Nous utilisons la fonction qrand() pour obtenir des entiers aléatoires. Cette fonction retourne en principe toujours les mêmes séries de nombres à chaque fois que vous lancez un programme ; utilisée normalement, elle ferait donc toujours apparaître la cible à la même position. Pour l'éviter, nous devons transmettre un germe initial aléatoire au premier appel de la fonction. Ce germe doit lui aussi être aléatoire afin d'éviter des séries de nombres aléatoires identiques. La solution adoptée ici est d'utiliser comme germe pseudo-aléatoire le nombre de secondes qui se sont écoulées depuis minuit.
D'abord nous créons une variable booléenne locale statique. Nous avons l'assurance qu'une variable statique comme celle-ci conserve sa valeur entre deux appels à la fonction.
La condition ne sera vraie qu'au premier appel de cette fonction car nous passons firstTime à false dans le bloc conditionnel.
Ensuite nous créons l'objet QTime "midnight", qui représente minuit. Puis, nous récupérons le nombre de secondes depuis minuit jusqu'à maintenant et l'utilisons comme germe aléatoire. Pour plus de détails, voir la documentation de QDate, QTime et QDateTime.
Enfin, nous calculons le point central de la cible. Nous le conservons dans le rectangle (x : 200, y : 35, width : 190, height : 255, les plages de valeur possibles pour x et y sont respectivement de 200 à 389 et de 35 à 289), dans un système de coordonnées où y prend la valeur 0 sur le bord inférieur du widget et augmente vers le haut, tandis que x prend comme d'habitude la valeur 0 sur le bord gauche du widget et augmente vers la droite.
Par expérience, nous avons constaté que cela permet toujours d'avoir une cible à portée de tir.
void
CannonField::
moveShot()
{
QRegion
region =
shotRect();
++
timerCount;
QRect
shotR =
shotRect();
Cette partie de l'événement du timer n'a pas changé par rapport au chapitre précédent.
if
(shotR.intersects(targetRect())) {
autoShootTimer->
stop();
emit
hit();
Ce test conditionnel vérifie si le rectangle du tir rencontre le rectangle de la cible. Si c'est le cas, le tir a touché la cible (aïe !) : nous stoppons alors le timer du tir, nous émettons le signal hit() pour indiquer au monde extérieur que la cible a été détruite et nous sortons finalement de la fonction.
Notez que l'on aurait pu créer une nouvelle cible sur la zone, mais comme CannonField est un composant, nous laissons une telle décision à l'utilisateur du composant.
}
else
if
(shotR.x() >
width() ||
shotR.y() >
height()) {
autoShootTimer->
stop();
emit
missed();
Cette condition est la même que dans le chapitre précédent, excepté qu'elle émet maintenant le signal missed() pour indiquer au monde extérieur cet échec.
}
else
{
region =
region.unite(shotR);
}
update(region);
}
Et le reste de la fonction reste inchangé.
CannonField::paintEvent() est comme avant, sauf que ceci a été ajouté :
paintTarget(painter);
Cette ligne assure que la cible est également dessinée quand il le faut.
void
CannonField::
paintTarget(QPainter
&
painter)
{
painter.setPen(Qt
::
black);
painter.setBrush(Qt
::
red);
painter.drawRect(targetRect());
}
Cette fonction privée dessine la cible, un rectangle rempli de rouge avec un bord noir.
QRect
CannonField::
targetRect() const
{
QRect
result(0
, 0
, 20
, 10
);
result.moveCenter(QPoint
(target.x(), height() -
1
-
target.y()));
return
result;
}
Cette fonction privée retourne le rectangle de la cible. A propos de newTarget(), souvenez-vous que la valeur 0 de l'ordonnée y du centre de la cible correspond au bord inférieur du widget. Nous calculons le point en coordonnées du widget avant d'appeler QRect::moveCenter().
Nous avons choisi ce changement de coordonnées pour fixer la distance entre la cible et le bas du widget. Souvenez-vous que le widget peut être redimmensionné par l'utilisateur ou par le programme à tout moment.
XIV-A-5. t12/main.cpp▲
Il n'y a pas de nouveaux membres dans la classe MyWidget, mais nous avons légèrement changé le constructeur pour configurer les labels de texte du nouveau LCDRange.
LCDRange *
angle =
new
LCDRange(tr("ANGLE"
));
Nous mettons "ANGLE" dans le label texte de l'angle.
LCDRange *
force =
new
LCDRange(tr("FORCE"
));
Nous mettons "FORCE" dans le label texte de la force.
XIV-B. Exécuter l'application▲
Le widget LCDRange paraît un peu bizarre : quand nous redimmensionnons le widget, le gestionnaire de couches intégré dans QVBoxLayout donne trop de place au label et pas assez au reste, provoquant ainsi un changement de tailles des deux LCDRange widgets. Nous corrigerons cela dans le prochain chapitre.
XIV-C. Exercices▲
Fabriquez un "bouton pour tricheur" qui, lorsqu'il est cliqué, fait en sorte que CannonField affiche la trajectoire du tir pendant 5 secondes.
Si vous avez fait l'exercice "tir rond" du chapitre précédent, essayez de changer le shotRect() en shotRegion() qui retourne une QRegion() permettant une détection de collision très précise.
Faites une cible mobile.
Assurez-vous que la cible est toujours créée entièrement à l'écran.
Assurez-vous que le widget ne peut pas être redimensionné et que la cible ne soit pas visible [indice : QWidget::setMinimumSize() est votre ami !].
Exercice pas facile : rendez possible des tirs simultanés [indice : faites une classe Tir].