显示/隐藏对话框的最佳方法

Posted

技术标签:

【中文标题】显示/隐藏对话框的最佳方法【英文标题】:Best approach to show / hide dialogs 【发布时间】:2018-11-12 11:11:25 【问题描述】:

我遇到了一个理论问题。我正在使用 pyqt5,但这可能是非常笼统且与框架无关的问题。 我有一个QMainwindow 坐在那里等待用户做事。用户可以根据自己的选择使用QMenu 和相关的快捷方式显示/隐藏 对话(QDockwidgets 的子类)(对于每个单独的对话来说,QAction 是一个可检查的QAction)。 我一直在努力有效地显示/隐藏对话。目前,我只是在启动时将它们全部启动,隐藏那些我一开始不想出现的。这使得触发对话变得容易,因为我可以 dialogue.show() /dialogue.hide() 取决于对话当前的可见性。 但我不敢相信这是最佳实践而且非常有效。

我已经尝试过(我目前没有在这台计算机上设置我的 pyqt 环境,所以我不得不剥离我的实际代码而无法测试它是否运行):

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *


class InfoPanel(QDockWidget):
    def __init__(self, title='Tool Box'):
        QDockWidget.__init__(self, title)
        self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
        self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)

        frame = QFrame()
        layout = QGridLayout()
        self.canvas = QGraphicsView()
        self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
        layout.addWidget(self.canvas)
        frame.setLayout(layout)
        self.setWidget(frame)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.showpanelAct = QAction("&Show Panel", self, enabled=True,checkable=True, shortcut="F10")
        self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
        self.viewMenu = QMenu("&View", self)
        self.viewMenu.addAction(self.showpanelAct)
        self.setDockOptions(QMainWindow.AnimatedDocks)

    def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
        if i == 0: #infopanel
            dialogueExists = True
            try: self.infoPanel
            #except NameError: #does not catch the error
            except:
                dialogueExists = False
            if dialogueExists:
                print('destroy')
                self.infoPanel.destroy()
            else:
                print('create')
                self.infoPanel = InfoPanel() #init
                self.infoPanel.show()

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

第一次有效,但在那之后,它似乎只会触发对话的破坏(令人惊讶的是,它不会破坏任何它只是继续进行的东西)。 为什么会这样?是否有一种标准方法来处理对话的显示隐藏?

【问题讨论】:

恕我直言,通常在程序启动时设置任何 GUI 内容(例如对话框),根据用户交互的需要显示/隐藏它,并在程序结束时将其销毁。我无法想象 GUI 对象消耗的内存值得按需创建/销毁它们。 (查看数据是个例外,例如大表的模型数据。当 GUI 的各个部分由于关闭对话框而隐藏时,我通常会丢弃这些数据。)这就是我在 Qt 中的做法。我以前在 gtkmm 中也这样做过,在 OSF/Motif 中也是如此。 (我不记得我从 Win3.1 的 GDI 开始时是如何做到的,可能是错误的。);-) 对于一种标准方式,您可以参考Qt doc中的大量示例。他们可能说明了他们(Qt 开发人员)认为应该如何使用它。 @Scheff 好的,所以在我的情况下,我可以对大多数对话执行此操作,并在程序开始时从启动画面隐藏对话的创建/隐藏。我确实有一些使用 mvc 的对话,为此我可能会在关闭它时销毁 data 以节省内存。我仍然很想知道为什么try: ... except: ... 方法不能按我的预期工作!? 节省内存只是目的的一半。我不知道您如何将数据与模型和视图相结合。我非常喜欢信号驱动的东西。 IE。我的数据中经常有信号通知更改。 UI 类只是连接到这些信号并自动更新它们。对于同一数据的多个(不同)视图,这尤其便于维护。然而,Qt 更新在某些情况下是昂贵的(即非常慢),尤其是关于 MVC 小部件。因此,我尝试将更新限制在绝对必要的数量(通过清除隐藏 UI 的模型)。 关于try: ... except: ...:你能把你的样本扩展到minimal reproducible example吗? 【参考方案1】:

我使用了暴露的 OP 的 MCVE,并试图让它在我的 Windows 10 上的 cygwin64 中运行。

起初我不得不应用一些小修复。 (OP 表示他在发布时无法对其进行测试。)

首先,我在bash的第一行插入了一个“小屋”以便于开始:

#!/usr/bin/python3

其次,self.viewMenu 没有出现。因此,我在之后插入了一行

        self.viewMenu = QMenu("&View", self)
        self.viewMenu.addAction(self.showpanelAct)

viewMenu 添加到主菜单栏:

        self.menuBar().addMenu(self.viewMenu)

修复了它。

第三,点击我得到的菜单项时:

Traceback (most recent call last):
  File "./testQDockPanelShowHide.py", line 27, in <lambda>
    self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
  File "./testQDockPanelShowHide.py", line 45, in showPanel
    self.infoPanel = InfoPanel() #init
  File "./testQDockPanelShowHide.py", line 17, in __init__
    self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
NameError: name 'QtGui' is not defined
Aborted (core dumped)

