在Pycharm编译器中利用PyQt5实现计算器应用

一、PyQt5介绍

PyQt5是一个用于Python的跨平台图形用户界面(GUI)开发库,它是Qt库的Python绑定。Qt本身是一个强大的C++库,提供了丰富的API来开发桌面、嵌入式以及移动设备上的图形界面应用程序。PyQt5使得Python开发者能够利用Qt的强大功能,用Python编写具有原生外观和感受的桌面应用程序,并且能够跨多种操作系统运行,包括Windows、macOS、Linux等。

1. 主要特点

  1. 跨平台: PyQt5编写的程序可以无需修改或只需少量修改就在不同操作系统上运行。
  2. 丰富的控件集: 提供了大量预建的用户界面元素,如按钮(Button)、标签(Label)、文本框(QLineEdit)、布局管理器(Layouts)、对话框(Dialogs)等,方便构建复杂的用户界面。
  3. 信号与槽机制: PyQt5采用了独特的信号与槽(Signals and Slots)机制,用于对象间的通信,使得组件间的交互更加灵活和易于管理。
  4. 绑定和封装: PyQt5是对Qt C++ API的直接绑定,允许访问Qt的几乎全部功能,同时也提供了一些Python友好的特性。
  5. QSS样式: 支持类似CSS的QSS(Qt样式表),可以轻松地为应用程序添加美观的界面样式。
  6. 多媒体和网络支持: 包含多媒体播放、网络编程等功能,可以开发功能丰富的应用程序。
  7. 高级功能: 包括多线程支持、数据库集成、XML处理、图形视图框架、打印支持等,满足复杂应用开发需求。

2. 应用领域

  • 桌面应用程序: 开发各种类型的桌面软件,如办公软件、教育工具、系统工具等。
  • 科学计算界面: 为数据分析、机器学习等科学计算项目提供图形界面。
  • 原型开发: 快速制作软件原型,验证产品概念。
  • 嵌入式系统: 开发嵌入式设备的图形界面,如智能家电、工业控制面板等。
  • 教育和培训: 创建教学辅助工具和互动学习软件。

3. 学习资源

  • 官方文档: PyQt5的官方网站和文档是学习的首选资源,提供了详细的API参考和教程。
  • 书籍: 有许多关于PyQt5和Qt编程的书籍,适合不同水平的读者。
  • 在线教程和视频: 网络上有大量的免费和付费课程,涵盖从基础到高级的主题。
  • 社区和论坛: 加入PyQt或Qt的社区和论坛,可以得到问题解答,学习他人经验。

二、PyQt5实战—计算器应用

1. 界面展示

在这里插入图片描述

2. 项目代码全解析

(1)导入所需模块

# 导入所需模块
import re  # 正则表达式库,用于字符串匹配和替换
import sys  # 系统特定参数和功能
from PyQt5.QtWidgets import (  # PyQt5 GUI 库中的各种组件
    QWidget, QApplication, QLineEdit, QPushButton,
    QLabel, QHBoxLayout, QVBoxLayout, QGridLayout,
    QDesktopWidget
)
from PyQt5.QtCore import Qt  # 控件的属性设置
from PyQt5.QtGui import QIcon  # 设置窗口图标

(2) QSS工具类定义

# QSSTool 工具类
class QSSTool:
    @staticmethod
    def qss(file_path, widget):
        with open(file_path) as f:
            widget.setStyleSheet(f.read())

(3)初始化函数

class CalculatorApplication(QWidget):  # 类名更具描述性
    def __init__(self):
        super().__init__()
        self.setObjectName('widget')
        self.init_user_interface()

    def init_user_interface(self):  # 更具描述性的方法名
        self.configure_layout()
        self.setWindowIcon(QIcon('./high.jpg'))  # 图标文件名更具体
        self.center_on_screen()
        self.setWindowTitle('计算器应用')
        self.input_field.textChanged.connect(self.update_label)

        self.show()

