PedanticHacker | 2021-03-19 07:02:21 UTC | #1
Me again. :slight_smile:
I was thinking of implementing a Settings window into my app. You know, a window with a bunch of those on/off switches.
How would you go about it? Would you subclass from QDialog() or from QWidget()? Also, are there any examples already for PyQt5/PyQt6 or PySide2/PySide6 to help me get started?
Can anyone explain QSettings() to me? 🤔
martin | 2021-03-19 07:59:34 UTC | #2
Yeah, I would probably go with QDialog
for something like this. You can subclass from the base to add any custom layout to the dialog and you still get the standard buttons etc. e.g. the following example from the book
Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.
class CustomDialog(QDialog):
def __init__(self, parent=None): # <1>
super().__init__(parent)
self.setWindowTitle("HELLO!")
QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
message = QLabel("Something happened, is that OK?")
self.layout.addWidget(message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
You can definitely use QSettings
for this, but I always found it to be a bit of a faff. You need to read your widgets and then write them to a settings object (and the reverse when you load them to display).
A while back I wrote a Python library to help with this pyqtconfig, which stores your settings in a Python dictionary and allows you to hook specific keys to specific widgets -- the link is 2 way, as in if you change the dictionary the widget will update, and if you change the widget the dictionary will update. Signals are fired on changes.
If you need it, that makes it easy to show immediate updates when settings are changed, do the "Apply" logic and allow them to be rolled back on Cancel/etc.
I haven't updated it in a while, so it might be a bit rusty. But it's on my todo list (also for PyQt6).
PedanticHacker | 2021-03-19 10:07:36 UTC | #3
Thanks for your answer, @martin! That was very helpful. :+1:
Also, I need help on merging a QGroupBox() with QRadioButton() widgets. I am playing with those two things and can't seem to merge them together. There's no example in the book and I am asking whether that can be added in the book, not just for me, but for others as well. Maybe an example of a Settings window with group boxes and radio buttons and all other related widgets would be very instructional. Just a thought.
PedanticHacker | 2021-03-19 12:01:42 UTC | #4
[quote="martin, post:2, topic:806"]
QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
[/quote]
While this is okay in PySide6, PyQt6 throws an error. It's better like this:
PySide6: QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
PyQt6: QBtn = QDialogButtonBox.StandardButtons.Ok | QDialogButtonBox.StandardButtons.Cancel
While PySide6 uses the singular form (StandardButton
), PyQt6 uses the plural form (StandardButtons
). That is also a distinction between PySide6 and PyQt6 to keep in mind. All enums are like this: singular in PySide6, plural in PyQt6.
martin | 2021-03-27 01:15:02 UTC | #5
Here's an example using the QGroupBox
-- the key is that QGroupBox
is a widget, so to add the widgets to it you need to add them to a layout & then apply that layout onto the group box. It works the rest out itself.
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QRadioButton, QApplication, QLabel, QGroupBox
class CustomDialog(QDialog):
def __init__(self, parent=None): # <1>
super().__init__(parent)
self.setWindowTitle("HELLO!")
QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
group_box = QGroupBox("Some radio buttons")
radio1 = QRadioButton("Radio 1")
radio2 = QRadioButton("Radio 1")
radio3 = QRadioButton("Radio 1")
radio_layout = QVBoxLayout()
radio_layout.addWidget(radio1)
radio_layout.addWidget(radio2)
radio_layout.addWidget(radio3)
group_box.setLayout(radio_layout)
buttonBox = QDialogButtonBox(QBtn)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
layout = QVBoxLayout()
layout.addWidget(group_box)
layout.addWidget(buttonBox)
self.setLayout(layout)
app = QApplication([])
dlg = CustomDialog()
dlg.exec_()
That should give you a dialog window with three options in a group, with them being exclusive.
[quote="PedanticHacker, post:4, topic:806"] While this is okay in PySide6, PyQt6 throws an error. It’s better like this: [/quote]
Ah yeah, I'm still writing with PyQt5. Find that change quite frustrating to be honest, ah well! :)
PedanticHacker | 2021-03-27 01:14:13 UTC | #6
[quote="martin, post:5, topic:806"] Ah yeah, I’m still writing with PyQt5. Find that change quite frustrating to be honest, ah well! :slight_smile: [/quote]
That bugs me, too. We'll all get used to it, eventually. Minor hickup. :wink:
Say, how can I now use those radio buttons so that my app responds accordingly to a radio button being in a different state (i.e., checked)?
martin | 2021-04-26 16:11:47 UTC | #7
The tricky part here is to read those settings and make them available elsewhere -- since once you destroy the dialog you can no longer read the values. That was the original reason I wrote the pyqtconfig module, to sync the data from config dialogs into a dictionary which you can then return as the "result" of a dialog.
You have a couple of options:
- connect signals on these, sending additional data (the value name) and receive this somewhere, to store it in a data object. The problem is you still need some way to set the initial state, and you can't postpone the changes until the dialog is OK/Applied
- load/unload the data from somewhere when opening the dialog (this is what I'd recommend)
You can implement something similar to pyqtconfig by creating a map of name, getter, setter and then using this to set/reset the values, e.g.
data = {
'checkbox1': Qt.Checked,
'checkbox2': Qt.Unchecked,
'somestring': "hello",
'int': 23
}
options = {
'checkbox1': (obj.checkbox1.checkState, obj.checkbox1.setCheckState)
'checkbox2': (obj.checkbox2.checkState, obj.checkbox2.setCheckState)
'somestring': (obj.strinput.text, obj.strinput.setText)
'int': (obj.spinbox1.value, obj.spinbox1.setValue)
}
# Set values from dictionary
for k, (_, setter) in options.items():
value = data[k]
setter(v)
# Copy values into dictionary
for k, (getter, ) in options.items():
data[k] = getter()
In pyqtconfig the mapper getter/setters are handled automatically based on the type of the widget. For this you just need a table of object types and getter setter names, e.g.
data = {
'checkbox1': Qt.Checked,
'checkbox2': Qt.Unchecked,
'somestring': "hello",
'int': 23
}
mappers = {
'QCheckBox': ('checkState', 'setCheckState'),
'QLineEdit': ('text', 'setText'),
'QSpinBox': ('value', 'setValue'),
}
options = {
'checkbox1': obj.checkbox1,
'checkbox2': obj.checkbox2,
'somestring': obj.strinput,
'int': obj.spinbox
}
# Set values from dictionary
for k, widget in options.items():
widget_cls = widget.__class__
getter_name, setter_name = mappers[widget_cls]
value = data[k]
setter = getattr(widget, setter)
setter(v)
# Copy values into dictionary
for k, widget in options.items():
widget_cls = widget.__class__
getter_name, setter_name = mappers[widget_cls]
getter = getattr(widget, getter_name)
data[k] = getter()
You can wrap this up into some kind of handler object -- I'd recommend a central "config" manager which you can pass a series of widgets and config names and it handles the rest.
fyi you could do the same thing using QSettings
and .setValue()
and .value()
in place of the dictionary data
gets/sets if you prefer. The types for all standard widgets should be handled automatically.
Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PySide2 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!