# 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.