警告

本部分包含从 C++ 自动翻译到 Python 的摘录,可能包含错误。

计算器示例#

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

../_images/calculator-example.png

计算器示例截图

示例包含两个类

  • Calculator 是带有所有计算器功能的计算器小部件。

  • Button 是用于每个计算器按钮的小部件。它继承自 QToolButton

我们将首先回顾 Calculator,然后我们将看看 Button

计算器类定义#

class Calculator(QWidget):

    Q_OBJECT
# public
    Calculator(QWidget parent = None)
# private slots
    def digitClicked():
    def unaryOperatorClicked():
    def additiveOperatorClicked():
    def multiplicativeOperatorClicked():
    def equalClicked():
    def pointClicked():
    def changeSignClicked():
    def backspaceClicked():
    def clear():
    def clearAll():
    def clearMemory():
    def readMemory():
    def setMemory():
    def addToMemory():

Calculator 类提供了一个简单的计算器小部件。它继承自 QDialog 并具有与计算器按钮相关联的几个私有槽。重写了 QObject::eventFilter() 来处理计算器显示上的鼠标事件。

按钮根据其行为分组。例如,所有的数字按钮(标注为 0 到 9)将数字附加到当前操作数。对于这些,我们将多个按钮连接到相同的槽(例如,digitClicked())。这些类别是数字、一元运算符(根号、x²、1/x)、加法运算符(+、-)和乘法运算符(×、÷)。其他按钮有自己的槽。

# private
template<typename PointerToMemberFunction>
def createButton(text,member):
def abortOperation():
calculate = bool(float rightOperand, QString pendingOperator)

私有函数 createButton() 用于小部件构造的一部分。在发生除以零、平方根操作应用于负数时调用 abortOperation()。调用 calculate() 应用二元运算符(+、-、×、或 ÷)。

sumInMemory = float()
sumSoFar = float()
factorSoFar = float()
pendingAdditiveOperator = QString()
pendingMultiplicativeOperator = QString()
waitingForOperand = bool()