(4)定义界面居中函数

    def center_on_screen(self):
        qr = self.geometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)

这段代码是用来使一个窗口在屏幕中心显示的,常见于许多图形界面应用程序的初始化设置中。下面逐行解释这段代码的含义:

  1. qr = self.geometry()
    这行代码获取了当前窗口(self代表的窗口实例)的几何信息,包括大小和位置。geometry()方法返回一个QRect对象,该对象包含了窗口左上角的坐标(x, y)以及窗口的宽度和高度。

  2. cp = QDesktopWidget().availableGeometry().center()
    这行代码用来获取屏幕的有效工作区域(即没有被系统任务栏或Dock等占用的部分)的中心点坐标。QDesktopWidget是Qt中用于处理与桌面环境交互的类,它的availableGeometry()方法返回的是当前可用屏幕区域的矩形范围,再调用center()方法则得到这个矩形的中心点坐标。

  3. qr.moveCenter(cp)
    最后,这行代码将之前获取的窗口矩形(qr)的中心点移动到屏幕有效工作区域的中心点(cp)。这意味着窗口的中心将与屏幕的有效工作区域中心对齐,从而实现窗口在屏幕中心显示的效果。

综上,整个函数的作用是使调用该方法的窗口在用户的屏幕上居中显示,无论屏幕的分辨率如何,都能提供较好的视觉体验。这是很多应用程序窗口初始化时常用的一种友好设置。

(5)构建界面基本格局

① 总体布局

    def configure_layout(self):
        main_layout = QVBoxLayout()

        input_layout = self.create_input_section()
        output_layout = self.create_output_label()
        buttons_layout = self.create_buttons_grid()

        main_layout.addLayout(input_layout)
        main_layout.addLayout(output_layout)
        main_layout.addLayout(buttons_layout)

        self.setLayout(main_layout)

② 输入布局

    def create_input_section(self):
        layout = QHBoxLayout()
        self.input_field = QLineEdit()
        self.input_field.setAlignment(Qt.AlignRight)  # 保持原逻辑,修正注释描述
        layout.addWidget(self.input_field)
        return layout

③ 输出布局

    def create_output_label(self):
        layout = QHBoxLayout()
        self.result_label = QLabel()
        self.result_label.setAlignment(Qt.AlignRight)
        layout.addWidget(self.result_label)
        return layout

④ 按钮网格布局

    def create_buttons_grid(self):
        grid = QGridLayout()

        buttons_texts = [
            '^', '√', '(', ')',
            'AC', 'Del', '+/-', '÷',
            '9', '8', '7', '×',
            '6', '5', '4', '-',
            '3', '2', '1', '+',
            '%', '0', '.', '='
        ]
        positions = [(i, j) for i in range(6) for j in range(4)]

        for name, position in zip(buttons_texts, positions):
            b = QPushButton(name)
            # 添加点击事件
            b.clicked.connect(self.on_button_clicked)

            # 给按钮添加 ID 属性
            if position[0] == 0:
                b.setObjectName('row1')
            if position[1] == 3:
                b.setObjectName('col1')
            if position[0] == 5:
                b.setObjectName('row2')
            if (position[0] == 5 and position[1] == 3):
                b.setObjectName('equal')

            grid.addWidget(b, *position)

        return grid

在代码中,position是一个包含两个元素的元组,通常来源于(i, j)这样的生成方式,其中:

  • position[0] 表示按钮所在的行索引,它是从0开始的整数。例如,如果position[0] == 0,则表示按钮位于布局的第一行;如果position[0] == 5,则表示按钮位于布局的第六行。

  • position[1] 表示按钮所在的列索引,同样是从0开始的整数。比如,如果position[1] == 0,说明按钮在该行的第一列;如果position[1] == 3,则表示按钮处于该行的第四列。