我必须承认我的 Python 知识非常有限。 (我是用 C++ 为同事编写 Python 绑定的人。所以,我的同事是真正的专家。最多,当我测试新实现的绑定是否符合预期时,我会玩一点 Python。)但是,我修改了

    self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))

到:

    self.canvas.setBackgroundBrush(QBrush(QColor(40, 40, 40)))

解决了这个问题。

在此之后,我得到了 OP 描述的行为,并仔细查看了我(和 OP)怀疑错误的地方:

    def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
        if i == 0: #infopanel
            dialogueExists = True
            try: self.infoPanel
            #except NameError: #does not catch the error
            except:
                dialogueExists = False
            if dialogueExists:
                print('destroy')
                self.infoPanel.destroy()
            else:
                print('create')
                self.infoPanel = InfoPanel() #init
                self.infoPanel.show()

我坚信 try: self.infoPanel 不会像 OP 认为的那样做。

它尝试访问self.infoPanel,直到第一次调用此方法时才存在。 (请注意,成员变量self.infoPanel 不存在。)因此,except: 分支被执行并设置了dialogueExists = False,几行之后导致self.infoPanel = InfoPanel() #init。现在,成员变量self.infoPanel 已经存在,并且try: self.infoPanel 将永远不会再次失败,直到销毁此MainWindow

出于好奇,我看了一下QWidget.destroy()(确保不会说错):

QWidget.destroy (self, bool destroyWindow = True, bool destroySubWindows = True)

释放窗口系统资源。如果 destroyWindow 为真,则销毁小部件窗口。

destroy() 为所有子小部件递归调用自身,将 destroySubWindows 传递给 destroyWindow 参数。要更好地控制子小部件的销毁,请先选择性地销毁子小部件。

这个函数通常从 QWidget 析构函数中调用。

绝对不会破坏成员变量self.infoPanel

在了解了这一点之后,修复就变得简单明了:

    def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
        if i == 0: #infopanel
            try: self.infoPanel
            #except NameError: #does not catch the error
            except:
                print('create')
                self.infoPanel = InfoPanel() #init
            if self.infoPanel.isVisible():
                self.infoPanel.hide()
            else:
                self.infoPanel.show()

顺便说一句。我用hide() 替换了destroy(),这使得InfoPanel() 的重新创建已过时。

我通过多次切换菜单项对此进行了测试——它现在按预期工作(至少看起来像这样)。

最后的完整样例:

#!/usr/bin/python3

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *


class InfoPanel(QDockWidget):
    def __init__(self, title='Tool Box'):
        QDockWidget.__init__(self, title)
        self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
        self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)

        frame = QFrame()
        layout = QGridLayout()
        self.canvas = QGraphicsView()
#        self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
        self.canvas.setBackgroundBrush(QBrush(QColor(40, 40, 40)))
        layout.addWidget(self.canvas)
        frame.setLayout(layout)
        self.setWidget(frame)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.showpanelAct = QAction("&Show Panel", self, enabled=True,checkable=True, shortcut="F10")
        self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
        self.viewMenu = QMenu("&View", self)
        self.viewMenu.addAction(self.showpanelAct)
        self.menuBar().addMenu(self.viewMenu)
        self.setDockOptions(QMainWindow.AnimatedDocks)

    def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
        if i == 0: #infopanel
            try: self.infoPanel
            #except NameError: #does not catch the error
            except:
                print('create')
                self.infoPanel = InfoPanel() #init
            if self.infoPanel.isVisible():
                self.infoPanel.hide()
            else:
                self.infoPanel.show()

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

【讨论】:

非常感谢。我回家后会检查这个!【参考方案2】:

在停止编码后,我的问题的解决方案非常明显。回到我的原始代码(它没有产生创建/销毁对话self.infoPanel按需的预期输出):

dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
  dialogueExists = False
if dialogueExists:
  print('destroy')
  self.infoPanel.destroy()
else:
  print('create')
  self.infoPanel = InfoPanel() #init
  self.infoPanel.show()

我的主要问题是我混淆了两个不同的东西。当我调用self.infoPanel.destroy() 时,Qt 破坏了对象self.infoPanel 中包含的小部件。但这并不意味着对象 self.infoPanel 不存在(这正是我使用try: ... 来查看对象是否存在的原因)。按需创建和销毁对话的简单而明显的方法显然涉及从环境中删除对象 (del self.infoPanel)。 工作代码是:

dialogueExists = True
try:
  self.infoPanel.destroy() #not sure this is needed, but I guess it doesn't hurt
  del self.infoPanel #this is the deletion of the actual object
except:
  dialogueExists = False
if not dialogueExists : 
  self.infoPanel = InfoPanel()

非常感谢您在决定是显示/隐藏对话还是创建/销毁对话方面提供的有用建议!

【讨论】:

以上是关于显示/隐藏对话框的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

在 iPhone 中创建“设置”对话框页面的最佳方法 [重复]

如何在显示/隐藏期间在 QTDialog 中添加效果?

错误的窗口令牌,您无法在创建 Activity 之前或在广播接收器中隐藏异常之后显示对话框

隐藏后如何重新显示QDialog?

显示最初隐藏的无模型对话框

Eric IDE,Qt 文件对话框:显示隐藏文件