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
设置布局。
唯一的问题是属性 有 以正确的顺序声明。你可以用不同的方法来规避。
使用单个 QStringList 动态 属性 将所有参数设置为(键,值)元组的伪字典。
使用您提到的 resizeEvent(或 showEvent),设置一个 class 标志,以避免多次调用 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 中并仅在它们的 属性 设置时显示它们,或者记住始终设置动态属性。
假设我有一个要在 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
设置布局。
唯一的问题是属性 有 以正确的顺序声明。你可以用不同的方法来规避。
使用单个 QStringList 动态 属性 将所有参数设置为(键,值)元组的伪字典。
使用您提到的 resizeEvent(或 showEvent),设置一个 class 标志,以避免多次调用 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 中并仅在它们的 属性 设置时显示它们,或者记住始终设置动态属性。