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

PyQt / Qt Designer - Extra parameters for promoted widget without a plugin

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

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 Designer 中,我可以轻松地为此推广一个 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()

这工作正常,除了 我没有看到修改我拥有的默认初始化参数的直接方法(在这种情况下为工具栏、图形大小和 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 Designer 中添加我需要的任何动态 属性,然后像这样使用它们:

,而不是将额外的参数传递给 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 Designer 修改所有 ui 参数。但是,我想避免对 setupUi() 的显式调用。因此,是否有任何 QWidget 方法总是在动态属性分配给对象后被调用(因此我可以覆盖它以在那里调用 setupUi())?

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

原回答:

我发现我可以为此使用 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)

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

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

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

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

  1. 使用单个 QStringList 动态 属性 将所有参数设置为(键,值)元组的伪字典。

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

    class Widget(QtWidgets.QWidget):
        shown = False
        [...]
        def showEvent(self, event):
            if not self.shown:
                self.shown = True
                self.setupUi()
    
  3. 只要设置 属性 时使用 insertWidget 根据它们的位置将每个小部件添加到布局中(这仅适用于 QBoxLayout 并且如果您知道小部件将具有固定的布局结构)

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