PyQt / Qt Designer - 没有插件的提升小部件的额外参数

Posted

技术标签:

【中文标题】PyQt / Qt Designer - 没有插件的提升小部件的额外参数【英文标题】:PyQt / Qt Designer - Extra parameters for promoted widget without a plugin 【发布时间】:2017-06-14 10:01:21 【问题描述】:

假设我有一个要在 Qt Designer 中使用的自定义 PyQt 小部件,例如用于 matplotlib 画布:

plot_widget.py:

from PyQt5 import QtWidgets
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib.backends.backend_qt5agg import (
    FigureCanvasQTAgg, NavigationToolbar2QT
)

class PlotWidget(QtWidgets.QWidget):

    def __init__(self, parent=None, toolbar=True, figsize=None, dpi=100):
        super(PlotWidget, self).__init__(parent)
        self.setupUi(toolbar, figsize, dpi)

    def setupUi(self, toolbar=True, figsize=None, dpi=100):
        layout = QtWidgets.QVBoxLayout(self)
        self.figure = Figure(figsize=figsize, dpi=dpi)
        self.canvas = FigureCanvasQTAgg(self.figure)
        if toolbar:
            self.toolbar = NavigationToolbar2QT(self.canvas, self)
            layout.addWidget(self.toolbar, 0)
        layout.addWidget(self.canvas, 1)

在 Qt 设计器中,我可以轻松地为此提升一个 QWidget,将头文件设置为“my_module/plot_widget.h”,并将其名称设置为 PlotWidget。然后,我可以通过动态加载从我的 ui 文件构造一个小部件,如下所示:

example.py:

from PyQt5 import QtWidgets, uic

class MyWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent)
        uic.loadUi('example.ui', self)
        self.show()

这很好用,除了 我没有看到修改默认初始化参数的直接方法(在本例中为工具栏、figsize 和 dpi)。

我知道有可能为此创建自定义插件,但我想避免它,因为我认为这会给我的简单需求增加不必要的复杂性。此外,我希望我的代码尽可能与 PySide / PyQt4 / PyQt5 兼容(为此,我实际上是直接使用 QtPy 而不是 PyQt5,虽然这有点离题),我并不完全相信如果我开始使用插件,我可以做到这一点。

因此,我想到了以下两种方法:

方法 1:以编程方式调用 setupUi()

基本上,我可以简化我的构造函数,以便不在那里调用 setupUi(),并在加载 ui 文件后执行该调用:

plot_widget.py:

class PlotWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(PlotWidget, self).__init__(parent)

    ...

example.py:

from PyQt5 import QtWidgets, uic

class MyWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent)
        uic.loadUi('example.ui', self)
        self.myPlotWidget.setupUi(figsize=(4, 4), dpi=200)
        self.show()

方法 2:使用动态属性

我可以在 Qt 设计器中添加我需要的任何动态属性,而不是向 setupUi() 传递额外的参数,然后像这样使用它们:

figsize = self.property('figsize')

如果它们没有被定义,它们将只是 None。问题是在将这些动态属性添加到对象之前调用了 init 构造函数(这似乎是逻辑),因此 figsize 在此代码中始终为 None:

plot_widget.py:

class PlotWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(PlotWidget, self).__init__(parent)

    def setupUi(self):
        layout = QtWidgets.QVBoxLayout(self)
        toolbar = self.property('toolbar')  # Always None
        figsize = self.property('figsize')  # Always None
        dpi = self.property('dpi')          # Always None
        self.figure = Figure(figsize=figsize, dpi=dpi)
        self.canvas = FigureCanvasQTAgg(self.figure)
        if toolbar:
            self.toolbar = NavigationToolbar2QT(self.canvas, self)
            layout.addWidget(self.toolbar, 0)
        layout.addWidget(self.canvas, 1)

这意味着我仍然需要在我的 example.py 文件中显式调用 setupUi(),除非我能找出其他东西。

结论

我更喜欢方法 2 的想法,因为它允许我直接从 Qt 设计器修改所有 ui 参数。但是,我想避免显式调用 setupUi()。因此,是否有任何 QWidget 方法在将动态属性分配给对象后总是被调用(因此我可以重写它以在那里调用 setupUi())?

另外,您是否发现这种方法有任何其他缺陷,或者您知道实现我想要实现的目标的任何其他替代方法吗?请注意,我使用了 matplotlib 画布作为示例(可能已经有一些特定的方法),但我想将它用于我可能需要的任何自定义小部件。

【问题讨论】:

【参考方案1】:

我知道距离您自己的回答已经过去了几个月,也许您已经找到了满足您需求的解决方案,也许我还没有完全理解您的情况。

