PyQt5 - QWidgets部件进阶教程之计算器

来源:互联网 发布:闪电邮mac版 编辑:程序博客网 时间:2024/05/22 11:36

  • 废话
  • 定义计算器类
    • 实现计算器部件类
    • 实现按钮类
    • 整体代码
  • 最终效果

废话

该案例包含两个类:
- 计算器是一个Calculator部件,包含所有计算器功能
- Button部件用于每个计算机按钮,它由QToolButton派生
我们先校验计算器,然后再看按钮


定义计算器类

from PyQt5.QtWidgets import *from PyQt5.QtCore import *import sysclass Calculator(QWidget):    def __init__(self):        super(Calculator, self).__init__()app = QApplication(sys.argv)cal = Calculatorcal.show()app.exec_()
  • 相关槽(cpp)
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();

按钮根据他们的行为分类组合,例如所有数字按钮在当前的操作数追加一个数字。为此我们将各类按钮连接到相同的槽(例如digitClicked()),类目有:数字,一元运算符、加减法运算符、乘除法运算符,其他的按钮有他们自己的槽。

    def createButton(self, text, member):        pass    def abortOperation(self):        pass    def calculate(self, rightOperand, pendingOperator):        pass
  • createButton()函数作为部件构造器的一部分
  • abortOperation() 当出现被零除或负数平方根时被调用
  • calculate()提供二进制操作(+, -, ×, ÷)
        self.sumInMemory = 0        self.sumSoFar = 0        self.factorSoFar = 0        self.pendingAdditiveOperator = ''        self.pendingMultiplicativeOperator = ''        self.waitingForOperand = True
  • sumInMemory包含计算器内存中存储的值
  • sumSoFar存储目前累积值,当用户点击‘=’,它被重计算并显示出来,清除所有并重置sumSoFar为零
  • 当做乘除法时,factorSoFar用来存储临时值
  • pendingAdditiveOperator存储用户点击的最终加法操作符
  • pendingMultiplicativeOperator存储用户点击的最终乘法操作符
  • 当计算器希望用户输入一个操作符时,waitingForOperand为真
    加法和乘法操作符对待不同,是因为他们有不同的优先级,例如1 + 2 ÷ 3可理解为1 + (2 ÷ 3),因为‘÷’比‘+’优先级高。
    一元运算符(像平发根)不需要特别的操作,当已知运算数并点击运算符按钮时,他们可以立即应用。

实现计算器部件类

构造器内,我们初始化计算器状态

        self.sumInMemory = 0.0        self.sumSoFar = 0.0        self.factorSoFar = 0.0        self.pendingAdditiveOperator = ''        self.pendingMultiplicativeOperator = ''        self.waitingForOperand = True        self.display = QLineEdit('0')        self.display.setReadOnly(True)        self.display.setAlignment(Qt.AlignRight)        self.display.setMaxLength(15)

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

        self.NumDigitButton = range(10)        self.digitButton = list(self.NumDigitButton)        for i in self.NumDigitButton:            self.digitButton[i] = self.creatButton(str(i), self.digitClicked)        self.pointButton = self.creatButton('.', self.pointClicked)        self.changeSignButton = self.creatButton('\302\261', self.changeSignClicked)        self.backspaceButoon = self.creatButton('BackSpace', self.backspaceClicked)        self.clearButton = self.creatButton('Clear', self.clear)        self.clearAllButton = self.creatButton('Clear All', self.clearAll)        self.clearMemoryButton = self.creatButton('MC', self.clearMemory)        self.readMemoryButton = self.creatButton('MR', self.readMemory)        self.setMemoryButton = self.creatButton('MS', self.setMemory)        self.addToMemoryButton = self.creatButton('M+', self.addToMemory)        self.divisionButton = self.creatButton('\303\267', self.multiplicativeOperatorClicked)        self.timesButton = self.creatButton('\303\227', self.multiplicativeOperatorClicked)        self.minusButton = self.creatButton('-', self.additiveOperatorClicked)        self.plusButton = self.creatButton('+', self.additiveOperatorClicked)        self.squrareRootButton = self.creatButton('Sqrt', self.unaryOperatorClicked)        self.powerButton = self.creatButton('x\302\262', self.unaryOperatorClicked)        self.reciprocalButton = self.creatButton('1/x', self.unaryOperatorClicked)        self.equalButton = self.creatButton('=', self.equalClicked)

