Custom Widgets
There are several approaches to developing and customizing a GUI with Taurus. The easiest approach (which may not even require programming in python) is to create a Taurus GUI using the TaurusGUI framework , populating it with panels created from existing Taurus or 3rd party widgets and attaching models to provide functionality. This is the most common (and recommended) approach, considering that many Taurus widgets can be further configured from the GUI itself and these configurations can be automatically restored, allowing for a level of customization that is sufficient for many applications.
Sometimes however, one may need to customize the widgets at a lower level (e.g., for accessing properties that are not accessible via the GUI, or for grouping several existing widgets together, etc). This can be done by using the Taurus widgets just as one would use any other Qt widget (either using the Qt Designer or in a purely programmatic way).
Finally, in some cases neither Taurus nor other third party modules provide the
required functionality, and a new widget needs to be created. If such widget
requires to interact with a control system or other data sources supported via
Taurus model objects, the recommended approach is
to create a widget that inherits both from a QWidget
(or a
QWidget-derived class) and from the
taurus.qt.qtgui.base.TaurusBaseComponent
mixin class (or one of its
derived mixin classes from taurus.qt.qtgui.base
). These Taurus mixin
classes provide several APIs that are expected from Taurus widgets, such as:
model support API
configuration API
logger API
formatter API
For this reason, this is sometimes informally called “Taurus-ifying a pure Qt
class”. The following is a simple example of creating a Taurus “power-meter”
widget that displays the value of its attached attribute model as a bar (like
e.g. in an equalizer). For this we are going to compose a QProgressBar
with a taurus.qt.qtgui.base.TaurusBaseComponent
mixin class:
1from taurus.external.qt import Qt
2from taurus.qt.qtgui.base import TaurusBaseComponent
3from taurus.qt.qtgui.application import TaurusApplication
4
5
6class PowerMeter(Qt.QProgressBar, TaurusBaseComponent):
7 """A Taurus-ified QProgressBar"""
8
9 # setFormat() defined by both TaurusBaseComponent and QProgressBar. Rename.
10 setFormat = TaurusBaseComponent.setFormat
11 setBarFormat = Qt.QProgressBar.setFormat
12
13 def __init__(self, parent=None, value_range=(0, 100)):
14 super(PowerMeter, self).__init__(parent=parent)
15 self.setOrientation(Qt.Qt.Vertical)
16 self.setRange(*value_range)
17 self.setTextVisible(False)
18
19 def handleEvent(self, evt_src, evt_type, evt_value):
20 """reimplemented from TaurusBaseComponent"""
21 try:
22 self.setValue(int(evt_value.rvalue.m))
23 except Exception as e:
24 self.info("Skipping event. Reason: %s", e)
25
26
27if __name__ == "__main__":
28 import sys
29
30 app = TaurusApplication()
31 w = PowerMeter()
32 w.setModel("eval:Q(60+20*rand())")
33 w.show()
34 sys.exit(app.exec_())
As you can see, the mixin class provides all the taurus fucntionality
regarding setting and subscribing to models, and all one needs to do is to
implement the handleEvent
method that will be called whenever the attached
taurus model is updated.
Note
if you create a generic enough widget which could be useful for other people, consider contributing it to Taurus, either to be included directly in the official taurus module or to be distributed as a Taurus plugin.
Tip
we recommend to try to use the highest level approach compatible
with your requirements, and limit the customization to the smallest
possible portion of code. For example: consider that you need a GUI that
includes a “virtual gamepad” widget to control a robot arm. Since such
“gamepad” is not provided by Taurus, we recommend that you implement only
the “gamepad” widget (maybe using the Designer to put together several
QPushButtons
within a TaurusWidget
) in a custom module
and then use that widget within a panel in a TaurusGUI (as opposed to
implementing the whole GUI with the Designer). In this way you improve the
re-usability of your widget and you profit from the built-in mechanisms
of the Taurus GUIs such as handling of perspectives, saving-restoring of
settings, etc
Multi-model support: model-composer
Before Taurus TEP20 (implemented in Taurus 5.1)
taurus.qt.qtgui.base.TaurusBaseComponent
and its derived classes only
provided support for a single model to be associated with the QWidget /
QObject. Because of this, many taurus widgets that required to be attached to
more than one model had to implement the multi-model support in their own
specific (and sometimes inconsistent) ways.
With the introduction of TEP20, the taurus base classes support multiple models. As an example, consider the following modification of the above “PowerMeter” class adding support for a second model consisting on an attribute that provides a color name that controls the background color of the bar:
1from taurus.external.qt import Qt
2from taurus.qt.qtgui.base import TaurusBaseComponent
3from taurus.qt.qtgui.application import TaurusApplication
4
5
6class PowerMeter2(Qt.QProgressBar, TaurusBaseComponent):
7 """A Taurus-ified QProgressBar with separate models for value and color"""
8
9 # setFormat() defined by both TaurusBaseComponent and QProgressBar. Rename.
10 setFormat = TaurusBaseComponent.setFormat
11 setBarFormat = Qt.QProgressBar.setFormat
12
13 modelKeys = ["power", "color"] # support 2 models (default key is "power")
14 _template = "QProgressBar::chunk {background: %s}" # stylesheet template
15
16 def __init__(self, parent=None, value_range=(0, 100)):
17 super(PowerMeter2, self).__init__(parent=parent)
18 self.setOrientation(Qt.Qt.Vertical)
19 self.setRange(*value_range)
20 self.setTextVisible(False)
21
22 def handleEvent(self, evt_src, evt_type, evt_value):
23 """reimplemented from TaurusBaseComponent"""
24 try:
25 if evt_src is self.getModelObj(key="power"):
26 self.setValue(int(evt_value.rvalue.m))
27 elif evt_src is self.getModelObj(key="color"):
28 self.setStyleSheet(self._template % evt_value.rvalue)
29 except Exception as e:
30 self.info("Skipping event. Reason: %s", e)
31
32
33if __name__ == "__main__":
34 import sys
35
36 app = TaurusApplication()
37 w = PowerMeter2()
38 w.setModel("eval:Q(60+20*rand())") # implicit use of key="power"
39 w.setModel("eval:['green','red','blue'][randint(3)]", key="color")
40 w.show()
41 sys.exit(app.exec_())
The relevant differences of the PowerMeter2 class with respect to the previous
single-model version have been highlighted in the above code snippet:
essentially one just needs to define the supported model keys in the
.modelKeys
class method and then handle the different possible sources of
the events received in handleEvent
. Note that the first key in
modelKeys
is to be used as the default when not explicitly passed to the
model API methods.
The multi-model API also facilitates the implementation of widgets that operate
on lists of models, by using the special constant MLIST
defined in
taurus.qt.qtgui.base
and also accessible as
TaurusBaseComponent.MLIST
. For example the following code implements a very
simple widget that logs events received from an arbitrary list of attributes:
1from taurus.external.qt import Qt
2from taurus.core import TaurusEventType
3from taurus.qt.qtgui.base import TaurusBaseComponent
4from taurus.qt.qtgui.application import TaurusApplication
5from datetime import datetime
6
7
8class EventLogger(Qt.QTextEdit, TaurusBaseComponent):
9 """A taurus-ified QTextEdit widget that logs events received
10 from an arbitrary list of taurus attributes
11 """
12
13 modelKeys = [TaurusBaseComponent.MLIST]
14
15 def __init__(self, parent=None):
16 super(EventLogger, self).__init__(parent=parent)
17 self.setMinimumWidth(800)
18
19 def handleEvent(self, evt_src, evt_type, evt_value):
20 """reimplemented from TaurusBaseComponent"""
21 line = "{}\t[{}]\t{}".format(
22 datetime.now(),
23 TaurusEventType.whatis(evt_type),
24 evt_src.getFullName(),
25 )
26 self.append(line)
27
28
29if __name__ == "__main__":
30 import sys
31
32 app = TaurusApplication()
33 w = EventLogger()
34 w.setModel(["eval:123", "tango:sys/tg_test/1/short_scalar", "eval:rand()"])
35 w.show()
36 sys.exit(app.exec_())
The multi-model API treats the MLIST
in a special way: when calling
setModel
with key=MLIST
, the model
argument is expected to be a
sequence of model names; new model keys are automatically added to the
widget’s modelList
attribute and the corresponding models are attached
using those keys. The new keys are of the form (MLIST, i)
where i
is
the index of the corresponding model name in the model sequence. The new models
can be accessed individually with the standard multi-model API using the
generated model keys.
Another typical pattern that can be implemented with the MLIST
support is
the model delegates container, where the widget does not handle the events by
itself but instead it dynamically creates other taurus subwidgets (e.g. when
the model is set) and then delegates the handling of events to those subwidgets
(similar to what taurus.qt.qtgui.panel.TaurusForm
does). The following
example shows a simplistic implementation of a form widget that shows the model
name and its value for each model attached to it:
1from taurus.external.qt import Qt
2from taurus.qt.qtgui.base import TaurusBaseComponent, MLIST
3from taurus.qt.qtgui.application import TaurusApplication
4from taurus.qt.qtgui.display import TaurusLabel
5
6
7class SimpleForm(Qt.QWidget, TaurusBaseComponent):
8 """A simple taurus form using the model list support from
9 TaurusBaseComponent.
10 """
11
12 modelKeys = [MLIST]
13
14 def __init__(self, parent=None):
15 super(SimpleForm, self).__init__(parent=parent)
16 self.setLayout(Qt.QFormLayout(self))
17
18 def setModel(self, model, *, key=MLIST):
19 """reimplemented from TaurusBaseComponent"""
20 TaurusBaseComponent.setModel(self, model, key=key)
21 _ly = self.layout()
22
23 if key is MLIST: # (re)create all rows
24 # remove existing rows
25 while _ly.rowCount():
26 _ly.removeRow(0)
27 # create new rows
28 for i, name in enumerate(model):
29 simple_name = self.getModelObj(key=(MLIST, i)).getSimpleName()
30 value_label = TaurusLabel()
31 value_label.setModel(name)
32 _ly.addRow(simple_name, value_label)
33 else: # update a single existing row
34 _, row = key # key must be of the form (MLIST, <i>)
35 name_label = _ly.itemAt(row, _ly.ItemRole.LabelRole).widget()
36 value_label = _ly.itemAt(row, _ly.ItemRole.FieldRole).widget()
37 name_label.setText(self.getModelObj(key=key).getSimpleName())
38 value_label.setModel(self.getModelName(key=key))
39
40
41if __name__ == "__main__":
42 import sys
43
44 app = TaurusApplication()
45 w = SimpleForm()
46 w.setModel(
47 [
48 "eval:foo=123;foo",
49 "eval:randint(99)",
50 "sys/tg_test/1/short_scalar",
51 "eval:randint(99)",
52 ]
53 )
54 w.show()
55 sys.exit(app.exec_())
Note that, contrary to previous examples, this form does not re-implement
the handleEvent
method (i.e. it ignores the events from its models) but
instead it calls setModel
on its subwidgets, letting them handle their
respective models’ events.