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.