Beispiel für einen Taschenrechner
Das Beispiel zeigt, wie man Signale und Slots verwendet, um die Funktionalität eines Taschenrechner-Widgets zu implementieren, und wie man QGridLayout verwendet, um untergeordnete Widgets in einem Raster zu platzieren.
Screenshot des Calculator-Beispiels
Das Beispiel besteht aus zwei Klassen:
Calculator
ist das Taschenrechner-Widget mit der gesamten Funktionalität des Taschenrechners.Button
ist das Widget, das für jede Schaltfläche des Taschenrechners verwendet wird. Es leitet sich von QToolButton ab.
Wir beginnen mit Calculator
und werfen dann einen Blick auf Button
.
Definition der Klasse 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();
Die Klasse Calculator
bietet ein einfaches Taschenrechner-Widget. Sie erbt von QDialog und verfügt über mehrere private Slots, die mit den Tasten des Rechners verbunden sind. QObject::eventFilter() wurde neu implementiert, um Mausereignisse auf dem Display des Taschenrechners zu verarbeiten.
Die Schaltflächen sind entsprechend ihrem Verhalten in Kategorien gruppiert. Zum Beispiel fügen alle Zifferntasten ( 0 bis 9) eine Ziffer an den aktuellen Operanden an. Für diese Tasten werden mehrere Tasten mit demselben Steckplatz verbunden (z. B. digitClicked()
). Die Kategorien sind Ziffern, unäre Operatoren (Sqrt, x², 1/x), additive Operatoren (+, -), und multiplikative Operatoren (×, ÷). Die anderen Tasten haben ihre eigenen Slots.
private: template<typename PointerToMemberFunction> Button *createButton(const QString &text, const PointerToMemberFunction &member); void abortOperation(); bool calculate(double rightOperand, const QString &pendingOperator);
Die private Funktion createButton()
wird als Teil der Widget-Konstruktion verwendet. abortOperation()
wird immer dann aufgerufen, wenn eine Division durch Null stattfindet oder wenn eine Quadratwurzel-Operation auf eine negative Zahl angewendet wird. calculate()
wendet einen binären Operator an (+, -, × oder ÷).
double sumInMemory; double sumSoFar; double factorSoFar; QString pendingAdditiveOperator; QString pendingMultiplicativeOperator; bool waitingForOperand;
Diese Variablen kodieren zusammen mit dem Inhalt der Anzeige des Taschenrechners ( QLineEdit) den Zustand des Taschenrechners:
sumInMemory
enthält den Wert, der im Speicher des Rechners gespeichert ist (mit MS, M+ oder MC).sumSoFar
speichert den bis dahin aufgelaufenen Wert. Wenn der Benutzer auf = klickt, wirdsumSoFar
neu berechnet und auf dem Display angezeigt. Clear All setztsumSoFar
auf Null zurück.factorSoFar
speichert einen temporären Wert, wenn Multiplikationen und Divisionen durchgeführt werden.pendingAdditiveOperator
speichert den letzten vom Benutzer angeklickten additiven Operator.pendingMultiplicativeOperator
speichert den zuletzt vom Benutzer angeklickten multiplikativen Operator.waitingForOperand
isttrue
, wenn der Taschenrechner erwartet, dass der Benutzer mit der Eingabe eines Operanden beginnt.
Additive und multiplikative Operatoren werden unterschiedlich behandelt, da sie einen unterschiedlichen Vorrang haben. So wird zum Beispiel 1 + 2 ÷ 3 als 1 + (2 ÷ 3) interpretiert, weil ÷ eine höhere Priorität hat als +.
Die folgende Tabelle zeigt, wie sich der Zustand des Rechners entwickelt, wenn der Benutzer einen mathematischen Ausdruck eingibt.
Benutzereingabe | Anzeige | Summe bis jetzt | Add. Op. | Faktor so weit | Mult. Op. | Wartet auf Operand? |
---|---|---|---|---|---|---|
0 | 0 | true | ||||
1 | 1 | 0 | false | |||
1 + | 1 | 1 | + | true | ||
1 + 2 | 2 | 1 | + | false | ||
1 + 2 ÷ | 2 | 1 | + | 2 | ÷ | true |
1 + 2 ÷ 3 | 3 | 1 | + | 2 | ÷ | false |
1 + 2 ÷ 3 - | 1.66667 | 1.66667 | - | true | ||
1 + 2 ÷ 3 - 4 | 4 | 1.66667 | - | false | ||
1 + 2 ÷ 3 - 4 = | -2.33333 | 0 | true |
Unäre Operatoren, wie z.B. Sqrt, erfordern keine besondere Behandlung; sie können sofort angewendet werden, da der Operand bereits bekannt ist, wenn die Schaltfläche des Operators angeklickt wird.
QLineEdit *display; enum { NumDigitButtons = 10 }; Button *digitButtons[NumDigitButtons]; };
Schließlich deklarieren wir die Variablen, die mit dem Display und den Schaltflächen zur Anzeige der Zahlen verbunden sind.
Implementierung der Rechnerklasse
Calculator::Calculator(QWidget *parent) : QWidget(parent), sumInMemory(0.0), sumSoFar(0.0) , factorSoFar(0.0), waitingForOperand(true) {
Im Konstruktor initialisieren wir den Zustand des Rechners. Die Variablen pendingAdditiveOperator
und pendingMultiplicativeOperator
müssen nicht explizit initialisiert werden, da der Konstruktor QString sie mit leeren Zeichenketten initialisiert. Es ist auch möglich, diese Variablen direkt in der Kopfzeile zu initialisieren. Dies wird member-initializaton
genannt und vermeidet eine lange Initialisierungsliste.
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);
Wir erstellen die QLineEdit, die das Display des Taschenrechners darstellt, und richten einige seiner Eigenschaften ein. Insbesondere legen wir fest, dass sie schreibgeschützt ist.
Wir vergrößern auch die Schriftart von display
um 8 Punkte.
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);
Für jede Schaltfläche rufen wir die private Funktion createButton()
mit der entsprechenden Textbeschriftung und einem Steckplatz für die Verbindung zur Schaltfläche auf.
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")); }
Das Layout wird von einer einzigen QGridLayout verwaltet. Der Aufruf QLayout::setSizeConstraint() stellt sicher, dass das Widget Calculator
immer in seiner optimalen Größe ( size hint) angezeigt wird, so dass der Benutzer die Größe des Rechners nicht verändern kann. Der Hinweis auf die Größe wird durch die Größe und size policy der untergeordneten Widgets bestimmt.
Die meisten untergeordneten Widgets belegen nur eine Zelle im Grid-Layout. Für diese brauchen wir nur eine Zeile und eine Spalte an QGridLayout::addWidget() zu übergeben. Die Widgets display
, backspaceButton
, clearButton
und clearAllButton
belegen mehr als eine Spalte; für diese müssen wir auch eine Zeilenspanne und eine Spaltenspanne übergeben.
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)); }
Wenn Sie eine der Zifferntasten des Taschenrechners drücken, wird das Signal clicked() der Taste ausgegeben, das den Steckplatz digitClicked()
auslöst.
Zuerst finden wir mit QObject::sender() heraus, welche Taste das Signal gesendet hat. Diese Funktion gibt den Absender als Zeiger QObject zurück. Da wir wissen, dass der Absender ein Button
Objekt ist, können wir den QObject sicher casten. Wir hätten einen Cast im Stil von C oder einen C++ static_cast<>()
verwenden können, aber als defensive Programmiertechnik verwenden wir qobject_cast(). Der Vorteil ist, dass ein Null-Zeiger zurückgegeben wird, wenn das Objekt den falschen Typ hat. Abstürze aufgrund von Null-Zeigern sind viel einfacher zu diagnostizieren als Abstürze aufgrund von unsicheren Casts. Sobald wir die Schaltfläche haben, extrahieren wir den Operator mit QToolButton::text().
Der Slot muss vor allem zwei Situationen berücksichtigen. Wenn display
"0" enthält und der Benutzer auf die Schaltfläche 0 klickt, wäre es dumm, "00" anzuzeigen. Und wenn der Rechner in einem Zustand ist, in dem er auf einen neuen Operanden wartet, ist die neue Ziffer die erste Ziffer dieses neuen Operanden; in diesem Fall muss jedes Ergebnis einer vorherigen Berechnung zuerst gelöscht werden.
Am Ende wird die neue Ziffer an den Wert in der Anzeige angehängt.
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; }
Der Slot unaryOperatorClicked()
wird immer dann aufgerufen, wenn eine der Schaltflächen des unären Operators angeklickt wird. Auch hier wird mit QObject::sender() ein Zeiger auf die angeklickte Schaltfläche abgerufen. Der Operator wird aus dem Text der Schaltfläche extrahiert und in clickedOperator
gespeichert. Der Operand wird aus display
abgerufen.
Dann führen wir die Operation aus. Wenn Sqrt auf eine negative Zahl oder 1/x auf Null angewendet wird, rufen wir abortOperation()
auf. Wenn alles gut geht, zeigen wir das Ergebnis der Operation in der Bearbeitungszeile an und setzen waitingForOperand
auf true
. Dadurch wird sichergestellt, dass, wenn der Benutzer eine neue Ziffer eingibt, diese als neuer Operand betrachtet wird, anstatt an den aktuellen Wert angehängt zu werden.
void Calculator::additiveOperatorClicked() { Button *clickedButton = qobject_cast<Button *>(sender()); if (!clickedButton) return; QString clickedOperator = clickedButton->text(); double operand = display->text().toDouble();
Der Slot additiveOperatorClicked()
wird aufgerufen, wenn der Benutzer auf die Schaltfläche + oder - klickt.
Bevor wir etwas mit dem angeklickten Operator machen können, müssen wir alle anstehenden Operationen behandeln. Wir beginnen mit den multiplikativen Operatoren, da diese einen höheren Vorrang haben als additive Operatoren:
if (!pendingMultiplicativeOperator.isEmpty()) { if (!calculate(operand, pendingMultiplicativeOperator)) { abortOperation(); return; } display->setText(QString::number(factorSoFar)); operand = factorSoFar; factorSoFar = 0.0; pendingMultiplicativeOperator.clear(); }
Wenn × oder ÷ zuvor angeklickt wurde, ohne dass danach = angeklickt wurde, ist der aktuelle Wert in der Anzeige der rechte Operand des × - oder ÷ -Operators, und wir können die Operation endlich durchführen und die Anzeige aktualisieren.
if (!pendingAdditiveOperator.isEmpty()) { if (!calculate(operand, pendingAdditiveOperator)) { abortOperation(); return; } display->setText(QString::number(sumSoFar)); } else { sumSoFar = operand; }
Wenn + oder - zuvor angeklickt wurde, ist sumSoFar
der linke Operand und der aktuelle Wert in der Anzeige ist der rechte Operand des Operators. Wenn kein additiver Operator ansteht, wird sumSoFar
einfach auf den Text in der Anzeige gesetzt.
pendingAdditiveOperator = clickedOperator; waitingForOperand = true; }
Schließlich können wir uns um den Operator kümmern, der gerade angeklickt wurde. Da wir den rechten Operanden noch nicht haben, speichern wir den angeklickten Operator in der Variablen pendingAdditiveOperator
. Wir werden die Operation später anwenden, wenn wir einen rechten Operanden haben, mit sumSoFar
als linkem Operanden.
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; }
Der Slot multiplicativeOperatorClicked()
ist ähnlich wie additiveOperatorClicked()
. Wir müssen uns hier nicht um anhängige additive Operatoren kümmern, da multiplikative Operatoren Vorrang vor additiven Operatoren haben.
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; }
Wie bei additiveOperatorClicked()
werden zunächst alle anstehenden multiplikativen und additiven Operatoren behandelt. Dann zeigen wir sumSoFar
an und setzen die Variable auf Null zurück. Das Zurücksetzen der Variablen auf Null ist notwendig, damit der Wert nicht doppelt gezählt wird.
void Calculator::pointClicked() { if (waitingForOperand) display->setText("0"); if (!display->text().contains('.')) display->setText(display->text() + tr(".")); waitingForOperand = false; }
Der Slot pointClicked()
fügt dem Inhalt von display
einen Dezimalpunkt hinzu.
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); }
Der Slot changeSignClicked()
ändert das Vorzeichen des Wertes in display
. Wenn der aktuelle Wert positiv ist, wird ein Minuszeichen vorangestellt; wenn der aktuelle Wert negativ ist, wird das erste Zeichen des Wertes (das Minuszeichen) entfernt.
void Calculator::backspaceClicked() { if (waitingForOperand) return; QString text = display->text(); text.chop(1); if (text.isEmpty()) { text = "0"; waitingForOperand = true; } display->setText(text); }
Mit backspaceClicked()
wird das Zeichen ganz rechts in der Anzeige entfernt. Wenn wir eine leere Zeichenkette erhalten, zeigen wir "0" an und setzen waitingForOperand
auf true
.
void Calculator::clear() { if (waitingForOperand) return; display->setText("0"); waitingForOperand = true; }
Der Slot clear()
setzt den aktuellen Operanden auf Null zurück. Das ist gleichbedeutend damit, dass Sie Backspace so oft anklicken, dass der gesamte Operand gelöscht wird.
void Calculator::clearAll() { sumSoFar = 0.0; factorSoFar = 0.0; pendingAdditiveOperator.clear(); pendingMultiplicativeOperator.clear(); display->setText("0"); waitingForOperand = true; }
Der Steckplatz clearAll()
setzt den Rechner auf seinen Ausgangszustand zurück.
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(); }
Der Steckplatz clearMemory()
löscht die im Speicher gehaltene Summe, readMemory()
zeigt die Summe als Operand an, setMemory()
ersetzt die Summe im Speicher durch die aktuelle Summe und addToMemory()
addiert den aktuellen Wert zum Wert im Speicher. Für setMemory()
und addToMemory()
rufen wir zunächst equalClicked()
auf, um sumSoFar
und den Wert in der Anzeige zu aktualisieren.
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; }
Die private Funktion createButton()
wird vom Konstruktor aus aufgerufen, um Rechnertasten zu erstellen.
void Calculator::abortOperation() { clearAll(); display->setText(tr("####")); }
Die private Funktion abortOperation()
wird immer dann aufgerufen, wenn eine Berechnung fehlschlägt. Sie setzt den Status des Rechners zurück und zeigt "####" an.
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; }
Die private Funktion calculate()
führt eine Binäroperation durch. Der rechte Operand ist durch rightOperand
gegeben. Bei additiven Operatoren ist der linke Operand sumSoFar
; bei multiplikativen Operatoren ist der linke Operand factorSoFar
. Die Funktion gibt false
zurück, wenn eine Division durch Null stattfindet.
Definition der Klasse Button
Werfen wir nun einen Blick auf die Klasse Button
:
class Button : public QToolButton { Q_OBJECT public: explicit Button(const QString &text, QWidget *parent = nullptr); QSize sizeHint() const override; };
Die Klasse Button
hat einen Convenience-Konstruktor, der eine Textbeschriftung und ein übergeordnetes Widget annimmt, und sie implementiert QWidget::sizeHint() neu, um mehr Platz um den Text herum zu schaffen, als die Klasse QToolButton normalerweise bietet.
Implementierung der Klasse Button
Button::Button(const QString &text, QWidget *parent) : QToolButton(parent) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); setText(text); }
Das Aussehen der Schaltflächen wird durch das Layout des Taschenrechner-Widgets über die Größe und size policy der untergeordneten Widgets des Layouts bestimmt. Der Aufruf der Funktion setSizePolicy() im Konstruktor stellt sicher, dass sich die Schaltfläche horizontal ausdehnt, um den gesamten verfügbaren Platz auszufüllen; standardmäßig dehnen sich QToolButtons nicht aus, um den verfügbaren Platz auszufüllen. Ohne diesen Aufruf hätten die verschiedenen Schaltflächen in einer Spalte eine unterschiedliche Breite.
QSize Button::sizeHint() const { QSize size = QToolButton::sizeHint(); size.rheight() += 20; size.rwidth() = qMax(size.width(), size.height()); return size; }
In sizeHint() versuchen wir, eine Größe zurückzugeben, die für die meisten Schaltflächen gut aussieht. Wir verwenden den Größenhinweis der Basisklasse (QToolButton), ändern ihn aber auf folgende Weise ab:
- Wir fügen 20 zur Komponente height des Größenhinweises hinzu.
- Die Komponente width des Größenhinweises wird mindestens so groß wie die Komponente height.
Dadurch wird sichergestellt, dass die Ziffern- und Bedienerschaltflächen bei den meisten Schriftarten quadratisch sind, ohne dass der Text auf den Schaltflächen Backspace, Clear und Clear All abgeschnitten wird.
Der Screenshot unten zeigt, wie das Calculator
Widget aussehen würde, wenn wir die horizontale Größenrichtlinie im Konstruktor nicht auf QSizePolicy::Expanding setzen würden und wenn wir QWidget::sizeHint() nicht neu implementieren würden.
Das Calculator-Beispiel mit Standard-Größenrichtlinien und Größenhinweisen
© 2025 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.