2

I have previously read several topics on this site and other platforms, but my code does still not work as desired. I seem not to be able to put all the puzzle pieces together and need someone to look over my code.

I'm writing a plugin for the program QtiPlot. Im using Python 2.7.6 and PyQt4. I created the Plugin GUI with QT Designer. I'm also new to Python. Im using these "old" resources because my predecessor has used them.

My current task is to develop the settings, i.e. being able to save and restore parameters. I found a template on this site for this purpose: Python PyQt4 functions to save and restore UI widget values?

I want to save parameters to an Ini-File. However I have problems with the QVariants. Instead of the strings I inserted in the plugin, the expression "PyQt4.QtCore.QVariant object at 0x08667FB0" is being saved. I already know that this is an issue due to the fact that the QVariants are not correcly converted back to Python objects.

So, in order to convert the QVariants back manually, I added the phrase "toString()" to the value assignment of QLineEdit-Objects in the restore function(the line commented out is the previous version). But then my QLineEdit-Blocks in the plugin are empty, which confuses me. I read in the documentation that an empty string is returned if the QVariant does not consist of one of the preset types, including string. But this happens although I entered a string before.

Does this mean that the strings are not correctly saved in the first place? Or else what does it mean, what do I miss or what am I doing wrong?

I also noticed that no values are stored in the Ini-File, so that part of the code is also still buggy. But I'm not sure if this due to the fact that the save-function doesn't work or because the Ini-construction is wrong itself.

Further, I tried to use the SIP-Module at the head of the configuration file to fix my problem(it is commented out, too, because it didn't work for me so far). But although I placed this at the head of the code, I then get the error "API 'QVariant' has already been set to version 1". I don't understand why or from what the SIP-instruction is overridden. Is there any way to fix this?

My Configuration program looks like this:

#import sip    
#sip.setapi('QVariant', 2)    
#sip.setapi('QString', 2)

import inspect    
import sys

from PyQt4.QtCore import *    
from PyQt4.QtGui import *

sys.path.append("C:\\Program Files (x86)\\QtiPlot\\app\\02 Python Packages")

import PyQt4_Save_Restore_UI_Widget_Values

class app_conf(QtGui.QWidget):

    def __init__(self):

        super(self.__class__, self).__init__()

        self.version = 1.0

        QtCore.QCoreApplication.setOrganizationName("Organization")
        QtCore.QCoreApplication.setApplicationName("Application")
        QtCore.QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, "C:\\Program Files (x86)\\QtiPlot\\app\\02 Python Packages\\saved.ini")
        self.settings = QtCore.QSettings("C:\\Program Files (x86)\\QtiPlot\\app\\02 Python Packages\\saved.ini", QSettings.IniFormat)

        from PyQt4 import uic

        self.ui = uic.loadUi(r"C:\Program Files (x86)/QtiPlot/app/03 UI Files/Config.ui")

        PyQt4_Save_Restore_UI_Widget_Values.gui_restore_settings(self.ui, self.settings)

        self.ui.closeEvent = self.closeEvent
        self.ui.show()


    def closeEvent(self, event):            
        PyQt4_Save_Restore_UI_Widget_Values.gui_save_settings(self.ui, self.settings)


window = app_conf()

And my settings module(PyQt4_Save_Restore_UI_Widget_Values.py) looks like this:

#===================================================================
# Module with functions to save & restore qt widget values
# Written by: Alan Lilly 
# Website: http://panofish.net
#===================================================================

import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import inspect


def gui_save_settings(ui, settings):

    #for child in ui.children():  # works like getmembers, but because it traverses the hierarachy, you would have to call guisave recursively to traverse down the tree

    for name, obj in inspect.getmembers(ui):
        #if type(obj) is QComboBox:  # this works similar to isinstance, but missed some field... not sure why?
        if isinstance(obj, QComboBox):
            name   = obj.objectName()      # get combobox name
            index  = obj.currentIndex()    # get current index from combobox
            text   = obj.itemText(index)   # get the text for current index
            settings.setValue(name, text)   # save combobox selection to registry

        if isinstance(obj, QLineEdit):
            name = obj.objectName()
            value = obj.text()
            settings.setValue(name, value)    # save ui values, so they can be restored next time

        if isinstance(obj, QCheckBox):
            name = obj.objectName()
            state = obj.checkState()
            settings.setValue(name, state)


def gui_restore_settings(ui, settings):

    for name, obj in inspect.getmembers(ui):
        if isinstance(obj, QComboBox):
            index  = obj.currentIndex()    # get current region from combobox
            #text   = obj.itemText(index)   # get the text for new selected index
            name   = obj.objectName()

            value = unicode(settings.value(name))  

            if value == "":
                continue

            index = obj.findText(value)   # get the corresponding index for specified string in combobox

            if index == -1:  # add to list if not found
                obj.insertItems(0,[value])
                index = obj.findText(value)
                obj.setCurrentIndex(index)
            else:
                obj.setCurrentIndex(index)   # preselect a combobox value by index    

        if isinstance(obj, QLineEdit):
            name = obj.objectName()
            #value = unicode(settings.value(name))  # get stored value from registry
            value = settings.value(name).toString()
            obj.setText(value)  # restore lineEditFile

        if isinstance(obj, QCheckBox):
            name = obj.objectName()
            value = settings.value(name)   # get stored value from registry
            if value != None:
                obj.setCheckState(value.toBool())   # restore checkbox


################################################################

if __name__ == "__main__":

    # execute when run directly, but not when called as a module.
    # therefore this section allows for testing this module!

    #print "running directly, not as a module!"

    sys.exit() 
Community
  • 1
  • 1
Maryn
  • 35
  • 6

