電卓の例

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

電卓の例のスクリーンショット

このサンプルは、2つのクラスで構成されています:

  • Calculator は電卓ウィジェットで、すべての電卓機能を持ちます。
  • Button は、電卓の各ボタンに使用されるウィジェットです。これは から派生します。QToolButton

まずCalculator を確認し、次にButton を見てみましょう。

電卓クラスの定義

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,,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 ÷ 31 + (2 ÷ 3) と解釈されます。これは、÷ の方が+ より優先順位が高いからです。

以下の表は、ユーザが数式を入力するときの電卓の状態の推移を示しています。

ユーザ入力表示これまでの合計加算。Op.これまでの係数マルチオペランドオペランド待ち?
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

Sqrt のような単項演算子は、特別な処理を必要としない。演算子ボタンがクリックされた時点でオペランドがすでに分かっているので、すぐに適用できる。

    QLineEdit *display;

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

最後に、ディスプレイと数字を表示するためのボタンに関連する変数を宣言します。

電卓クラスの実装

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

コンストラクタで、電卓の状態を初期化する。QString コンストラクタで空文字列に初期化されるため、pendingAdditiveOperatorpendingMultiplicativeOperator 変数を明示的に初期化する必要はありません。これらの変数をヘッダーで直接初期化することも可能です。これは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"));
}

レイアウトは1つのQGridLayoutQLayout::setSizeConstraint() 呼び出しは、Calculator ウィジェットが常に最適なサイズ (size hint) で表示されるようにし、ユーザが電卓のサイズを変更できないようにします。サイズのヒントは、子ウィジェットのサイズとsize policy によって決まります。

ほとんどの子ウィジェットは、グリッドレイアウトの1つのセルを占めるだけです。これらの場合、QGridLayout::addWidget() に行と列を渡すだけでよい。displaybackspaceButtonclearButtonclearAllButton ウィジェットは、複数の列を占有します。これらについては、行スパンと列スパンも渡す必要があります。

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));
}

電卓の数字ボタンの1つを押すと、そのボタンのclicked ()信号が発せられ、digitClicked() スロットがトリガーされます。

まず、QObject::sender ()を使って、どのボタンがシグナルを発信したかを調べます。この関数は、送信者をQObject ポインタとして返します。送信者はButton オブジェクトであることがわかっているので、QObject を安全にキャストすることができます。CスタイルのキャストやC++のstatic_cast<>() を使用することもできましたが、防御的なプログラミングテクニックとして、qobject_cast ()を使用します。この利点は、オブジェクトの型が正しくない場合、ヌル・ポインタが返されることである。ヌル・ポインタによるクラッシュは、安全でないキャストによるクラッシュよりも診断がはるかに簡単です。ボタンが手に入ったら、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() を呼び出します。すべてがうまくいったら、操作の結果を行編集に表示し、waitingForOperandtrue に設定します。これにより、ユーザが新しい桁を入力した場合、その桁は現在の値に追加されるのではなく、新しいオペランドとして考慮されます。

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」を表示し、waitingForOperandtrue に設定する。

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;
}

privatecalculate() 関数は2進演算を行います。右オペランドはrightOperand で与えられる。加算演算子の場合、左オペランドはsumSoFar である。乗算演算子の場合、左オペランドはfactorSoFar である。ゼロによる除算が発生した場合、関数はfalse を返します。

ボタン・クラスの定義

次に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(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)のサイズ・ヒントを再利用しますが、次のように変更します:

  • サイズ・ヒントのheight コンポーネントに 20 を加えます。
  • サイズヒントのwidth コンポーネントをheight コンポーネントと同じ大きさにします。

これにより、ほとんどのフォントで、BackspaceClearClear All ボタンのテキストが切り捨てられることなく、数字ボタンと演算子ボタンが正方形になります。

下のスクリーンショットは、コンストラクタで水平サイズポリシーをQSizePolicy::Expanding に設定せずQWidget::sizeHint() を再実装しなかった場合のCalculator ウィジェットの様子を示しています。

デフォルトのサイズポリシーとサイズヒントを使用した電卓の例

サンプルプロジェクト @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。