Search code examples
pythonpyqtpyqt5qtreewidgetqsettings

Save Qtreewidget item and restore it with selection


I need support to my little software with Qwidget and QTreeWidgets. see below figure.

enter image description here

This is how my Qwidget looks like. I want to restore Qtreewidget items and widget when I close the window and restore it with previous selection. as Marked on the figure below.As you see in my scripts below, I have used Qsettings, I have tried to fix it with pickle but does not work.

enter image description here

Any improvement in coding is welcoming.

from PyQt5 import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import*
from PyQt5.QtGui import *
import sys
import pickle

iconroot = QFileInfo(__file__).absolutePath()
ORGANIZATION_NAME = 'Circularcolumn App'
ORGANIZATION_DOMAIN = 'Circular shape'
APPLICATION_NAME = 'QSettings program'
SETTINGS_TRAY = 'settings/tray'

class usedcircularshape(QDialog):
    def __init__(self, parent=None):
    super().__init__(parent)

    self.setWindowTitle("Frequently used shape")
    self.setWindowIcon(QIcon(iconroot+"/images/circularcolumnnorebar.png"))
    #self.setStyleSheet("background-color:#f2f2f2")

    self.addbutton = QPushButton("Add")
    self.addbutton.clicked.connect(self.add)
    self.deletebutton = QPushButton("Delete")
    self.deletebutton.clicked.connect(self.delete)

    self.okbutton = QPushButton("Ok")
    self.okbutton.setCursor(Qt.PointingHandCursor)
    #self.okbutton.clicked.connect(self.hidethiswindow)
    self.okbutton.clicked.connect(self.savesetting)

    self.cancelbutton = QPushButton("Cancel")
    self.cancelbutton.setCursor(Qt.PointingHandCursor)
    self.cancelbutton.clicked.connect(self.loadsetting)
    #self.cancelbutton.clicked.connect(self.close)

    self.addimage()
    self.qlabeltodefinesection()
    self.treewidget()

    self.sectionnamecircular = QLabel('Section name: ')
    self.sectionnamecircularindata = QLineEdit('Define en name to section')
    self.sectionnamecircularindata.setObjectName("sectionnamecircularindata")
    self.sectionnamecircular.setBuddy(self.sectionnamecircularindata)
    self.sectionnamecircular.setFocus()
    self.grid_sectionname = QHBoxLayout()
    self.grid_sectionname.addWidget(self.sectionnamecircular)
    self.grid_sectionname.addWidget(self.sectionnamecircularindata)

    self.boxlayout = QGridLayout()
    self.boxlayout.addLayout(self.grid_sectionname,0,0,1,2)
    self.boxlayout.addWidget(self.treewidget,1,0,5,2)
    self.boxlayout.addWidget(self.addbutton,2,2)
    self.boxlayout.addWidget(self.deletebutton,3,2)
    self.boxlayout.addWidget(self.imagelabel,6,0)
    self.boxlayout.addLayout(self.qlabelhboxgrid ,6,1)
    self.boxlayout.addWidget(self.okbutton,8,1)
    self.boxlayout.addWidget(self.cancelbutton,8,2)
    self.setLayout(self.boxlayout)
    try:
        self.loadsetting()
    except ( ValueError, TypeError):
        pass

