IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Débuter dans la création d'interfaces graphiques avec PyQt 4

Image non disponible


précédentsommairesuivant

XIV. Accrochons des briques en l'air

Image non disponible

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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
    QString text() const;

La fonction retourne le label.

 
Sélectionnez
    void setText(const QString &text);

Cette partie configure le label.

 
Sélectionnez
private:
    void init();

Comme nous avons maintenant deux constructeurs, nous avons choisi de placer l'initialisation commune dans la fonction privée init().

 
Sélectionnez
    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

 
Sélectionnez
LCDRange::LCDRange(QWidget *parent)
    : QWidget(parent)
{
    init();
}

Ce constructeur appelle la fonction init(), qui contient le code d'initialisation commun.

 
Sélectionnez
LCDRange::LCDRange(const QString &text, QWidget *parent)
    : QWidget(parent)
{
    init();
    setText(text);
}

Ce constructeur appelle d'abord init() et configure ensuite le label.

 
Sélectionnez
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.

 
Sélectionnez
QString LCDRange::text() const
{
    return label->text();
}

Cette fonction renvoie le texte du label.

 
Sélectionnez
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.

 
Sélectionnez
    void newTarget();

Ce slot crée une cible à une nouvelle position.

 
Sélectionnez
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).

 
Sélectionnez
    void paintTarget(QPainter &painter);

Cette fonction privée dessine la cible.

 
Sélectionnez
    QRect targetRect() const;

Cette fonction privée retourne le rectangle lié à la cible.

 
Sélectionnez
    QPoint target;

Cette variable privée contient le centre de la cible.

XIV-A-4. t12/cannonfield.cpp

 
Sélectionnez
#include <stdlib.h>

Nous incluons le fichier d'en-tête stdlib.h car nous avons besoin de la fonction qrand().

 
Sélectionnez
    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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
    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.

 
Sélectionnez
    } 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.

 
Sélectionnez
    } 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é :

 
Sélectionnez
    paintTarget(painter);

Cette ligne assure que la cible est également dessinée quand il le faut.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
    LCDRange *angle = new LCDRange(tr("ANGLE"));

Nous mettons "ANGLE" dans le label texte de l'angle.

 
Sélectionnez
    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].


précédentsommairesuivant

Copyright © 2009 - 2021 Developpez.com LLC Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.