QSettings Usage

@martin, would you be willing to document the usage of QSettings? Maybe in your book, or here.

I haven’t found any good information while roaming throught the Internet. Well, I have some rough ideas, but I want to know how to use QSettings by having a dialog window (with radio buttons and all that) and have that to serve the purpose of a settings UI and then have QSettings to serve the purpose of a settings manager. I’m having a hard time putting those two things together to work in concert.

I don’t want to introduce another dependency in my application by using pyqtconfig. I’d like to master QSettings and make them work in concert with my custom settings dialog. A little help would be very welcome.

So far, I have implemented this API to handle loading and saving the settings:

from PyQt6.QtCore import QSettings


class SettingsManager:
    """Create a settings manager for the SuperChess application."""

    def __init__(self):

        self.settings = QSettings("SuperChess", "settings")

    def load(self):
        """Manage loading all the settings."""
        self.settings.value("engine white")

    def save(self):
        """Manage saving all the settings."""
        is_engine_white_checked = settings_dialog.engine_white.isChecked()

        self.settings.beginGroup("chess engine");
        self.settings.setValue("engine white", is_engine_white_checked)
        self.settings.endGroup()

And this is an implementation of my custom settings dialog:

from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QGroupBox, QRadioButton, QVBoxLayout


class SettingsDialog(QDialog):
    """Create a settings dialog to edit all the settings of the application."""

    def __init__(self):

        super().__init__()

        self.set_attributes()
        self.create_elements()
        self.set_layout()
        self.connect_signals_to_slots()

    def set_attributes(self):
        """
        Set attributes to the settings dialog.

        Move the settings dialog to the top left corner and give it a title.
        """
        self.move(0, 0)
        self.setWindowTitle("Settings")

    def create_elements(self):
        """Create groups of radio buttons as settings elements."""
        self.engine_settings = QGroupBox("Chess engine")
        self.engine_black = QRadioButton(text="Plays as Black")
        self.engine_black.setChecked(True)
        self.engine_white = QRadioButton(text="Plays as White")
        self.engine_white.setChecked(False)

        _buttons = QDialogButtonBox.StandardButtons
        self.button_box = QDialogButtonBox(_buttons.Ok | _buttons.Cancel)

    def set_layout(self):
        """Set a layout for the settings elements."""
        _engine_settings_vertical_layout = QVBoxLayout()
        _engine_settings_vertical_layout.addWidget(self.engine_white)
        _engine_settings_vertical_layout.addWidget(self.engine_black)
        self.engine_settings.setLayout(_engine_settings_vertical_layout)

        _window_vertical_layout = QVBoxLayout()
        _window_vertical_layout.addWidget(self.engine_settings)
        _window_vertical_layout.addWidget(self.button_box)
        self.setSizePolicy(attributes.minimum_size_policy)
        self.setLayout(_window_vertical_layout)

    def connect_signals_to_slots(self):
        """Execute the proper action when a signal is emitted."""
        self.button_box.accepted.connect(self.accept)
        self.button_box.rejected.connect(self.reject)

I can’t seem to hook this up in my application. By hooking up, I mean to use the value (True or False) of a settings element so that my application responds according to it. How can I do that?

To use the value, you can read it back out of your settings object in your application, using .value(). You’ll need to make sure you’re using the same QSettings instance to write and read to. But also, unfortunately, there is no way to know that a setting has been updated (another thing I added to pyqtconfig!)

A very rough workaround is to connect (using signals) anything that might be affected by a dialog setting change, to update after the dialog has been OKd. For example, add a settings_changed /xxxx_settings_changed (if you have settings groups) signal to your settings manager. If you ensure this signal is emitted after the modified settings have been written then things should work OK.

The downside is that this could potentially update/refresh a lot of things that don’t need it, but as long as the dialog is fairly related stuff it should be OK.

Here’s a more complete working example. This creates a window with a pushbutton to show a settings dialog, and a text display of the current setting state. The dialog uses the settings manager to set the initial state of it’s widgets, and when the dialog is accepted, the value fo those widgets is written back to settings.

The manager has a custom signal to notify the application that the settings have been saved (you could also check explicitly for changes).