def treewidget(self):
    self.treewidget = QTreeWidget(self)
    self.treewidget.setColumnCount(1)
    self.treewidget.setColumnWidth(1,20)
    self.treewidget.setHeaderItem(QTreeWidgetItem(['Standard Section Library']))
    #self.treewidget.addTopLevelItem(QTreeWidgetItem(['Standard Sectiontype']))
    self.treewidget.setRootIsDecorated(True)

    self.firstparentitem = QTreeWidgetItem(self.treewidget)
    self.firstparentitem.setText(0,'Circular shapes')
    self.firstparentitem.setIcon(0,QIcon(iconroot+"/images/circularcolumnnorebar.png"))

    standardsectionlist = ["D100","D150","D200","D250","D300","D350","D400","D450","D500","D550","D600","D650"
                           ,"D700","D750","D800","D850","D900","D950","D1000"]

    for i in standardsectionlist:
        self.firstparentitem.addChild(QTreeWidgetItem(["%s"%i]))

    self.secondparentitem = QTreeWidgetItem(self.treewidget)
    self.secondparentitem.setText(0,'Customized')
    self.secondparentitem.setIcon(0,QIcon(iconroot+"/images/circularcolumnnorebar.png"))    
    self.secondchilditem = QTreeWidgetItem(["D235"])    
    self.secondparentitem.insertChild(0,self.secondchilditem)
    self.secondchilditem.setChildIndicatorPolicy(QTreeWidgetItem.DontShowIndicator)
    self.treewidget.move(15,15)
    self.treewidget.setGeometry(15,15,200,600)
    self.treewidget.setAlternatingRowColors(True)
    self.treewidget.expandItem ( self.firstparentitem )
    self.show() 

    print(self.treewidget.headerItem().text(0))
    print(self.treewidget.columnCount())
    print(self.treewidget.currentColumn())

    print(self.treewidget.indexFromItem(self.firstparentitem).row())
    print(self.firstparentitem.childCount())
    print(self.firstparentitem.child(1).text(0))
    print(self.firstparentitem.text(0))
    print(self.treewidget.headerItem().text(0))
    print(self.treewidget.topLevelItem(0).text(0))
    print(self.firstparentitem.isSelected())
    print(self.treewidget.selectedItems())
    print(self.secondchilditem.text(1))

    branchstyle = '''QTreeWidget {border:none;} 

    QTreeView::branch:has-siblings:!adjoins-item {
        border-image: url(images/vline.png) 0;}

    QTreeView::branch:has-siblings:adjoins-item {
        border-image: url(images/branch-more.png) 0;}

    QTreeView::branch:!has-children:!has-siblings:adjoins-item {
        border-image: url(images/branch-end.png) 0;}

    QTreeView::branch:has-children:!has-siblings:closed,
    QTreeView::branch:closed:has-children:has-siblings {
        border-image: none;
        image: url(images/branch-closed.png);}

    QTreeView::branch:open:has-children:!has-siblings,
    QTreeView::branch:open:has-children:has-siblings {
        border-image: none;
        image: url(images/branch-open.png);}'''

    self.treewidget.setStyleSheet(branchstyle)
    self.treewidget.itemClicked.connect(self.currentitem)
    self.treewidget.currentItemChanged.connect(self.current_item_changed)

   #@QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem, QtWidgets.QTreeWidgetItem)
def current_item_changed(self, current, previous):
   #print('\ncurrent: {}, \nprevious: {}'.format(current, previous))
    print(current.text(0),previous)


def add(self):
    text, ok = QInputDialog.getText(self, "Add custom section", "Enter section geometry f.ex as D325 or just 325 in mm: ")

    if ok:
        self.secondchilditem = QTreeWidgetItem(["%s"% text])
        self.secondparentitem.insertChild(0,self.secondchilditem)
        self.treewidget.expandItem ( self.secondparentitem )
        self.gettext = text

    print(self.secondparentitem.child(0), self.gettext)


def delete(self):
    self.secondparentitem.takeChild(0)