据我所知,我会在 Designer 中为小部件设置动态属性,然后重载 QObject.setProperty(),或者更好的是,在自定义小部件中使用 pyqtPropertyin,然后使用@property.setter 设置布局。 唯一的问题是属性必须以正确的顺序声明。你可以用不同的方法规避。

    使用单个 QStringList 动态属性将所有参数设置为 (key, value) 元组的假字典。

    使用你提到的resizeEvent(或showEvent),设置一个类标志,以避免多次调用setupUi:

    class Widget(QtWidgets.QWidget):
        shown = False
        [...]
        def showEvent(self, event):
            if not self.shown:
                self.shown = True
                self.setupUi()
    

    只要设置了属性,就使用 insertWidget 根据它们的位置将每个小部件添加到布局中(这仅适用于 QBoxLayout 并且如果您知道小部件将具有固定的布局结构)

或者,您可以为自定义小部件使用另一个 ui,添加所有可能提升的 plotlib 小部件,然后在 @property.setter 装饰器中使用 setVisible();然后您决定是在 init 中隐藏它们,并且仅在设置它们的属性时才显示它们,或者记住始终设置动态属性。

【讨论】:

pyqtProperty 方法似乎在动态属性相互独立时运行良好,或者当我希望在设计器中定义所有相关的动态属性时(在这种情况下很容易检查哪个属性是最后一个被设置的属性并在那时调用setupUi),或者当多次调用setupUi 方法相对便宜时(在这种情况下我不需要做那个检查)。当然足以在许多场合对自定义小部件进行一些设计时调整。不过,对于PlotWidget,我可能会坚持使用resizeEvent 方法。【参考方案2】:

原答案:

我发现我可能可以为此使用resizeEvent,但我不知道它是否适用于任何情况(即ui加载器是否在所有情况下都会始终触发resizeEvent):

class PlotWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(PlotWidget, self).__init__(parent)
        self._alreadySetup = False

    def resizeEvent(self, event):
        # This call can actually be after the super() call and still work
        self.setupUi()
        super(PlotWidget, self).resizeEvent(event)

    def setupUi(self):
        if self._alreadySetup:
            return
        self._alreadySetup = True
        layout = QtWidgets.QVBoxLayout(self)
        ...

因此,除非我发现这种方法存在问题,或者我可以找出更聪明的方法(或者其他人可以),否则我会认为这是我问题的答案。

编辑:请注意,如果小部件永远不可见,则永远不会调用 resizeEvent。这通常不是问题,但在某些情况下(例如在单元测试期间或在显示小部件之前进行大量初始化)可能会发生。

替代方法:

也可以使用延迟初始化,如下所示:

class PlotWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(PlotWidget, self).__init__(parent)
        self._figure = None
        self._canvas = None
        self._toolbar = None
        self._initialized = False

    @property
    def figure(self):
        self.setupUi()
        return self._figure

    @property
    def canvas(self):
        self.setupUi()
        return self._canvas

    @property
    def toolbar(self):
        self.setupUi()
        return self._toolbar

    def setupUi(self):
        if self._initialized:
            return
        self._initialized = True
        layout = QtWidgets.QVBoxLayout(self)
        toolbar = self.property('toolbar')
        figsize = self.property('figsize')
        dpi = self.property('dpi')
        self._figure = Figure(figsize=figsize, dpi=dpi)
        self._canvas = FigureCanvasQTAgg(self._figure)
        if toolbar:
            self._toolbar = NavigationToolbar2QT(self._canvas, self)
            layout.addWidget(self._toolbar, 0)
        layout.addWidget(self._canvas, 1)

加载 .ui 文件时 uic 模块没有任何理由访问这些属性,因此只有程序员可以在方便时这样做。然而,这也可能带来一些不便,例如:

在某些时候,您至少需要访问其中一个属性才能使内容可用。例如,在用户按下按钮之前,绘图可能不会被绘制,在此之前它看起来就像一个空的小部件 除非使用一些巧妙的 descriptor,否则您可能需要为每个正在使用的惰性属性生成一个 Python 属性,这最终可能会非常冗长

【讨论】:

以上是关于PyQt / Qt Designer - 没有插件的提升小部件的额外参数的主要内容,如果未能解决你的问题,请参考以下文章

如何配置 Qt Designer 以在 OSX 中加载 PyQt 小部件?

pyqt5.8.2没有qt Designer和assistant exe

PyCharm+PyQt5+Qt Designer配置

PyQt5 图形界面 - Qt Designer设置简体中文方法演示,Qt Designer字体设置,Qt Designer工具单独安装包获取,Qt Designer简体中文语言包获取

PyQt Qt Designer 窗口最小化按钮列表

如何为 WINDOWS 10 安装 Qt Designer for PyQt5