为什么 QWidget 会被销毁? (PyQt)
Why is QWidget being destroyed? (PyQt)
所以我有主要的window。当我单击主 window 中的按钮时,会创建一个新的小部件(在新的 window 中):
self.doorButton.clicked.connect(self.open_door)
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
新QWidget或doorwin只有一个widget - tableWidget
我使用对象 self.doors 来填充 table。现在因为我有一个工作线程 (QThread) 更新所述对象 (self.doors),我使用 QTimer 每 1 秒重新填充一次 table。
class doorwinActions(Ui_doorWin):
def __init__(self,doors):
self.doors = doors
# update setupUi
def setupUi(self, Widget):
super().setupUi(Widget)
Widget.move(self.left, self.top) # set location for window
Widget.setWindowTitle(self.title) # change title
self.timer = QTimer()
self.timer.timeout.connect(lambda:self.popTable())
self.timer.start(1000)
def popTable(self):
mutex.lock()
entries = len(self.doors)
self.tableWidget.setRowCount(entries)
for i in range(entries):
self.tableWidget.setItem(i,0,QtWidgets.QTableWidgetItem(str(self.doors[i][0])))
self.tableWidget.setItem(i,1,QtWidgets.QTableWidgetItem(self.doors[i][1].get_id()))
self.tableWidget.setItem(i,2,QtWidgets.QTableWidgetItem(self.doors[i][1].get_name()))
self.tableWidget.setItem(i,3,QtWidgets.QTableWidgetItem(str(self.doors[i][2])))
mutex.unlock()
当我运行这个程序的时候,运行很顺利。它会打开一个新的 window。如果 self.doors 对象在 window 打开时更新,则 GUI 会反映更改。
但是,如果我重新打开 window,就会出现问题。如果我关闭 window 然后再次单击按钮,程序会崩溃并显示错误:
RuntimeError: wrapped C/C++ QTableWidget 类型的对象已被删除
根据我对代码的了解,当我关闭 window 时,整个小部件 window(和 table)将被删除。当我单击 doorButton 时,会创建新的 Qwidget/table。那么,问题来了,它为什么要删除刚刚创建的东西?
什么(某种)有效? - 如果我将门 window 的设置移动到主 window 的设置,它会起作用.所以 open_door 函数只是:
def open_door(self):
self.doorwin.show()
其余的将在主要 window 设置中。但问题是,即使我关闭 window,QTimer 仍在后台运行,只是在消耗处理能力。
所以,要么,
- 如何在 window 关闭或
时停止事件
- 如何阻止 table 小部件被删除?
我找到了解决方案。我提出的两个解决方案是相同的。如果我在 window 关闭时停止 QTimer,它不再给我错误。
self.exitButton.clicked.connect(self.close_win)
def close_win(self):
self.timer.stop()
self.Widget.close()
你的主要问题是垃圾collection。
当你这样做时:
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
您正在创建一个新 QWidget。它绝对 没有 其他参考,但 self.doorwin
。这意味着如果您再次调用 open_door
,您将 覆盖 self.doorwin
,并且由于之前的小部件未在其他任何地方引用,它将被删除 及其所有内容。
现在,QTimer 很棘手。您在 doorwinActions
中创建了一个 QTimer,即使 QTimer 没有 parent,它们也可以持久存在:它们会一直运行直到它们被停止或删除,并且它们只能被显式删除或当它们的 parent 被删除(使用静态 QTimer.singleShot()
函数创建的计时器除外)。
最后,你必须记住 PyQt(和 PySide 一样)是一个 binding。它与在 Qt 中创建的 objects(我们称它们为“C++ objects”)创建“连接”,通过这些绑定我们可以访问那些 objects,它们的函数等等继续,通过 python references.
但是,最重要的是,两个 object 可以有不同的生命周期:
- 可以删除python引用,Qtobject还可以存在;
- Qt object 可以被破坏,但我们仍然有 python object referenced(过去时)它;
这正是您的情况:self.doorui
object 被覆盖,但它有一个 object(QTimer,self.timer
)仍然存在活着,所以 Python 垃圾收集器将 不会 删除它,计时器仍然可以调用 popTable
。但是,此时,小部件 (self.doorwin
) 及其内容已在“C++ 端”被破坏,这会导致您的崩溃:而 self.tableWidget
仍然作为 python 引用,实际的小部件与其 parent 小部件一起被销毁,并且调用其函数会导致致命错误,因为绑定无法找到实际的 object.
现在,如何解决?
有多种选择,这取决于您需要用它做什么 window。但是,在此之前,还有更重要的事情。
您一直在手动编辑由 pyuic
生成的文件,但这些文件并非用于此目的。它们将被视为“资源”文件,仅用于其目的(setupUi
方法),并且 never、EVER 需要手动编辑。这样做被认为是不好的做法,并且出于多种原因在概念上是错误的 - 他们的 header 明确警告了这一点。要阅读有关这些文件的普遍接受方法的更多信息,请阅读关于 using Designer.
的官方指南
其中一个原因与上面解释的垃圾 collection 问题完全相关。
请注意,也不鼓励单独使用 subclass pyuic 形式 class(如果您想扩展 widget 行为则毫无意义);最常见、公认和建议的做法是创建一个 class 继承自 Qt 小部件 class 和 UI class.以下代码假定您已 重新创建 具有 pyuic
的文件并命名为 ui_doorwin.py
:
# ...
from ui_doorwin import Ui_DoorWin
# ...
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self) # <-- IMPORTANT! Note the "self" argument
self.timer.timeout.connect(self.popTable)
self.timer.start(1000)
def popTable(self):
# ...
使用上面的代码,您可以确定无论何时由于任何原因删除小部件,计时器都会随之被销毁,因此不会调用该函数来尝试访问 object 不不存在了。
如果您需要继续使用 window 的现有实例,解决方案非常简单:创建一个 None
实例(或 class)属性并检查它是否已经存在在创建新的之前存在:
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if not self.doorwin:
self.doorwin = DoorWin()
self.doorwin.show()
以上代码不会阻止 table 更新,这可能是您不想要的,因此您可以根据 window 实际显示的时间来选择启动和停止计时器:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self)
self.timer.timeout.connect(self.popTable)
def showEvent(self, event):
if not event.spontaneous():
self.timer.start()
def hideEvent(self, event):
if not event.spontaneous():
self.timer.stop()
上面的 event.spontaneous()
检查是为了防止在 show/hide 事件是由系统调用引起时停止计时器,例如最小化 window 或更改桌面。由您决定是否让计时器继续运行并处理所有数据,即使 window 未显示。
然后,如果你想在 window 关闭时完全销毁 和 在打开新的 时,请执行以下操作:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
# ... (as above)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
然后确保小部件存在(请注意,如果它被用户关闭,引用仍然存在):
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if self.doorwin:
try:
self.doorwin.close()
except RuntimeError:
pass
self.doorwin = DoorWin()
self.doorwin.show()
所以我有主要的window。当我单击主 window 中的按钮时,会创建一个新的小部件(在新的 window 中):
self.doorButton.clicked.connect(self.open_door)
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
新QWidget或doorwin只有一个widget - tableWidget
我使用对象 self.doors 来填充 table。现在因为我有一个工作线程 (QThread) 更新所述对象 (self.doors),我使用 QTimer 每 1 秒重新填充一次 table。
class doorwinActions(Ui_doorWin):
def __init__(self,doors):
self.doors = doors
# update setupUi
def setupUi(self, Widget):
super().setupUi(Widget)
Widget.move(self.left, self.top) # set location for window
Widget.setWindowTitle(self.title) # change title
self.timer = QTimer()
self.timer.timeout.connect(lambda:self.popTable())
self.timer.start(1000)
def popTable(self):
mutex.lock()
entries = len(self.doors)
self.tableWidget.setRowCount(entries)
for i in range(entries):
self.tableWidget.setItem(i,0,QtWidgets.QTableWidgetItem(str(self.doors[i][0])))
self.tableWidget.setItem(i,1,QtWidgets.QTableWidgetItem(self.doors[i][1].get_id()))
self.tableWidget.setItem(i,2,QtWidgets.QTableWidgetItem(self.doors[i][1].get_name()))
self.tableWidget.setItem(i,3,QtWidgets.QTableWidgetItem(str(self.doors[i][2])))
mutex.unlock()
当我运行这个程序的时候,运行很顺利。它会打开一个新的 window。如果 self.doors 对象在 window 打开时更新,则 GUI 会反映更改。 但是,如果我重新打开 window,就会出现问题。如果我关闭 window 然后再次单击按钮,程序会崩溃并显示错误:
RuntimeError: wrapped C/C++ QTableWidget 类型的对象已被删除
根据我对代码的了解,当我关闭 window 时,整个小部件 window(和 table)将被删除。当我单击 doorButton 时,会创建新的 Qwidget/table。那么,问题来了,它为什么要删除刚刚创建的东西?
什么(某种)有效? - 如果我将门 window 的设置移动到主 window 的设置,它会起作用.所以 open_door 函数只是:
def open_door(self):
self.doorwin.show()
其余的将在主要 window 设置中。但问题是,即使我关闭 window,QTimer 仍在后台运行,只是在消耗处理能力。
所以,要么,
- 如何在 window 关闭或 时停止事件
- 如何阻止 table 小部件被删除?
我找到了解决方案。我提出的两个解决方案是相同的。如果我在 window 关闭时停止 QTimer,它不再给我错误。
self.exitButton.clicked.connect(self.close_win)
def close_win(self):
self.timer.stop()
self.Widget.close()
你的主要问题是垃圾collection。
当你这样做时:
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
您正在创建一个新 QWidget。它绝对 没有 其他参考,但 self.doorwin
。这意味着如果您再次调用 open_door
,您将 覆盖 self.doorwin
,并且由于之前的小部件未在其他任何地方引用,它将被删除 及其所有内容。
现在,QTimer 很棘手。您在 doorwinActions
中创建了一个 QTimer,即使 QTimer 没有 parent,它们也可以持久存在:它们会一直运行直到它们被停止或删除,并且它们只能被显式删除或当它们的 parent 被删除(使用静态 QTimer.singleShot()
函数创建的计时器除外)。
最后,你必须记住 PyQt(和 PySide 一样)是一个 binding。它与在 Qt 中创建的 objects(我们称它们为“C++ objects”)创建“连接”,通过这些绑定我们可以访问那些 objects,它们的函数等等继续,通过 python references.
但是,最重要的是,两个 object 可以有不同的生命周期:
- 可以删除python引用,Qtobject还可以存在;
- Qt object 可以被破坏,但我们仍然有 python object referenced(过去时)它;
这正是您的情况:self.doorui
object 被覆盖,但它有一个 object(QTimer,self.timer
)仍然存在活着,所以 Python 垃圾收集器将 不会 删除它,计时器仍然可以调用 popTable
。但是,此时,小部件 (self.doorwin
) 及其内容已在“C++ 端”被破坏,这会导致您的崩溃:而 self.tableWidget
仍然作为 python 引用,实际的小部件与其 parent 小部件一起被销毁,并且调用其函数会导致致命错误,因为绑定无法找到实际的 object.
现在,如何解决?
有多种选择,这取决于您需要用它做什么 window。但是,在此之前,还有更重要的事情。
您一直在手动编辑由 pyuic
生成的文件,但这些文件并非用于此目的。它们将被视为“资源”文件,仅用于其目的(setupUi
方法),并且 never、EVER 需要手动编辑。这样做被认为是不好的做法,并且出于多种原因在概念上是错误的 - 他们的 header 明确警告了这一点。要阅读有关这些文件的普遍接受方法的更多信息,请阅读关于 using Designer.
其中一个原因与上面解释的垃圾 collection 问题完全相关。
请注意,也不鼓励单独使用 subclass pyuic 形式 class(如果您想扩展 widget 行为则毫无意义);最常见、公认和建议的做法是创建一个 class 继承自 Qt 小部件 class 和 UI class.以下代码假定您已 重新创建 具有 pyuic
的文件并命名为 ui_doorwin.py
:
# ...
from ui_doorwin import Ui_DoorWin
# ...
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self) # <-- IMPORTANT! Note the "self" argument
self.timer.timeout.connect(self.popTable)
self.timer.start(1000)
def popTable(self):
# ...
使用上面的代码,您可以确定无论何时由于任何原因删除小部件,计时器都会随之被销毁,因此不会调用该函数来尝试访问 object 不不存在了。
如果您需要继续使用 window 的现有实例,解决方案非常简单:创建一个 None
实例(或 class)属性并检查它是否已经存在在创建新的之前存在:
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if not self.doorwin:
self.doorwin = DoorWin()
self.doorwin.show()
以上代码不会阻止 table 更新,这可能是您不想要的,因此您可以根据 window 实际显示的时间来选择启动和停止计时器:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self)
self.timer.timeout.connect(self.popTable)
def showEvent(self, event):
if not event.spontaneous():
self.timer.start()
def hideEvent(self, event):
if not event.spontaneous():
self.timer.stop()
上面的 event.spontaneous()
检查是为了防止在 show/hide 事件是由系统调用引起时停止计时器,例如最小化 window 或更改桌面。由您决定是否让计时器继续运行并处理所有数据,即使 window 未显示。
然后,如果你想在 window 关闭时完全销毁 和 在打开新的 时,请执行以下操作:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
# ... (as above)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
然后确保小部件存在(请注意,如果它被用户关闭,引用仍然存在):
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if self.doorwin:
try:
self.doorwin.close()
except RuntimeError:
pass
self.doorwin = DoorWin()
self.doorwin.show()