def currentitem(self):
    print(self.treewidget.currentItem().text(0),self.treewidget.selectedItems())
    self.itemtext = self.treewidget.currentItem().text(0)

    if self.itemtext == self.firstparentitem.text(0) or self.itemtext == self.secondparentitem.text(0):
        return None
    elif self.itemtext == self.treewidget.topLevelItem(0).text(0):
        return None
    elif self.itemtext == None:
        return None
    else:
        self.select_circular_section = int(self.itemtext.translate({ord('D'):None}))   
        print(self.itemtext, self.treewidget.selectedItems, self.select_circular_section)

        area = str(format(3.1416/4*(self.select_circular_section)**2,'0.2E'))
        inerti = str(format(3.1416/64*pow(self.select_circular_section,4),'0.2E'))

        self.qlabelcirculardiameterselected = QLabel('')
        qlabelcircularareaselected = QLabel('')
        qlabelcircularinertimomentselected = QLabel("")
        emptylabel1 = QLabel('     ')

        self.qlabelcirculardiameterselected.setText('%s    mm '% self.select_circular_section)
        qlabelcircularareaselected.setText('{}    mm2 ' .format(area))
        qlabelcircularinertimomentselected.setText("%s    mm4 " %(inerti))

        qlabelhboxgridselected = QGridLayout()
        qlabelhboxgridselected.addWidget(emptylabel1,0,0)
        qlabelhboxgridselected.addWidget(self.qlabelcirculardiameterselected,1,0)
        qlabelhboxgridselected.addWidget(qlabelcircularareaselected,2,0)
        qlabelhboxgridselected.addWidget(qlabelcircularinertimomentselected,3,0)
        qlabelhboxgridselected.addWidget(emptylabel1,4,0,5,0)       

        return print(self.itemtext, self.treewidget.selectedItems, self.select_circular_section), self.boxlayout.addLayout(qlabelhboxgridselected ,6,2),self.qlabelcirculardiameterselected     

def addimage(self):
    self.imagelabel = QLabel()
    self.circularimage = QPixmap(iconroot+"/images/circularcolumnnorebard.png").scaled(230,230,Qt.KeepAspectRatio)
    self.imagelabel.setPixmap(self.circularimage) 
    self.imagelabel.setGeometry(15,15,15,15)

def hidethiswindow(self):   
    if self.itemtext == self.firstparentitem.text(0) or self.itemtext == self.secondparentitem.text(0):
        QMessageBox.about(self,'Error selection','Please, select a section not a text')
    elif self.itemtext == self.treewidget.topLevelItem(0).text(0):
        QMessageBox.about(self,'Error selection','Please, select a section not a text')
    elif self.itemtext == None:
        QMessageBox.about(self,'Error selection','Please, select a section not a text')
    else:
        self.savesetting()
        self.hide() 

def qlabeltodefinesection(self):    
    self.qlabelcirculardiameter = QLabel('    D = ')
    self.qlabelcirculararea = QLabel('    A = ')
    self.qlabelcircularinertimoment = QLabel("    I = ")
    self.emptylabel = QLabel('     ')   
    self.qlabelhboxgrid = QGridLayout()
    self.qlabelhboxgrid.addWidget(self.emptylabel,0,0)
    self.qlabelhboxgrid.addWidget(self.qlabelcirculardiameter,1,0)
    self.qlabelhboxgrid.addWidget(self.qlabelcirculararea,2,0)
    self.qlabelhboxgrid.addWidget(self.qlabelcircularinertimoment,3,0)
    self.qlabelhboxgrid.addWidget(self.emptylabel,4,0,5,0)


def savesetting(self):
    settings = QSettings(ORGANIZATION_NAME,APPLICATION_NAME)
    #settings = QSettings('config.ini',QSettings.IniFormat)
    settings.beginGroup('D')
    settings.setValue(SETTINGS_TRAY,self.geometry())
    settings.setValue("LineEdit",self.sectionnamecircularindata.text())
    settings.setValue("Selectitem",self.treewidget.currentItem())
    settings.setValue("Label",self.qlabelcirculardiameterselected)
    settings.endGroup()
    print('Saved', )
    #self.hide()   

def loadsetting(self):
    settings = QSettings(ORGANIZATION_NAME,APPLICATION_NAME)
    #settings = QSettings('config.ini',QSettings.IniFormat)
    settings.beginGroup('D')
    myrect = settings.value(SETTINGS_TRAY)
    restorelineEdit = settings.value("LineEdit",'')
    restoreselectsection = settings.value("Selectitem",)
    restoreqlabel =  settings.value("Label",'')
    self.setGeometry(myrect)
    self.sectionnamecircularindata.setText(restorelineEdit)
    self.treewidget.setCurrentItem(restoreselectsection)
    settings.endGroup()        

if __name__ == "__main__":

