XIII. Tire-lui dessus !▲
Dans cet exemple, nous introduisons un timer pour créer un tir animé.
XIII-A. Analyse du code ligne par ligne▲
XIII-A-1. t11/cannonfield.h▲
CannonField est maintenant capable de tirer.
void
shoot();
Appeler ce slot fera tirer le canon à condition qu'un projectile ne soit pas déjà dans les airs.
private
slots
:
void
moveShot();
Ce slot privé est utilisé pour déplacer, en utilisant QTimer, le projectile quand il est en l'air.
private
:
void
paintShot(QPainter
&
painter);
Cette fonction privée dessine le tir.
QRect
shotRect() const
;
Cette fonction privée renvoie, si un projectile est en l'air, le rectangle qui contient la trajectoire de tir ; sinon, elle retourne un rectangle indéfini
int
timerCount;
QTimer
*
autoShootTimer;
float
shootAngle;
float
shootForce;
}
;
Ces variables privées contiennent les informations qui décrivent le tir. Le timerCount calcule le temps écoulé depuis le dernier tir. shootAngle représente l'angle du canon et shootForce la puissance du canon.
XIII-A-2. t11/cannonfield.cpp▲
#include
<math.h>
Nous incluons math.h pour pouvoir utiliser les fonctions trigonométriques sin() et cos(). Une alternative pourrait être d'inclure l'en-tête cmath, qui est plus moderne. Malheureusement certaines plateformes UNIX ne le supportent toujours pas correctement.
CannonField::
CannonField(QWidget
*
parent)
:
QWidget
(parent)
{
currentAngle =
45
;
currentForce =
0
;
timerCount =
0
;
autoShootTimer =
new
QTimer
(this
);
connect
(autoShootTimer, SIGNAL
(timeout()), this
, SLOT
(moveShot()));
shootAngle =
0
;
shootForce =
0
;
setPalette(QPalette
(QColor
(250
, 250
, 200
)));
setAutoFillBackground(true
);
}
Nous initialisons nos nouvelles variables privées et nous connectons le signal QTimer::timeout() à moveShot(). Nous déplaçons le projectile chaque fois que le timer du canon dépasse le délai choisi (5 millisecondes ; voir ci-dessous).
void
CannonField::
shoot()
{
if
(autoShootTimer->
isActive())
return
;
timerCount =
0
;
shootAngle =
currentAngle;
shootForce =
currentForce;
autoShootTimer->
start(5
);
}
Cette fonction déclenche le tir, sauf si un projectile est déjà en l'air. timerCount est remis à zéro, shootAngle et shootForce sont définis avec les valeurs d'angle et de puissance choisies. Et, enfin, nous démarrons le timer.
void
CannonField::
moveShot()
{
QRegion
region =
shotRect();
++
timerCount;
QRect
shotR =
shotRect();
if
(shotR.x() >
width() ||
shotR.y() >
height()) {
autoShootTimer->
stop();
}
else
{
region =
region.unite(shotR);
}
update(region);
}
moveShot() est le slot qui déplace le projectile ; il est appelé toutes les 5 millisecondes pendant que QTimer tire. Ses tâches sont de calculer la nouvelle position, de mettre à jour l'écran en dessinant le projectile à sa nouvelle position et, si nécessaire, d'arrêter le timer. Pour commencer, nous créons une QRegion qui conserve le rectangle de tir précédent. Une QRegion est capable de garder les caractéristiques de n'importe quelle région ; ici, nous en utilisons une pour simplifier le dessin. shotRect() renvoie le rectangle du slot. Ce sera expliqué en détail un peu plus bas. Ensuite, nous incrémentons le timerCount qui a pour effet de déplacer « d'un cran » le projectile sur sa trajectoire. Puis nous déterminons le nouveau rectangle de tir. Si le projectile a franchi le bord droit ou le bord inférieur du widget, nous arrêtons le timer ; sinon, nous ajoutons le nouveau shotRect() à la QRegion. Enfin, nous redessinons la QRegion, ce qui va envoyer un simple événement "paint" pour la mise à jour des seuls rectangles qui en ont besoin.
void
CannonField::
paintEvent(QPaintEvent
*
/* event */
)
{
QPainter
painter(this
);
paintCannon(painter);
if
(autoShootTimer->
isActive())
paintShot(painter);
}
Par rapport au chapitre précédent, l'événement "paint" a été simplifié. La majeure partie de la logique a été déplacée dans les nouvelles fonctions paintShot() et paintCannon().
void
CannonField::
paintShot(QPainter
&
painter)
{
painter.setPen(Qt
::
NoPen);
painter.setBrush(Qt
::
black);
painter.drawRect(shotRect());
}
Cette méthode privée dessine le tir en peignant un rectangle noir. Nous laissons de côté l'implémentation de paintCannon(), semblable à QWidget::paintEvent(), réimplanté du dernier chapitre.
QRect
CannonField::
shotRect() const
{
const
double
gravity =
4
;
double
time =
timerCount /
20.0
;
double
velocity =
shootForce;
double
radians =
shootAngle *
3.14159265
/
180
;
double
velx =
velocity *
cos(radians);
double
vely =
velocity *
sin(radians);
double
x0 =
(barrelRect.right() +
5
) *
cos(radians);
double
y0 =
(barrelRect.right() +
5
) *
sin(radians);
double
x =
x0 +
velx *
time;
double
y =
y0 +
vely *
time -
0.5
*
gravity *
time *
time;
QRect
result(0
, 0
, 6
, 6
);
result.moveCenter(QPoint
(qRound
(x), height() -
1
-
qRound
(y)));
return
result;
}
Cette fonction calcule les coordonnées du centre du projectile et renvoie le rectangle contenant le projectile. Il tient compte de la puissance initiale et de l'angle du canon et ajoute timerCount au temps passé. La formule utilisée est la formule standard de Newton pour un mouvement sans frottement, sous l'effet de la gravité. Pour simplifier, nous avons négligé l'effet relativiste d'Einstein. Nous calculons le point central dans un système où les coordonnées y augmentent vers le haut. Après avoir calculé la position du point central, nous créons un QRect de taille 6 x 6 que nous centrons (à l'aide des points que nous avons calculés). En même temps, nous convertissons le point en coordonnées pour le widget (voir le système de coordonnées). La fonction qRound() est une fonction inline définie dans QtGlobal, et incluse dans tous les en-têtes de Qt. qRound() convertit un nombre double (réel en double précision) dans l'entier integer le plus proche.
XIII-A-3. t11/main.cpp▲
class
MyWidget : public
QWidget
{
public
:
MyWidget(QWidget
*
parent =
0
);
}
;
On ajoute simplement un bouton poussoir nommé Shoot.
QPushButton
*
shoot =
new
QPushButton
(tr("&Shoot"
));
shoot->
setFont(QFont
("Times"
, 18
, QFont
::
Bold));
Dans le constructeur, nous créons et configurons le bouton exactement comme nous l'avions fait avec le bouton Quit.
connect
(shoot, SIGNAL
(clicked()), cannonField, SLOT
(shoot()));
Connexion du signal clicked() du bouton poussoir au slot shoot() de CannonField.
XIII-B. Exécuter l'application▲
Le canon peut tirer mais il n'a pas de cible.
XIII-C. Exercices▲
Représentez le tir par un cercle (suggestion : QPainter::drawEllipse() peut aider). Changez la couleur du canon quand il vise le ciel.