这些变量以及计算器显示的内容(一个 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

一元运算符,如开平方根,不需要特别处理;它们可以立即应用,因为当运营商按钮被点击时,操作数已经已知。

display = QLineEdit()

enum { NumDigitButtons = 10 }
digitButtons[NumDigitButtons] = Button()

最后,我们声明与显示和显示数字的按钮相关的变量。

计算器类实现#

def __init__(self, parent):
    super().__init__(parent)
    self.sumInMemory = 0.0
    self.sumSoFar = 0.0
    , factorSoFar(0.0), waitingForOperand(True)

在构造函数中,我们初始化计算器的状态。变量 pendingAdditiveOperatorpendingMultiplicativeOperator 不需要显式初始化,因为 QString 构造函数会将它们初始化为空字符串。也可以直接在头文件中初始化这些变量。这称为 成员初始化,可以避免长的初始化列表。

display = QLineEdit("0")
display.setReadOnly(True)
display.setAlignment(Qt.AlignRight)
display.setMaxLength(15)
font = display.font()
font.setPointSize(font.pointSize() + 8)
display.setFont(font)

我们创建了代表计算器显示屏的 QLineEdit 并设置了一些属性。特别地,我们将它设置为只读。

我们还把 display 的字体放大了 8 点。

for i in range(0, NumDigitButtons):
    digitButtons[i] = createButton(QString.number(i), Calculator.digitClicked)
pointButton = createButton(tr("."), Calculator::pointClicked)
changeSignButton = createButton(tr("\302\261"), Calculator::changeSignClicked)
backspaceButton = createButton(tr("Backspace"), Calculator::backspaceClicked)
clearButton = createButton(tr("Clear"), Calculator::clear)
clearAllButton = createButton(tr("Clear All"), Calculator::clearAll)
clearMemoryButton = createButton(tr("MC"), Calculator::clearMemory)
readMemoryButton = createButton(tr("MR"), Calculator::readMemory)
setMemoryButton = createButton(tr("MS"), Calculator::setMemory)
addToMemoryButton = createButton(tr("M+"), Calculator::addToMemory)
divisionButton = createButton(tr("\303\267"), Calculator::multiplicativeOperatorClicked)
timesButton = createButton(tr("\303\227"), Calculator::multiplicativeOperatorClicked)
minusButton = createButton(tr("-"), Calculator::additiveOperatorClicked)
plusButton = createButton(tr("+"), Calculator::additiveOperatorClicked)
squareRootButton = createButton(tr("Sqrt"), Calculator::unaryOperatorClicked)
powerButton = createButton(tr("x\302\262"), Calculator::unaryOperatorClicked)
reciprocalButton = createButton(tr("1/x"), Calculator::unaryOperatorClicked)
equalButton = createButton(tr("="), Calculator::equalClicked)

对于每个按钮,我们使用适当的文本标签和按钮的插槽调用私有函数 createButton()

mainLayout = 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 i in range(1, NumDigitButtons):
    row = ((9 - i) / 3) + 2
    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 处理。setSizeConstraint() 调用确保 Calculator 小部件始终以其最佳大小显示(其 size hint),防止用户调整计算器的大小。大小提示由子小部件的大小和 size policy 决定。

大多数子小部件只占用网格布局中的一个单元格。对于这些,我们只需要向 addWidget() 方法传递行号和列号。对于 displaybackspaceButtonclearButtonclearAllButton 小部件,它们占用的列数超过一个,因此我们还需要传递行跨度和列跨度。

def digitClicked(self):

    clickedButton = Button(sender())
    digitValue = clickedButton.text().toInt()
    if display.text() == "0" and digitValue == 0.0:
        return
    if waitingForOperand:
        display.clear()
        waitingForOperand = False

    display.setText(display.text() + QString.number(digitValue))

按下计算器的一个数字按钮将发射按钮的 clicked() 信号,该信号将触发 digitClicked() 接口。

首先,我们通过使用 QObject::sender() 函数找到发出信号的按钮。这个函数将发送者作为一个 QObject 指针返回。既然我们知道发送者是一个 Button 对象,我们可以安全地进行类型转换。我们可以使用 C 风格的转换或 C++ 的 static_cast<>(),但是作为一种防御性编程技术,我们使用 qobject_cast()。优点是如果对象类型不正确,则返回一个空指针。由于空指针造成的崩溃比不安全转换造成的崩溃更容易诊断。一旦我们获得按钮,我们就使用 text() 提取操作符。

插槽需要考虑两种特殊情况。如果 display 包含“0”,并且用户点击了 0 按钮,显示“00”将是愚蠢的。并且如果计算器处于等待新操作数的状态,新数字将是新操作数的第一位数字;在这种情况下,必须首先清除之前的计算结果。

最后,将新数字追加到显示屏中的值。

def unaryOperatorClicked(self):
clickedButton = Button(sender())
clickedOperator = clickedButton.text()
operand = display.text().toDouble()
result = 0.0
if clickedOperator == tr("Sqrt"):
    if operand < 0.0:
        abortOperation()
        return

    result = std.sqrt(operand)
 elif clickedOperator == tr("x\302\262"):
    result = std.pow(operand, 2.0)
 elif 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 获取。

然后我们执行操作。如果将平方根应用于负数或在 1/x 中为零,我们调用 abortOperation()。如果一切顺利,我们将操作的结果显示在行编辑器中,并将 waitingForOperand 设置为 true。这样确保如果用户输入新的数字,该数字将被视为新的操作数,而不是附加到当前值。

def additiveOperatorClicked(self):
clickedButton = Button(sender())
if not clickedButton:
  return
clickedOperator = clickedButton.text()
operand = display.text().toDouble()

当用户单击 + 或 - 按钮时,将调用 additiveOperatorClicked() 接口。

在我们可以真正对所点击的运算符做出处理后,我们必须处理任何挂起的操作。我们首先从乘法运算符开始,因为它们的优先级高于加法运算符

if not pendingMultiplicativeOperator.isEmpty():
if not calculate(operand, pendingMultiplicativeOperator):
    abortOperation()
    return

display.setText(QString.number(factorSoFar))
operand = factorSoFar
factorSoFar = 0.0
pendingMultiplicativeOperator.clear()

如果之前单击了 × 或 ÷,而没有随后单击 =,则显示屏中的当前值是 × 或 ÷ 的右操作数,我们最终可以执行操作并更新显示屏。

if not pendingAdditiveOperator.isEmpty():
    if not calculate(operand, pendingAdditiveOperator):
        abortOperation()
        return

    display.setText(QString.number(sumSoFar))
else:
    sumSoFar = operand

如果之前单击了 + 或 -,则 sumSoFar 是左操作数,而显示屏中的当前值是运算符的右操作数。如果没有挂起的加法运算符,则 sumSoFar 将简单地设置为显示屏中的文本。

pendingAdditiveOperator = clickedOperator
waitingForOperand = True

最后,我们可以处理刚刚点击的运算符。由于我们还没有右操作数,我们将点击的运算符存储在变量pendingAdditiveOperator中。当有了右操作数后,我们将使用sumSoFar作为左操作数应用这个运算。

def multiplicativeOperatorClicked(self):

    clickedButton = Button(sender())
    if not clickedButton:
      return
    clickedOperator = clickedButton.text()
    operand = display.text().toDouble()
    if not pendingMultiplicativeOperator.isEmpty():
        if not calculate(operand, pendingMultiplicativeOperator):
            abortOperation()
            return

        display.setText(QString.number(factorSoFar))
    else:
        factorSoFar = operand

    pendingMultiplicativeOperator = clickedOperator
    waitingForOperand = True

slot multiplicativeOperatorClicked()additiveOperatorClicked()类似。我们在这里不需要担心待处理的加法运算符,因为乘法运算符比加法运算符有优先级。

def equalClicked(self):

    operand = display.text().toDouble()
    if not pendingMultiplicativeOperator.isEmpty():
        if not calculate(operand, pendingMultiplicativeOperator):
            abortOperation()
            return

        operand = factorSoFar
        factorSoFar = 0.0
        pendingMultiplicativeOperator.clear()

    if not pendingAdditiveOperator.isEmpty():
        if not calculate(operand, pendingAdditiveOperator):
            abortOperation()
            return

        pendingAdditiveOperator.clear()
    else:
        sumSoFar = operand

    display.setText(QString.number(sumSoFar))
    sumSoFar = 0.0
    waitingForOperand = True

与在additiveOperatorClicked()中的做法一样,我们首先处理任何待处理的乘法和加法运算符。然后显示sumSoFar并将变量重置为0。将变量重置为0是必要的,以避免重复计数。

def pointClicked(self):

    if waitingForOperand:
        display.setText("0")
    if not display.text().contains('.'):
        display.setText(display.text() + tr("."))
    waitingForOperand = False

slot pointClicked()将小数点添加到display中的内容。

def changeSignClicked(self):

    text = display.text()
    value = text.toDouble()
    if value > 0.0:
        text.prepend(tr("-"))
     elif value < 0.0:
        text.remove(0, 1)

    display.setText(text)

slot changeSignClicked()变更display中数值的符号。如果当前数值为正,我们添加负号;如果当前数值为负,我们从数值中删除第一个字符(负号)。

def backspaceClicked(self):

    if waitingForOperand:
        return
    text = display.text()
    text.chop(1)
    if text.isEmpty():
        text = "0"
        waitingForOperand = True

    display.setText(text)

slot backspaceClicked()移除显示中的最右边字符。如果得到空字符串,我们显示“0”并将变量waitingForOperand设置为true

def clear(self):

    if waitingForOperand:
        return
    display.setText("0")
    waitingForOperand = True

slot clear()将当前操作数重置为0。这相当于连续点击Backspace按钮直到整个操作数被删除。

def clearAll(self):

    sumSoFar = 0.0
    factorSoFar = 0.0
    pendingAdditiveOperator.clear()
    pendingMultiplicativeOperator.clear()
    display.setText("0")
    waitingForOperand = True

slot clearAll()将计算器重置到初始状态。

def clearMemory(self):

    sumInMemory = 0.0

def readMemory(self):

    display.setText(QString.number(sumInMemory))
    waitingForOperand = True

def setMemory(self):

    equalClicked()
    sumInMemory = display.text().toDouble()

def addToMemory(self):

    equalClicked()
    sumInMemory += display.text().toDouble()

slot clearMemory()从存储中删除总和。函数readMemory()显示总和作为操作数,函数setMemory()用当前总和替换内存中的总和,函数addToMemory()将当前值添加到内存中的值。在setMemory()addToMemory()中,我们首先调用函数equalClicked()来更新sumSoFar和显示中的数值。

template<typename PointerToMemberFunction>
Button Calculator.createButton(QString text, PointerToMemberFunction member)

    button = Button(text)
    button.clicked.connect(this, member)
    return button

私有函数createButton()在构造函数中被调用来创建计算器按钮。

def abortOperation(self):

    clearAll()
    display.setText(tr("####"))

私有函数abortOperation()在计算失败时被调用。它重置计算器状态并显示“####”。

def calculate(self, float rightOperand, QString pendingOperator):

    if pendingOperator == tr("+"):
        sumSoFar += rightOperand
     elif pendingOperator == tr("-"):
        sumSoFar -= rightOperand
     elif pendingOperator == tr("\303\227"):
         = rightOperand
     elif pendingOperator == tr("\303\267"):
        if rightOperand == 0.0:
            return False
        factorSoFar /= rightOperand

    return True

私有函数calculate()执行二进制运算。右操作数由rightOperand给出。对于加法运算符,左操作数是sumSoFar;对于乘法运算符,左操作数是factorSoFar。如果发生除以零,函数返回false

按钮类定义

现在让我们看看Button类。

class Button(QToolButton):

    Q_OBJECT
# public
    Button = explicit(QString text, QWidget parent = None)
    QSize sizeHint() override

按钮(Button)类有一个方便的构造函数,接受一个文本标签和父小部件,并且重写了 sizeHint() 方法,以便为文本提供比 QToolButton 通常提供的更多空间。

按钮类实现#

def __init__(self, text, parent):
    super().__init__(parent)

    setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
    setText(text)

按钮的外观是通过计算器小部件的布局来确定的,这取决于布局子部件的大小和 sizePolicy。构造函数中调用 setSizePolicy() 函数确保按钮会水平扩展以填充所有可用空间;默认情况下,QToolButton 不会扩展以填充可用空间。如果不调用此函数,同一列中的不同按钮的宽度可能会不同。

def sizeHint(self):
size = QToolButton.sizeHint()
size.rheight() += 20
size.rwidth() = qMax(size.width(), size.height())
return size

sizeHint() 中,我们尝试返回一个对大多数按钮都好看的大小。我们重用了基类(QToolButton)的尺寸提示,但按照以下方式进行了修改:

  • 我们将高度组件的值加上20。

  • 我们使得尺寸提示中的宽度组件至少与高度相同。

这确保了大多数字体下,数字和运算按钮将呈正方形,而不会截断Backspace、Clear和Clear All按钮上的文本。

以下截图显示了如果在构造函数中我们没有将水平大小策略设置为Expanding,并且我们没有重写sizeHint(),计算器小部件将如何显示。

../_images/calculator-ugly.png

具有默认大小策略和大小提示的计算器示例

示例项目 @ code.qt.io