为什么 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 仍在后台运行,只是在消耗处理能力。

所以,要么,

  1. 如何在 window 关闭或
  2. 时停止事件
  3. 如何阻止 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 方法),并且 neverEVER 需要手动编辑。这样做被认为是不好的做法,并且出于多种原因在概念上是错误的 - 他们的 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()