前言
PyQt5 是 Python 绑定 Qt5 的 GUI 框架,功能强大且易于上手。本文将从零开始,教你如何使用 Qt Designer 设计计算器界面,并在 PyQt5 中实现一个表达式输入式 计算器------即用户依次点击数字和运算符,上方显示完整的算式(如 1+2),按下等号后才计算结果(显示 3)。同时会涵盖 Qt Designer 的基本操作(如调整控件字体、恢复面板、信号槽连接),以及解决常见的"重复连接导致程序崩溃"问题。
读完本文,你将掌握:
-
Qt Designer 的常用设置与界面恢复
-
PyQt5 信号槽的手动与自动连接机制
-
使用
eval安全计算简单表达式 -
完整计算器项目的编码与调试技巧
一、环境准备
-
Python 3.7+
-
PyQt5:
pip install pyqt5 pyqt5-tools -
Qt 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.4 eval 安全吗?
本文计算器只允许用户通过按钮输入数字和运算符,不涉及直接键盘输入,因此 eval 是安全的。若需扩展,可考虑使用 ast.literal_eval 或自己实现表达式解析器。
八、扩展建议
-
添加小数点按钮和退格按钮(
<--)。 -
支持键盘输入(重写
keyPressEvent)。 -
使用
math模块支持平方、开根等运算。 -
美化界面(设置样式表、圆角按钮等)。
结语
通过本文,你不仅学会了一个实用的计算器项目,还掌握了 Qt Designer 与 PyQt5 协同开发的基本流程。信号槽机制是 Qt 的核心,多加练习便能灵活运用。希望这篇教程对你有所帮助,欢迎在评论区交流讨论!