对每个按钮调用createButton()函数,设置相应的文本标签和按钮对应的槽。

        self.mainLayout = QGridLayout()        self.mainLayout.setSizeConstraint(QLayout.SetFixedSize)        self.mainLayout.addWidget(self.display, 0, 0, 1, 6)        self.mainLayout.addWidget(self.backspaceButoon, 1, 0, 1, 2)        self.mainLayout.addWidget(self.clearButton, 1, 2, 1, 2)        self.mainLayout.addWidget(self.clearAllButton, 1, 4, 1, 2)        self.mainLayout.addWidget(self.clearMemoryButton, 2, 0)        self.mainLayout.addWidget(self.readMemoryButton, 3, 0)        self.mainLayout.addWidget(self.setMemoryButton, 4, 0)        self.mainLayout.addWidget(self.addToMemoryButton, 5, 0)        for i in self.NumDigitButton:            if i > 0:                row = ((9 - i) / 3) + 2                column = ((i - 1) % 3) + 1                self.mainLayout.addWidget(self.digitButton[i], row, column)        self.mainLayout.addWidget(self.digitButton[0], 5, 1)        self.mainLayout.addWidget(self.pointButton, 5, 2)        self.mainLayout.addWidget(self.changeSignButton, 5, 3)        self.mainLayout.addWidget(self.divisionButton, 2, 4)        self.mainLayout.addWidget(self.timesButton, 3, 4)        self.mainLayout.addWidget(self.minusButton, 4, 4)        self.mainLayout.addWidget(self.plusButton, 5, 4)        self.mainLayout.addWidget(self.squrareRootButton, 2, 5)        self.mainLayout.addWidget(self.powerButton, 3, 5)        self.mainLayout.addWidget(self.reciprocalButton, 4, 5)        self.mainLayout.addWidget(self.equalButton, 5, 5)        self.setLayout(self.mainLayout)        self.setWindowTitle('Calculator')
  • 通过单独的QGridLayout操作布局,调用setSizeConstraint()来确保计算器部件以最优尺寸显示,阻止用户改变尺寸,尺寸提示由尺寸和子部件尺寸策略决定。
  • 大部分子部件只占用网格布局的一个单元,这样我们只需向addWidget()传递row和column。display、backspaceButton、clearButton和clearAllButton部件占用超过一列,这样我们必须传递行跨度和列跨度。
    def digitClicked(self):        clickedButton = self.sender()        digitValue = int(clickedButton.text())        if self.waitingForOperand:            self.display.clear()            self.waitingForOperand = False        self.display.setText(self.display.text() + str(digitValue))
  • 点击计算器的一个数字按钮将会发射按钮的clicked()信号,这会出发digitClicked()槽。
  • 首先,我们需要使用sender()找出哪个按钮发射信号,这个函数将发射者作为对象返回。一旦我们得知那个按钮,就可以使用text()提取运算符。
  • 槽需要考虑两个特定情况
    • 如果显示中包含‘0’,用户在点击‘0’按钮时,它会显示‘00’
    • 如果计算器进入等待新的操作符状态时,新的数字是新操作数的第一个数字,这样的情况下,前面任何计算结果必须首先清零
    def unaryOperatorClicked(self):        clickedButton = self.sender()        clickedOperator = clickedButton.text()        operand = float(self.display.text())        result = 0.0        if clickedOperator == 'Sqrt':            if operand < 0.0:                self.abortOperation()                return            result = math.sqrt(operand)        elif clickedOperator == 'x\302\262':            result = math.pow(operand, 2.0)        elif clickedOperator == '1/x':            if operand == 0.0:                self.abortOperation()                return            result = 1.0 / operand        self.display.setText(str(result))        self.waitingForOperand = True
  • unaryOperatorClicked()槽当一元操作符按钮点击时被调用,同样适用sender()获取对象,操作符通过按钮文本提取并存储在clickedOperator,运算数通过display获得。
  • 当提供给Sqrt负数或1/x到0时,调用abortOperation(),如果一切顺利,会在行编辑中显示运算结果,并将waitingForOperand设置为True,这样确保如果用户输入新数字时,数字会被认为是新运算数,而不是插入到当前值后
    def additiveOperatorClicked(self):        clickedButton = self.sender()        clickedOperator = clickedButton.text()        operand = float(self.display.text())        if len(self.pendingMultiplicativeOperator) != 0:            if not self.calculate(operand, self.pendingMultiplicativeOperator):                self.abortOperation()                return            self.display.setText(str(self.factorSoFar))            operand = self.factorSoFar            self.factorSoFar = 0.0            self.pendingMultiplicativeOperator = ''        if len(self.pendingAdditiveOperator) != 0:            if not self.calculate(operand, self.pendingAdditiveOperator):                self.abortOperation()                return            self.display.setText(str(self.sumSoFar))        else:            self.sumSoFar = operand        self.pendingAdditiveOperator = clickedOperator        self.waitingForOperand = True
  • 当用户点击‘+’或‘-’时,additiveOperatorClicked()槽被调用,在我们可以实际对点击的操作符做什么之前,我们必须处理所有挂起操作,先从乘法运算开始,自高优先级到加法运算。
  • 如果‘×’或‘÷’先点击,之后并未点击‘=’,display中的当前值是右运算数,最后我们可以执行运算并更新display
  • 如果‘+’或‘-’先点击,sumSoFar是左运算数,display中的当前值是右运算数,如果这里没有待加运算,sumSoFar会直接设置到display的文本中
  • 当没有右运算数时,我们将点击的运算符存储在pendingAdditiveOperator变量,这会在之后出现右运算数时,与作为左运算数的sumSoFar进行计算。
    def multiplicativeOperatorClicked(self):        clickedButton = self.sender()        clickedOperator = clickedButton.text()        operand = float(self.display.text())        if len(self.pendingMultiplicativeOperator) != 0:            if not self.calculate(operand, self.pendingMultiplicativeOperator):                self.abortOperation()                return            self.display.setText(self.factorSoFar)        else:            self.factorSoFar = operand        self.pendingMultiplicativeOperator = clickedOperator        self.waitingForOperand = True

