本页内容

计算器示例

本示例演示了如何使用信号和槽来实现计算器小部件的功能,以及如何使用QGridLayout 将子小部件放置在网格中。

基础计算器应用程序

计算器示例的屏幕截图

该示例由两个类组成:

  • 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) ,因为÷ 的优先级高于+ 。

下表展示了用户输入数学表达式时计算器状态的变化过程。

用户输入显示当前求和结果加法运算当前因数乘法运算正在等待操作数?
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)
{

在构造函数中,我们初始化计算器的状态。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 决定。

大多数子控件在网格布局中仅占用一个单元格。对于这些控件,我们只需向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()。 其优点在于,如果对象类型不匹配,将返回一个空指针。由空指针引起的崩溃比由不安全强制转换引起的崩溃更容易诊断。获得按钮后,我们使用QToolButton::text()提取操作符。

该槽函数需要特别考虑两种情况。 如果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 ` 并将其重置为零。将变量重置为零是必要的,以避免将该值计数两次。

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 ),但对其进行了以下修改:

  • 我们将大小提示的height 部分加 20。
  • 我们将大小提示中width 的组件值设置为至少与height 的值相同。

这可确保在绝大多数字体下,数字和运算符按钮均呈正方形,且不会截断Backspace 、Clear 以及Clear All 按钮上的文本。

下图截图展示了,如果我们在构造函数中未将水平尺寸策略设置为QSizePolicy::Expanding ,且未重写QWidget::sizeHint()方法,Calculator 控件会呈现何种外观。

按键排列不齐的计算器

采用默认尺寸策略和尺寸提示的“计算器”示例

示例项目 @ 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.