Sur cette page

Exemple de calculatrice

Cet exemple montre comment utiliser les signaux et les slots pour implémenter les fonctionnalités d'un widget de calculatrice, et comment utiliser l'QGridLayout pour placer des widgets enfants dans une grille.

Application de calculatrice de base

Capture d'écran de l'exemple de la calculatrice

L'exemple se compose de deux classes :

  • Calculator est le widget de calculatrice, qui intègre toutes les fonctionnalités de la calculatrice.
  • Button est le widget utilisé pour chacune des touches de la calculatrice. Il dérive de ` QToolButton`.

Nous commencerons par passer en revue la classe ` Calculator`, puis nous examinerons la classe ` Button`.

Définition de la classe Calculator

class Calculator : public QWidget
{
    Q_OBJECT

public:
    Calculator(QWidget *parent = nullptr);

private slots:
    void digitClicked();
    void unaryOperatorClicked();
    void additiveOperatorClicked();
    void multiplicativeOperatorClicked();
    void equalClicked();
    void pointClicked();
    void changeSignClicked();
    void backspaceClicked();
    void clear();
    void clearAll();
    void clearMemory();
    void readMemory();
    void setMemory();
    void addToMemory();

La classe Calculator fournit un widget de calculatrice simple. Elle hérite de QDialog et dispose de plusieurs slots privés associés aux boutons de la calculatrice. La méthode QObject::eventFilter() est réimplémentée pour gérer les événements de souris sur l'affichage de la calculatrice.

Les boutons sont regroupés en catégories en fonction de leur comportement. Par exemple, tous les boutons de chiffres (libellés de 0 à 9) ajoutent un chiffre à l’opérande actuel. Pour ceux-ci, nous connectons plusieurs boutons au même slot (par exemple, digitClicked()). Les catégories sont les suivantes : chiffres, opérateurs unaires (Sqrt, x², 1/x), opérateurs additifs (+, -) et opérateurs multiplicatifs (×, ÷). Les autres boutons disposent de leurs propres emplacements.

private:
    template<typename PointerToMemberFunction>
    Button *createButton(const QString &text, const PointerToMemberFunction &member);
    void abortOperation();
    bool calculate(double rightOperand, const QString &pendingOperator);

La fonction privée ` createButton() ` est utilisée dans le cadre de la construction du widget. La fonction ` abortOperation() ` est appelée chaque fois qu’une division par zéro se produit ou lorsqu’une opération de racine carrée est appliquée à un nombre négatif. La fonction ` calculate() ` applique un opérateur binaire (+, -, × ou ÷).

    double sumInMemory;
    double sumSoFar;
    double factorSoFar;
    QString pendingAdditiveOperator;
    QString pendingMultiplicativeOperator;
    bool waitingForOperand;

Ces variables, associées au contenu de l'écran de la calculatrice ( QLineEdit), codent l'état de la calculatrice :