在网格布局(QGridLayout)中,元素的位置由行和列共同决定,所以通过这两个索引值,可以精确地定位到网格中的每一个按钮。在上述代码片段中,就是依据这些位置信息来分别为不同位置的按钮设置特定的对象名称,以便后续的样式设计或逻辑处理。

(6)按钮的槽函数

    def on_button_clicked(self):
        button_text = self.sender().text()
        current_input = self.input_field.text()

        # 按下如下按钮,将进行字符串拼接
        if button_text in [
            '9', '8', '7', '6', '5', '4',
            '3', '2', '1', '0', '+', '-',
            '×', '÷', '(', ')', '.', '√',
            '%'
        ]:
            cursor_position = self.input_field.cursorPosition()  # 获取当前光标位置

            # 保存当前文本和光标位置,以便在插入后恢复光标位置
            current_text = self.input_field.text()

            # 在光标位置插入文本
            new_text = current_text[:cursor_position] + button_text + current_text[cursor_position:]

            self.input_field.setText(new_text)  # 设置新文本
            self.input_field.setCursorPosition(cursor_position + len(button_text))  # 将光标移到新插入文本之后的位置

上述关于鼠标光标插入文本部分代码的工作原理:

  1. 获取光标位置:

    cursor_position = self.input_field.cursorPosition()
    

    这行代码的作用是获取QLineEdit(即input_field)中光标当前所在的位置。光标位置是从0开始计数的,0表示在文本的最开始位置。

  2. 保存当前文本和光标位置:

    current_text = self.input_field.text()
    

    这里,我们保存了input_field当前的全部文本内容到变量current_text中。这一步是为了接下来构建一个新的字符串,其中包含了在光标位置插入的新文本。

  3. 在光标位置插入文本:

    new_text = current_text[:cursor_position] + button_text + current_text[cursor_position:]
    

    这段代码是实际执行文本插入的关键部分。它通过字符串切片的方式构造了一个新的字符串:

    • current_text[:cursor_position] 获取从字符串开头到光标位置前的所有字符。
    • button_text 是要插入的文本,即按钮上的字符。
    • current_text[cursor_position:] 获取从光标位置到字符串末尾的所有字符。
      将这三部分拼接起来,就形成了一个新的字符串,其中button_text位于原光标位置。
  4. 设置新文本:

    self.input_field.setText(new_text)
    

    这行代码将上面构造的包含新插入文本的new_text设置回input_field,实际上替换了原来的内容,但现在包含了在指定位置插入的字符。

  5. 移动光标位置:

    self.input_field.setCursorPosition(cursor_position + len(button_text))
    

    最后,这行代码用来更新光标的位置。因为我们已经在光标位置插入了文本,所以光标应当跳过新插入的文本长度,以保持在新文本的末尾。len(button_text)获取了插入文本的长度,将其加到原来的光标位置上,确保光标移动到了正确的位置。

综上所述,这段代码有效地实现了在用户点击按钮时,将按钮上的文本插入到QLineEdit控件当前光标所在位置,并且在插入后自动将光标移动到新文本的末尾,保证了良好的用户体验。

        # 按下 ^ 按钮,平方 将进行字符串替换
        elif button_text == '^':
            self.input_field.setText(current_input + '^')

        # 按下 AC 按钮,将进行字符串清空
        elif button_text == 'AC':
            self.input_field.setText('')
            self.result_label.setText('')

        # 按下 Del 按钮,退格 删除一个字符
        elif button_text == 'Del':
            self.input_field.setText(current_input[:-1])

        # 按下 +/- 按钮,添加负数
        elif button_text == '+/-':
            x = re.findall("([\d]+)", current_input)
            if x:
                self.input_field.setText(current_input.rstrip(x[-1]) + '(-' + x[-1] + ')')
            else:
                self.input_field.setText('(-' + current_input + ')')

        # 按下  = 按钮,计算结果
        elif button_text == '=':
            self.calculate_result(current_input)

        # 仅仅使逻辑清晰,do nothing
        else:
            ...

