6.4. Create a Composite Widget from Existing Ones

Warning

We recommend to use Widget Composition in python. Declarative databinding is available only in python.

The previous two sections (Creating a New Widget, Specialize an Existing Widget) covered single widget creation. It is a common requirement for widget to be composed of several other widgets.

Widget composition can be achieved by creation a new widget that inherits from QWidget, and populating it with several member widgets that have as a parent the declared widget.

This was discussed briefly in the Qt and UI Files section. Here we will expand on it a bit more.

Tip

The CompositeWidget presented here in part of the examples/composite_widget project.

6.4.1. UI Declaration

The UI will consist of one QWidget, that has a QGroupBox and a Form Layout that contains:

  • Two QLabel_: one for “Widget Name”, and another for “Property Value”.

  • One QLineEdit_ with an initial value of “Widget Name”.

  • One QHorizontalSlider_ with an initial value of 0.

../_images/widgets_composite_widgets-1.png

It is important that the name of the top-level QWidget is “CompositeWidget”. Please change it on the Object Inspector tab. See the figure for reference.

6.4.2. Widget Class

Please refer to examples/complex_application/app/src/cut/examples/complex_application/widgets/composite_widget.py.

 1#!/usr/bin/env python3
 2
 3import sys
 4from taurus.external.qt import Qt
 5from cut.examples.complex_application.widgets.ui_composite_widget import Ui_CompositeWidget
 6
 7
 8class CompositeWidget(Qt.QWidget):
 9    """Composite Widget.
10
11    It uses declarative UI defined in ui_composite_widget.ui.
12    Provides two properties, which in turn excersize the the widgets defined in the UI.
13
14    It shows how to expose a cleaner interface using Properties, Signals and Slots.
15    """
16
17    def __init__(self, parent=None):
18        Qt.QWidget.__init__(self, parent)
19        self._numerical_value = 0
20        self._widget_name = "Widget Name"
21        self.ui = Ui_CompositeWidget()
22        self.ui.setupUi(self)
23
24        # Connects the value from the slider to the widget's numerical_value property.
25        self.ui.propertySlider.valueChanged.connect(self.set_numerical_value)
26        self.numerical_value_changed.connect(self.ui.propertySlider.setValue)
27        # Connects the text from the line edit to the widget's widget_name property.
28        self.ui.widgetNameLineEdit.textChanged.connect(self.set_widget_name)
29        self.widget_name_changed.connect(self.ui.widgetNameLineEdit.setText)
30
31    def get_numerical_value(self) -> int:
32        """Gets the numerical_value property
33        :returns: The value of the property
34        :rtype: double
35        """
36        return self._numerical_value
37
38    def set_numerical_value(self, new_value: int):
39        """Sets the numerical value to new_value
40        :param[in]: new_value int with value to set in the numerical_value property
41        """
42        if(self._numerical_value != new_value):
43            print("set_numerical_value: {}".format(new_value))
44            self._numerical_value = new_value
45            self.numerical_value_changed.emit(self._numerical_value)
46
47    ## Signal that indicates that the numerical_value has changed.
48    numerical_value_changed = Qt.Signal(int)
49    ## Property that holds the numerical_value.
50    numerical_value = Qt.pyqtProperty(int, get_numerical_value, set_numerical_value, notify=numerical_value_changed)
51
52    def get_widget_name(self) -> str:
53            """Gets the widget_name property
54            :returns: The widget_name property value
55            :rtype: str
56            """
57            return self._widget_name
58
59    def set_widget_name(self, new_value: str):
60        """Sets the widget_name to new_value
61        :param[in]: new_value str with value to set in the widget_name property
62        """
63        if(self._widget_name != new_value):
64            print("set_widget_name: {}".format(new_value))
65            self._widget_name = new_value
66            self.widget_name_changed.emit(self._widget_name)
67
68    ## Signal that indicates that the widget_name has changed.
69    widget_name_changed = Qt.Signal(str)
70    ## Property that holds the widget_name.
71    widget_name = Qt.pyqtProperty(int, get_widget_name, set_widget_name, notify=widget_name_changed)
72
73
74# This method is never used by the application, but as a developer they are handy during early
75# development. You can call this python module using:
76#     python3 -m cut.examples.complex_application.widgets.composite_widget
77if __name__ == "__main__":
78    # Since the TaurusApplication is not used in the CompositeWidget class, it is imported here.
79    from taurus.qt.qtgui.application import TaurusApplication
80
81    app = TaurusApplication(sys.argv)
82    widget = CompositeWidget()
83    widget.show()
84    sys.exit(app.exec_())

This class includes two properties:

It is good practice to declare properties at the CompositeWidget level, so that there is only one interface with the rest of the application.

The properties are connected bi-directionally. Qt can detect bidirectional connections and will not enter an endless loop (you can go ahead and try commenting the if checks). Regardless, the if check is not there to avoid endless loops, but to avoid emitting the signal when developers sets manually its value twice to the same value. Signal are often meant to signal change in the property. If this is not the behaviour you are looking for, you can remove the if check.