QCoreApplication.setApplicationName(ORGANIZATION_NAME)
QCoreApplication.setOrganizationDomain(ORGANIZATION_DOMAIN)
QCoreApplication.setApplicationName(APPLICATION_NAME)

app = QApplication(sys.argv)
subwindow=usedcircularshape()
subwindow.show()
app.exec()

What does the code do?

This is engineering software, to concrete columns, the idea is a user should be able to pick a standard section e.g. D350, D means diameter and 350 is diameter of circular concrete column in unit mm. User has ability to add custom shapes of circular columns. And when user click and select in from Qtreewidget a section, this section should remain and be global available for further calculation, which is not shown her. This is a widget to geometry defining within big concrete software. I would below further explain the code. First I create Qdialog and create Qtreewidget, an image for clarification and Qlabel based an item selection. Within Qtreewidget I first create a parentitem and give it an name “ Circular shapes”

self.firstparentitem = QTreeWidgetItem(self.treewidget)
self.firstparentitem.setText(0,'Circular shapes')

Then parentitem has children, to define and add those, first I create a list with standard sections

    standardsectionlist = ["D100","D150","D200","D250","D300","D350","D400","D450","D500","D550","D600","D650"
                       ,"D700","D750","D800","D850","D900","D950","D1000"]

And do for condition to add children to parentitem.

    for i in standardsectionlist:
    self.firstparentitem.addChild(QTreeWidgetItem(["%s"%i]))

later I define second parentitem and give a name “Customized” and define a list then later add item of list as child to second parentitem. For user should be able to add and remove childitem from second parentitem I create 2 buttons

self.addbutton = QPushButton("Add")
self.deletebutton = QPushButton("Delete")

Addbutton has a function to add children item to second parentitem. Deletebutton has a function to remove first childitem from second parentitem.

def currentitem(self):

currentitem function has a function when user click and select an item within Qtreewidget, it takes currenitem text and remove D letter from it and convert it into int, then display it as D = Diameter, A = Area ect…

def hidethiswindow(self)

hidethiswindow function this one guide the user either to select an item or to cancel the widget, in case user by mistake has selected headeritem, a error message will arise and tell to select child item. Powerfull is’t it!

The rest code is to display and Qsettings. Hope this explains the code.


