PyQt5入门实战:从零实现一个表达式输入式计算器(附完整代码)
前言
PyQt5 是 Python 绑定 Qt5 的 GUI 框架,功能强大且易于上手。本文将从零开始,教你如何使用Qt Designer设计计算器界面,并在 PyQt5 中实现一个表达式输入式计算器——即用户依次点击数字和运算符,上方显示完整的算式(如1+2),按下等号后才计算结果(显示3)。同时会涵盖 Qt Designer 的基本操作(如调整控件字体、恢复面板、信号槽连接),以及解决常见的“重复连接导致程序崩溃”问题。
读完本文,你将掌握:
Qt Designer 的常用设置与界面恢复
PyQt5 信号槽的手动与自动连接机制
使用
eval安全计算简单表达式完整计算器项目的编码与调试技巧
一、环境准备
Python 3.7+
PyQt5:
pip install pyqt5 pyqt5-toolsQt Designer(随
pyqt5-tools安装,或单独下载)
安装完成后,在终端输入designer即可启动 Qt Designer(Windows 下可能在Python安装目录\Scripts\pyqt5designer.exe)。
二、使用 Qt Designer 设计 UI
2.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.ui):
pyuic5 jisuanqi.ui -o jisuanqi.py方法2,使用外部工具PyUIC
生成的文件jisuanqi.py包含了界面类的定义(Ui_MainWindow),但没有任何业务逻辑。我们不会直接修改这个文件(因为每次修改 UI 后重新生成会覆盖手动代码),而是通过继承的方式编写主程序。
四、实现计算器逻辑(表达式输入模式)
4.1 核心思路
显示区(QLineEdit):只读,右对齐。初始显示
0。数字按钮:将数字追加到当前表达式末尾;如果当前显示的是计算结果,则先清空再追加。
运算符按钮:追加运算符,但要避免连续运算符(例如
1++2自动纠正为1+2)。等号:计算当前表达式(使用
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/Slots(F4),从按钮拖拽到窗口,选择信号和槽。生成的 UI 代码会自动包含connect。方式二:在代码中手动连接(推荐,灵活可控)
如上述代码,在initSignals方法中逐个connect。这种方式可以传递自定义参数(如数字字符),也便于调试。
⚠️注意:两种方式不要混用,否则一个按钮会被连接两次,导致程序崩溃(常见于初学者)。如果之前在设计器中连接过,请按 F4 进入编辑模式,删除所有红色箭头线,再重新生成 UI 代码。
五、计算器逻辑代码详解
5.1 核心设计思路
显示区:
QLineEdit只读,右对齐,初始显示"0"。数字按钮:将数字追加到当前表达式末尾;如果当前显示的是计算结果,则先清空再追加。
运算符按钮:追加运算符,但要避免连续运算符(例如
1++2自动纠正为1+2)。等号:使用
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_MainWindow(UI 控件)。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)结果后接运算符:例如用户先算出
2+3=5,再按+希望继续计算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 内置函数,可以计算字符串形式的算术表达式(如"1+2*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效果演示:
依次点击
1+2,显示区显示1+2点击
=,显示3点击
+3=,显示6点击
clear,显示0尝试除以 0,显示“错误:除数不能为零”
七、常见问题与解决方案
7.1 点击按钮后程序立即退出
原因:UI 文件中存在信号连接(自动生成),同时代码中又手动连接了一遍,且参数不一致(如无参 vs 有参),导致类型错误。
解决:在 Qt Designer 中按 F4,删除所有连接线,保存后重新pyuic5。
7.2 修改字体大小后,所有控件都变了
原因:不小心选中了父窗口(如centralwidget)修改了font属性,子控件会继承。
解决:只选中目标控件修改字体;若已误改,右键父窗口的font属性 → 恢复为默认值。
7.3 连续按运算符会显示如1++2
解决:代码中已通过if last_char in "+-*/": self.lineEdit.setText(current[:-1] + op)自动替换。
7.4eval安全吗?
本文计算器只允许用户通过按钮输入数字和运算符,不涉及直接键盘输入,因此eval是安全的。若需扩展,可考虑使用ast.literal_eval或自己实现表达式解析器。
八、扩展建议
添加小数点按钮和退格按钮(
<--)。支持键盘输入(重写
keyPressEvent)。使用
math模块支持平方、开根等运算。美化界面(设置样式表、圆角按钮等)。
结语
通过本文,你不仅学会了一个实用的计算器项目,还掌握了 Qt Designer 与 PyQt5 协同开发的基本流程。信号槽机制是 Qt 的核心,多加练习便能灵活运用。希望这篇教程对你有所帮助,欢迎在评论区交流讨论!
