PySide6 GUI 编程(38):信号拦截与 lambda 槽函数

2024-08-31 11:08:43 浏览数 (1)

自定义信号

在之前的文章:PySide6 GUI 编程(3):信号槽机制中已经探讨过关于自定义信号的场景。在一些更追求灵活性的场景下,我们需要自定义信号,以此触发更多自定义的行为。

示例代码

代码语言:python代码运行次数:0复制
from __future__ import annotations

from PySide6.QtCore import QObject, Signal


def show_signal_parameters(*args):
    if len(args) == 0:
        print('信号发送的参数为空')
        return
    if len(args) != 1:
        print('信号发送的参数个数错误')
        return
    print('信号发送的参数:', args[0])


class MySignals(QObject):
    """
    Signal只能在继承自QObject的类中使用
    这是因为Signal和Slot机制是Qt的一个核心特性
    而这个特性是通过QObject类实现的
    """
    my_signal_1 = Signal(str)
    my_signal_2 = Signal(int)
    my_signal_3 = Signal(float)
    my_signal_4 = Signal(bool)
    my_signal_5 = Signal(list)
    my_signal_6 = Signal(dict)
    my_signal_7 = Signal(tuple)
    my_signal_8 = Signal(set)
    my_signal_9 = Signal(object)

    def __init__(self):
        super().__init__()
        self.my_signal_1.connect(show_signal_parameters)
        self.my_signal_2.connect(show_signal_parameters)
        self.my_signal_3.connect(show_signal_parameters)
        self.my_signal_4.connect(show_signal_parameters)
        self.my_signal_5.connect(show_signal_parameters)
        self.my_signal_6.connect(show_signal_parameters)
        self.my_signal_7.connect(show_signal_parameters)
        self.my_signal_8.connect(show_signal_parameters)
        self.my_signal_9.connect(show_signal_parameters)

        self.my_signal_1.emit('1')
        self.my_signal_2.emit(2)
        self.my_signal_3.emit(3.000)
        self.my_signal_4.emit(4)
        self.my_signal_5.emit([5, ])
        self.my_signal_6.emit({'6': 6})
        self.my_signal_7.emit((7,))
        self.my_signal_8.emit({8, })
        self.my_signal_9.emit([9, '9', ])


if __name__ == '__main__':
    MySignals()

运行效果

自定义信号与信号参数自定义信号与信号参数

使用匿名函数对信号进行拦截

在标准的 PySide6 信号中,信号与槽函数的入参总是固定的,这虽然可以在常见的信号使用上带来方便,但是也会限制一些更灵活的使用,比如对于按钮点击行为,当我希望获取更多的信息时,标准的信号与槽函数便不能满足要求。

因此有必要对原生的信号做拦截,并重新处理或打包信号的参数,并将其传递给自定义的函数做处理。

示例代码

代码语言:python代码运行次数:0复制
from __future__ import annotations

from datetime import datetime

from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget


def on_button_clicked(clicked: bool):
    """
    标准的槽函数,只接收 clicked:bool 参数
    """
    print('按钮被点击(标准槽函数on_button_clicked):', clicked, datetime.now().isoformat())


def on_button_clicked_2(clicked: bool, button: QPushButton):
    """
    自定义中间槽函数
    接收 clicked:bool 和 button:QPushButton 参数
    其中 clicked:bool 是按钮被点击时发送的参数,是标准的槽函数参数
    中间槽函数会额外接收 button:QPushButton 参数
    """
    print('按钮被点击(中间槽函数on_button_clicked_2):', clicked, button.text(), datetime.now().isoformat())


# def on_button_clicked_4(clicked: bool, button: QPushButton):
#     print('按钮被点击(中间槽函数on_button_clicked_4,结合 functools.partial 使用):', clicked, button.text(),
#           datetime.now().isoformat())


class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('信号拦截以及信号参数修改')
        button1 = QPushButton('测试按钮1 (标准槽函数)')
        button1.setCheckable(True)  # 设置为checkable, 这样 slot 函数中接收到的参数就是 True
        button1.clicked.connect(on_button_clicked)

        button2 = QPushButton('测试按钮2 (lambda表达式作为中间槽函数)')
        button2.setCheckable(True)
        # 使用lambda 表达式作为中间槽函数
        button2.clicked.connect(lambda button_clicked: on_button_clicked_2(clicked = button_clicked, button = button2))

        button3 = QPushButton('测试按钮3 (纯 lambda表达式槽函数)')
        button3.setCheckable(True)
        # 使用lambda 表达式作为中间槽函数
        # button=button3 是一个默认参数
        # 它的值在 lambda 函数定义时就已经确定
        # 因此即使在槽函数调用时 button 变量的值发生改变
        # 传递给槽函数的 button 参数的值仍然是 lambda 函数定义时的值
        button3.clicked.connect(
            lambda clicked, button = button3: print('按钮被点击(纯 lambda 表达式槽函数):',
                                                    clicked, button.text(),
                                                    datetime.now().isoformat()))

        # button4 = QPushButton('测试按钮4 (functools.partial 方式会报错)')
        # button4.setCheckable(True)
        # # functools.partial 可以生成一个新的函数,该新函数具有与原函数相同的行为,但某些参数已经预设了值
        # # 在当前的场景下,信号连接到槽函数时,我们使用了 functools.partial 创建了一个新的函数,
        # # 它将 button 作为预设参数传递给 on_button_clicked
        # # 但是在当前这种场景下,使用 functools.partial 的方式并不适用,因为预设参数会被放在其他参数之前,导致参数传递出现问题
        # # 运行时会报错:TypeError: on_button_clicked_4() missing 1 required positional argument: 'clicked'
        # new_func = functools.partial(on_button_clicked_4, button = button4)
        # button4.clicked.connect(new_func)

        v_main_layout = QVBoxLayout()
        v_main_layout.addWidget(button1)
        v_main_layout.addWidget(button2)
        v_main_layout.addWidget(button3)
        # v_main_layout.addWidget(button4)

        container = QWidget()
        container.setLayout(v_main_layout)
        self.setCentralWidget(container)