Solution

  • What you have to do is save the data of each item for this I have created the itemToTuple method that returns a tuple of the data to be saved, also given that saved tuple you must set those properties and for that you use the tupleToItem function (if you want add more information of the items you only have to modify those methods). But to save all the items of QTreeWidget, the whole tree must be traversed. For this, the dataFromChild method that returns the data of the item and its children is used. The inverse process, that is, given the data must be established in all the items, dataToChild must be used, which establishes the respective data to the item and its children.

    Also I have divided the application into several classes to have it sorted, if you want the information of a widget to be saved you must create a method called writeSettings(...) with a format similar to the ones I show then the application will call in the closeEvent(...) method. Similarly, you can create the readSettings(...) method and call it in its constructor.

    import os
    import sys
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    iconroot = os.path.dirname(__file__)
    ORGANIZATION_NAME = 'Circularcolumn App'
    ORGANIZATION_DOMAIN = 'Circular shape'
    APPLICATION_NAME = 'QSettings program'
    SETTINGS_TRAY = 'settings/tray'
    
    QSS = """
    QTreeWidget{
        border:none;
     } 
    
    QTreeView::branch:has-siblings:!adjoins-item {
       border-image: url(images/vline.png) 0;
    }
    
    QTreeView::branch:has-siblings:adjoins-item {
        border-image: url(images/branch-more.png) 0;
    }
    
    QTreeView::branch:!has-children:!has-siblings:adjoins-item {
        border-image: url(images/branch-end.png) 0;
    }
    
    QTreeView::branch:has-children:!has-siblings:closed,
    QTreeView::branch:closed:has-children:has-siblings {
        border-image: none;
        image: url(images/branch-closed.png);
    }
    
    QTreeView::branch:open:has-children:!has-siblings,
    QTreeView::branch:open:has-children:has-siblings {
        border-image: none;
        image: url(images/branch-open.png);
    }
    """
    
    
    class TreeWidget(QtWidgets.QTreeWidget):
        currentTextChanged = QtCore.pyqtSignal(str)
    
        def __init__(self, parent=None):
            super(TreeWidget, self).__init__(parent)
            self.currentItemChanged.connect(self.onCurrentItemChanged)
            self.setHeaderLabel('Standard Section Library')
            self.setRootIsDecorated(True)
            self.setAlternatingRowColors(True)
            self.readSettings()
            self.expandAll()
    
        def onCurrentItemChanged(self, current, previous):
            if current not in [self.topLevelItem(ix) for ix in range(self.topLevelItemCount())]:
                self.currentTextChanged.emit(current.text(0))
    
        def readSettings(self):
            settings = QtCore.QSettings()
            settings.beginGroup("TreeWidget")
            values = settings.value("items")
            if values is None:
                self.loadDefault()
            else:
                TreeWidget.dataToChild(values, self.invisibleRootItem())
                self.customized_item = None
                for ix in range(self.topLevelItemCount()):
                    tlevel_item = self.topLevelItem(ix)
                    if tlevel_item.text(0) == "Customized":
                        self.customized_item = tlevel_item
            settings.endGroup()
    
        def writeSettings(self):
            settings = QtCore.QSettings()
            settings.beginGroup("TreeWidget")
            settings.setValue("items", TreeWidget.dataFromChild(self.invisibleRootItem()))
            settings.endGroup()
    
        def loadDefault(self):
            standardsectionlist = ["D100","D150","D200","D250","D300","D350","D400","D450","D500",
            "D550","D600","D650","D700","D750","D800","D850","D900","D950","D1000"]
            rootItem = QtWidgets.QTreeWidgetItem(self, ['Circular shapes'])
            rootItem.setIcon(0, QtGui.QIcon(os.path.join(iconroot,"images/circularcolumnnorebar.png")))
            for element in standardsectionlist:
                rootItem.addChild(QtWidgets.QTreeWidgetItem([element]))
    
            self.customized_item = QtWidgets.QTreeWidgetItem(self, ["Customized"])
            self.customized_item.setIcon(0, QtGui.QIcon(os.path.join(iconroot,"images/circularcolumnnorebar.png")))
    
        @staticmethod
        def dataToChild(info, item):
            TreeWidget.tupleToItem(info["data"], item)
            for val in info["childrens"]:
                child = QtWidgets.QTreeWidgetItem()
                item.addChild(child)
                TreeWidget.dataToChild(val, child)
    
        @staticmethod
        def tupleToItem(t, item):
            # set values to item
            ba, isSelected = t
            ds = QtCore.QDataStream(ba)
            ds >> item
            item.setSelected(isSelected) 
    
        @staticmethod
        def dataFromChild(item):
            l = []
            for i in range(item.childCount()):
                child = item.child(i)
                l.append(TreeWidget.dataFromChild(child))
            return {"childrens": l, "data": TreeWidget.itemToTuple(item)}
    
        @staticmethod
        def itemToTuple(item):
            # return values from item
            ba = QtCore.QByteArray()
            ds = QtCore.QDataStream(ba, QtCore.QIODevice.WriteOnly)
            ds << item
            return ba, item.isSelected()
    
    
    class InfoWidget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(InfoWidget, self).__init__(parent)
            hlay = QtWidgets.QHBoxLayout(self)
            plabel = QtWidgets.QLabel()
            pixmap = QtGui.QPixmap(os.path.join(iconroot, "images/circularcolumnnorebard.png"))\
                        .scaled(230, 230, QtCore.Qt.KeepAspectRatio)
            plabel.setPixmap(pixmap)
            hlay.addWidget(plabel)
            self.ilabel = QtWidgets.QLabel()
            hlay.addWidget(self.ilabel)
            hlay.addStretch()
            self.readSettings()
    
        @QtCore.pyqtSlot(str)
        def setData(self, text):
            try:
                circular_section = int(text.translate({ord('D'): ""}))
                area = (3.1416/4)*(circular_section**2)
                inertia = (3.1416/64)*circular_section**4
                fmt = "D = {}mm\nA = {:0.2E}mm2\n I  = {:0.2E}mm4"
                self.ilabel.setText(fmt.format(circular_section, area, inertia))
            except ValueError:
                pass
    
        def readSettings(self):
            settings = QtCore.QSettings()
            settings.beginGroup("InfoWidget")
            self.ilabel.setText(settings.value("text", ""))
            settings.endGroup()
    
        def writeSettings(self):
            settings = QtCore.QSettings()
            settings.beginGroup("InfoWidget")
            settings.setValue("text", self.ilabel.text())
            settings.endGroup()
    
    
    class CircularDialog(QtWidgets.QDialog):
        def __init__(self, parent=None):
            super(CircularDialog, self).__init__(parent)
            grid = QtWidgets.QGridLayout(self)
    
            self.tree = TreeWidget()
            self.infoWidget = InfoWidget()
    
            section_lay = QtWidgets.QHBoxLayout()
            section_label = QtWidgets.QLabel("Section name: ")
            section_edit = QtWidgets.QLineEdit('Define en name to section')
            section_lay.addWidget(section_label)
            section_lay.addWidget(section_edit)
    
            self.tree.currentTextChanged.connect(self.infoWidget.setData)
    
            button_layout = QtWidgets.QVBoxLayout()
            add_button = QtWidgets.QPushButton("Add")
            add_button.clicked.connect(self.addItem)
            delete_button = QtWidgets.QPushButton("Delete")
            delete_button.clicked.connect(self.removeItem)
            button_layout.addWidget(add_button, alignment=QtCore.Qt.AlignBottom)
            button_layout.addWidget(delete_button, alignment=QtCore.Qt.AlignTop)
    
            buttonBox = QtWidgets.QDialogButtonBox()
            buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
            buttonBox.accepted.connect(self.accept) 
            buttonBox.rejected.connect(self.reject)
    
            # for windows, posible bug
            self.accepted.connect(self.write_all_data)
            self.rejected.connect(self.write_all_data)
    
            grid.addLayout(section_lay, 0, 0)
            grid.addWidget(self.tree, 1, 0)
            grid.addLayout(button_layout, 1, 1)
            grid.addWidget(self.infoWidget, 2, 0, 1, 2)
            grid.addWidget(buttonBox, 3, 0, 1, 2)
            self.readSettings()
    
        def readSettings(self):
            settings = QtCore.QSettings()
            settings.beginGroup("CircularDialog")
            self.setGeometry(settings.value("geometry", QtCore.QRect(300, 300, 400, 600)))
            settings.endGroup()
    
        def writeSettings(self):
            settings = QtCore.QSettings()
            settings.beginGroup("CircularDialog")
            settings.setValue("geometry", self.geometry())
            settings.endGroup()
    
        def closeEvent(self, event):
            self.write_all_data()
            super(CircularDialog, self).closeEvent(event)
    
        def write_all_data(self):
            for children in self.findChildren(QtWidgets.QWidget) + [self]:
                if hasattr(children, "writeSettings"):
                    children.writeSettings()
    
        def addItem(self):
            text, ok = QtWidgets.QInputDialog.getText(self, "Add custom section", 
                "Enter section geometry f.ex as D325 or just 325 in mm: ")
            if ok:
                it = QtWidgets.QTreeWidgetItem([text])
                self.tree.customized_item.addChild(it)
    
        def removeItem(self):
            it = self.tree.customized_item.takeChild(0)
            del it
    
    
    if __name__ == '__main__':
        QtCore.QCoreApplication.setApplicationName(ORGANIZATION_NAME)
        QtCore.QCoreApplication.setOrganizationDomain(ORGANIZATION_DOMAIN)
        QtCore.QCoreApplication.setApplicationName(APPLICATION_NAME)
    
        app = QtWidgets.QApplication(sys.argv)
        app.setStyleSheet(QSS)
        w = CircularDialog()
        w.show()
        sys.exit(app.exec_())