这段代码难点是针对计算器程序中按钮+/-的点击事件处理逻辑。当用户点击按钮+/-时,该段代码的目的是将当前输入框中的数字表达式的符号进行反转,即正数变负数,负数变正数。下面是详细的步骤解析:

  1. 查找数字序列:

    • 首先,使用正则表达式re.findall("([\d]+)", current_input)current_input(即输入框当前显示的文本)中查找所有连续的数字序列。这一步是为了定位最后一个输入的数字,以便知道在哪里插入负号。
  2. 判断是否有数字找到:

    • if x: 检查是否找到了数字序列。如果有至少一个数字被找到(即x不是空列表),则进入条件块执行;如果没有数字被找到,则执行else块。
  3. 处理找到数字的情况:

    • 如果有数字找到,使用x[-1]获取列表x中的最后一个元素,即当前输入的最后一个数字。
    • current_input.rstrip(x[-1])移除current_input末尾的这个数字,以准备重新构造字符串。
    • 然后,构造新的字符串,形式为在移除的数字前加上负号和括号,即'(-' + x[-1] + ')',并将其与之前current_input剩余的部分拼接起来,通过setText方法更新输入框的文本。
  4. 处理未找到数字的情况:

    • 如果没有数字被找到,说明输入框中可能没有数字,或者全部是操作符和括号等非数字字符。这种情况下,简单地在输入框文本前加上负号,即'(-' + current_input + ')',然后更新输入框的文本。

\d 是正则表达式中的一个特殊字符,代表“数字”的意思。它是一个预定义字符类,匹配任何十进制数字,相当于 [0-9] 的简写。在正则表达式中,当你想匹配一个或多个数字字符时,可以使用 \d 来代替写出所有单独数字的集合(0到9)。例如,\d+ 会匹配一个或多个连续的数字字符,\d{3} 则会精确匹配三个数字字符。在你给出的上下文中,[\d]+ 用来查找一个或多个连续的数字字符。

总之,这段代码实现了当用户点击+/-按钮时,反转输入框中最后一个有效数字的正负号,如果没有数字,则在输入的表达式前加上负号,以实现对整个表达式的正负转换。

(7)完善输出模块的显示功能

    def update_label(self):
        text = self.input_field.text()  # 获取输入框当前的文本
        if text:  # 确保文本不为空
            # 获取最后一个字符,注意这里直接取最后一个字符,不判断是否为数字,根据实际情况调整
            last_digit = text[-1]
            self.result_label.setText(last_digit)  # 更新标签的文本为最后一位数字
        else:
            # 如果输入框为空,可以考虑清空标签或设置默认文本
            self.result_label.clear()  # 或者 self.lastDigitLabel.setText("无")

(8)结果计算函数

    def calculate_result(self, current_input):
        """
        current_input:用户输入(行编辑控件中)的文本
        计算 在标签中显示结果
        将用户输入的文本进行处理,判断是否合法,并提炼成符合 python 语法的表达式进行计算
        """

        # 逻辑判断 检测、提炼、计算
        # 计算表达式是否为空
        if current_input == '':
            return

        # 计算表达式是否有乘法运算
        if '×' in current_input:
            current_input = current_input.replace('×', '*')

        # 计算表达式是否有除法运算
        if '÷' in current_input:
            current_input = current_input.replace('÷', '/')

        # 计算表达式是否有百分数运算
        if '%' in current_input:
            s = re.findall('([\.\d]+%)', current_input)
            if s:
                for i in s:
                    line_text = current_input.replace(i, str(eval(i[:-1] + '/100')))
            else:
                self.result_label.setText('错误')
                return

        # 计算表达式是否有二次方根运算
        if '√' in current_input:
            s = re.findall('(√[\d]+)', current_input)
            if s:
                for i in s:
                    line_text = current_input.replace(i, i[1:] + '**0.5')
            else:
                self.result_label.setText('错误')
                return

        # 计算表达式是否有开方运算
        if '^' in current_input:
            current_input = current_input.replace('^', '**')

        # 谨防不合法表达式
        try:
            self.result_label.setText(str(eval(current_input)))
            # self.input_field.setText(str(eval(current_input)))

        except:
            self.result_label.setText('错误')

