使用 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)
更好,尤其是从可读性的角度来看;但仍然不是特别好。我们仍然必须确保 Frame
和 AnotherFrame
都有按钮的引用。我们还应该避免对很多属性这样做,因为这会使主实例因实际只使用一次的成员而变得拥挤不堪。
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()
”违反了面向对象的模块化原则。但是,当连接了许多信号时,访问发送方可能会很有用到单个插槽。"
我想用一个例子来解释我的问题。
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)
更好,尤其是从可读性的角度来看;但仍然不是特别好。我们仍然必须确保 Frame
和 AnotherFrame
都有按钮的引用。我们还应该避免对很多属性这样做,因为这会使主实例因实际只使用一次的成员而变得拥挤不堪。
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()
”违反了面向对象的模块化原则。但是,当连接了许多信号时,访问发送方可能会很有用到单个插槽。"