  • sumInMemory contient la valeur stockée dans la mémoire de la calculatrice (à l'aide de MS, M+ ou MC).
  • sumSoFar stocke la valeur accumulée jusqu’à présent. Lorsque l’utilisateur clique sur =, sumSoFar est recalculé et affiché à l’écran. Clear All remet sumSoFar à zéro.
  • factorSoFar stocke une valeur temporaire lors des opérations de multiplication et de division.
  • pendingAdditiveOperator enregistre le dernier opérateur d'addition sur lequel l'utilisateur a cliqué.
  • pendingMultiplicativeOperator enregistre le dernier opérateur multiplicatif sur lequel l'utilisateur a cliqué.
  • waitingForOperand est « true » lorsque la calculatrice attend que l’utilisateur commence à saisir un opérande.

Les opérateurs additifs et multiplicatifs sont traités différemment car ils ont des priorités différentes. Par exemple, « 1 + 2 ÷ 3 » est interprété comme « 1 + (2 ÷ 3) » car « ÷ » a une priorité plus élevée que « + ».

Le tableau ci-dessous montre l’évolution de l’état de la calculatrice au fur et à mesure que l’utilisateur saisit une expression mathématique.

Saisie de l’utilisateurAffichageSomme cumuléeOpération d'additionFacteur cumuléOpération de multiplicationEn attente d’un opérande ?
00true
110false
1 +11+true
1 + 221+false
1 + 2 ÷21+2÷true
1 + 2 ÷ 331+2÷false
1 + 2 ÷ 3 -1,666671,66667-true
1 + 2 ÷ 3 - 441,66667-false
1 + 2 ÷ 3 - 4 =-2,333330true

Les opérateurs unaires, tels que Sqrt, ne nécessitent aucun traitement particulier ; ils peuvent être appliqués immédiatement puisque l’opérande est déjà connu lorsque l’on clique sur le bouton de l’opérateur.

    QLineEdit *display;

    enum { NumDigitButtons = 10 };
    Button *digitButtons[NumDigitButtons];
};

Enfin, nous déclarons les variables associées à l'affichage et aux boutons utilisés pour afficher les chiffres.

Implémentation de la classe Calculatrice

Calculator::Calculator(QWidget *parent)
    : QWidget(parent), sumInMemory(0.0), sumSoFar(0.0)
    , factorSoFar(0.0), waitingForOperand(true)
{

Dans le constructeur, nous initialisons l’état de la calculatrice. Les variables ` pendingAdditiveOperator ` et ` pendingMultiplicativeOperator ` n’ont pas besoin d’être initialisées explicitement, car le constructeur ` QString ` les initialise à des chaînes vides. Il est également possible d’initialiser ces variables directement dans l’en-tête. C’est ce qu’on appelle l’ member-initializaton, qui permet d’éviter une longue liste d’initialisation.

    display = new QLineEdit("0");
    display->setReadOnly(true);
    display->setAlignment(Qt::AlignRight);
    display->setMaxLength(15);

    QFont font = display->font();
    font.setPointSize(font.pointSize() + 8);
    display->setFont(font);

Nous créons l'QLineEdit représentant l'affichage de la calculatrice et configurons certaines de ses propriétés. En particulier, nous la définissons comme étant en lecture seule.

Nous agrandissons également la police de l’ display de 8 points.

    for (int i = 0; i < NumDigitButtons; ++i)
        digitButtons[i] = createButton(QString::number(i), &Calculator::digitClicked);

    Button *pointButton = createButton(tr("."), &Calculator::pointClicked);
    Button *changeSignButton = createButton(tr("\302\261"), &Calculator::changeSignClicked);

    Button *backspaceButton = createButton(tr("Backspace"), &Calculator::backspaceClicked);
    Button *clearButton = createButton(tr("Clear"), &Calculator::clear);
    Button *clearAllButton = createButton(tr("Clear All"), &Calculator::clearAll);

    Button *clearMemoryButton = createButton(tr("MC"), &Calculator::clearMemory);
    Button *readMemoryButton = createButton(tr("MR"), &Calculator::readMemory);
    Button *setMemoryButton = createButton(tr("MS"), &Calculator::setMemory);
    Button *addToMemoryButton = createButton(tr("M+"), &Calculator::addToMemory);

    Button *divisionButton = createButton(tr("\303\267"), &Calculator::multiplicativeOperatorClicked);
    Button *timesButton = createButton(tr("\303\227"), &Calculator::multiplicativeOperatorClicked);
    Button *minusButton = createButton(tr("-"), &Calculator::additiveOperatorClicked);
    Button *plusButton = createButton(tr("+"), &Calculator::additiveOperatorClicked);

    Button *squareRootButton = createButton(tr("Sqrt"), &Calculator::unaryOperatorClicked);
    Button *powerButton = createButton(tr("x\302\262"), &Calculator::unaryOperatorClicked);
    Button *reciprocalButton = createButton(tr("1/x"), &Calculator::unaryOperatorClicked);
    Button *equalButton = createButton(tr("="), &Calculator::equalClicked);

Pour chaque bouton, nous appelons la fonction privée ` createButton() ` en lui passant le libellé approprié et un slot permettant de le relier au bouton.

    QGridLayout *mainLayout = new QGridLayout;
    mainLayout->setSizeConstraint(QLayout::SetFixedSize);
    mainLayout->addWidget(display, 0, 0, 1, 6);
    mainLayout->addWidget(backspaceButton, 1, 0, 1, 2);
    mainLayout->addWidget(clearButton, 1, 2, 1, 2);
    mainLayout->addWidget(clearAllButton, 1, 4, 1, 2);

    mainLayout->addWidget(clearMemoryButton, 2, 0);
    mainLayout->addWidget(readMemoryButton, 3, 0);
    mainLayout->addWidget(setMemoryButton, 4, 0);
    mainLayout->addWidget(addToMemoryButton, 5, 0);

    for (int i = 1; i < NumDigitButtons; ++i) {
        int row = ((9 - i) / 3) + 2;
        int column = ((i - 1) % 3) + 1;
        mainLayout->addWidget(digitButtons[i], row, column);
    }

    mainLayout->addWidget(digitButtons[0], 5, 1);
    mainLayout->addWidget(pointButton, 5, 2);
    mainLayout->addWidget(changeSignButton, 5, 3);

    mainLayout->addWidget(divisionButton, 2, 4);
    mainLayout->addWidget(timesButton, 3, 4);
    mainLayout->addWidget(minusButton, 4, 4);
    mainLayout->addWidget(plusButton, 5, 4);

    mainLayout->addWidget(squareRootButton, 2, 5);
    mainLayout->addWidget(powerButton, 3, 5);
    mainLayout->addWidget(reciprocalButton, 4, 5);
    mainLayout->addWidget(equalButton, 5, 5);
    setLayout(mainLayout);

    setWindowTitle(tr("Calculator"));
}

La mise en page est gérée par un seul ` QGridLayout`. L’appel à ` QLayout::setSizeConstraint()` garantit que le widget ` Calculator ` s’affiche toujours à sa taille optimale (son ` size hint`), empêchant ainsi l’utilisateur de redimensionner la calculatrice. L’indication de taille est déterminée par la taille et l’ size policy des widgets enfants.

La plupart des widgets enfants n’occupent qu’une seule cellule dans la mise en page en grille. Pour ceux-ci, il suffit de passer une ligne et une colonne à la méthode ` QGridLayout::addWidget()`. Les widgets ` display`, ` backspaceButton`, ` clearButton` et ` clearAllButton ` occupent plusieurs colonnes ; pour ceux-ci, il faut également passer une étendue de lignes et une étendue de colonnes.

void Calculator::digitClicked()
{
    Button *clickedButton = qobject_cast<Button *>(sender());
    int digitValue = clickedButton->text().toInt();
    if (display->text() == "0" && digitValue == 0.0)
        return;

    if (waitingForOperand) {
        display->clear();
        waitingForOperand = false;
    }
    display->setText(display->text() + QString::number(digitValue));
}

Appuyer sur l’une des touches numériques de la calculatrice émet le signal ` clicked()` de cette touche, ce qui déclenche le slot ` digitClicked() `.

Tout d’abord, nous déterminons quelle touche a envoyé le signal à l’aide de ` QObject::sender()`. Cette fonction renvoie l’émetteur sous la forme d’un pointeur ` QObject `. Comme nous savons que l’émetteur est un objet ` Button `, nous pouvons effectuer sans risque un transtypage de ` QObject`. Nous aurions pu utiliser un transtypage de style C ou un ` static_cast<>()` de C++, mais par souci de programmation défensive, nous utilisons un ` qobject_cast()`. L’avantage est que si l’objet est d’un type incorrect, un pointeur null est renvoyé. Les plantages dus à des pointeurs null sont bien plus faciles à diagnostiquer que ceux dus à des conversions non sécurisées. Une fois que nous avons le bouton, nous extrayons l’opérateur à l’aide de ` QToolButton::text()`.

Le slot doit notamment prendre en compte deux situations. Si display contient « 0 » et que l’utilisateur clique sur le bouton « 0 », il serait absurde d’afficher « 00 ». Et si la calculatrice est dans un état où elle attend un nouvel opérande, le nouveau chiffre est le premier chiffre de ce nouvel opérande ; dans ce cas, tout résultat d’un calcul précédent doit d’abord être effacé.

À la fin, nous ajoutons le nouveau chiffre à la valeur affichée à l'écran.

void Calculator::unaryOperatorClicked()
{
    Button *clickedButton = qobject_cast<Button *>(sender());
    QString clickedOperator = clickedButton->text();
    double operand = display->text().toDouble();
    double result = 0.0;

    if (clickedOperator == tr("Sqrt")) {
        if (operand < 0.0) {
            abortOperation();
            return;
        }
        result = std::sqrt(operand);
    } else if (clickedOperator == tr("x\302\262")) {
        result = std::pow(operand, 2.0);
    } else if (clickedOperator == tr("1/x")) {
        if (operand == 0.0) {
            abortOperation();
            return;
        }
        result = 1.0 / operand;
    }
    display->setText(QString::number(result));
    waitingForOperand = true;
}

La fonction ` unaryOperatorClicked() ` est appelée chaque fois qu’un des boutons d’opérateur unaire est cliqué. Là encore, un pointeur vers le bouton cliqué est récupéré à l’aide de ` QObject::sender()`. L’opérateur est extrait du texte du bouton et stocké dans ` clickedOperator`. L’opérande est obtenu à partir de ` display`.

Nous effectuons ensuite l’opération. Si Sqrt est appliqué à un nombre négatif ou si 1/x est appliqué à zéro, nous appelons abortOperation(). Si tout se passe bien, nous affichons le résultat de l’opération dans le champ de saisie et nous définissons waitingForOperand sur true. Cela garantit que si l’utilisateur saisit un nouveau chiffre, celui-ci sera considéré comme un nouvel opérande, au lieu d’être ajouté à la valeur actuelle.

void Calculator::additiveOperatorClicked()
{
    Button *clickedButton = qobject_cast<Button *>(sender());
    if (!clickedButton)
      return;
    QString clickedOperator = clickedButton->text();
    double operand = display->text().toDouble();

L'événement « additiveOperatorClicked() » est déclenché lorsque l'utilisateur clique sur le bouton « + » ou « - ».

Avant de pouvoir effectuer une opération sur l’opérateur sélectionné, nous devons traiter toutes les opérations en attente. Nous commençons par les opérateurs multiplicatifs, car ceux-ci ont une priorité plus élevée que les opérateurs additifs :

    if (!pendingMultiplicativeOperator.isEmpty()) {
        if (!calculate(operand, pendingMultiplicativeOperator)) {
            abortOperation();
            return;
        }
        display->setText(QString::number(factorSoFar));
        operand = factorSoFar;
        factorSoFar = 0.0;
        pendingMultiplicativeOperator.clear();
    }

Si l’on a cliqué auparavant sur « × » ou « ÷ » sans cliquer ensuite sur « = », la valeur actuellement affichée correspond à l’opérande de droite de l’opérateur « × » ou « ÷ » ; nous pouvons alors enfin effectuer l’opération et mettre à jour l’affichage.

    if (!pendingAdditiveOperator.isEmpty()) {
        if (!calculate(operand, pendingAdditiveOperator)) {
            abortOperation();
            return;
        }
        display->setText(QString::number(sumSoFar));
    } else {
        sumSoFar = operand;
    }

Si l’on a cliqué auparavant sur « + » ou « - », « sumSoFar » est l’opérande de gauche et la valeur actuelle affichée correspond à l’opérande de droite de l’opérateur. S’il n’y a pas d’opérateur d’addition en attente, « sumSoFar » est simplement défini comme étant le texte affiché.

    pendingAdditiveOperator = clickedOperator;
    waitingForOperand = true;
}

Enfin, nous pouvons nous occuper de l’opérateur sur lequel on vient de cliquer. Comme nous n’avons pas encore d’opérande de droite, nous stockons l’opérateur cliqué dans la variable pendingAdditiveOperator. Nous appliquerons l’opération plus tard, lorsque nous aurons un opérande de droite, avec sumSoFar comme opérande de gauche.

void Calculator::multiplicativeOperatorClicked()
{
    Button *clickedButton = qobject_cast<Button *>(sender());
    if (!clickedButton)
      return;
    QString clickedOperator = clickedButton->text();
    double operand = display->text().toDouble();

    if (!pendingMultiplicativeOperator.isEmpty()) {
        if (!calculate(operand, pendingMultiplicativeOperator)) {
            abortOperation();
            return;
        }
        display->setText(QString::number(factorSoFar));
    } else {
        factorSoFar = operand;
    }

    pendingMultiplicativeOperator = clickedOperator;
    waitingForOperand = true;
}

L'emplacement ` multiplicativeOperatorClicked() ` est similaire à ` additiveOperatorClicked()`. Nous n’avons pas à nous soucier ici des opérateurs additionnels en attente, car les opérateurs multiplicatifs ont priorité sur les opérateurs additionnels.

void Calculator::equalClicked()
{
    double operand = display->text().toDouble();

    if (!pendingMultiplicativeOperator.isEmpty()) {
        if (!calculate(operand, pendingMultiplicativeOperator)) {
            abortOperation();
            return;
        }
        operand = factorSoFar;
        factorSoFar = 0.0;
        pendingMultiplicativeOperator.clear();
    }
    if (!pendingAdditiveOperator.isEmpty()) {
        if (!calculate(operand, pendingAdditiveOperator)) {
            abortOperation();
            return;
        }
        pendingAdditiveOperator.clear();
    } else {
        sumSoFar = operand;
    }

    display->setText(QString::number(sumSoFar));
    sumSoFar = 0.0;
    waitingForOperand = true;
}

Comme dans ` additiveOperatorClicked()`, nous commençons par traiter les opérateurs multiplicatifs et additifs en attente. Ensuite, nous affichons ` sumSoFar ` et remettons la variable à zéro. Il est nécessaire de remettre la variable à zéro pour éviter de compter deux fois la valeur.

void Calculator::pointClicked()
{
    if (waitingForOperand)
        display->setText("0");
    if (!display->text().contains('.'))
        display->setText(display->text() + tr("."));
    waitingForOperand = false;
}

L'emplacement « pointClicked() » ajoute une virgule décimale au contenu de « display ».

void Calculator::changeSignClicked()
{
    QString text = display->text();
    double value = text.toDouble();

    if (value > 0.0) {
        text.prepend(tr("-"));
    } else if (value < 0.0) {
        text.remove(0, 1);
    }
    display->setText(text);
}

Le slot « changeSignClicked() » change le signe de la valeur contenue dans « display ». Si la valeur actuelle est positive, on ajoute un signe moins au début ; si la valeur actuelle est négative, on supprime le premier caractère de la valeur (le signe moins).

void Calculator::backspaceClicked()
{
    if (waitingForOperand)
        return;

    QString text = display->text();
    text.chop(1);
    if (text.isEmpty()) {
        text = "0";
        waitingForOperand = true;
    }
    display->setText(text);
}

L'backspaceClicked() supprime le caractère situé le plus à droite de l'affichage. Si l'on reçoit une chaîne vide, on affiche « 0 » et on définit waitingForOperand sur true.

void Calculator::clear()
{
    if (waitingForOperand)
        return;

    display->setText("0");
    waitingForOperand = true;
}

Le slot « clear() » remet l’opérande actuel à zéro. Cela revient à cliquer suffisamment de fois sur « Backspace » pour effacer l’intégralité de l’opérande.

void Calculator::clearAll()
{
    sumSoFar = 0.0;
    factorSoFar = 0.0;
    pendingAdditiveOperator.clear();
    pendingMultiplicativeOperator.clear();
    display->setText("0");
    waitingForOperand = true;
}

L'emplacement « clearAll() » réinitialise la calculatrice à son état initial.

void Calculator::clearMemory()
{
    sumInMemory = 0.0;
}

void Calculator::readMemory()
{
    display->setText(QString::number(sumInMemory));
    waitingForOperand = true;
}

void Calculator::setMemory()
{
    equalClicked();
    sumInMemory = display->text().toDouble();
}

void Calculator::addToMemory()
{
    equalClicked();
    sumInMemory += display->text().toDouble();
}

Le slot « clearMemory() » efface la somme conservée en mémoire, « readMemory() » affiche la somme en tant qu’opérande, « setMemory() » remplace la somme en mémoire par la somme actuelle, et « addToMemory() » ajoute la valeur actuelle à la valeur en mémoire. Pour « setMemory() » et « addToMemory() », nous commençons par appeler « equalClicked() » afin de mettre à jour « sumSoFar » et la valeur affichée à l’écran.

template<typename PointerToMemberFunction>
Button *Calculator::createButton(const QString &text, const PointerToMemberFunction &member)
{
    Button *button = new Button(text);
    connect(button, &Button::clicked, this, member);
    return button;
}

La fonction privée createButton() est appelée depuis le constructeur pour créer les boutons de la calculatrice.

void Calculator::abortOperation()
{
    clearAll();
    display->setText(tr("####"));
}

La fonction privée ` abortOperation() ` est appelée chaque fois qu’un calcul échoue. Elle réinitialise l’état de la calculatrice et affiche « #### ».

bool Calculator::calculate(double rightOperand, const QString &pendingOperator)
{
    if (pendingOperator == tr("+")) {
        sumSoFar += rightOperand;
    } else if (pendingOperator == tr("-")) {
        sumSoFar -= rightOperand;
    } else if (pendingOperator == tr("\303\227")) {
        factorSoFar *= rightOperand;
    } else if (pendingOperator == tr("\303\267")) {
        if (rightOperand == 0.0)
            return false;
        factorSoFar /= rightOperand;
    }
    return true;
}

La fonction privée ` calculate() ` effectue une opération binaire. L’opérande de droite est donné par ` rightOperand`. Pour les opérateurs d’addition, l’opérande de gauche est ` sumSoFar` ; pour les opérateurs de multiplication, l’opérande de gauche est ` factorSoFar`. La fonction renvoie ` false ` si une division par zéro se produit.

Définition de la classe Button

Examinons maintenant la classe ` Button ` :

class Button : public QToolButton
{
    Q_OBJECT

public:
    explicit Button(const QString &text, QWidget *parent = nullptr);

