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-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)

布局采用 QVBoxLayoutQHBoxLayout 嵌套,使按钮排列整齐。具体步骤:

  1. 从左侧"Widget Box"拖入一个 QWidget 到 centralwidget 上,作为按钮容器。

  2. 选中该容器,右键 → 布局 → 垂直布局。

  3. 在垂直布局中依次添加三个水平布局,每个水平布局内放入 5 个按钮。

  4. 调整按钮大小和文本,最终布局如下:

另外在下方放置一个 clear 按钮,与显示面板对齐。

提示 :为了代码中便于识别,建议在"对象查看器"中给按钮重命名(如 pushButton_1pushButton_add 等),但也可以保留默认的 pushButtonpushButton_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_displayedTrue),则清空显示并重置标志,然后从新数字开始。

  • 前导零处理 :如果当前只有单个 "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 的核心,多加练习便能灵活运用。希望这篇教程对你有所帮助,欢迎在评论区交流讨论!

相关推荐
喂_balabala2 小时前
Kotlin-属性委托
android·开发语言·kotlin
dashizhi20152 小时前
如何禁止外来设备连接内网wifi、禁止外来电脑接入单位局域网?
开发语言·网络·php
不想写代码的星星2 小时前
类型萃取:重生之我在幼儿园修炼类型学
开发语言·c++
csbysj20202 小时前
C++ 接口(抽象类)
开发语言
亚空间仓鼠2 小时前
Python学习日志(四):实例
开发语言·python·学习
Fanfanaas2 小时前
Linux 系统编程 进程篇 (二)
linux·运维·服务器·c语言·开发语言·学习
油丶酸萝卜别吃2 小时前
高效处理数组差异:JS中新增、删除、交集的最优解(Set实现)
开发语言·前端·javascript
HoneyMoose2 小时前
Npmp 安装时候提示警告: error (ERR_INVALID_THIS)
开发语言
gskyi2 小时前
时间格式化神器:智能显示相对时间
开发语言·javascript·ecmascript