在Python中,tryexcept语句用于异常处理,目的是捕获并处理在程序运行过程中可能出现的错误,从而避免程序因未预料的错误而直接崩溃。在上述代码片段中,tryexcept块的使用是为了安全地执行计算表达式的求值操作。

具体解释如下:

  1. try块 (try:后面直到except之前的代码):

    • self.result_label.setText(str(eval(current_input))): 这行代码尝试使用eval()函数计算current_input字符串中表示的数学表达式的值,并将结果转换为字符串形式设置到result_label中。eval()是一个强大的函数,它能够解析并执行传入的字符串为有效的Python表达式,这在计算用户输入的数学表达式时非常有用,但也存在安全风险(比如执行恶意代码),因此在实际应用中应谨慎使用。
  2. except块 (except:后面的代码):

    • try块内的代码执行时,如果发生了任何异常(比如current_input不是一个有效的Python表达式,或者计算过程中出现了除以零等错误),except块就会被执行。
    • self.result_label.setText('错误'): 这行代码将在捕获到异常时设置result_label的文本为“错误”,向用户表明计算过程中出现了问题,而不是让程序直接崩溃,提高了用户体验和程序的健壮性。

因此,这里的try-except结构保证了即使在表达式计算失败的情况下,程序也能优雅地处理错误,并给用户一个友好的反馈,而不是直接报错退出。

(9)固定主函数模板

if __name__ == '__main__':
    app = QApplication(sys.argv)
    e = CalculatorApplication()
    QSSTool.qss('calculator.qss', app)  # 应用外部样式表
    sys.exit(app.exec_())

这段代码是Python程序的标准入口点,当脚本作为主程序运行时,其下的代码块将被执行。这里是PyQt5应用程序启动和执行的主要流程。详细解释如下:

  1. if __name__ == '__main__'::
    这是一个常见的Python idiom,用于检查当前文件是否作为主程序直接被执行,而非被其他模块导入。只有当这个条件为真时,下面的代码才会执行。这有助于区分模块的导入行为与直接执行行为,便于复用代码。

  2. app = QApplication(sys.argv):
    这一行创建了Qt应用程序对象QApplication,它是所有Qt GUI程序的基础。sys.argv是一个列表,包含了命令行参数,传递给QApplication可以处理命令行参数,比如打开特定文件等。这个步骤是启动任何Qt应用程序所必需的。

  3. e = CalculatorApplication():
    实例化了CalculatorApplication类,这是自定义的PyQt5应用程序窗口类,包含了用户界面的设计和逻辑。这个对象e就是你的计算器应用程序实例。

  4. QSSTool.qss('calculator.qss', app):
    这行代码调用了自定义模块QSSTool中的方法qss,用于应用一个外部的QSS(Qt样式表)文件到应用程序上。calculator.qss是样式表文件的路径,这个文件定义了应用程序的外观和风格,类似于网页的CSS。通过这种方式,你可以轻松地更改应用程序的视觉效果,而无需修改代码。

  5. sys.exit(app.exec_()):
    这是启动Qt事件循环的代码,app.exec_()开始监听和处理用户事件(如鼠标点击、键盘输入等)。sys.exit()确保当调用app.exec_()的事件循环结束时(比如用户关闭窗口),Python程序能正常退出,而不是继续执行后续可能不存在的代码。

总结起来,这段代码完成了初始化Qt应用程序、设置界面样式、实例化自定义窗口类以及启动事件循环等一系列动作,是PyQt5应用程序标准的启动流程。

(10)QSS样式

