PyQt5入门实战:从零实现一个表达式输入式计算器(附完整代码)

张开发
2026/4/16 3:48:56 15 分钟阅读

分享文章

PyQt5入门实战:从零实现一个表达式输入式计算器(附完整代码)
前言PyQt5 是 Python 绑定 Qt5 的 GUI 框架功能强大且易于上手。本文将从零开始教你如何使用Qt Designer设计计算器界面并在 PyQt5 中实现一个表达式输入式计算器——即用户依次点击数字和运算符上方显示完整的算式如12按下等号后才计算结果显示3。同时会涵盖 Qt Designer 的基本操作如调整控件字体、恢复面板、信号槽连接以及解决常见的“重复连接导致程序崩溃”问题。读完本文你将掌握Qt Designer 的常用设置与界面恢复PyQt5 信号槽的手动与自动连接机制使用eval安全计算简单表达式完整计算器项目的编码与调试技巧一、环境准备Python 3.7PyQt5pip install pyqt5 pyqt5-toolsQt Designer随pyqt5-tools安装或单独下载安装完成后在终端输入designer即可启动 Qt DesignerWindows 下可能在Python安装目录\Scripts\pyqt5designer.exe。二、使用 Qt Designer 设计 UI2.1 新建主窗口打开 Qt Designer选择Main Window点击“创建”。窗口默认尺寸 800x600可后续调整。2.2 添加控件我们需要一个QLabel作为标题“一个简单的计算器”一个QLineEdit作为显示面板只读、右对齐16 个QPushButton数字 0-9、运算符、-、*、/、等号、清除clear布局采用QVBoxLayout和QHBoxLayout嵌套使按钮排列整齐。具体步骤从左侧“Widget Box”拖入一个QWidget到 centralwidget 上作为按钮容器。选中该容器右键 → 布局 → 垂直布局。在垂直布局中依次添加三个水平布局每个水平布局内放入 5 个按钮。调整按钮大小和文本最终布局如下另外在下方放置一个clear按钮与显示面板对齐。提示为了代码中便于识别建议在“对象查看器”中给按钮重命名如pushButton_1、pushButton_add等但也可以保留默认的pushButton、pushButton_2等。2.3 调整字体大小选中标题QLabel在右侧“属性编辑器”中找到font属性点击...按钮在弹出的字体对话框中设置点大小为 17 或更大。这样只有标题字体变大其他控件不受影响。如果你不小心关闭了“属性编辑器”或“对象查看器”可通过菜单视图 → 属性编辑器 / 对象查看器重新打开或直接点击视图 → 重置为默认布局。2.4 保存 UI 文件保存为jisuanqi.ui。三、将 UI 转换为 Python 代码方法1在终端执行确保当前目录包含jisuanqi.uipyuic5 jisuanqi.ui -o jisuanqi.py方法2使用外部工具PyUIC生成的文件jisuanqi.py包含了界面类的定义Ui_MainWindow但没有任何业务逻辑。我们不会直接修改这个文件因为每次修改 UI 后重新生成会覆盖手动代码而是通过继承的方式编写主程序。四、实现计算器逻辑表达式输入模式4.1 核心思路显示区QLineEdit只读右对齐。初始显示0。数字按钮将数字追加到当前表达式末尾如果当前显示的是计算结果则先清空再追加。运算符按钮追加运算符但要避免连续运算符例如12自动纠正为12。等号计算当前表达式使用eval显示结果并标记当前显示为结果。清除重置显示为0清除标记。4.2 完整代码新建main.py或你喜欢的名字内容如下import sys from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtCore import Qt from jisuanqi import Ui_MainWindow # 导入生成的 UI 类 class Calculator(QMainWindow, Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) # 加载 UI 布局 self.initUI() # 界面微调 self.initSignals() # 连接信号与槽 self.reset_expression() # 初始化状态 def initUI(self): self.lineEdit.setReadOnly(True) self.lineEdit.setAlignment(Qt.AlignRight) self.lineEdit.setText(0) def initSignals(self): # 数字按钮 0-9 self.pushButton.clicked.connect(lambda: self.on_digit_clicked(1)) self.pushButton_2.clicked.connect(lambda: self.on_digit_clicked(2)) self.pushButton_3.clicked.connect(lambda: self.on_digit_clicked(3)) self.pushButton_6.clicked.connect(lambda: self.on_digit_clicked(4)) self.pushButton_7.clicked.connect(lambda: self.on_digit_clicked(5)) self.pushButton_8.clicked.connect(lambda: self.on_digit_clicked(6)) self.pushButton_11.clicked.connect(lambda: self.on_digit_clicked(7)) self.pushButton_12.clicked.connect(lambda: self.on_digit_clicked(8)) self.pushButton_13.clicked.connect(lambda: self.on_digit_clicked(9)) self.pushButton_14.clicked.connect(lambda: self.on_digit_clicked(0)) # 运算符 self.pushButton_4.clicked.connect(lambda: self.on_operator_clicked()) self.pushButton_5.clicked.connect(lambda: self.on_operator_clicked(-)) self.pushButton_9.clicked.connect(lambda: self.on_operator_clicked(*)) self.pushButton_10.clicked.connect(lambda: self.on_operator_clicked(/)) # 等号和清除 self.pushButton_15.clicked.connect(self.on_equal_clicked) self.pushButton_16.clicked.connect(self.on_clear_clicked) def on_digit_clicked(self, digit): current self.lineEdit.text() # 如果当前显示的是错误或计算结果按数字时重新开始 if current in (错误除数不能为零, 计算错误) or self.result_displayed: self.lineEdit.clear() self.result_displayed False current # 避免前导零显示为 0 时按数字直接替换 if current 0: self.lineEdit.setText(digit) else: self.lineEdit.setText(current digit) def on_operator_clicked(self, op): current self.lineEdit.text() # 如果当前显示的是结果将结果作为第一个操作数再追加运算符 if self.result_displayed: self.lineEdit.setText(current op) self.result_displayed False return # 空或仅 0 不允许运算符开头 if not current or current 0: return # 避免连续运算符如果最后一个字符是运算符则替换 last_char current[-1] if last_char in -*/: self.lineEdit.setText(current[:-1] op) else: self.lineEdit.setText(current op) def on_equal_clicked(self): expr self.lineEdit.text() # 避免空表达式或结尾为运算符 if not expr or expr[-1] in -*/: return try: # eval 执行算术运算注意仅信任自己构建的表达式 result eval(expr) if isinstance(result, float) and result.is_integer(): result int(result) self.lineEdit.setText(str(result)) self.result_displayed True except Exception: self.lineEdit.setText(表达式错误) self.result_displayed True def on_clear_clicked(self): self.lineEdit.setText(0) self.reset_expression() def reset_expression(self): self.result_displayed False if __name__ __main__: app QApplication(sys.argv) window Calculator() window.show() sys.exit(app.exec_())4.3 信号槽的两种连接方式方式一在 Qt Designer 中直接连接适合简单内置槽如close()点击菜单Edit → Edit Signals/SlotsF4从按钮拖拽到窗口选择信号和槽。生成的 UI 代码会自动包含connect。方式二在代码中手动连接推荐灵活可控如上述代码在initSignals方法中逐个connect。这种方式可以传递自定义参数如数字字符也便于调试。⚠️注意两种方式不要混用否则一个按钮会被连接两次导致程序崩溃常见于初学者。如果之前在设计器中连接过请按 F4 进入编辑模式删除所有红色箭头线再重新生成 UI 代码。五、计算器逻辑代码详解5.1 核心设计思路显示区QLineEdit只读右对齐初始显示0。数字按钮将数字追加到当前表达式末尾如果当前显示的是计算结果则先清空再追加。运算符按钮追加运算符但要避免连续运算符例如12自动纠正为12。等号使用eval()计算当前表达式显示结果并标记当前显示为结果。清除重置显示为0清除标记。5.2 完整代码逐段讲解import sys from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtCore import Qt from jisuanqi import Ui_MainWindow # 导入生成的 UI 类sys提供命令行参数和程序退出功能。PyQt5.QtWidgets提供QApplication应用管理和QMainWindow主窗口基类。PyQt5.QtCore.Qt包含对齐方式等枚举值如Qt.AlignRight。jisuanqi.Ui_MainWindow由pyuic5生成的界面类包含所有控件的定义和布局。class Calculator(QMainWindow, Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) # 加载 UI 布局 self.initUI() # 界面微调 self.initSignals() # 连接信号与槽 self.reset_expression() # 初始化状态多重继承自QMainWindow窗口行为和Ui_MainWindowUI 控件。setupUi(self)Ui_MainWindow提供的方法根据设计创建所有控件并布局。后续调用三个自定义方法完成界面调整、事件绑定和状态初始化。initUI– 界面微调def initUI(self): self.lineEdit.setReadOnly(True) self.lineEdit.setAlignment(Qt.AlignRight) self.lineEdit.setText(0)将显示框设为只读用户不能直接键盘输入只能通过按钮操作。文本右对齐符合计算器习惯。初始显示0。initSignals– 连接信号与槽def initSignals(self): # 数字按钮 0-9 self.pushButton.clicked.connect(lambda: self.on_digit_clicked(1)) self.pushButton_2.clicked.connect(lambda: self.on_digit_clicked(2)) self.pushButton_3.clicked.connect(lambda: self.on_digit_clicked(3)) self.pushButton_6.clicked.connect(lambda: self.on_digit_clicked(4)) self.pushButton_7.clicked.connect(lambda: self.on_digit_clicked(5)) self.pushButton_8.clicked.connect(lambda: self.on_digit_clicked(6)) self.pushButton_11.clicked.connect(lambda: self.on_digit_clicked(7)) self.pushButton_12.clicked.connect(lambda: self.on_digit_clicked(8)) self.pushButton_13.clicked.connect(lambda: self.on_digit_clicked(9)) self.pushButton_14.clicked.connect(lambda: self.on_digit_clicked(0)) # 运算符 self.pushButton_4.clicked.connect(lambda: self.on_operator_clicked()) self.pushButton_5.clicked.connect(lambda: self.on_operator_clicked(-)) self.pushButton_9.clicked.connect(lambda: self.on_operator_clicked(*)) self.pushButton_10.clicked.connect(lambda: self.on_operator_clicked(/)) # 等号和清除 self.pushButton_15.clicked.connect(self.on_equal_clicked) self.pushButton_16.clicked.connect(self.on_clear_clicked)为每个按钮的clicked信号连接对应的处理函数。数字和运算符按钮使用lambda传递具体字符。因为on_digit_clicked需要一个参数数字字符而按钮的clicked信号会传递一个布尔值表示按钮状态直接用self.on_digit_clicked(1)会立即执行所以用lambda包装成无参函数点击时才调用并传入1。等号和清除直接连接函数它们不需要额外参数。⚠️重要按钮的对象名如pushButton_2取决于.ui文件中的对象名。如果设计时重命名了按钮这里需要相应修改。on_digit_clicked– 处理数字按钮def on_digit_clicked(self, digit): current self.lineEdit.text() # 如果当前显示的是错误或计算结果按数字时重新开始 if current in (错误除数不能为零, 计算错误) or self.result_displayed: self.lineEdit.clear() self.result_displayed False current # 避免前导零显示为 0 时按数字直接替换 if current 0: self.lineEdit.setText(digit) else: self.lineEdit.setText(current digit)current获取当前显示框中的文本。如果显示的是错误信息如表达式错误或者刚刚显示了一个计算结果self.result_displayed为True则清空显示并重置标志然后从新数字开始。前导零处理如果当前只有单个0初始状态或刚清除直接替换为按下的数字避免01。否则将数字追加到末尾。on_operator_clicked– 处理运算符def on_operator_clicked(self, op): current self.lineEdit.text() # 如果当前显示的是结果将结果作为第一个操作数再追加运算符 if self.result_displayed: self.lineEdit.setText(current op) self.result_displayed False return # 空或仅 0 不允许运算符开头 if not current or current 0: return # 避免连续运算符如果最后一个字符是运算符则替换 last_char current[-1] if last_char in -*/: self.lineEdit.setText(current[:-1] op) else: self.lineEdit.setText(current op)结果后接运算符例如用户先算出235再按希望继续计算5...。此时将当前结果作为第一个操作数追加运算符并清除结果标志。防止空开头如果表达式为空或仅为0不允许以运算符开头避免出现1这样不完整的表达式。注意这样会无法输入负数如-5如需支持负数可改进逻辑。避免连续运算符如果当前表达式最后一个字符已经是运算符如3再按-则替换成3-而不是变成3-。on_equal_clicked– 计算表达式def on_equal_clicked(self): expr self.lineEdit.text() # 避免空表达式或结尾为运算符 if not expr or expr[-1] in -*/: return try: result eval(expr) if isinstance(result, float) and result.is_integer(): result int(result) self.lineEdit.setText(str(result)) self.result_displayed True except Exception: self.lineEdit.setText(表达式错误) self.result_displayed True获取当前表达式如果为空或末尾是运算符如3则直接返回不进行计算。eval(expr)Python 内置函数可以计算字符串形式的算术表达式如12*3→7。⚠️安全警告eval能执行任意代码但由于我们的输入完全由按钮产生只包含数字、运算符和可能的小数点在受控环境下是安全的。生产环境建议使用ast.literal_eval或自行编写解析器。结果美化如果计算结果是浮点数但实际是整数如2.0转换为整数显示2。错误处理捕获所有异常如除零、语法错误等显示表达式错误。设置self.result_displayed True以便下次输入数字时自动清空当前结果。on_clear_clicked– 清除def on_clear_clicked(self): self.lineEdit.setText(0) self.reset_expression() def reset_expression(self): self.result_displayed False将显示重置为0并重置结果标志。5.3 主程序入口if __name__ __main__: app QApplication(sys.argv) window Calculator() window.show() sys.exit(app.exec_())创建QApplication实例每个 PyQt 程序必须有且只有一个。创建计算器窗口并显示。进入事件循环程序结束时返回退出码。六、运行与测试在终端执行python main.py效果演示依次点击12显示区显示12点击显示3点击3显示6点击clear显示0尝试除以 0显示“错误除数不能为零”七、常见问题与解决方案7.1 点击按钮后程序立即退出原因UI 文件中存在信号连接自动生成同时代码中又手动连接了一遍且参数不一致如无参 vs 有参导致类型错误。解决在 Qt Designer 中按 F4删除所有连接线保存后重新pyuic5。7.2 修改字体大小后所有控件都变了原因不小心选中了父窗口如centralwidget修改了font属性子控件会继承。解决只选中目标控件修改字体若已误改右键父窗口的font属性 → 恢复为默认值。7.3 连续按运算符会显示如12解决代码中已通过if last_char in -*/: self.lineEdit.setText(current[:-1] op)自动替换。7.4eval安全吗本文计算器只允许用户通过按钮输入数字和运算符不涉及直接键盘输入因此eval是安全的。若需扩展可考虑使用ast.literal_eval或自己实现表达式解析器。八、扩展建议添加小数点按钮和退格按钮--。支持键盘输入重写keyPressEvent。使用math模块支持平方、开根等运算。美化界面设置样式表、圆角按钮等。结语通过本文你不仅学会了一个实用的计算器项目还掌握了 Qt Designer 与 PyQt5 协同开发的基本流程。信号槽机制是 Qt 的核心多加练习便能灵活运用。希望这篇教程对你有所帮助欢迎在评论区交流讨论

更多文章