# Properly setting up interactive elements such as QSpinBoxes or QComboBoxes, e.g. according to project specific data

Suppose you have a main window and open a dialog to do some kind of specific operation on your data. The dialog has interactive elements such as QSpinBoxes and QComboBoxes that determine properties of the operation. For convenience, the default (or most recent) values are stored in the project data, and are updated according to the values held by the interactive elements whenever the dialog is closed with acceptance.

Since I use Qt Designer to create the interfaces, my approach is to make use of the accessibleName attribute (I've yet to figure out what it is actually intended for) and give all the interactive elements a unique accessibleName. I then store the default values in a dict in the main window, from where they are passed on to the dialog when it is opened. During the dialog's initialization, a crawler scans through the dialog's layer to find all the elements with an accessibleName and sets their values according to the settings dict.

Here is some code to show how I handle that situation at the moment:

from PyQt5.QtWidgets import (
QMainWindow, QDialog, QLayout, QGroupBox, QDoubleSpinBox, QSpinBox,
)

class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self._operation_settings = {
'spinbox': 1,
'combobox': {
'names': ['item1', 'item2', 'item3'],
'selected': 0,
},
'groupbox': True,
}

def _open_operation_dialog(self):
dialog = OperationsDialog(self._operation_settings)
dialog.exec_()

class OperationsDialog(QDialog):
def __init__(self, settings):
super().__init__()
self._settings = settings
self._interactive_elements = \
collect_interactive_elements(self.layout())
self._setup_interactive_elements()

def _setup_interactive_elements(self) -> None:
elements = self._interactive_elements
for key, value in self._settings.items():
if isinstance(elements[key], (QDoubleSpinBox, QSpinBox)):
elements[key].setValue(value)
elements[key].setChecked(value)
elif (
isinstance(elements[key], QGroupBox)
and elements[key].isCheckable()
):
elements[key].toggled.connect(self._toggle_group_box)
elements[key].setChecked(value)
elif isinstance(elements[key], QComboBox):
elements[key].setCurrentIndex(value['selected'])
elif isinstance(elements[key], QTimeEdit):
elements[key].setTime(value)

def accept(self) -> None:
self._update_settings()
super().accept()

def _update_settings(self) -> None:
for key, element in self._interactive_elements.items():
if isinstance(element, (QDoubleSpinBox, QSpinBox)):
self._settings[key] = element.value()
self._settings[key] = element.isChecked()
elif isinstance(element, QComboBox):
self._settings[key]['selected'] = element.currentIndex()
elif isinstance(element, QTimeEdit):
self._settings[key] = element.time()

def collect_interactive_elements(layout: QLayout) -> dict:
elements = {}
crawl(layout, elements)
return elements

def crawl(layout: QLayout, elements: dict) -> None:
for i in range(layout.count()):
child = layout.itemAt(i)
if child.widget() is not None:
widget = child.widget()
if widget.accessibleName():
elements[widget.accessibleName()] = widget
if isinstance(widget, QGroupBox):
crawl(widget.layout(), elements)
elif isinstance(widget, QGroupBox):
crawl(widget.layout(), elements)
elif child.layout() is not None:
crawl(child.layout(), elements)


Is this an okay way to do it? How is something like this usually done?

• Welcome to the Code Review Community where we review code that is working as expected and provide suggestions on how to improve that code. Asking a how to question generally indicates the code isn't working as expected. If the code is working as expected please change the title of the question to indicate what the code is doing. Dec 17, 2020 at 13:03
• Hi, thanks for letting me know. I changed the title.
– mapf
Dec 17, 2020 at 13:08

In Qt, all objects can have names, so use QObject::objectName for that. The accessibleName is used for the accessibility interfaces (i.e. screen readers and such). In Qt Designer, all objects have names already, and those names are both objectName as well as the name of the member in the ui object (but you load them dynamically, so the latter doesn't apply).

You also don't need to select on types: it'd be perhaps better to have some fixed mapping between the property names in the dict and the methods used to access those properties of the objects.

• Thank you! So you would suggest using the objectNamess in the dinctionary to store the corresponding information? And then use something like getattr or setattr? Could you elaborate on your second point? I don't really understand what you mean by fixed mapping. I distinguishd between types because the values of different types need to be set up differently.
– mapf
Dec 23, 2020 at 20:46
• There is a finite number of properties across all the widget types you'll be configuring, and Qt is pretty good about keeping the properties that have same name also be of same function and type. QObject properties are a Qt thing, I don't know if they mapped them to Python properties - just use QObject::property and QObject::setProperty. You can also use QMetaProperty - it's faster than lookup by name. Dec 27, 2020 at 1:26
• Thank you! I think I start to see what you are getting at but it would be great if you could use some of my code to give an example, because I don't know how I would implement this.
– mapf
Dec 27, 2020 at 8:57
• Hi, I still haven't figured out what you mean by your second point. Could you please elaborate?
– mapf
Jan 11, 2021 at 9:21

Not being all that familiar with the Python interface to Qt, I've only picked up on some minor points that can be improved:

def crawl(layout: QLayout, elements: dict) -> None:


should have a more specific type hint for the dictionary. I don't know what the value type is (QWidget?), but the key type is almost certainly str, so something like Dict[str, QWidget]. This type hint would also be used as the return type for collect_interactive_elements.

Consider refactoring crawl so that rather than mutating a dictionary, it yields pair tuples; also don't call child.widget() twice:

def collect_interactive_elements(layout: QLayout) -> Dict[str, QWidget]:
return dict(crawl(layout))

def crawl(layout: QLayout) -> Iterable[
Tuple[str, QWidget]
]:
for i in range(layout.count()):
child = layout.itemAt(i)
widget = child.widget()
if widget is not None:
name = widget.accessibleName()
if name:
yield name, widget
if isinstance(widget, QGroupBox):
yield from crawl(widget.layout())
elif isinstance(widget, QGroupBox):
yield from crawl(widget.layout())
elif child.layout() is not None:
yield from crawl(child.layout())

• Hi, thanks a lot for taking the time! I didn't even know you can be this specific with type hints. I like those suggestions.
– mapf
Dec 23, 2020 at 17:51