2
\$\begingroup\$

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,
    QTimeEdit, QCheckBox, QRadioButton, QComboBox
)
from PyQt5.uic import loadUi


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        loadUi('main_window.ui', self)
        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__()
        loadUi('operations_dialog.ui', self)
        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)
            elif isinstance(elements[key], (QCheckBox, QRadioButton)):
                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].addItems(value['names'])
                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()
            elif isinstance(element, (QGroupBox, QCheckBox, QRadioButton)):
                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?

\$\endgroup\$
2
  • \$\begingroup\$ 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. \$\endgroup\$
    – pacmaninbw
    Dec 17 '20 at 13:03
  • \$\begingroup\$ Hi, thanks for letting me know. I changed the title. \$\endgroup\$
    – mapf
    Dec 17 '20 at 13:08
1
+50
\$\begingroup\$

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.

\$\endgroup\$
4
  • \$\begingroup\$ 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. \$\endgroup\$
    – mapf
    Dec 23 '20 at 20:46
  • \$\begingroup\$ 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. \$\endgroup\$ Dec 27 '20 at 1:26
  • \$\begingroup\$ 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. \$\endgroup\$
    – mapf
    Dec 27 '20 at 8:57
  • \$\begingroup\$ Hi, I still haven't figured out what you mean by your second point. Could you please elaborate? \$\endgroup\$
    – mapf
    Jan 11 '21 at 9:21
1
\$\begingroup\$

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())
\$\endgroup\$
1
  • \$\begingroup\$ 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. \$\endgroup\$
    – mapf
    Dec 23 '20 at 17:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.