#widget {
    background-color: #F0FFFF;
    width: 530px;
    height: 650px;
    max-width: 600px;
    min-width: 400px;
    max-height: 700px;
    min-height: 360px;
}

QLineEdit {
    background-color: transparent;
    border: none;
    width: 300px;
    height: 20px;
    max-height: 60px;
    min-height: 30px;
    font: 500 25px "Segoe UI";
    margin-left: 60px;
    margin-right: 60px;
    color: #6E6E6E;
}

QLabel {
    border: none;
    width: 300px;
    height: 80px;
    max-height: 80px;
    min-height: 30px;
    font: 500 50px "Segoe UI";
    margin-left: 60px;
    margin-top: -5px;
    margin-right: 60px;
    margin-bottom: 10px;
    color: black;
    padding-bottom: -5px
}

QPushButton {
    background-color: #FEFEFE;
    border: 2px solid #F5FFFA;
    width: 120px;
    height: 50px;
    max-width: 100px;
    min-width: 30px;
    max-height: 60px;
    min-height: 40px;
    font: 550 25px "Segoe UI";
    color: black;
    border-radius: 10px;
}

QPushButton:pressed {
    background-color: #FFFFFF;
    border: none;
    border: 2px groove #000080;
    border-radius: 10px;
    width: 40px;
    height: 40px;
}

#row1 {
    color: #4169E1;
}

#col1 {
    color: #4169E1;
}

#row2 {
    color: #4169E1;
}

QPushButton#equal {
    background-color: #4682B4;
    color: #FFFFFF;
}

这段QSS(Qt StyleSheet)代码展示了如何使用CSS-like规则来定制Qt应用程序的图形界面元素的外观。QSS允许开发者以声明式的方式来设计和控制Qt界面组件的样式,包括颜色、字体、边框、背景、尺寸等,极大地增强了应用程序的视觉效果和用户体验。下面是这段QSS代码中几个关键部分的分析,以及一些常用的QSS语句示例:

1. 选择器(Selectors)

  • 基本选择器:如.QLineEdit,用于选择所有的QLineEdit组件。
  • ID选择器:如#widget,通过组件的objectName来精确选择单个组件。
  • 伪状态选择器:如QPushButton:pressed,定义组件在特定状态(如被按下)下的样式。

2. 属性设置

  • 背景色background-color: #F0FFFF;,用于设置组件的背景颜色。
  • 边框border: 2px solid #F5FFFA;,定义组件边框的宽度、样式和颜色。
  • 字体font: 500 25px "Segoe UI";,设置字体的粗细、大小和家族。
  • 尺寸width: 300px; height: 20px;,直接设定组件的宽度和高度。
  • 内外边距margin-left: 60px; padding-bottom: -5px;,控制组件与其他元素的距离(外边距)和内容与边界的距离(内边距)。

3. 特殊效果

  • 圆角border-radius: 10px;,使按钮等组件边缘呈现圆角。
  • 颜色变化color: #6E6E6E;,改变文字或图标颜色。
  • 伪状态样式:在组件的不同状态下(如按下、悬停)改变样式,如QPushButton:pressed中的颜色和边框变化。

常用QSS语句示例

  • 文本对齐text-align: center;
  • 透明度opacity: 0.5;
  • 阴影效果box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3);
  • 渐变背景background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #E0E0E0, stop:1 #FFFFFF);

这段代码示例中,通过精细控制各个组件的样式,比如为QWidget设置了天蓝色的背景,QLineEditQLabel分别定义了透明背景和特定字体样式,而按钮则根据不同的角色(普通按钮、特殊行列按钮、等于按钮)有不同的颜色和边框效果,展示了QSS在美化和统一应用程序界面设计方面的能力。

3. 实用网站推荐

方便寻找十六进制颜色代码的网站:https://www.sioe.cn/yingyong/yanse-rgb-16/
在这里插入图片描述

  • 31
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值