如何保存和恢复每个小部件实例唯一的小部件属性?

Posted

技术标签:

【中文标题】如何保存和恢复每个小部件实例唯一的小部件属性?【英文标题】:How to save and restore widget properties that is unique for each instance of the widget? 【发布时间】:2020-10-05 05:31:55 【问题描述】:

我希望能够在我的 PyQt5 应用程序中保存和恢复小部件状态(属性和值)。小部件状态应保存到 .ini 文件中。这已在以下 *** 答案中得到证明:

How to save text in QLineEdits in PyQt even if the Widget gets closed? Loading Widgets properly while restoring from QSettings

但是,上述问题的答案并没有解决我在下面的示例代码中面临的特定问题。我希望设置 .ini 文件的保存和恢复对于正在运行的父小部件(主窗口应用程序)的每个实例都是唯一的。因此,在保存小部件属性时,对所有小部件的迭代应该只发生在父小部件(主窗口)的子小部件上,而不是遍历当前在全局应用程序中运行的所有小部件。

我认为下面示例代码中的问题与for w in QtWidgets.qApp.allWidgets(): 行有关。我认为这一行会遍历 PyQt5 全局应用程序中当前打开的所有小部件。但是,当同一个父widget有多个实例时,objectName会出现重复。尽管可以在 init() 期间为每个实例指定一个唯一名称(例如:app_name)并在 QSettings 键中进行说明,但这可能不是最好的普遍适用的解决方案。因此,我该如何解决我面临的问题?如何让settings_save() 函数遍历父小部件(主窗口应用程序)实例的所有子小部件,而不是遍历当前在全局应用程序中运行的所有父小部件?在 Qt 文档中,我找不到类似于 allWidgets() 的函数,它允许我指定父窗口小部件(例如:QMainWindow)并给我它下面的所有窗口小部件和对象。如果可以在父小部件中获取所有小部件,那么我可以轻松修改函数settings_save(),不仅包括 QSetting 变量作为参数,还包括我要为其保存设置的小部件实例。

ma​​in_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://***.com/a/60028282/4988010
    if isinstance(val, QtGui.QPixmap):
        return not val.isNull()
    return True

def settings_restore(settings):
    # https://***.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://***.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】:

在进一步研究 Qt 文档后,我找到了解决方案。每个QWidget 都是QObject 类型,它有一个名为findChildren() 的方法。此方法可用于指定要为其保存/恢复设置的父窗口小部件。

以下是创建的有助于保存/恢复设置的方法。我的贡献是创建函数settings_get_all_widgets()。保存/恢复功能最初是由@eyllanesc 创建的,我为解决这个问题而对其进行了修改。

def settings_get_all_widgets(parent):
    # Possible fix to the issue: 
    # https://***.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://***.com/a/60028282/4988010
    # https://github.com/eyllanesc/***/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://***.com/a/60028282/4988010
    # https://github.com/eyllanesc/***/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://***.com/a/60028282/4988010
    # https://github.com/eyllanesc/***/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))

以下是包含原始问题解决方案的自包含工作代码。

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://***.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://***.com/a/60028282/4988010
    # https://github.com/eyllanesc/***/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://***.com/a/60028282/4988010
    # https://github.com/eyllanesc/***/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://***.com/a/60028282/4988010
    # https://github.com/eyllanesc/***/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_()

【讨论】:

以上是关于如何保存和恢复每个小部件实例唯一的小部件属性?的主要内容,如果未能解决你的问题,请参考以下文章

Qt4:从 QDockedWidget 的子类访问 QtDesigner 创建的小部件

每个选项卡小部件的 PyQt 选项

如何禁用 WordPress 的小部件块编辑器?

显示当前小部件类之外的小部件

私人JavaScript小部件

我在列中有几个扩展的小部件。首先我展开一个小部件。当我展开第二个时,第一个应该自动折叠