The mapping from widgets to the settings is handled as described above (same as in pyqtconfig basically). The settings are stored using QSettings and will persist between runs of the application – edit the settings, shutdown and restart, and they will be as you left them.

from PyQt6.QtWidgets import QWidget, QPushButton, QLineEdit, QLabel, QDialog, QApplication, QVBoxLayout, QGroupBox, QRadioButton, QDialogButtonBox
from PyQt6.QtCore import QSettings, pyqtSignal, QObject

class SettingsManager(QObject):
    """Create a settings manager for the SuperChess application."""

    widget_mappers = {
        'QCheckBox': ('checkState', 'setCheckState'),
        'QLineEdit': ('text', 'setText'),
        'QSpinBox': ('value', 'setValue'),
        'QRadioButton': ('isChecked', 'setChecked'),
    }

    settings_changed = pyqtSignal()

    def __init__(self):
        super().__init__()

        self.settings = QSettings("MyApp", "settings")

    def update_widgets_from_settings(self, map):
        for name, widget in map.items():
            cls = widget.__class__.__name__
            getter, setter = self.widget_mappers.get(cls, (None, None))
            value = self.settings.value(name)
            print("load:", getter, setter, value)
            if setter and value is not None:
                fn = getattr(widget, setter)
                fn(value)  # Set the widget.

    def update_settings_from_widgets(self, map):
        for name, widget in map.items():
            cls = widget.__class__.__name__
            getter, setter = self.widget_mappers.get(cls, (None, None))
            print("save:", getter, setter)
            if getter:
                fn = getattr(widget, getter)
                value = fn()                
                print("-- value:", value)
                if value is not None:
                    self.settings.setValue(name, value) # Set the settings.

        # Notify watcher of changed settings.
        self.settings_changed.emit()


# Define this in another module, import to use.
settings_manager = SettingsManager()


class SettingsDialog(QDialog):
    """Create a settings dialog to edit all the settings of the application."""

    def __init__(self):
        super().__init__()

        """Create groups of radio buttons as settings elements."""
        self.player = QGroupBox("Player")
        self.player_name = QLineEdit()

        self.engine = QGroupBox("Chess engine")
        self.engine_black = QRadioButton(text="Plays as Black")
        self.engine_black.setChecked(True)
        self.engine_white = QRadioButton(text="Plays as White")
        self.engine_white.setChecked(False)

        _buttons = QDialogButtonBox.StandardButtons
        self.button_box = QDialogButtonBox(_buttons.Ok | _buttons.Cancel)
        self.button_box.accepted.connect(self.accept)
        self.button_box.rejected.connect(self.reject)

        slayout = QVBoxLayout()
        slayout.addWidget(self.player)
        slayout.addWidget(self.player_name)
        slayout.addWidget(self.engine)
        slayout.addWidget(self.engine_white)
        slayout.addWidget(self.engine_black)
        slayout.addWidget(self.button_box)
        self.setLayout(slayout)

        self.map = {
            'player': self.player_name, 
            'black': self.engine_black, 
            'white': self.engine_white
        }

        self.load_settings()
        self.accepted.connect(self.save_settings)

    def load_settings(self):
        """ Reload the settings from the settings store """
        settings_manager.update_widgets_from_settings(self.map)


    def save_settings(self):
        """ Triggered when the dialog is accepted; copys settings values to the settings manager """
        settings_manager.update_settings_from_widgets(self.map)


class MainWindow(QWidget):

    def __init__(self):
        super().__init__()

        self.button = QPushButton("Press for settings")
        self.label = QLabel()

        self.button.pressed.connect(self.edit_settings)

        settings_manager.settings_changed.connect(self.update_label)
        self.update_label()

        layout = QVBoxLayout()
        layout.addWidget(self.button)
        layout.addWidget(self.label)
        self.setLayout(layout)

    def edit_settings(self):
        dlg = SettingsDialog()
        dlg.exec()

    def update_label(self):
        data = {
            'player': settings_manager.settings.value('player'),
            'white': settings_manager.settings.value('white'),
            'black': settings_manager.settings.value('black'),
        }

        self.label.setText(str(data))


app = QApplication([])

w = MainWindow()
w.show()

app.exec()

Now I just need to write the article :wink: