電卓の例
この例では、シグナルとスロットを使用して電卓ウィジェットの機能を実装する方法、およびQGridLayout を使用して子ウィジェットをグリッドに配置する方法を示しています。

電卓の例のスクリーンショット
このサンプルは2つのクラスで構成されています:
Calculatorは、電卓の全機能を備えた電卓ウィジェットです。Buttonは、電卓の各ボタンに使用されるウィジェットです。これはQToolButton を継承しています。
まずCalculator について復習し、次にButton について見ていきます。
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();Calculator クラスは、シンプルな電卓ウィジェットを提供します。このクラスはQDialog から継承されており、電卓のボタンに関連付けられたいくつかのプライベートスロットを持っています。QObject::eventFilter()は、電卓のディスプレイ上のマウスイベントを処理するために再実装されています。
ボタンは、その動作に応じてカテゴリごとにグループ化されています。例えば、すべての数字ボタン(0 から9 まで)は、現在のオペランドに数字を追加します。これらについては、複数のボタンを同じスロット(例:digitClicked() )に接続します。 カテゴリには、数字、単項演算子(Sqrt 、x² 、1/x )、加算演算子(+ 、- )、乗算演算子(× 、÷ )があります。その他のボタンには、それぞれ独自のスロットが用意されています。
private:
template<typename PointerToMemberFunction>
Button *createButton(const QString &text, const PointerToMemberFunction &member);
void abortOperation();
bool calculate(double rightOperand, const QString &pendingOperator);プライベート関数createButton() は、ウィジェットの構築の一部として使用されます。abortOperation() は、ゼロ除算が発生したとき、または負の数に平方根演算が適用されたときに呼び出されます。calculate() は、二項演算子(+ 、- 、× 、または÷ )を適用します。
double sumInMemory;
double sumSoFar;
double factorSoFar;
QString pendingAdditiveOperator;
QString pendingMultiplicativeOperator;
bool waitingForOperand;これらの変数と、電卓のディスプレイに表示されている内容(QLineEdit )が組み合わさって、電卓の状態を表現します:
sumInMemoryには、電卓のメモリに格納された値が含まれます(MS 、M+ 、またはMC を使用)。sumSoFarこれまでに累積された値を保持します。ユーザーが「= 」をクリックすると、「sumSoFar」が再計算され、ディスプレイに表示されます。「Clear All 」は「sumSoFar」をゼロにリセットします。factorSoFar乗算や除算を行う際の一時的な値を保存します。pendingAdditiveOperatorユーザーが最後にクリックした加算演算子を保存します。pendingMultiplicativeOperatorユーザーが最後にクリックした乗算演算子を保存します。waitingForOperand電卓がユーザーによるオペランドの入力開始を待機している状態では、trueとなります。
加算演算子と乗算演算子は、優先順位が異なるため、異なる扱いを受けます。たとえば、1 + 2 ÷ 3 は、÷ の優先順位が+ よりも高いため、1 + (2 ÷ 3) と解釈されます。
以下の表は、ユーザーが数式を入力するにつれて電卓の状態がどのように変化するかを示しています。
| ユーザー入力 | 表示 | 現在の合計 | 加算演算子 | これまでの因数 | 加算演算 | オペランドを待っていますか? |
|---|---|---|---|---|---|---|
| 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 |
Sqrt などの単項演算子は、特別な処理を必要としません。演算子ボタンをクリックした時点でオペランドがすでに判明しているため、直ちに適用できます。
QLineEdit *display;
enum { NumDigitButtons = 10 };
Button *digitButtons[NumDigitButtons];
};最後に、表示に関連する変数と、数値を表示するために使用されるボタンを宣言します。
Calculator クラスの実装
Calculator::Calculator(QWidget *parent)
: QWidget(parent), sumInMemory(0.0), sumSoFar(0.0)
, factorSoFar(0.0), waitingForOperand(true)
{コンストラクタ内で、電卓の状態を初期化します。pendingAdditiveOperator およびpendingMultiplicativeOperator 変数は、QString のコンストラクタによって空の文字列に初期化されるため、明示的に初期化する必要はありません。また、ヘッダー内でこれらの変数を直接初期化することも可能です。これは「member-initializaton 」と呼ばれ、長い初期化リストを避けることができます。
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);電卓のディスプレイを表す `QLineEdit ` を作成し、そのプロパティの一部を設定します。特に、読み取り専用に設定します。
また、display のフォントサイズを8ポイント拡大します。
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);各ボタンについて、適切なテキストラベルとボタンに接続するためのスロットを引数として、createButton() のプライベート関数を呼び出します。
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"));
}レイアウトは単一の `QGridLayout` によって処理されます。`QLayout::setSizeConstraint()` の呼び出しにより、`Calculator ` ウィジェットが常に最適なサイズ(その `size hint`)で表示されることが保証され、ユーザーが電卓のサイズを変更できないようにします。サイズのヒントは、子ウィジェットのサイズと `size policy ` によって決定されます。
ほとんどの子ウィジェットは、グリッドレイアウト内で 1 セルしか占有しません。これらの場合、QGridLayout::addWidget() には行と列のみを渡せば十分です。display 、backspaceButton 、clearButton 、およびclearAllButton ウィジェットは複数の列を占有するため、これらの場合は行スパンと列スパンも渡す必要があります。
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));
}電卓の数字ボタンのいずれかを押すと、そのボタンのclicked()シグナルが発信され、digitClicked() スロットがトリガーされます。
まず、QObject::sender() を使用して、どのボタンがシグナルを送信したかを特定します。この関数は、送信元をQObject ポインタとして返します。送信元がButton オブジェクトであることがわかっているため、QObject を安全にキャストできます。C スタイルのキャストや C++ のstatic_cast<>() を使用することも可能でしたが、防御的プログラミングの手法として、qobject_cast() を使用します。 この利点は、オブジェクトの型が正しくない場合、nullポインタが返される点にあります。nullポインタによるクラッシュは、安全でない型変換によるクラッシュよりもはるかに診断しやすいです。ボタンを取得したら、QToolButton::text() を使用して演算子を抽出します。
このスロットでは、特に2つの状況を考慮する必要があります。display に「0」が含まれており、ユーザーが「0 」ボタンをクリックした場合、「00」を表示するのは不自然です。また、電卓が新しいオペランドを待機している状態にある場合、新しい桁はその新しいオペランドの最初の桁となります。その場合は、以前の計算結果をすべて先にクリアする必要があります。
最後に、新しい桁をディスプレイ上の値に追加します。
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;
}unaryOperatorClicked() スロットは、単項演算子ボタンのいずれかがクリックされるたびに呼び出されます。ここでも、QObject::sender() を使用してクリックされたボタンへのポインタを取得します。演算子はボタンのテキストから抽出され、clickedOperator に格納されます。オペランドはdisplay から取得されます。
その後、演算を実行します。Sqrt が負の数に対して適用された場合、または1/x がゼロに対して適用された場合は、abortOperation() を呼び出します。すべてが正常に処理された場合、演算結果をラインエディットに表示し、waitingForOperand をtrue に設定します。これにより、ユーザーが新しい数字を入力した際、その数字が現在の値に追加されるのではなく、新しいオペランドとして扱われるようになります。
void Calculator::additiveOperatorClicked()
{
Button *clickedButton = qobject_cast<Button *>(sender());
if (!clickedButton)
return;
QString clickedOperator = clickedButton->text();
double operand = display->text().toDouble();ユーザーが「+ 」または「- 」ボタンをクリックすると、「additiveOperatorClicked() 」スロットが呼び出されます。
クリックされた演算子に対して実際に処理を行う前に、保留中の演算を処理する必要があります。加算演算子よりも優先順位が高いため、まず乗算演算子から処理を開始します:
if (!pendingMultiplicativeOperator.isEmpty()) {
if (!calculate(operand, pendingMultiplicativeOperator)) {
abortOperation();
return;
}
display->setText(QString::number(factorSoFar));
operand = factorSoFar;
factorSoFar = 0.0;
pendingMultiplicativeOperator.clear();
}もし、以前に「× 」または「÷ 」がクリックされ、その後「= 」がクリックされていない場合、表示されている現在の値は「× 」または「÷ 」演算子の右辺のオペランドとなります。そこで、ようやく演算を実行し、表示を更新することができます。
if (!pendingAdditiveOperator.isEmpty()) {
if (!calculate(operand, pendingAdditiveOperator)) {
abortOperation();
return;
}
display->setText(QString::number(sumSoFar));
} else {
sumSoFar = operand;
}もし以前に+ または- がクリックされていた場合、sumSoFar が左オペランドとなり、表示されている現在の値がその演算子の右オペランドとなります。保留中の加算演算子がない場合、sumSoFar は単に表示されているテキストに設定されます。
pendingAdditiveOperator = clickedOperator;
waitingForOperand = true;
}最後に、クリックされた演算子の処理を行います。まだ右辺のオペランドがないため、クリックされた演算子をpendingAdditiveOperator 変数に格納します。演算の適用は、右辺のオペランドが用意された後、sumSoFar を左辺のオペランドとして行います。
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;
}multiplicativeOperatorClicked() スロットはadditiveOperatorClicked() と同様です。乗算演算子は加算演算子よりも優先されるため、ここでは保留中の加算演算子について心配する必要はありません。
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;
}additiveOperatorClicked() と同様に、まず保留中の乗算演算子と加算演算子を処理します。その後、sumSoFar を表示し、変数をゼロにリセットします。変数をゼロにリセットするのは、値が2回カウントされるのを防ぐために必要です。
void Calculator::pointClicked()
{
if (waitingForOperand)
display->setText("0");
if (!display->text().contains('.'))
display->setText(display->text() + tr("."));
waitingForOperand = false;
}pointClicked() スロットは、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);
}changeSignClicked() スロットは、display 内の値の符号を変更します。現在の値が正の場合、マイナス記号を先頭に付加し、現在の値が負の場合、値の先頭文字(マイナス記号)を削除します。
void Calculator::backspaceClicked()
{
if (waitingForOperand)
return;
QString text = display->text();
text.chop(1);
if (text.isEmpty()) {
text = "0";
waitingForOperand = true;
}
display->setText(text);
}backspaceClicked() は、表示の最右端の文字を削除します。空の文字列が渡された場合、「0」を表示し、waitingForOperand をtrue に設定します。
void Calculator::clear()
{
if (waitingForOperand)
return;
display->setText("0");
waitingForOperand = true;
}clear() スロットは、現在のオペランドをゼロにリセットします。これは、オペランド全体を消去するのに十分な回数、Backspace をクリックすることと同等です。
void Calculator::clearAll()
{
sumSoFar = 0.0;
factorSoFar = 0.0;
pendingAdditiveOperator.clear();
pendingMultiplicativeOperator.clear();
display->setText("0");
waitingForOperand = true;
}「clearAll() 」スロットは、電卓を初期状態にリセットします。
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();
}clearMemory() スロットはメモリに保持されている合計値を消去し、readMemory() は合計値をオペランドとして表示し、setMemory() はメモリ内の合計値を現在の合計値に置き換え、addToMemory() は現在の値をメモリ内の値に加算します。setMemory() およびaddToMemory() については、まずequalClicked() を呼び出してsumSoFar とディスプレイ上の値を更新します。
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;
}コンストラクタから非公開のcreateButton() 関数が呼び出され、電卓のボタンが作成されます。
void Calculator::abortOperation()
{
clearAll();
display->setText(tr("####"));
}計算に失敗するたびに、プライベート関数 `abortOperation() ` が呼び出されます。この関数は、電卓の状態をリセットし、「####」を表示します。
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;
}プライベート関数 `calculate() ` は二項演算を実行します。右オペランドは `rightOperand` で指定されます。加算演算子の場合、左オペランドは `sumSoFar` であり、乗算演算子の場合、左オペランドは `factorSoFar` です。この関数は、ゼロ除算が発生した場合に `false ` を返します。
Buttonクラスの定義
それでは、Button クラスを見てみましょう:
class Button : public QToolButton
{
Q_OBJECT
public:
explicit Button(const QString &text, QWidget *parent = nullptr);
QSize sizeHint() const override;
};Button クラスには、テキストラベルと親ウィジェットを受け取る便利なコンストラクタがあり、QWidget::sizeHint() を再実装して、QToolButton が通常提供するよりもテキストの周囲に広い余白を確保しています。
Button クラスの実装
Button::Button(const QString &text, QWidget *parent)
: QToolButton(parent)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
setText(text);
}ボタンの外観は、レイアウトの子ウィジェットのサイズおよびsize policy によって、電卓ウィジェットのレイアウトを通じて決定されます。コンストラクタ内でsetSizePolicy()関数を呼び出すことで、ボタンが利用可能なスペース全体を埋めるように水平方向に拡張されることが保証されます。デフォルトでは、QToolButtonは利用可能なスペースを埋めるように拡張されません。 この呼び出しがない場合、同じ列にある異なるボタンの幅がバラバラになってしまいます。
QSize Button::sizeHint() const
{
QSize size = QToolButton::sizeHint();
size.rheight() += 20;
size.rwidth() = qMax(size.width(), size.height());
return size;
}sizeHint() では、ほとんどのボタンで見栄えの良いサイズを返すよう努めています。基底クラス(QToolButton )のサイズヒントを再利用しますが、以下の点で変更を加えています:
これにより、ほとんどのフォントにおいて、Backspace 、Clear 、およびClear All の各ボタン上のテキストが切り捨てられることなく、数字および演算子ボタンが正方形になることが保証されます。
以下のスクリーンショットは、コンストラクタで水平サイズポリシーをQSizePolicy::Expanding に設定せず、QWidget::sizeHint()を再実装しなかった場合に、Calculator ウィジェットがどのように表示されるかを示しています。

デフォルトのサイズポリシーとサイズヒントを使用した「Calculator」の例
© 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.