multiplicativeOperatorClicked()槽与additiveOperatorClicked()类似,这里我们不需要担心待加运算数,因为乘法运算符比加法运算符优先级高。

    def equalClicked(self):        operand = float(self.display.text())        if len(self.pendingMultiplicativeOperator) != 0:            if not self.calculate(operand, self.pendingMultiplicativeOperator):                self.abortOperation()                return            operand = self.factorSoFar            self.factorSoFar = 0.0            self.pendingMultiplicativeOperator = ''        if len(self.pendingAdditiveOperator) != 0:            if not self.calculate(operand, self.pendingAdditiveOperator):                self.abortOperation()                return            self.pendingAdditiveOperator = ''        else:            self.sumSoFar = operand        self.display.setText(str(self.sumSoFar))        self.sumSoFar = 0.0        self.waitingForOperand = True

像additiveOperatorClicked(),仍从处理挂起乘法和加法运算符开始,然后显示sumSoFar,并将变量归零,重置变量是为避免二次计算。

    def pointClicked(self):        if self.waitingForOperand:            self.display.setText('0')        if '.' not in self.display.text():            self.display.setText(self.display.text() + '.')        self.waitingForOperand = False

pointClicked()槽在当前显示内容加小数点

    def changeSignClicked(self):        text = self.display.text()        value = float(text)        if value > 0.0:            text = '-' + text        elif value < 0.0:            text = text[1:]        self.display.setText(str(text))

changeSignClicked()槽改变当前显示值的符号,如果当前值为正,我们加个符号;如果当前值为负,我们移除值的第一个字符,这里面使用了python字符串切片的方法。

    def backspaceClicked(self):        if self.waitingForOperand:            return        text = self.display.text()        text = text[0:-1]        if len(text) == 0:            text = '0'            self.waitingForOperand = True        self.display.setText(str(text))