2 Answers2

2

As in Python 2 and with SIP old API, toString() will return a QString. You have to force it to a Python string with str for both the get and the set methods. Concerning the QCheckBox, I used the toInt method instead of the toBool one (which works fine for me).

Here is a modified version of your save and restore functions:

def gui_save_settings(ui, settings):

    for _, obj in inspect.getmembers(ui):
        name   = obj.objectName()

        # Set QComboBox setting
        if isinstance(obj, QComboBox):
            value = str(obj.currentText()) # get current text from combobox
            settings.setValue(name, value)   # save combobox selection to registry

        # Set QLineEdit setting
        if isinstance(obj, QLineEdit):
            value = str(obj.text())
            settings.setValue(name, value)    # save ui values, so they can be restored next time

        # Set QCheckBox setting
        if isinstance(obj, QCheckBox):
            value = int(checkbox.isChecked())
            settings.setValue(name, value)

def gui_restore_settings(ui, settings):

    for _, obj in inspect.getmembers(ui):
        name   = obj.objectName()

        # Get QComboBox setting
        if isinstance(obj, QComboBox):
            value = str(settings.value(name).toString())  
            if value == "":
                continue

            index = obj.findText(value)   # get the corresponding index for specified string in combobox

            if index == -1:  # add to list if not found
                obj.addItem(value)
                index = obj.findText(value)

            obj.setCurrentIndex(index)   # preselect a combobox value by index
            continue

        # Get QLineEdit setting
        if isinstance(obj, QLineEdit):
            value = str(settings.value(name).toString())
            obj.setText(value)  # restore lineEditFile
            continue

        # Get QCheckBox setting
        if isinstance(obj, QCheckBox):
            value, res = settings.value(key).toInt() # get stored value from registry
            if res:
                obj.setCheckState(value)   # restore checkbox
            continue
Frodon
  • 3,684
  • 1
  • 16
  • 33
  • Thanks for your answer! I see I have indeed missed something. However when I try your code, I get the following error: Syntax error: invalid syntax, for the line: value, res = settings.value(key).toInt()) # get stored value from registry. Why are two parameters assigned in this line? – Maryn Sep 20 '16 at 07:13
  • @Maryn According to the [documentation](http://pyqt.sourceforge.net/Docs/PyQt4/qvariant.html#toInt) `toInt` returns a tuple. Edit: there was an extra parenthesis, I fixed it. – Frodon Sep 20 '16 at 09:07
  • Thanks again! Now I don't have any errors, but still no values are saved. I use an INI-file and have stored some default values there. As I long as I use this file, the defaults are restored. When I stop using the INI-file, again I only store the "PyQt4.QtCore.QVariant object at..." strings. I suspect that this maybe happens because I didn't set up my app correctly? Can the values be stored to the INI-file in the current configuration? Is a QtGUI.QApplication assignment(like for a "__main__" file) necessary because I only load my code from QtiPlot? What else could be the reason? – Maryn Sep 20 '16 at 14:24
  • @Maryn indeed I don't use the same constructor as you. I use `QtCore.QSettings("Organisation", "application")` and I let Qt deal with the target settings file. You can try this constructor and then use `setDefaultFormat (Format format)` ti specify your file format. – Frodon Sep 20 '16 at 14:30
  • Thanks again! I tried to use your constructor, but that doesn't change the described behavior. I mean, when I use your constructor with the "QtCore.QSettings.IniFormat", then the values should be written to an Ini-file, right? Shouldn't I be able to locate this file in the application folder? Because I don't see any new file there and I can therefore not check if the values are stored into such a file. I still don't understand why it doesn't work. I suppose I make a little mistake somewhere that prohibits the desired functionality. Do I have to change the order of the QSettings-part? – Maryn Sep 20 '16 at 15:45
  • @Maryn See [this page](http://pyqt.sourceforge.net/Docs/PyQt4/qsettings.html#platform-specific-notes) about the storage location of the settings. It depends on your platform. – Frodon Sep 20 '16 at 15:47
  • I have solved the issue that no values were stored. I had to add the line "self.ui.closeEvent = self.closeEvent" in my __init__ method. Another solution would be showing the ui-window explicitly in the main part of the code(i.e. "behind" the class definition). – Maryn Sep 23 '16 at 08:28
1

The best solution is to use sip.setapi. But to get it to work properly, it must be invoked before the first import of PyQt in your application. So it needs to go in the main script, not the config module:

#===================================================================
# Module with functions to save & restore qt widget values
# Written by: Alan Lilly 
# Website: http://panofish.net
#===================================================================
import sys
import sip
sip.setapi('QVariant', 2)    
sip.setapi('QString', 2)
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import inspect

This will ensure QVariant and QString are always automatically converted to ordinary python types - so no need to use unicode(), toString(), toBool(), etc.

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • Thanks for the answer! I'm afraid this is what I already tried. I added these lines at the head of my code (see the lines commented out). I also tried to load only the two files shown above into the QtiPlot-application. It seems that PyQt is already previously invoked and I don't know how I could override this behavior. Maybe I should consider using Python 3 instead? – Maryn Sep 18 '16 at 08:15
  • @Maryn. Yes, it looks like QtiPilot pre-loads PyQt4, so you can't call `setapi` at the right time. Switching to Python 3 would solve this issue, because it uses the v2 API by default. However, it would seem that your QtiPilot installation must be compiled against Python 2, otherwise I think you would see errors when it tried to load your script. If that is the case, you would need a Python 3 compatible version of QtiPilot as well. If you can't switch to Python 3, please edit your question and add the `Config.ui` file so that others can test your code. – ekhumoro Sep 18 '16 at 12:40