-1

Following on from an earlier question I can now create a Pyqt QTreeWidget from a dictionary, edit the tree and save it to and edited dictionary. However when I comes to dictionary values which contain a list of dictionaries, it compiles the values under the same parent (see dictionary where the 'Property' key contains a list of dictionaries, and output to QTreeWidget below)

self.d = {'TestName': {'Ref': 'ABC/DEF', 'Property': [{'Number': '2', 'Zipcode': '0002234',
                                                           'KeyAvailable': 'Yes'}, {'Number': '3',
                                                                                    'Zipcode': '2342444'}]}}

QtreeWidget

For neatness it would probably be nicer to have another parent of 'Property' for the second list item (dictionary). The main problem I have is converting back into a dictionary, as at the moment the last entry overwrites the previous, with the additional problem if there's an extra key in one of the dictionaries (in this case 'KeyAvailable').

Here's the output converting back to a dictionary:

{'TestName': {'Ref': 'ABC/DEF', 'Property': {'Number': '3', 'Zipcode': '2342444', 'KeyAvailable': 'Yes'}}}

Is there a simple solution for handling these list instances in my 'tree_from_dict' and 'tree_2_dict' functions below?

import sys
from PyQt5.QtWidgets import QWidget, QApplication, QTreeWidget, QTreeWidgetItem, QPushButton, QMainWindow
from PyQt5.QtCore import *


class MyMainWindow(QMainWindow):

    def __init__(self, dialog):
        QMainWindow.__init__(self)

        self.d = {'TestName': {'Ref': 'ABC/DEF', 'Property': [{'Number': '2', 'Zipcode': '0002234',
                                                               'KeyAvailable': 'Yes'}, {'Number': '3',
                                                                                        'Zipcode': '2342444'}]}}

        self.setWindowTitle('Example')
        self.setGeometry(20, 20, 400, 400)

        central = QWidget(self)
        self.setCentralWidget(central)

        self.button = QPushButton('Save button', central)
        self.button.move(100, 350)

        self.tree = QTreeWidget(self.centralWidget())
        self.tree.setGeometry(QRect(30, 30, 300, 300))

        self.tree.setColumnCount(2)
        self.tree.setHeaderLabels(["XML Property", "Value"])

        self.tree.itemDoubleClicked.connect(self.editItem)
        self.button.clicked.connect(self.save_changes)

        self.tree_from_dict(data=self.d, parent=self.tree)

    def editItem(self, item, column):

        try:
            if column == 1:
              item.setFlags(item.flags() | Qt.ItemIsEditable)
            else:
                pass
        except Exception as e:
            print(e)

    def tree_from_dict(self, data=None, parent=None):
        for key, value in data.items():
            item = QTreeWidgetItem(parent)

            item.setText(0, key)

            if isinstance(value, dict):
                self.tree_from_dict(data=value, parent=item)
            elif isinstance(value, list):
                [self.tree_from_dict(i, parent=item) for idx, i in enumerate(value)]

            else:
                item.setText(1, value)

    def save_changes(self):
        d = self.tree_2_dict(self.tree.invisibleRootItem())
        print(d)

    def tree_2_dict(self, parent, d=None):
        if d is None:
            d = {}
        for index in range(parent.childCount()):
            child = parent.child(index)
            if child.childCount():
                self.tree_2_dict(child, d.setdefault(child.text(0), {}))
            else:
                d[child.text(0)] = child.text(1)
        return d


if __name__ == '__main__':
    app = QApplication(sys.argv)
    dialog = QMainWindow()
    foo = MyMainWindow(dialog)
    foo.show()
    sys.exit(app.exec_())
    
MikG
  • 1,019
  • 1
  • 15
  • 34
  • Do you have control over the structure, or is it coming from a third-party? Do you need to preserve that specific data structure within your application? You need to decide how you want to present the information *first*, and then design an appropriate data structure to suit that. – ekhumoro Sep 29 '21 at 12:02
  • Hi @ekhumoro, I'm using xmltodict, however just on the 'Body' part of my XMLs. I then look to present the body tags and values to a user by presenting the dict in the form of a QTreeWidget, allowing them to make any edits. I'm then looking to convert these edits back into a dict, for use later on in creating an XML. Duplicate tag names in the XML are converted into a single key with a lists of dicts by xmltodict, so I am looking to work with that format, to make it easier to create the XML through an xmltodict.unparse function. – MikG Sep 29 '21 at 12:23
  • I should add, Ideally I'd want to present duplicate parents in the QTreeview, for each list item (dict), but i'm more concerned to work with the xmltodict format when converting the Qtreewidget back to a dict – MikG Sep 29 '21 at 12:26
  • Would your solution consist of containing multiple _Property_ fields or would you rather prefer to have only one (as shown in your example) ? Do you want to obtain the original XML structure when calling the function `tree_2_dict`? – angelogro Sep 29 '21 at 16:48
  • Hi @angelogro, ideally I'd hope to have multiple 'Property' fields in the QTreeWidget, to give a cleaner look, or if there's a clearer way to separate the two dict contents under the same 'Property' parent? But if that's tricky to do, my main concern is mostly around getting the original dictionary structure back again when running the tree_2_dict function after any edits have been made in the QTreeWidget, which would eventually enable me to run an xmltodict.unparse to build the XML back again. – MikG Sep 29 '21 at 17:46

1 Answers1

0

I'm quite sure this is not the most elegant solution. The function now checks whether a list is passed as data and adds the property as many times as there are elements in that list.

def tree_from_dict(self, data=None, parent=None):
    for key, value in data.items():
        item = QTreeWidgetItem(parent)
        item.setText(0, key)

        if isinstance(value, dict):               
            self.tree_from_dict(data=value, parent=item)
        elif isinstance(value, list):               
            for idx, i in enumerate(value):
                if idx!=0:
                    item = QTreeWidgetItem(parent)
                    item.setText(0, key)
                self.tree_from_dict(i, parent=item)
        else:
            item.setText(1, value)

The other function now additionally checks whether a property is already a key of the dict. If so it converts the corresponding value to a list and appends the new value.

 def tree_2_dict(self, item, dic):
    # Variable for saving all occuring property names in that scope
    childrenNames=[]
    for index in range(item.childCount()):
        child = item.child(index)
        if child.text(0) not in childrenNames:
            childrenNames.append(child.text(0))
            if child.childCount():
                dic[child.text(0)]=self.tree_2_dict(child, {})
            else:
                dic[child.text(0)] = child.text(1)
        else:
            ele = dic[child.text(0)]
            
            if child.childCount():
                child_dic =self.tree_2_dict(child,  {})
                if isinstance(ele,list):
                    ele.append(child_dic)
                else:
                    ele=[ele,child_dic]
            else:
                if isinstance(ele,list):
                    ele.append(child.text(1))
                else:
                    ele=[ele,child.text(1)]
            dic[child.text(0)] = ele
    return dic
angelogro
  • 124
  • 8
  • Thanks @angelogro, this is exactly what I was hoping to achieve. The QTreeWidget looks much better with having duplicate parent names in the tree rather than having all children bundled under a single parent. I'll take a while to go over what you've done. Thanks again! – MikG Sep 30 '21 at 08:46