backspaceClicked()移除显示中最右边的字符,如果得到空字符串,将显示‘0’,并将waitingForOperand设置为True

    def clear(self):        if self.waitingForOperand:            return        self.display.setText('0')        self.waitingForOperand = True

clear()槽重置当前运算数为零,它相当于点很多次退格来清除目前的运算数。

    def clearAll(self):        self.sumSoFar = 0.0        self.factorSoFar = 0.0        self.pendingAdditiveOperator = ''        self.pendingMultiplicativeOperator = ''        self.display.setText('0')        self.waitingForOperand = True

clearAll()槽将计算器重置为初始化状态

    def clearMemory(self):        self.sumInMemory = 0.0    def readMemory(self):        self.display.setText(str(self.sumInMemory))        self.waitingForOperand = True    def setMemory(self):        self.equalClicked()        self.sumInMemory = float(self.display.text())    def addToMemory(self):        self.equalClicked()        self.sumInMemory += float(self.display.text())
  • clearMemory()清除内存中的总数
  • readMemory()显示总数作为运算数
  • setMemory()用当前总数替换内存中的总数
  • addToMemory()将当前总数加到内存中的值
    def creatButton(self, text, member):        button = Button(text)        button.clicked.connect(member)        return button

createButton()用来构建计算器按钮

    def abortOperation(self):        self.clearAll()        self.display.setText('####')

当计算失败时调用abortOperation(),它重置计算器状态并显示’####’

    def calculate(self, rightOperand, pendingOperator):        if pendingOperator == '+':            self.sumSoFar += rightOperand        elif pendingOperator == '-':            self.sumSoFar -= rightOperand        elif pendingOperator == '\303\227':            self.factorSoFar *= rightOperand        elif pendingOperator == '\303\267':            if rightOperand == 0.0:                return False            self.factorSoFar /= rightOperand        return True
  • calculate()函数执行二元运算,右运算数通过rightOperand赋值
  • 加法运算中,左运算数是sumSoFar
  • 乘法运算中,左运算数是factorSoFar
  • 如果除零,函数返回错误

实现按钮类

按钮类构造器包括文本标签和父部件,并重写sizeHint()来给提供文本周围更多的空间。

class Button(QToolButton):    def __init__(self, text):        super(Button, self).__init__()        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)        self.setText(text)    def sizeHint(self):        size = QSize(QToolButton().sizeHint())        size.setHeight(size.height() + 20)        size.setWidth(max(size.width(), size.height()))        return size
  • 按钮的外观定义,由计算器部件的布局通过设置尺寸和子部件布局的尺寸策略决定
  • setSizePolicy()的调用是确保按钮可以横向扩展,填满所有可用空间,默认下,QToolButtons不填充可用空间。若不调用,同列的不同按钮的宽度会不一
  • 在sizeHint()中,我们尝试返回一个为所有按钮看起来还不错的尺寸,这里复用了sizeHint()的基础类(QToolButton),但从下面的方式进行修改:
    • 将sizeHint()高度增加20
    • 将sizeHint()宽度设成至少与高度相当
      这样来确保大多数字体下,数字和运算符按钮是方的,不会在Backspace、Clear和Clear All上截断文本。

整体代码

