Reputation: 1608
Let's say I have a custom PyQt widget that I want to use in Qt Designer, such as for a 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)
In the Qt Designer, I can easily promote a QWidget for this, with the header file set to "my_module/plot_widget.h" and its name set to PlotWidget. Then, I can construct a widget from my ui file by loading it dynamically such as this:
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()
This works fine, except for the fact that I don't see a straightforward way of modifying the default init parameters that I have (toolbar, figsize and dpi in this case).
I know that there is the possibility of creating custom plugins for this, but I would like to avoid it, as I think it would be adding an unnecessary complexity to my simple needs. In addition, I want my code to be as compatible with PySide / PyQt4 / PyQt5 as possible (to that end, I am actually using QtPy instead of PyQt5 directly, although this is a bit off-topic), and I am not fully convinced I could achieve that if I start using plugins.
Hence, I thought of the following two approaches:
Basically, I can simplify my constructor so that setupUi() is not called there, and do that call after loading the ui file:
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()
Instead of passing extra arguments to setupUi(), I can add any dynamic property that I need in the Qt Designer, and then use them such as this:
figsize = self.property('figsize')
If they are not defined, they will just be None. The problem is that the init constructor is called before those dynamic properties are added to the object (which seems logic), and hence figsize would always be None in this code:
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)
Which means that I would still need an explicit call to setupUi() in my example.py file, unless I can figure out something else.
I like much more the idea of Approach 2, as it allows me to modify all the ui parameters directly from the Qt Designer. However, I would like to avoid that explicit call to setupUi(). Hence, is there any QWidget method that always gets called after the dynamic properties have been assigned to the object (and hence I could override this to make the call to setupUi() there)?
Also, do you see any other flaw in this approach or do you know of any other alternative method of achieving what I am trying to achieve? Note that I have used a matplotlib canvas as an example (for which there might be some specific approaches already), but I would like to use this for any custom widget that I might need.
Upvotes: 5
Views: 2932
Reputation: 48399
I know some months have passed since your own answer, maybe you've already found a solution that satisfies your needs and maybe I've not completely understood your situation.
From what I can understand, I'd set dynamic properties for the widget in Designer, and then overload QObject.setProperty()
or, better, use pyqtProperty
in the custom widget, then use the @property.setter
to setup the layout.
The only issue would be that the properties have to be declared in the right order. You could circumvent with different approaches.
use a single QStringList dynamic property to set all the parameters as a fake dictionary of (key, value) tuples.
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()
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)
Alternatively, you could just use another ui for the custom widget, add all possible promoted plotlib widgets and then use setVisible() in the @property.setter
decorator; then you decide wether to hide them on the init and show them only when their property is set, or remember to always set the dynamic properties.
Upvotes: 2
Reputation: 1608
I found out that I may be able to use resizeEvent for this, but I don't know if it will work in any situation (i.e. whether the ui loader will always trigger the resizeEvent under all circumstances):
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)
...
Hence, unless I detect issues with this approach or I can figure out something more clever (or somebody else can), I will consider this the answer to my question.
Edit: Just be aware that, if the widget is never visible, the resizeEvent will never be called. This is normally not a problem, but on certain occasions (such as during unit tests or doing heavy initialization before showing a widget) it may happen.
Lazy initialization can also be used for this as follows:
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)
The uic module does not have any reason to access these properties while loading the .ui file, and hence only the programmer can do so at his convenience. Nevertheless, this can also have a few inconveniences, such as:
Upvotes: 0