计算器示例
该示例展示如何使用信号和槽来实现计算器组件的功能,以及如何使用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()
在组件构建过程中使用。遇到零除或对负数应用平方根操作时,将调用 abortOperation()
。函数 calculate()
应用二元运算符(+、-、× 或 ÷)。
double sumInMemory; double sumSoFar; double factorSoFar; QString pendingAdditiveOperator; QString pendingMultiplicativeOperator; bool waitingForOperand;
以下变量,以及计算器显示屏的内容(一个 QLineEdit),编码了计算器的状态。
sumInMemory
包含计算器内的存储值(使用 MS、M+ 或 MC)。sumSoFar
存储到目前为止累积的值。当用户点击 = 时,重新计算sumSoFar
并在显示屏上显示。 全部清除 将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(QWidget *parent) : QWidget(parent), sumInMemory(0.0), sumSoFar(0.0) , factorSoFar(0.0), waitingForOperand(true) {
在构造函数中,我们初始化计算器的状态。变量 pendingAdditiveOperator
和 pendingMultiplicativeOperator
不需要显式初始化,因为 QString 构造函数将它们初始化为空字符串。也可以直接在头文件中初始化这些变量。这被称为 成员初始化
,可以避免长的初始化列表。
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),但按照以下方式进行了修改
这确保了在大多数字体中,数字和运算按钮将是正方形的,而不会截断退格,清除和清除所有按钮上的文本。
下面的截图显示了如果我们在构造函数中没有设置水平大小策略为QSizePolicy::Expanding,并且我们没有重新实现QWidget::sizeHint,那么Calculator
小部件会看起来如何。
使用默认大小策略和大小提示的默认示例
© 2024 The Qt Company Ltd. 本文档中包含的文档贡献归各自所有者版权所有。本提供的文档根据由自由软件基金会发布的GNU自由文档许可协议版本1.3的条款进行许可。Qt和相关标志是芬兰及/或世界上其他国家的The Qt Company Ltd.的商标。所有其他商标均为其各自所有者的财产。