使用 Signal 在 parent 和 child 之间进行通信的最优雅方式 - PyQt5 & PySide2

The most elegant way to communicate between parent and child using Signal - PyQt5 & PySide2

我想用一个例子来解释我的问题。

parent 和 children 之间的交流方式不止一种,如果我没记错的话。

如果 parent 和 child 直接通信...例如像这样 (1):

class Main(QWidget):
  def __init__(self):
    super().__init__()
    self.button = QPushButton("click me")

我这里没有任何问题。但是当有 child 的 child 并且当我想与 child 的 child 交流时,这对我来说变得更加复杂。例如 (2):

class Button(QPushButton):
  def __init__(self, parent=None):
    super().__init__(parent)
    self.setText("click me")

class AnotherFrame(QFrame):
  def __init__(self, parent=None):
    super().__init__(parent)
    self.button = Button(self)

class Frame(QFrame):
  def __init__(self, parent=None):
    super().__init__(parent)
    self.another_frame = AnotherFrame(self)

class Main(QWidget):
  def __init__(self):
    super().__init__()
    self.frame = Frame(self)

对于第一个例子(1)我可以直接在main中连接信号class。

class Main(QWidget):
  def __init__(self):
    super().__init__()
    self.button = QPushButton("click me")
    self.button.clicked.connect(self.print_clicked)
  
  @staticmethod
  def print_clicked():
    print("clicked")

但是例如(2),我该怎么办?这是解决方案吗? :

class Main(QWidget):
  def __init__(self):
    super().__init__()
    self.frame = Frame()
    self.frame.another_frame.button.clicked(self.print_clicked)

但我听说我们不应该在 OOP 中直接访问 objects。我需要在这里使用 getter 和 setter 吗?像这样:

  self.frame.get_another_frame().get_button().clicked(self.print_clicked)

否则我也可以这样做:

class Button(QPushButton):
  def __init__(self, parent=None):
    super().__init__(parent)
    self.setText("click me")
    self.connect(self.parent().parent().print_clicked)

那么对于 (2) 主要 class 和按钮 class 之间的最佳通信方式是什么?我想在主要 class.

中收听点击信号和 运行 print_clicked 功能

(我是新来的,我还在学习英语,如果我不善于解释我的问题,请告诉我)

虽然封装和信息隐藏通常被认为是同一方面的一部分,但对成员的间接访问本身并不是“禁止”(或不鼓励)的。请记住,设计模式和实践不是绝对规则,它们的存在是为了为良好的编程提供指导。

虽然正式地, 只允许通过函数访问子成员更好,但这不会造成不必要的复杂化。

Python 提供 composition(但我认为在这种情况下正确的术语应该是“聚合”),而“良好做法”建议您应避免访问超过一定级别,创建函数和 subclasses 只是为了获得对单个组件的 once 访问权限,因为“这就是应该如何完成”并不是很有用。[ 1]

很明显,这不是很优雅:

self.frame.another_frame.button.clicked(self.print_clicked)

但是,对于非常简单的情况,这真的没有什么可耻的,只要您完全确定对象层次结构将始终相同。

一种可能的替代方法是递归地使子属性成为当前实例的成员:

class Frame(QFrame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.another_frame = AnotherFrame(self)
        self.button = self.another_frame.button


class Main(QWidget):
    def __init__(self):
        super().__init__()
        self.frame = Frame(self)
        self.button = self.frame.button
        self.button.clicked(self.print_clicked)

更好,尤其是从可读性的角度来看;但仍然不是特别好。我们仍然必须确保 FrameAnotherFrame 都有按钮的引用。我们还应该避免对很多属性这样做,因为这会使主实例因实际只使用一次的成员而变得拥挤不堪。

Qt 幸运地提供了信号和信号 chaining,这个系统提供了一种更好、更模块化的方式来 connect objects 而不会太深入在结构中同时保留模块化和一定程度的封装:只要签名兼容[2],你可以直接将一个信号连接到另一个信号,连接的信号将在原来的是。

在下面的示例中,我正在创建与 QAbstractButton.clicked 具有相同签名的信号(这是一个 bool 表示选中状态),但是如果您对此不感兴趣,您可以显然在“上层”省略了这一点。

class AnotherFrame(QFrame):
    buttonClicked = pyqtSignal(bool) # or Signal(bool) for PySide
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button = Button(self)
        self.button.clicked.connect(self.buttonClicked)


class Frame(QFrame):
    buttonClicked = pyqtSignal(bool)
    def __init__(self, parent=None):
        super().__init__(parent)
        self.another_frame = AnotherFrame(self)
        self.another_frame.buttonClicked.connect(self.buttonClicked)


class Main(QWidget):
    def __init__(self):
        super().__init__()
        self.frame = Frame(self)
        self.frame.buttonClicked.connect(self.print_clicked)

这种方法要好得多,原因有很多:我们不需要以任何方式访问按钮,因此我们甚至可以从 class 中删除按钮而不会出现任何 AttributeError 异常, 只要信号存在.
这在创建原型 classes 时很有用,因此您可以有一个仅提供信号的基础 Frame class,然后使用子 classes 改变它们的行为或方面,同时保持相同的界面:

class BaseFrame(QFrame):
    buttonClicked = pyqtSignal(bool)


class FrameA(BaseFrame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.another_frame = AnotherFrame(self)
        self.another_frame.buttonClicked.connect(self.buttonClicked)


class FrameB(BaseFrame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.something_else = SomethingElse(self)


class Main(QWidget):
    def __init__(self):
        super().__init__()
        self.frame = FrameB(self)
        self.frame.buttonClicked.connect(self.print_clicked)

在这种情况下,即使 FrameB 没有包含按钮的内部框架,我们仍然可以将信号连接到函数:它显然不会做任何事情,但封装会正确无需尝试访问对象树并因缺少属性而冒异常风险。

唯一(部分)缺点是 self.sender()[3] 的结果将不是发出原始信号的对象,而是“最后”一个实际上连接到插槽的。在这个答案之上的情况下,它将 return 按钮实例(因为我们连接到对原始发件人的引用),而在上面的情况下它将 return 框架(“所有者” “我们连接到的信号)。
如果您需要知道信号的来源(在任何级别),您可以创建一个也发送实例的信号。
例如,假设您对 checked 参数不感兴趣,但对发出信号的帧感兴趣:

class AnotherFrame(QFrame):
    buttonClicked = pyqtSignal(bool)
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button = Button(self)
        self.button.clicked.connect(self.buttonClicked)


class Frame(QFrame):
    buttonClicked = pyqtSignal(object)
    def __init__(self, parent=None):
        super().__init__(parent)
        self.another_frame = AnotherFrame(self)
        self.another_frame.buttonClicked.connect(self.emitButtonClicked)

    def emitButtonClicked(self):
        self.buttonClicked.emit(self)


class Main(QWidget):
    def __init__(self):
        super().__init__()
        self.frame = Frame(self)
        self.frame.buttonClicked.connect(self.print_clicked)

    def print_clicked(self, frame):
        print(frame)

[1] 记住import this的一些要点:“特殊情况还不足以打破规则。-尽管实用性胜过纯粹性。”
[2] 为了连接两个信号,目标信号必须具有相同类型的参数,并且参数数量等于或小于源:Signal(int, bool) 不能连接到 Signal(int, str), 但可以连接到 Signal(int)Signal().
[3] 正如文档中所报告的那样,sender() ”违反了面向对象的模块化原则。但是,当连接了许多信号时,访问发送方可能会很有用到单个插槽。"