if __name__ == '__main__':
    app = QApplication()
    ins = MyWindow()
    ins.show()
    app.exec()
匿名函数参数示例匿名函数参数示例

运行效果

基于 lambda 槽函数进行信号拦截基于 lambda 槽函数进行信号拦截

使用匿名函数时的陷阱

匿名函数中变量引用问题是一个很常见的陷阱,尤其在循环中使用匿名函数时,容易形成一些错觉。

示例代码

代码语言:python代码运行次数:0复制
from __future__ import annotations

from PySide6.QtGui import QFont
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget


class MyMainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('lambda 槽函数中使用参数传值的例子')

        v_main_layout = QVBoxLayout()

        for i in range(10):
            button = QPushButton(str(i), parent = self)
            # 由于 lambda 函数没有自己的 i 变量
            # 它会捕获外部作用域中的 i 的引用
            # 这意味着,当按钮被点击时
            # self.button_clicked_1 方法将接收到循环结束时的 i 的值
            # 而不是按钮创建时的值
            button.clicked.connect(lambda clicked: self.button_clicked_1(i))
            # 使用了默认参数 value = i 来捕获当前的 i 值
            # 由于默认参数在 lambda 函数定义时就被评估
            # 它会捕获每次循环迭代时的 i 的当前值
            # 这样,当按钮被点击时,self.button_clicked_2 方法将接收到正确的值
            # 即与该按钮相关联的值
            button.clicked.connect(lambda clicked, value = i: self.button_clicked_2(value))
            # 使用闭包确保每个按钮的点击事件都能正确地传递其对应的 i 值
            button.clicked.connect(self.create_click_handler(i))
            v_main_layout.addWidget(button)

        self.label_1 = QLabel('label_1')
        self.label_1.setFont(QFont('UbuntuMono NF', 20))
        self.label_2 = QLabel('label_2')
        self.label_2.setFont(QFont('UbuntuMono NF', 20))
        self.label_3 = QLabel('label_3')
        self.label_3.setFont(QFont('UbuntuMono NF', 20))

        v_main_layout.addWidget(QLabel('-' * 16))
        v_main_layout.addWidget(self.label_1)

        v_main_layout.addWidget(QLabel('-' * 16))
        v_main_layout.addWidget(self.label_2)

        v_main_layout.addWidget(QLabel('-' * 16))
        v_main_layout.addWidget(self.label_3)

        container = QWidget()
        container.setLayout(v_main_layout)

        self.setCentralWidget(container)

    def button_clicked_1(self, value: int):
        self.label_1.setText('button_clicked_1, value={}'.format(str(value)))

    def button_clicked_2(self, value: int):
        self.label_2.setText('button_clicked_2, value={}'.format(str(value)))

    # 闭包
    def create_click_handler(self, value: int):
        def on_click(_: bool):
            self.label_3.setText('button_clicked_3, value={}'.format(str(value)))

        return on_click


if __name__ == '__main__':
    app = QApplication()
    ins = MyMainWindow()
    ins.show()
    app.exec()
核心逻辑核心逻辑
通过闭包的方式也可以避免引用问题通过闭包的方式也可以避免引用问题

运行效果

错误的 lambda 参数引用错误的 lambda 参数引用

通过匿名函数的方式设置槽函数的默认参数

基于匿名函数,我们可以对槽函数进行更多的改造,设置默认函数便是改造项之一。

示例代码

代码语言:python代码运行次数:0复制
from __future__ import annotations

import time
from datetime import datetime

from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget


def on_window_title_changed_1(title: str = 'default-title-1', time_str: str = datetime.now().isoformat()):
    print('on_window_title_changed_1, title=', title, 'time_str=', time_str, )


def on_window_title_changed_2(title: str = 'default-title-2', time_str: str = datetime.now().isoformat()):
    print('on_window_title_changed_2, title=', title, 'time_str=', time_str, )


def on_window_title_changed_3(title: str = 'default-title-3', time_str: str = datetime.now().isoformat()):
    print('on_window_title_changed_3, title=', title, 'time_str=', time_str, )


def on_window_title_changed_4(title: str = 'default-title-3', time_str: str = datetime.now().isoformat()):
    print('on_window_title_changed_4, title=', title, 'time_str=', time_str, )


class MyMainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('MyMainWindow')

        self.windowTitleChanged.connect(on_window_title_changed_1)
        self.windowTitleChanged.connect(lambda: on_window_title_changed_2())
        self.windowTitleChanged.connect(lambda x: on_window_title_changed_3(title = x))
        self.windowTitleChanged.connect(lambda x: on_window_title_changed_4(title = x, time_str = str(time.time_ns())))

        button = QPushButton('更改标题')
        button.clicked.connect(lambda: self.setWindowTitle('new-title-{}'.format(time.time_ns())))

        v_main_layout = QVBoxLayout()
        v_main_layout.addWidget(button)

        container = QWidget()
        container.setLayout(v_main_layout)
        self.setCentralWidget(container)


if __name__ == '__main__':
    app = QApplication()
    ins = MyMainWindow()
    ins.show()
    app.exec()
基于 lambda 函数的默认参数使用基于 lambda 函数的默认参数使用

运行效果

运行效果运行效果

0 人点赞