首页 / 资讯中心 / 文章详情

PyQt6-第03讲-信号与槽PyQt的灵魂

PyQt6-第03讲-信号与槽PyQt的灵魂 ★ FEATURED ARTICLE
第 3 讲 | 信号与槽PyQt 的灵魂本讲目标搞懂信号槽机制掌握 connect 的三种写法和 lambda 传参的经典坑写出一个能真算数的计算器。配套代码cases/c03_signal_slot.py一、一句话说清信号与槽信号Signal 某个控件身上发生了什么事槽Slot 一件要去做的事把两者连起来就是信号槽按钮.clicked.connect(某个函数)意思是当按钮被点击时去执行这个函数。PyQt 里几乎所有交互都长这样场景信号槽点按钮btn.clicked打开文件输入框内容变了edit.textChanged更新搜索提示下拉框换选项combo.currentIndexChanged重新加载数据表格选中行table.itemSelectionChanged回填表单定时器到点timer.timeout刷新图表为什么要有这套机制因为它把控件和业务彻底解耦了。QPushButton根本不知道你点击它之后要干什么它只负责在点击发生时喊一嗓子我被人点了。谁爱听谁听。这样你换按钮、换逻辑互不影响。二、connect 的三种写法写法 1连到自定义方法最常用classMyWindow(QWidget):def__init__(self):super().__init__()self.btnQPushButton(保存,self)self.btn.clicked.connect(self.on_save)# ★ 注意只写函数名不带括号defon_save(self):print(保存被点击了)新手第一大坑就在这里connect(self.on_save)不带括号。self.btn.clicked.connect(self.on_save)# ✔ 把函数本身交给 connectself.btn.clicked.connect(self.on_save())# ✘ 立刻执行了 on_save把返回值交给了 connect第二种写法在窗口创建时就执行一次on_save()然后 connect 到None点击按钮毫无反应还会报错。记住connect 的参数是一个函数对象不是函数调用结果。写法 2连到内置槽省事Qt 控件自带很多现成的槽不用自己写self.btn_clear.clicked.connect(self.edit.clear)# 清空输入框self.btn_copy.clicked.connect(self.edit.copy)# 复制self.btn_quit.clicked.connect(self.close)# 关窗self.slider.valueChanged.connect(self.progress.setValue)# 滑块联动进度条滑块联动进度条这一行值得记下来是这个写法最经典的用法valueChanged发出一个 intsetValue正好接收一个 int类型对上了直接连。写法 3lambda 传参一个槽处理多个控件假设有 10 个按钮点击后都要执行同一个逻辑只是参数不同fortextin[1,2,3,4,5]:btnQPushButton(text)btn.clicked.connect(lambda_,ttext:self.on_key(t))# ★ 关键写法clicked信号本身会带一个是否被选中的布尔值所以 lambda 的第一个参数必须留着写_表示不用从第二个参数开始才是你传的值。而ttext这个默认参数是必须的不能省。看错误版本错在哪# ✘ 错误示范闭包陷阱fortextin[1,2,3,4,5]:btn.clicked.connect(lambda:self.on_key(text))区别在于没有把text的值固定下来。lambda 里的text是个自由变量它引用的是循环变量本身等按钮真被点击时循环早就结束了text永远是最后一个值5。结果就是点哪个按钮都执行on_key(5)。用ttext把当前值冻结成默认参数每次循环生成一个独立的闭包问题就解决了。这个坑语言层面叫闭包晚绑定不只在 PyQt 里会遇到但只要记住“循环里连 lambda一律写成lambda _, 参数名变量: ...”就够了。三、信号可以带参数信号不只是通知还能携带数据。常见的有self.edit.textChanged.connect(lambdas:print(f现在的内容是{s}))# 输出现在的内容是abcself.combo.currentIndexChanged.connect(lambdai:print(f选中第{i}项))self.checkbox.stateChanged.connect(lambdastate:print(f勾选状态{bool(state)}))注意stateChanged传的是整数0 未选 / 2 选中Qt6 里也可能是 1。想要布尔值就自己转一下。如果只是想判断有没有勾选直接用self.checkbox.isChecked()更省事。当你不知道某个控件会发什么信号、参数是什么最快的办法是把所有信号都接到打印上点一遍看输出——第 4 讲的案例就是这么做的。四、自定义信号进阶但很常用有时需要在自定义类里往外发通知。比如你的数据加载器加载完了想告诉界面fromPyQt6.QtCoreimportpyqtSignalclassDataLoader(QObject):# 类属性里声明信号参数类型写在括号里progresspyqtSignal(int,str)# 百分比, 描述finishedpyqtSignal(object)# 结果对象failedpyqtSignal(str)# 错误信息定义好之后就能像内置信号一样用self.loader.progress.connect(lambdapct,msg:self.progress_bar.setValue(pct))这三行自定义信号本课程第 8 讲开始会反反复复出现——所有耗时任务都靠它把进度和结果从子线程传回界面。现在先混个眼熟就行。五、断开与临时阻塞btn.clicked.disconnect(self.on_save)# 断开某个连接btn.clicked.disconnect()# 断开这个信号的所有连接edit.blockSignals(True)# 临时屏蔽这个控件的所有信号edit.setText(程序设置的初始值)# 这时候 textChanged 不会触发edit.blockSignals(False)# 记得打开blockSignals用得很多。典型场景用代码填充输入框时不想触发校验逻辑。忘了恢复blockSignals(False)会造成输入框好像死了排查起来很费时间。六、完整案例一个能真算数的计算器结构上就三件事① 用 QGridLayout 排九宫格KEYS[[7,8,9,/],[4,5,6,*],[1,2,3,-],[0,.,,],]gridQGridLayout()grid.setSpacing(8)forr,rowinenumerate(KEYS):forc,textinenumerate(row):btnQPushButton(text)btn.setMinimumSize(60,52)btn.setStyleSheet(self._btn_style(text))btn.clicked.connect(lambda_,ttext:self.on_key(t))# ★ 本讲重点grid.addWidget(btn,r,c)② 所有按钮汇到同一个入口方法不要让每个按钮连一个独立的函数那样 16 个按钮就要 16 个函数。统一入口defon_key(self,text:str)-None:iftext:self._evaluate()returniftextin-*/:ifself._exprandself._expr[-1]in-*/:self._exprself._expr[:-1]# 连按两个运算符替换掉前一个self._exprtextelse:self._exprtext self.display.setText(self._expror0)self.hint.setText(f提示表达式{self._expror(空)})③ 用一个 QLabel 显示内部状态这是本案例一个刻意的设计界面上除了显示屏还有一行灰色的提示文字实时显示当前累积的表达式。调试信号槽逻辑时一个能看到内部状态的小标签比 print 到控制台有用十倍——因为它是跟着界面刷新的。运行起来就是这样上面是显示屏中间是状态提示下面是按键区。示例代码里用eval()算结果只为教学演示真实项目要做表达式解析绝不能用 eval 处理用户输入。七、四个坑坑 1connect后面加了括号前面说过最高频必须记住坑 2同一个信号连了两次同一个函数结果是点一次按钮执行两次。常见于初始化代码被调用了两次比如你手动调了一次、又在某个事件里调了一次。用disconnect()清一下即可。坑 3在槽函数里做耗时操作界面卡死按钮点下去槽函数里跑一个 30 秒的循环整个界面就不响应了——鼠标转圈、窗口拖不动。这是主线程被占住了第 8 讲会专门讲怎么解决把活儿丢给子线程。坑 4循环变量没有冻结上面那个闭包陷阱八、完整代码# -*- coding: utf-8 -*-第 3 讲 案例 03信号与槽 —— 一个能算的简易计算器 知识点 - signal信号与 slot槽的机制谁变了、通知谁、干什么 - button.clicked.connect(...) 的三种连接写法 - 用 lambda 给多个按钮传不同参数最常用的技巧 - QGridLayout 做九宫格 - 用单一入口方法统一处理所有按键 importsysfrompathlibimportPath sys.path.insert(0,str(Path(__file__).resolve().parent.parent))fromPyQt6.QtCoreimportQt# noqa: E402fromPyQt6.QtWidgetsimport(QGridLayout,QLabel,QLineEdit,# noqa: E402QPushButton,QVBoxLayout,QWidget)fromutils.envimportrun# noqa: E402classCalculator(QWidget):四则运算计算器演示信号与槽最核心的用法# 按键布局4 行 4 列KEYS[[7,8,9,/],[4,5,6,*],[1,2,3,-],[0,.,,],]def__init__(self)-None:super().__init__()self.setWindowTitle(简易计算器)self.resize(300,380)# ---- 显示屏用只读 QLineEdit用户可以选中复制 ----self.displayQLineEdit(0)self.display.setReadOnly(True)self.display.setAlignment(Qt.AlignmentFlag.AlignRight)self.display.setStyleSheet(QLineEdit { font-size:22px; padding:8px; background:#f5f6f8; border:1px solid #dcdfe6; border-radius:4px; })# ---- 提示条显示当前内部状态方便调试 ----self.hintQLabel(提示0)self.hint.setStyleSheet(color:#8a8f99; font-size:12px;)# ---- 按钮区 ----gridQGridLayout()grid.setSpacing(8)forr,rowinenumerate(self.KEYS):forc,textinenumerate(row):btnQPushButton(text)btn.setMinimumSize(60,52)btn.setStyleSheet(self._btn_style(text))# ★ 核心把按键字符通过 lambda 传给同一个处理方法# 直接写 self.on_key(text) 会在窗口创建时就执行所以必须包一层 lambdabtn.clicked.connect(lambda_,ttext:self.on_key(t))grid.addWidget(btn,r,c)# ---- 清空键单独一行 ----clear_btnQPushButton(清空 (C))clear_btn.setMinimumHeight(40)clear_btn.setStyleSheet(self._btn_style(C))clear_btn.clicked.connect(self.on_clear)rootQVBoxLayout(self)root.setContentsMargins(14,14,14,14)root.setSpacing(10)root.addWidget(self.display)root.addWidget(self.hint)root.addLayout(grid)root.addWidget(clear_btn)# 内部运算状态self._expr# 累积的表达式self._last# 上一个操作数# ---------- 样式运算符和等号配色不同 ----------staticmethoddef_btn_style(text:str)-str:base(QPushButton { border:1px solid #dcdfe6; border-radius:4px; font-size:16px; background:white; }QPushButton:hover { background:#eef2f8; })iftextin-*/:returnbase.replace(background:white,background:#eaf1ff)iftext:return(QPushButton { background:#2d7ff9; color:white; border:none; border-radius:4px; font-size:16px; }QPushButton:hover { background:#1c6be0; })iftextC:return(QPushButton { background:#fff1f0; color:#d4380d; border:1px solid #ffccc7; border-radius:4px; font-size:14px; })returnbase# ---------- 统一的按键入口所有按钮都汇到这里 ----------defon_key(self,text:str)-None:iftext:self._evaluate()returniftextin-*/:# 已经有运算符先算掉前一步ifself._exprandself._expr[-1]in-*/:self._exprself._expr[:-1]self._exprtextelse:self._exprtext self.display.setText(self._expror0)self.hint.setText(f提示表达式{self._expror(空)})defon_clear(self)-None:self._exprself.display.setText(0)self.hint.setText(提示已清空)def_evaluate(self)-None:注意这里用 eval 只是教学演示真实项目要做表达式解析try:resulteval(self._expr)# noqa: S307resultround(result,6)self.hint.setText(f提示{self._expr}{result})self.display.setText(str(result))self._exprstr(result)# 支持连续计算exceptExceptionasexc:# noqa: BLE001self.hint.setText(f提示表达式错误{type(exc).__name__})self._exprif__name____main__:sys.exit(run(Calculator,shotc03_calculator,width300,height380))九、小结信号槽 事件通知机制语法是信号.connect(槽)三种写法自定义方法、内置槽、lambda 传参两个必须记住的细节connect 不写括号循环里连 lambda 要tvar冻结变量自定义信号pyqtSignal是后面多线程章节的基础到这里界面能排整齐第 2 讲按钮也能响应第 3 讲了。下一讲把 PyQt 里最常用的 12 个控件一次过一遍——每个控件我都会接上信号监听让你亲眼看到这个控件到底什么时候发信号、发什么参数这是学控件最快的方法。本讲是《PyQt6 客户端编程从入门到实战》专栏的第 3 讲。专栏共 22 讲、22 个可运行案例。
阅读完成 · 觉得有帮助?
咨询建站