How to save and restore widget properties that is unique for each instance of the widget?

1.8k views Asked by At

I would like the ability to save and restore the widget state (properties and values) in my PyQt5 app. The widget state should be saved to an .ini file. This has already been demonstrated in the following Stackoverflow answers:

However, the answers to above questions do not address a specific issue I am facing in my example code below. I want the saving and restoring from the settings .ini file to be unique for every instance of the parent widget (main window app) that is running. Therefore, when saving the widget properties, the iteration over all the widgets should only occur on the children of the parent widget (main window) and not through all the widgets that are currently running in the global application.

I think the issue in my example code below is related to the line for w in QtWidgets.qApp.allWidgets():. I think this line iterates over ALL the widgets that are currently open in PyQt5 global application. However, when there are multiple instances of the same parent widget, there will be duplication in the objectName. Although a unique name can be given to each instance (ex: app_name) during init() and be accounted for in the QSettings key, this may not be the best universally adaptable solution. Therefore how can I fix the issue I am facing? How can I get settings_save() function to iterate through all the children widget of the instance of the parent widget (main window app) and NOT over ALL the parent widgets currently running in the global application? In Qt docs, I can't find a function similar to allWidgets() that will allow me to specify the parent widget (ex: QMainWindow) and give me all the widgets and objects under it. If getting all the widgets inside a parent widget was possible, then I can easily modify the function settings_save() include not only the QSetting variable as the argument, but also the instance of the widget for which I want to save the settings for.

enter image description here

main_app.py

import sys
from PyQt5 import QtWidgets, uic, QtCore, QtGui
from PyQt5.QtCore import QFileInfo, QSettings, QObject
from PyQt5.QtWidgets import qApp

from ui_mainwindow import Ui_MainWindow

def settings_value_is_valid(val):
    # https://stackoverflow.com/a/60028282/4988010
    if isinstance(val, QtGui.QPixmap):
        return not val.isNull()
    return True

def settings_restore(settings):
    # https://stackoverflow.com/a/60028282/4988010
    finfo = QtCore.QFileInfo(settings.fileName())

    if finfo.exists() and finfo.isFile():
        for w in QtWidgets.qApp.allWidgets():
            if w.objectName() and not w.objectName().startswith("qt_"):
            # if w.objectName():
                mo = w.metaObject()
                for i in range(mo.propertyCount()):
                    prop = mo.property(i)
                    name = prop.name()
                    last_value = w.property(name)
                    key = "{}/{}".format(w.objectName(), name)
                    # print(prop, name, last_value, key)
                    if not settings.contains(key):
                        continue
                    val = settings.value(key, type=type(last_value),)
                    if (
                        val != last_value
                        and settings_value_is_valid(val)
                        and prop.isValid()
                        and prop.isWritable()
                    ):
                        w.setProperty(name, val)

def settings_save(settings):
    # https://stackoverflow.com/a/60028282/4988010
    for w in QtWidgets.qApp.allWidgets():
        if w.objectName() and not w.objectName().startswith("qt_"):
        # if w.objectName():
            mo = w.metaObject()
            for i in range(mo.propertyCount()):
                prop = mo.property(i)
                name = prop.name()
                key = "{}/{}".format(w.objectName(), name)
                val = w.property(name)
                if settings_value_is_valid(val) and prop.isValid() and prop.isWritable():
                    settings.setValue(key, w.property(name))

class MyApp(QtWidgets.QMainWindow):
    
    def __init__(self, app_name='DefaultAppName'):
        super(MyApp, self).__init__()
        
        self.settings = QSettings("./temp/gui_settings-{}.ini".format(app_name), QSettings.IniFormat)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        
        # Set window titlte and qlineedit to indicate the name of app instance
        self.ui.app_instance_name.setText(app_name)
        self.setWindowTitle(app_name)

        # Load the saved config file saved from previous app usage 
        self.config_widgets_load_settings()

        self.ui.action_save_current_config.triggered.connect(self.config_widgets_save_settings)
        self.ui.action_load_config.triggered.connect(self.config_widgets_load_settings)
        self.ui.action_clear_config_file.triggered.connect(self.config_clear_settings)

    def config_widgets_save_settings(self):
        # Write current state to the settings config file
        settings_save(self.settings)

    def config_widgets_load_settings(self):
        # Load settings config file 
        settings_restore(self.settings)

    def config_clear_settings(self):
        # Clear the settings config file 
        self.settings.clear()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    # First instance of the MyApp 
    app_one = MyApp(app_name='App#1')
    app_one.show()
    
    # Second instance of the MyApp
    app_two = MyApp(app_name='App#2')
    app_two.show()

    app.exec_()