from PyQt5.QtWidgets import *from PyQt5.QtCore import *from PyQt5.QtGui import *import sys, mathclass Calculator(QWidget):    def __init__(self):        super(Calculator, self).__init__()        self.sumInMemory = 0.0        self.sumSoFar = 0.0        self.factorSoFar = 0.0        self.pendingAdditiveOperator = ''        self.pendingMultiplicativeOperator = ''        self.waitingForOperand = True        self.display = QLineEdit('0')        self.display.setReadOnly(True)        self.display.setAlignment(Qt.AlignRight)        self.display.setMaxLength(15)        self.font = QFont()        self.font.setPointSize(self.font.pointSize() + 8)        self.display.setFont(self.font)        self.NumDigitButton = range(10)        self.digitButton = list(self.NumDigitButton)        for i in self.NumDigitButton:            self.digitButton[i] = self.creatButton(str(i), self.digitClicked)        self.pointButton = self.creatButton('.', self.pointClicked)        self.changeSignButton = self.creatButton('\302\261', self.changeSignClicked)        self.backspaceButoon = self.creatButton('BackSpace', self.backspaceClicked)        self.clearButton = self.creatButton('Clear', self.clear)        self.clearAllButton = self.creatButton('Clear All', self.clearAll)        self.clearMemoryButton = self.creatButton('MC', self.clearMemory)        self.readMemoryButton = self.creatButton('MR', self.readMemory)        self.setMemoryButton = self.creatButton('MS', self.setMemory)        self.addToMemoryButton = self.creatButton('M+', self.addToMemory)        self.divisionButton = self.creatButton('\303\267', self.multiplicativeOperatorClicked)        self.timesButton = self.creatButton('\303\227', self.multiplicativeOperatorClicked)        self.minusButton = self.creatButton('-', self.additiveOperatorClicked)        self.plusButton = self.creatButton('+', self.additiveOperatorClicked)        self.squrareRootButton = self.creatButton('Sqrt', self.unaryOperatorClicked)        self.powerButton = self.creatButton('x\302\262', self.unaryOperatorClicked)        self.reciprocalButton = self.creatButton('1/x', self.unaryOperatorClicked)        self.equalButton = self.creatButton('=', self.equalClicked)        self.mainLayout = QGridLayout()        self.mainLayout.setSizeConstraint(QLayout.SetFixedSize)        self.mainLayout.addWidget(self.display, 0, 0, 1, 6)        self.mainLayout.addWidget(self.backspaceButoon, 1, 0, 1, 2)        self.mainLayout.addWidget(self.clearButton, 1, 2, 1, 2)        self.mainLayout.addWidget(self.clearAllButton, 1, 4, 1, 2)        self.mainLayout.addWidget(self.clearMemoryButton, 2, 0)        self.mainLayout.addWidget(self.readMemoryButton, 3, 0)        self.mainLayout.addWidget(self.setMemoryButton, 4, 0)        self.mainLayout.addWidget(self.addToMemoryButton, 5, 0)        for i in self.NumDigitButton:            if i > 0:                row = ((9 - i) / 3) + 2                column = ((i - 1) % 3) + 1                self.mainLayout.addWidget(self.digitButton[i], row, column)        self.mainLayout.addWidget(self.digitButton[0], 5, 1)        self.mainLayout.addWidget(self.pointButton, 5, 2)        self.mainLayout.addWidget(self.changeSignButton, 5, 3)        self.mainLayout.addWidget(self.divisionButton, 2, 4)        self.mainLayout.addWidget(self.timesButton, 3, 4)        self.mainLayout.addWidget(self.minusButton, 4, 4)        self.mainLayout.addWidget(self.plusButton, 5, 4)        self.mainLayout.addWidget(self.squrareRootButton, 2, 5)        self.mainLayout.addWidget(self.powerButton, 3, 5)        self.mainLayout.addWidget(self.reciprocalButton, 4, 5)        self.mainLayout.addWidget(self.equalButton, 5, 5)        self.setLayout(self.mainLayout)        self.setWindowTitle('Calculator')    def digitClicked(self):        clickedButton = self.sender()        digitValue = int(clickedButton.text())        if self.waitingForOperand:            self.display.clear()            self.waitingForOperand = False        self.display.setText(self.display.text() + str(digitValue))    def unaryOperatorClicked(self):        clickedButton = self.sender()        clickedOperator = clickedButton.text()        operand = float(self.display.text())        result = 0.0        if clickedOperator == 'Sqrt':            if operand < 0.0:                self.abortOperation()                return            result = math.sqrt(operand)        elif clickedOperator == 'x\302\262':            result = math.pow(operand, 2.0)        elif clickedOperator == '1/x':            if operand == 0.0:                self.abortOperation()                return            result = 1.0 / operand        self.display.setText(str(result))        self.waitingForOperand = True    def additiveOperatorClicked(self):        clickedButton = self.sender()        clickedOperator = clickedButton.text()        operand = float(self.display.text())        if len(self.pendingMultiplicativeOperator) != 0:            if not self.calculate(operand, self.pendingMultiplicativeOperator):                self.abortOperation()                return            self.display.setText(str(self.factorSoFar))            operand = self.factorSoFar            self.factorSoFar = 0.0            self.pendingMultiplicativeOperator = ''        if len(self.pendingAdditiveOperator) != 0:            if not self.calculate(operand, self.pendingAdditiveOperator):                self.abortOperation()                return            self.display.setText(str(self.sumSoFar))        else:            self.sumSoFar = operand        self.pendingAdditiveOperator = clickedOperator        self.waitingForOperand = True    def multiplicativeOperatorClicked(self):        clickedButton = self.sender()        clickedOperator = clickedButton.text()        operand = float(self.display.text())        if len(self.pendingMultiplicativeOperator) != 0:            if not self.calculate(operand, self.pendingMultiplicativeOperator):                self.abortOperation()                return            self.display.setText(self.factorSoFar)        else:            self.factorSoFar = operand        self.pendingMultiplicativeOperator = clickedOperator        self.waitingForOperand = True    def equalClicked(self):        operand = float(self.display.text())        if len(self.pendingMultiplicativeOperator) != 0:            if not self.calculate(operand, self.pendingMultiplicativeOperator):                self.abortOperation()                return            operand = self.factorSoFar            self.factorSoFar = 0.0            self.pendingMultiplicativeOperator = ''        if len(self.pendingAdditiveOperator) != 0:            if not self.calculate(operand, self.pendingAdditiveOperator):                self.abortOperation()                return            self.pendingAdditiveOperator = ''        else:            self.sumSoFar = operand        self.display.setText(str(self.sumSoFar))        self.sumSoFar = 0.0        self.waitingForOperand = True    def pointClicked(self):        if self.waitingForOperand:            self.display.setText('0')        if '.' not in self.display.text():            self.display.setText(self.display.text() + '.')        self.waitingForOperand = False    def changeSignClicked(self):        text = self.display.text()        value = float(text)        if value > 0.0:            text = '-' + text        elif value < 0.0:            text = text[1:]        self.display.setText(str(text))    def backspaceClicked(self):        if self.waitingForOperand:            return        text = self.display.text()        text = text[0:-1]        if len(text) == 0:            text = '0'            self.waitingForOperand = True        self.display.setText(str(text))    def clear(self):        if self.waitingForOperand:            return        self.display.setText('0')        self.waitingForOperand = True    def clearAll(self):        self.sumSoFar = 0.0        self.factorSoFar = 0.0        self.pendingAdditiveOperator = ''        self.pendingMultiplicativeOperator = ''        self.display.setText('0')        self.waitingForOperand = True    def clearMemory(self):        self.sumInMemory = 0.0    def readMemory(self):        self.display.setText(str(self.sumInMemory))        self.waitingForOperand = True    def setMemory(self):        self.equalClicked()        self.sumInMemory = float(self.display.text())    def addToMemory(self):        self.equalClicked()        self.sumInMemory += float(self.display.text())    def creatButton(self, text, member):        button = Button(text)        button.clicked.connect(member)        return button    def abortOperation(self):        self.clearAll()        self.display.setText('####')    def calculate(self, rightOperand, pendingOperator):        if pendingOperator == '+':            self.sumSoFar += rightOperand        elif pendingOperator == '-':            self.sumSoFar -= rightOperand        elif pendingOperator == '\303\227':            self.factorSoFar *= rightOperand        elif pendingOperator == '\303\267':            if rightOperand == 0.0:                return False            self.factorSoFar /= rightOperand        return Trueclass Button(QToolButton):    def __init__(self, text):        super(Button, self).__init__()        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)        self.setText(text)    def sizeHint(self):        size = QSize(QToolButton().sizeHint())        size.setHeight(size.height() + 20)        size.setWidth(max(size.width(), size.height()))        return sizeapp = QApplication(sys.argv)cal = Calculator()cal.show()app.exec_()

最终效果

这里写图片描述

0 0