    QSize sizeHint() const override;
};

La classe Button dispose d’un constructeur pratique qui prend en paramètre une étiquette de texte et un widget parent, et elle réimplémente la méthode QWidget::sizeHint() afin de fournir davantage d’espace autour du texte que ce que la méthode QToolButton fournit normalement.

Implémentation de la classe Button

Button::Button(const QString &text, QWidget *parent)
    : QToolButton(parent)
{
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
    setText(text);
}

L’apparence des boutons est déterminée par la mise en page du widget « calculator » via la taille et l’ size policy des widgets enfants de cette mise en page. L’appel à la fonction setSizePolicy() dans le constructeur garantit que le bouton s’étendra horizontalement pour occuper tout l’espace disponible ; par défaut, les QToolButtons ne s’étendent pas pour occuper l’espace disponible. Sans cet appel, les différents boutons d’une même colonne auraient des largeurs différentes.

QSize Button::sizeHint() const
{
    QSize size = QToolButton::sizeHint();
    size.rheight() += 20;
    size.rwidth() = qMax(size.width(), size.height());
    return size;
}

Dans la fonction sizeHint(), nous essayons de renvoyer une taille qui convient à la plupart des boutons. Nous réutilisons l’indication de taille de la classe de base (QToolButton), mais nous la modifions de la manière suivante :

  • Nous ajoutons 20 au composant « height » de l’indication de taille.
  • Nous fixons la composante « width » de la suggestion de taille à une valeur au moins égale à celle de « height ».

Cela garantit que, avec la plupart des polices, les boutons de chiffres et d’opérateurs seront carrés, sans tronquer le texte des boutons « Backspace », « Clear » et « Clear All ».

La capture d’écran ci-dessous montre à quoi ressemblerait le widget Calculator si nous n’avions pas défini la politique de taille horizontale sur QSizePolicy::Expanding dans le constructeur et si nous n’avions pas réimplémenté la méthode QWidget::sizeHint().

Calculatrice dont les touches ne sont pas alignées

L'exemple « Calculator » avec les politiques de taille par défaut et les indications de taille

Projet d'exemple sur code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.