计算器示例
该示例展示了如何使用信号和槽来实现计算器部件的功能,以及如何使用QGridLayout 在网格中放置子部件。
计算器示例截图
该示例由两个类组成:
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,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()
函数是 widget 结构的一部分。每当出现除以 0 或对负数进行平方根运算时,就会调用abortOperation()
。calculate()
应用二进制运算符 (+,-,×, 或÷)。
double sumInMemory; double sumSoFar; double factorSoFar; QString pendingAdditiveOperator; QString pendingMultiplicativeOperator; bool waitingForOperand;
这些变量,连同计算器显示屏的内容(QLineEdit ),对计算器的状态进行编码:
sumInMemory
包含存储在计算器内存中的数值(使用 , , 或 )。MS M+ MCsumSoFar
存储当前累积的数值。当用户点击 时, 将被重新计算并显示在显示屏上。 将 重置为零。=sumSoFar
Clear AllsumSoFar
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(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
类:
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) 的尺寸提示,但对其进行了如下修改:
这样可以确保在大多数字体中,数字和运算符按钮都是方形的,而不会截断Backspace 、Clear 和Clear All 按钮上的文本。
下面的截图显示了如果我们不在构造函数中将水平尺寸策略设置为QSizePolicy::Expanding ,也不重新实现QWidget::sizeHint() 的情况下,Calculator
widget 的外观。
带有默认大小策略和大小提示的计算器示例
© 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.