PyQt / Qt Designer - 提升小部件时不使用插件添加额外参数

4
假设我有一个自定义的PyQt小部件,我想在Qt Designer中使用它,比如用于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 Designer中,我可以轻松地将QWidget推广到这个领域,将头文件设置为“my_module/plot_widget.h”,名称设置为PlotWidget。然后,我可以通过动态加载构建一个小部件,例如:

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()

这很好用,但我还没有找到一种直接修改默认的初始化参数(在这种情况下是工具栏、画布大小和分辨率)的简单方法

我知道有可能创建自定义插件来解决这个问题,但我想避免这样做,因为我认为这会给我简单的需求增加不必要的复杂性。另外,我希望我的代码尽可能与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()

方法二:使用动态属性

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

figsize = self.property('figsize')

如果它们没有被定义,它们将只是空值。问题在于 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(),除非我可以想出其他方法。
结论
我更喜欢第二种方法的想法,因为它允许我直接从Qt Designer修改所有ui参数。然而,我想避免那个显式调用setupUi()。因此,是否有任何QWidget方法总是在动态属性分配给对象后被调用(因此我可以重写它来在那里调用setupUi())?
另外,您是否看到这种方法中的任何其他缺陷,或者您是否知道实现我所尝试实现的任何其他替代方法?请注意,我已经使用matplotlib画布作为示例(对于其可能已经存在一些特定方法),但我想将其用于我可能需要的任何自定义小部件。
2个回答

2

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

从我的理解来看,在设计师中为小部件设置动态属性,然后重载QObject.setProperty()或者更好地使用pyqtProperty在自定义小部件中,然后使用@property.setter来设置布局。 唯一的问题是属性必须按正确的顺序声明。你可以通过不同的方法来规避这个问题。

  1. use a single QStringList dynamic property to set all the parameters as a fake dictionary of (key, value) tuples.

  2. use resizeEvent (or showEvent) as you mentioned, setting a class flag, to avoid multiple calls to setupUi:

    class Widget(QtWidgets.QWidget):
        shown = False
        [...]
        def showEvent(self, event):
            if not self.shown:
                self.shown = True
                self.setupUi()
    
  3. add each widget to the layout according to their position using insertWidget whenever their property is set (this only works for QBoxLayout and if you know the widgets will have a fixed layout structure)

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


pyqtProperty方法在动态属性彼此独立或者我期望所有依赖的动态属性被定义在设计器中(在这种情况下,很容易检查哪个属性是最后一个被设置的,并在那时调用setupUi),或调用setupUi方法不太昂贵时似乎效果很好(在这种情况下,我不需要进行检查)。在许多情况下,这足以对自定义窗口小部件进行设计时调整。但对于PlotWidget,我可能会坚持使用resizeEvent方法。 - FernAndr

0

原始回答:

我发现可能可以使用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 模块没有任何理由访问这些属性,因此只有程序员可以在方便的时候这样做。然而,这也可能会带来一些不便,例如:

  • 您需要在某个时刻访问至少一个属性以使内容可用。例如,直到用户按下按钮之前,绘图可能不会被绘制,此时它将只是一个空的小部件
  • 除非使用一些聪明的 描述符,否则您可能需要为每个懒惰属性生成一个 Python 属性,这可能会变得相当冗长

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接