信号和缺少位置参数
Posted
技术标签:
【中文标题】信号和缺少位置参数【英文标题】:Signals and Missing Positional Arguments 【发布时间】:2016-07-15 15:36:40 【问题描述】:我在 QtDesigner 中开发了两个窗口(SourceForm、DestinationForm)并使用 pyuic5 来转换它们的 .ui 页面。我正在使用第三类WController
作为使用堆叠小部件在两个窗口之间导航的一种方式。我在SourceForm
中有一个按钮,它用一些数据填充treeWidget
,handle_treewidget_itemchange
方法指示当treeWidget
中的特定项目通过使用self.treeWidget.itemChanged.connect(self.handle_treewidget_itemchange)
被选中或取消选中时会发生什么。我的理解是itemChanged.connect
会自动将更改的行和列发送到插槽,但是当handle_treewidget_itemchange(self,row,col)
第一次被调用时,我的脚本会因 TypeError 而崩溃:
TypeError: handle_treewidget_itemchange() missing 2 required positional arguments: 'row' and 'col'
如果我取出 row
和 col
参数,脚本运行良好。当我最初在 SourceForm
.py 文件本身中同时拥有方法和调用时,我的代码按预期工作......也许这只是一个范围问题?我开始认为尝试使用 PyQt 而对 Python 仍然缺乏经验是个坏主意:(
我已尝试将代码精简到基本要素:
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import pyqtSlot
from imp_sourceform import Ui_SourceForm
from imp_destform import Ui_DestinationForm
class WController(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(WController, self).__init__(parent)
self.central_widget = QtWidgets.QStackedWidget()
self.setCentralWidget(self.central_widget)
self.sourcewindow = SourceForm()
self.destinationwindow = DestinationForm()
self.central_widget.addWidget(self.sourcewindow)
self.central_widget.addWidget(self.destinationwindow)
self.central_widget.setCurrentWidget(self.sourcewindow)
self.sourcewindow.selectdestinationsbutton.clicked.connect(lambda: self.navigation_control(1))
self.destinationwindow.backbutton.clicked.connect(lambda: self.navigation_control(0))
def navigation_control(self, topage):
if topage == 1:
self.central_widget.setCurrentWidget(self.destinationwindow)
elif topage == 0:
self.central_widget.setCurrentWidget(self.sourcewindow)
class SourceForm(QtWidgets.QWidget, Ui_SourceForm):
def __init__(self):
super(SourceForm, self).__init__()
self.setupUi(self)
self.treeWidget.itemChanged.connect(self.handle_treewidget_itemchange)
@pyqtSlot()
def handle_treewidget_itemchange(self,row,col):
if row.parent() is None and row.checkState(col) == QtCore.Qt.Unchecked:
for x in range(0,row.childCount()):
row.child(x).setCheckState(0, QtCore.Qt.Unchecked)
elif row.parent() is None and row.checkState(col) == QtCore.Qt.Checked:
for x in range(0,row.childCount()):
row.child(x).setCheckState(0, QtCore.Qt.Checked)
else:
pass
class DestinationForm(QtWidgets.QWidget, Ui_DestinationForm):
def __init__(self):
super(DestinationForm, self).__init__()
self.setupUi(self)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = WController()
window.show()
sys.exit(app.exec_())
【问题讨论】:
有趣的是,无论函数定义如何,PySide2 都可以使用不带参数的 @Slot() 装饰,但 PyQt5 不能,正如我在从 PySide2 迁移到 PyQt5 期间发现的那样。感谢这个答案引导我找到我的问题和解决方案。 【参考方案1】:使用pyqtSlot
时需要小心,因为它太容易破坏它正在装饰的插槽的签名。在您的情况下,它已将插槽重新定义为没有参数,这解释了您收到该错误消息的原因。简单的解决方法是简单地删除它,因为没有它,您的示例将运行良好。
pyqtSlot
的主要目的是允许定义多个不同的插槽重载,每个重载都有不同的签名。有时在进行跨线程连接时也可能需要它。但是,这些用例相对较少,在大多数 PyQt/PySide 应用程序中,根本不需要使用 pyqtSlot
。信号可以连接到任何 python 可调用对象,无论它是否被装饰为插槽。
【讨论】:
pyqtSlot 创建一个真正的 Qt 插槽,当具有该插槽的 QObject 被销毁/垃圾收集(Qt 断开任何与已删除 QObject 的连接)时,连接就会中断。如果我错了,请纠正我,但如果不使用@pyqtSlot
,则连接将无法跟踪 QObject,因此只需保留对可调用对象的引用,从而保留对 QObject 的引用,防止其破坏.恕我直言,最好尽可能使用@pyqtSlot
天哪,太尴尬了,它现在可以工作了!非常感谢!我只是在使用 @pyqtSlot()
装饰器,所以我可以轻松找到我的插槽。我(危险地)认为这种形式是良性的。我最初将handle_treewidget_itemchange
方法及其连接语句放在SourceForm
中(一旦我想调整GUI,我很快就知道这是一个糟糕的主意),并且插槽使用pyqtslot
装饰器正确接收了行和列参数完好无损...这就是为什么我从没想过将其作为潜在的修复方法删除。
我刚刚测试了我所说的:pastebin.com/W5tFkWGL 连接到真正的插槽不会干扰引用计数,这与使用“任何可调用”作为插槽不同。所以,我真的认为使用“any callable”作为connect
的目标是一个坏主意,应尽可能避免,将其替换为pyqtSlot
。
@λuser。正如您从pyqt docs 中看到的那样,该API 专门设计用于接受任何可调用的pythjon,这就是那里显示的所有示例代码使用它的方式。因此,不使用pyqtSlot
绝不是一个“坏主意”——这就是 API 的预期使用方式。
@ekhumoro 是的,您不必使用pyqtSlot
,但它会包含比应有的更多引用,并且 that 是有问题的,请阅读my answer下面【参考方案2】:
这个问题已经有一个公认的答案,但我还是会给出我的答案。
有问题的插槽连接到itemChanged(QTreeWidgetItem *item, int column) 信号,所以pyqtSlot
应该看起来像@pyqtSlot(QTreeWidgetItem, int)
。
现在,正如ekhumoro
指出的那样,PyQt 接受将信号连接到任何 Python 可调用对象,无论是方法、lambda 还是具有__call__
方法的函子。但是这样做比使用@pyqtSlot
更不安全。
例如,当源 QObject(将发出信号)被破坏或目标 QObject(拥有 Qt 插槽)被破坏时,Qt 会自动断开连接。例如,如果您删除了一个小部件,则无需向它发出信号表明其他地方发生了某些事情。如果你使用@pyqtSlot
,在你的类中创建了一个真正的Qt slot,所以这个断开机制可以应用。另外,Qt 不持有对目标 QObject 的强引用,因此可以将其删除。
如果你使用任何可调用的,例如一个非修饰的绑定方法,Qt 将无法识别连接的目标 QObject。更糟糕的是,由于您传递了一个 Python 可调用对象,它将持有对它的强引用,而可调用对象(绑定方法)又将持有对最终 QObject 的引用,因此您的目标 QObject 不会被垃圾收集,直到您手动断开它,或者移除源 QObject。
看到这段代码,你可以启用一个连接或另一个,观察行为上的差异,这表明窗口是否可以被垃圾收集:
from PyQt5.QtWidgets import QApplication, QMainWindow
app = QApplication([])
def create_win():
win = QMainWindow()
win.show()
# case 1
# app.aboutToQuit.connect(win.repaint) # this is a qt slot, so win can be deleted
# case 2
# app.aboutToQuit.connect(win.size) # this is not a qt slot, so win can't be deleted
# win should get garbage-collected here
create_win()
app.exec_()
【讨论】:
您是否认真建议我们应该永远连接到未装饰为插槽的可调用对象?这似乎是一个任意且完全不必要的限制。对于我们无法控制的可调用对象,例如您自己的示例中的win.size
呢?您唯一应该担心 pyqt 程序中的垃圾收集是当它开始导致真正的、可测量的问题时。你的例子没有表明这一点,所以我不明白你想在这里表达什么。无论如何,主窗口应该在应用程序的整个生命周期内都处于活动状态,因此显式删除它几乎没有意义。
@ekhumoro 我不是说“从不”,我说的是“尽可能避免”,在 OP 的情况下这很简单。我的例子牵强,是的,它只是一个最小的测试用例来展示我所说的。不,这不是问题的真实实例,因为它巧妙地发生在更复杂的场景中。这是一个:用一个正常的、持久的 QMainWindow 转置我的示例,错误对象是一个首选项 QDialog。当首选项对话框关闭时,它不会被删除,因为将保留对其方法之一的引用。然后,QDialog 将继续接收信号发射......
@ekhumoro 我试图制作一个更现实的例子,但发现我错了部分:关于连接到未修饰的方法,Python 方法不包含对对象,但 C++ 方法可以:pastebin.com/XS4q2aHu 关闭标签将在使用 C++ 方法时继续打印,例如 size
,但不是使用 Python 方法(例如 faultySlot
)。
喜欢与否,这就是 API 的设计方式。我真的不明白你对我的回答有什么反感(我认为是你否决了它)。肯定有数百万行从不使用插槽装饰器的 pyqt/pyside 代码,而且没有更糟。毕竟,直到最近(PyQt-4.5)才引入pyqtSlot
。真正需要它的场景相对较少。在向初学者提供建议时,我认为最好让事情变得相当简单,只处理他们试图解决的实际问题。
回到 Qt4,这个功能有很多错误,在将信号连接到 lambda 时我经历了很多随机的崩溃。在跟踪问题很多小时后,我通过用 @pyqtSlot 替换那些通过合法方法连接的笨拙 lambda 来解决它。 PyQt5 的情况似乎有improved,但它仍然是一个半生不熟的错误功能,带有未记录的、难以理解的陷阱,所以我建议不要使用它,以避免其他人使用@pyqtSlot
节省几秒钟但是当它适得其反时会浪费时间。以上是关于信号和缺少位置参数的主要内容,如果未能解决你的问题,请参考以下文章
TypeError: fit() 缺少 1 个必需的位置参数:'y',