QPushButton.clicked.connect关联方法时,居然有多余的参数?

303 阅读5分钟

在用PyQt5写窗体程序时,按钮QPushButton的点击事件clicked经常需要关联不同的方法,当我打算在这个被关联的方法上加上自定义的装饰器注解以完成某些功能时,我发现装饰器捕捉到的方法一直有一个额外的 boolean 类型参数。

代码复现

窗体

首先是当事方:带按钮QPushButton的窗体和被关联的目标方法,这里用 func_target 命名

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QMainWindow
import sys

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        print('窗体布局与控件基础设置')
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(500, 50)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setInputMethodHints(QtCore.Qt.ImhNone)
        self.centralwidget.setObjectName("centralwidget")
        self.btn = QtWidgets.QPushButton(self.centralwidget)
        self.btn.setGeometry(QtCore.QRect(0, 0, 100, 30))
        self.btn.setText('测试按钮')
        
    
class MainUi(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(MainUi, self).__init__(parent)
        print('窗体初始化')
        self.setupUi(self)
        print('初始化结束')
        # 按钮点击事件绑定方法
        self.btn.clicked.connect(self.func_target)

    def func_target(self):
        print('测试按钮绑定的方法')


if __name__ == '__main__':
    try:
        myapp = QtWidgets.QApplication(sys.argv)
        ui = MainUi()
        ui.show()
        sys.exit(myapp.exec_())
    except Exception as e:
        print(e)

理论上QT窗口样式的编写和主题程序可以不用分开,我个人的开发习惯是用 QtDesigner 设计好窗体布局后用 PyUIC 工具将 ui 文件转成 py 文件,转换完的文件一般就是前面 UI_MainWindow 类的格式,所以才分了两个类。具体操作可参考 PyQt5教程:如何在PyCharm中添加Qt Designer、PyUIC、PyRcc外部工具?

上面的代码执行后就是这样的窗体。

image.png

装饰器

装饰器是 python 中一种 体感上类似 spring 框架中 “注解” 的机制,它的调用也是用@写在目标方法上面,但本质是装饰器模式。原理是在目标代码块外面包裹上额外的代码,再暴露出去调用,算是 “语法糖” 。

def dec_log(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result
    return wrapper

spring 的注解在没有对应方法的情况下只是一个标签,假如要自己开发一个注解以及配套的功能,通常需要写两个文件:一个是@interface注解本身,需要用@Target指定注解可以应用的位置、用@Retention指定注解的生命周期、用@Documented将注解包含在 Javadoc 中、用@Inherited允许子类继承父类的注解;另外一个则是配套的切面类,类外面需要@Aspect标记它是一个切面类、@Component标记它是一个组件,在类的内部,用@Around("@annotation(自定义注解名)")拦截标注有这个自定义注解的方法,并将其挂在想要运行的方法上,参考这篇文章《spring的自定义注解(作者:p-knowledge)》。后面这一步还可以进一步拆分,可参考这篇文章:《spring自定义注解(作者:熟悉的新风景)》。

spring的暂且按下另说,我前面的这个装饰器在运行时反正是报错了:

Traceback (most recent call last):

File "test.py", line 49, in wrapper

result = func(*args, **kwargs)

TypeError: func_target() takes 1 positional argument but 2 were given

image.png

目标方法只有一个self参数,这还是因为在 class 类必然自带的参数,多余的是什么参数呢?

问题原因

多余的是一个布尔值 False 。

在前面装饰器方法下def wrapper(*args, **kwargs):后面增加一行打印 args 和 kwargs 的方法,就会看到报错前打印了这么一段参数。

image.png

前面的 __main__.MainUi 是窗体类对象本身,也就是 func_targetself 参数,而后面的 False 其实是控件 QPushButton 继承的抽象类 QAbstractButtonclicked 方法的自带参数,意义当前控件是否被选中

可以在初始化窗口的关联按钮点击事件与目标方法的代码前再增加两行代码,设置 btn 的 CheckabledChecked 值,这两个需要配套使用的。

self.btn.setCheckable(True)
self.btn.setChecked(True)

执行后,就会发现按钮呈点击态,也就是平常戳一下时出现的样子。

image.png

这个时候的按钮依然可以点击,且也会触发前面绑定的事件。

前面的报错这里就简单处理为 wrapper 内调用 func 时只传递第一个参数,当然实际使用时需要更多判断,毕竟这个装饰器肯定也不止用在一个类的方法里。

def dec_log(func):
    def wrapper(*args, **kwargs):
        print(args,kwargs)
        result = func(args[0]) # 只传递第一个参数
        return result
    return wrapper

重新启动窗体,点击按钮,会发现打印出来的值是这样的。

image.png

最后一次点击传入的 checked 是 false,按钮状态也不再是被选中了。

image.png

至于为什么必须是clicked而不能是click,因为前者是一个信号,可以有关联操作,后者是普通方法,因此不具备关联操作的功能。

至于如何辨别什么方法是信号,得等我再研究研究了……