关于python:PyQt / Qt Designer-无需插件的升级小部件的额外参数

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

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

plot_widget.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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文件,从ui文件中构建一个小部件:

example.py:

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
class PlotWidget(QtWidgets.QWidget):

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

    ...

example.py:

1
2
3
4
5
6
7
8
9
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:使用动态属性

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

1
figsize = self.property('figsize')

如果未定义,则它们将为"无"。问题在于,在将那些动态属性添加到对象之前,会调用init构造函数(这似乎是逻辑上的),因此在此代码中,figsize始终为None:

plot_widget.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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画布为例(可能已经有一些特定的方法),但是我想将其用于我可能需要的任何自定义小部件。


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

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

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

  • 如前所述,使用resizeEvent(或showEvent)来设置类标志,以避免多次调用setupUi:

    1
    2
    3
    4
    5
    6
    7
    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上,并仅在设置了它们的属性时才显示它们,或者记住始终设置动态属性。


    原始答案:

    我发现我可以为此使用resizeEvent,但是我不知道它在任何情况下是否都可以工作(即ui加载程序是否在所有情况下都将始终触发resizeEvent):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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。通常这不是问题,但是在某些情况下(例如在单元测试期间或在显示小部件之前进行大量初始化)可能会发生。

    替代方法:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    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文件时没有任何理由访问这些属性,因此只有程序员才能方便地这样做。但是,这也会带来一些不便,例如:

    • 您将需要在某些时候访问至少一个属性以使内容可用。例如,在用户按下按钮之前,可能不会绘制该图,直到它看起来像一个空的小部件为止
    • 除非使用一些巧妙的描述符,否则您可能需要为每个使用的惰性属性生成一个Python属性,这最终可能会很冗长