ui_mainwindow.py

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(330, 202)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.gridLayoutWidget = QtWidgets.QWidget(self.centralwidget)
        self.gridLayoutWidget.setGeometry(QtCore.QRect(20, 80, 291, 51))
        self.gridLayoutWidget.setObjectName("gridLayoutWidget")
        self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.gridLayout.setObjectName("gridLayout")
        self.lineEdit1 = QtWidgets.QLineEdit(self.gridLayoutWidget)
        self.lineEdit1.setObjectName("lineEdit1")
        self.gridLayout.addWidget(self.lineEdit1, 2, 0, 1, 1)
        self.pushButton1 = QtWidgets.QPushButton(self.gridLayoutWidget)
        self.pushButton1.setCheckable(True)
        self.pushButton1.setObjectName("pushButton1")
        self.gridLayout.addWidget(self.pushButton1, 2, 1, 1, 1)
        self.spinBox1 = QtWidgets.QSpinBox(self.gridLayoutWidget)
        self.spinBox1.setMaximum(10000)
        self.spinBox1.setProperty("value", 37)
        self.spinBox1.setObjectName("spinBox1")
        self.gridLayout.addWidget(self.spinBox1, 2, 2, 1, 1)
        self.label = QtWidgets.QLabel(self.gridLayoutWidget)
        self.label.setObjectName("label")
        self.gridLayout.addWidget(self.label, 1, 0, 1, 3)
        self.horizontalLayoutWidget = QtWidgets.QWidget(self.centralwidget)
        self.horizontalLayoutWidget.setGeometry(QtCore.QRect(50, 20, 241, 31))
        self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.label_2 = QtWidgets.QLabel(self.horizontalLayoutWidget)
        self.label_2.setObjectName("label_2")
        self.horizontalLayout.addWidget(self.label_2)
        self.app_instance_name = QtWidgets.QLineEdit(self.horizontalLayoutWidget)
        self.app_instance_name.setObjectName("app_instance_name")
        self.horizontalLayout.addWidget(self.app_instance_name)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 330, 21))
        self.menubar.setObjectName("menubar")
        self.menuFile = QtWidgets.QMenu(self.menubar)
        self.menuFile.setObjectName("menuFile")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.action_save_current_config = QtWidgets.QAction(MainWindow)
        self.action_save_current_config.setObjectName("action_save_current_config")
        self.action_load_config = QtWidgets.QAction(MainWindow)
        self.action_load_config.setObjectName("action_load_config")
        self.action_clear_config_file = QtWidgets.QAction(MainWindow)
        self.action_clear_config_file.setObjectName("action_clear_config_file")
        self.menuFile.addAction(self.action_save_current_config)
        self.menuFile.addAction(self.action_load_config)
        self.menuFile.addAction(self.action_clear_config_file)
        self.menubar.addAction(self.menuFile.menuAction())

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.lineEdit1.setText(_translate("MainWindow", "This is default text"))
        self.pushButton1.setText(_translate("MainWindow", "Push me"))
        self.label.setText(_translate("MainWindow", "My simple app with various widgets"))
        self.label_2.setText(_translate("MainWindow", "App instance name"))
        self.menuFile.setTitle(_translate("MainWindow", "File"))
        self.action_save_current_config.setText(_translate("MainWindow", "Save current config"))
        self.action_load_config.setText(_translate("MainWindow", "Load config"))
        self.action_clear_config_file.setText(_translate("MainWindow", "Clear config file"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())
1

There are 1 answers

0
Zythyr On

I figured out the solution after digging through Qt documentations further. Every QWidget is of type QObject, which has a method called findChildren(). This method can be used to specify the parent widget for which you want to save/restore the settings for.

Below are the methods created that will help assist in saving/restoring the settings. My contribution is the creation of the function settings_get_all_widgets(). The saving/restoring functions were originally created by @eyllanesc which were modified by me as a solution to this question.

def settings_get_all_widgets(parent):
    # Possible fix to the issue: 
    # https://stackoverflow.com/questions/64202927/how-to-save-and-restore-widget-properties-that-is-unique-for-each-instance-of-th 
    
    if parent:
        # Find all children inside the given parent that is of type QWidget 
        all_widgets = parent.findChildren(QtWidgets.QWidget)
        if parent.isWidgetType():
            # If parent is of type QWidget, add the parent itself to the list 
            all_widgets.append(parent)
    else:
        # If no parent is given then get all the widgets from all the PyQt applications 
        all_widgets = QtWidgets.qApp.allWidgets()
    
    return all_widgets

def settings_value_is_valid(val):
    # Originally adapted from:
    # https://stackoverflow.com/a/60028282/4988010
    # https://github.com/eyllanesc/stackoverflow/issues/26#issuecomment-703184281
    if isinstance(val, QtGui.QPixmap):
        return not val.isNull()
    return True

def settings_restore(settings, parent=None):
    # Originally adapted from:
    # https://stackoverflow.com/a/60028282/4988010
    # https://github.com/eyllanesc/stackoverflow/issues/26#issuecomment-703184281

    if not settings:
        return

    all_widgets = settings_get_all_widgets(parent)

    finfo = QtCore.QFileInfo(settings.fileName())
    if finfo.exists() and finfo.isFile():
        for w in all_widgets:
            if w.objectName() and not w.objectName().startswith("qt_"):
            # if w.objectName():
                mo = w.metaObject()
                for i in range(mo.propertyCount()):
                    prop = mo.property(i)
                    name = prop.name()
                    last_value = w.property(name)
                    key = "{}/{}".format(w.objectName(), name)
                    if not settings.contains(key):
                        continue
                    val = settings.value(key, type=type(last_value),)
                    if (
                        val != last_value
                        and settings_value_is_valid(val)
                        and prop.isValid()
                        and prop.isWritable()
                    ):
                        w.setProperty(name, val)

def settings_save(settings, parent=None):
    # Originally adapted from:
    # https://stackoverflow.com/a/60028282/4988010
    # https://github.com/eyllanesc/stackoverflow/issues/26#issuecomment-703184281

    if not settings:
        return
    all_widgets = settings_get_all_widgets(parent)
    
    for w in all_widgets:
        if w.objectName() and not w.objectName().startswith("qt_"):
            mo = w.metaObject()
            for i in range(mo.propertyCount()):
                prop = mo.property(i)
                name = prop.name()
                key = "{}/{}".format(w.objectName(), name)
                val = w.property(name)
                if settings_value_is_valid(val) and prop.isValid() and prop.isWritable():
                    settings.setValue(key, w.property(name))

Below is the self contained working code which contains the solution to the original question.

import sys
from PyQt5 import QtWidgets, uic, QtCore, QtGui
from PyQt5.QtCore import QFileInfo, QSettings, QObject
from PyQt5.QtWidgets import qApp
# from ui_mainwindow import Ui_MainWindow


def settings_get_all_widgets(parent):
    # Possible fix to the issue: 
    # https://stackoverflow.com/questions/64202927/how-to-save-and-restore-widget-properties-that-is-unique-for-each-instance-of-th 
    
    if parent:
        # Find all children inside the given parent that is of type QWidget 
        all_widgets = parent.findChildren(QtWidgets.QWidget)
        if parent.isWidgetType():
            # If parent is of type QWidget, add the parent itself to the list 
            all_widgets.append(parent)
    else:
        # If no parent is given then get all the widgets from all the PyQt applications 
        all_widgets = QtWidgets.qApp.allWidgets()
    
    return all_widgets

def settings_value_is_valid(val):
    # Originally adapted from:
    # https://stackoverflow.com/a/60028282/4988010
    # https://github.com/eyllanesc/stackoverflow/issues/26#issuecomment-703184281
    if isinstance(val, QtGui.QPixmap):
        return not val.isNull()
    return True

def settings_restore(settings, parent=None):
    # Originally adapted from:
    # https://stackoverflow.com/a/60028282/4988010
    # https://github.com/eyllanesc/stackoverflow/issues/26#issuecomment-703184281

    if not settings:
        return

    all_widgets = settings_get_all_widgets(parent)

    finfo = QtCore.QFileInfo(settings.fileName())
    if finfo.exists() and finfo.isFile():
        for w in all_widgets:
            if w.objectName() and not w.objectName().startswith("qt_"):
            # if w.objectName():
                mo = w.metaObject()
                for i in range(mo.propertyCount()):
                    prop = mo.property(i)
                    name = prop.name()
                    last_value = w.property(name)
                    key = "{}/{}".format(w.objectName(), name)
                    if not settings.contains(key):
                        continue
                    val = settings.value(key, type=type(last_value),)
                    if (
                        val != last_value
                        and settings_value_is_valid(val)
                        and prop.isValid()
                        and prop.isWritable()
                    ):
                        w.setProperty(name, val)

def settings_save(settings, parent=None):
    # Originally adapted from:
    # https://stackoverflow.com/a/60028282/4988010
    # https://github.com/eyllanesc/stackoverflow/issues/26#issuecomment-703184281

    if not settings:
        return
    all_widgets = settings_get_all_widgets(parent)
    
    for w in all_widgets:
        if w.objectName() and not w.objectName().startswith("qt_"):
            mo = w.metaObject()
            for i in range(mo.propertyCount()):
                prop = mo.property(i)
                name = prop.name()
                key = "{}/{}".format(w.objectName(), name)
                val = w.property(name)
                if settings_value_is_valid(val) and prop.isValid() and prop.isWritable():
                    settings.setValue(key, w.property(name))

class MyApp(QtWidgets.QMainWindow):
    
    def __init__(self, app_name='DefaultAppName'):
        super(MyApp, self).__init__()
        
        self.settings = QSettings("./temp/gui_settings-{}.ini".format(app_name), QSettings.IniFormat)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        
        # Set window titlte and qlineedit to indicate the name of app instance
        self.ui.app_instance_name.setText(app_name)
        self.setWindowTitle(app_name)

        # Load the saved config file saved from previous app usage 
        self.config_widgets_load_settings()

        self.ui.action_save_current_config.triggered.connect(self.config_widgets_save_settings)
        self.ui.action_load_config.triggered.connect(self.config_widgets_load_settings)
        self.ui.action_clear_config_file.triggered.connect(self.config_clear_settings)

    def config_widgets_save_settings(self):
        # Write current state to the settings config file
        settings_save(self.settings, self)

    def config_widgets_load_settings(self):
        # Load settings config file 
        settings_restore(self.settings, self)

    def config_clear_settings(self):
        # Clear the settings config file 
        self.settings.clear()

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(330, 202)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.gridLayoutWidget = QtWidgets.QWidget(self.centralwidget)
        self.gridLayoutWidget.setGeometry(QtCore.QRect(20, 80, 291, 51))
        self.gridLayoutWidget.setObjectName("gridLayoutWidget")
        self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.gridLayout.setObjectName("gridLayout")
        self.lineEdit1 = QtWidgets.QLineEdit(self.gridLayoutWidget)
        self.lineEdit1.setObjectName("lineEdit1")
        self.gridLayout.addWidget(self.lineEdit1, 2, 0, 1, 1)
        self.pushButton1 = QtWidgets.QPushButton(self.gridLayoutWidget)
        self.pushButton1.setCheckable(True)
        self.pushButton1.setObjectName("pushButton1")
        self.gridLayout.addWidget(self.pushButton1, 2, 1, 1, 1)
        self.spinBox1 = QtWidgets.QSpinBox(self.gridLayoutWidget)
        self.spinBox1.setMaximum(10000)
        self.spinBox1.setProperty("value", 37)
        self.spinBox1.setObjectName("spinBox1")
        self.gridLayout.addWidget(self.spinBox1, 2, 2, 1, 1)
        self.label = QtWidgets.QLabel(self.gridLayoutWidget)
        self.label.setObjectName("label")
        self.gridLayout.addWidget(self.label, 1, 0, 1, 3)
        self.horizontalLayoutWidget = QtWidgets.QWidget(self.centralwidget)
        self.horizontalLayoutWidget.setGeometry(QtCore.QRect(50, 20, 241, 31))
        self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.label_2 = QtWidgets.QLabel(self.horizontalLayoutWidget)
        self.label_2.setObjectName("label_2")
        self.horizontalLayout.addWidget(self.label_2)
        self.app_instance_name = QtWidgets.QLineEdit(self.horizontalLayoutWidget)
        self.app_instance_name.setObjectName("app_instance_name")
        self.horizontalLayout.addWidget(self.app_instance_name)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 330, 21))
        self.menubar.setObjectName("menubar")
        self.menuFile = QtWidgets.QMenu(self.menubar)
        self.menuFile.setObjectName("menuFile")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.action_save_current_config = QtWidgets.QAction(MainWindow)
        self.action_save_current_config.setObjectName("action_save_current_config")
        self.action_load_config = QtWidgets.QAction(MainWindow)
        self.action_load_config.setObjectName("action_load_config")
        self.action_clear_config_file = QtWidgets.QAction(MainWindow)
        self.action_clear_config_file.setObjectName("action_clear_config_file")
        self.menuFile.addAction(self.action_save_current_config)
        self.menuFile.addAction(self.action_load_config)
        self.menuFile.addAction(self.action_clear_config_file)
        self.menubar.addAction(self.menuFile.menuAction())

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.lineEdit1.setText(_translate("MainWindow", "This is default text"))
        self.pushButton1.setText(_translate("MainWindow", "Push me"))
        self.label.setText(_translate("MainWindow", "My simple app with various widgets"))
        self.label_2.setText(_translate("MainWindow", "App instance name"))
        self.menuFile.setTitle(_translate("MainWindow", "File"))
        self.action_save_current_config.setText(_translate("MainWindow", "Save current config"))
        self.action_load_config.setText(_translate("MainWindow", "Load config"))
        self.action_clear_config_file.setText(_translate("MainWindow", "Clear config file"))


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    # First instance of the MyApp 
    app_one = MyApp(app_name='App#1')
    app_one.show()
    
    # Second instance of the MyApp
    app_two = MyApp(app_name='App#2')
    app_two.show()

    app.exec_()