QTreeWidget to Mirror python Dictionary

Question:

Is there a way to make a QTreeWidget mirror the changes made to an internal data structure such as dictionary? It seems like they would have created this functionality within the api, because there are many programs which may interact with QTreeWidgets from multiple GUI areas, but the main purpose required of the QTreeWidget is to show a data structure at any point in time. The documentation for QtGui items is not that simple for me to grasp as it usually refers to C documentation, and I’m not certain how it transfers to python.

So essentially what I would like is the simplest manner to make a QTreeWidget show a nested dictionary, where the top level corresponds to the keys and the sub level corresponds to the values. Also, if the values are dictionaries, use the keys in that level and make sub levels for the values, etc.

Is this easily doable? I have not been able to find anything to do simple mirroring of data structres like this yet.

Asked By: chase

||

Answers:

This is a straightforward implementation:

def fill_item(item, value):
  item.setExpanded(True)
  if type(value) is dict:
    for key, val in sorted(value.iteritems()):
      child = QTreeWidgetItem()
      child.setText(0, unicode(key))
      item.addChild(child)
      fill_item(child, val)
  elif type(value) is list:
    for val in value:
      child = QTreeWidgetItem()
      item.addChild(child)
      if type(val) is dict:      
        child.setText(0, '[dict]')
        fill_item(child, val)
      elif type(val) is list:
        child.setText(0, '[list]')
        fill_item(child, val)
      else:
        child.setText(0, unicode(val))              
      child.setExpanded(True)
  else:
    child = QTreeWidgetItem()
    child.setText(0, unicode(value))
    item.addChild(child)

def fill_widget(widget, value):
  widget.clear()
  fill_item(widget.invisibleRootItem(), value)

I added list support just in case anyone needs it.

Usage:

d = { 'key1': 'value1', 
  'key2': 'value2',
  'key3': [1,2,3, { 1: 3, 7 : 9}],
  'key4': object(),
  'key5': { 'another key1' : 'another value1',
            'another key2' : 'another value2'} }

widget = QTreeWidget()
fill_widget(widget, d)
widget.show()

Result:

screenshot

Answered By: Pavel Strakhov

Just because I recently needed this implementation for Python3 and PyQt5 here is a slightly shorter (and complete) port of the given example:

from PyQt5.QtWidgets import  QApplication, QTreeWidget, QTreeWidgetItem

class ViewTree(QTreeWidget):
    def __init__(self, value) -> None:
        super().__init__()
        self.fill_item(self.invisibleRootItem(), value)

    @staticmethod
    def fill_item(item: QTreeWidgetItem, value) -> None:
        if value is None:
            return
        elif isinstance(value, dict):
            for key, val in sorted(value.items()):
                ViewTree.new_item(item, str(key), val)
        elif isinstance(value, (list, tuple)):
            for val in value:
                if isinstance(val, (str, int, float)):
                    ViewTree.new_item(item, str(val))
                else:
                    ViewTree.new_item(item, f"[{type(val).__name__]}", val)
        else:
            ViewTree.new_item(item, str(value))

    @staticmethod
    def new_item(parent: QTreeWidgetItem, text:str, val=None) -> None:
        child = QTreeWidgetItem([text])
        ViewTree.fill_item(child, val)
        parent.addChild(child)
        child.setExpanded(True)


if __name__ == '__main__':
    app = QApplication([])
    window = ViewTree({
        'key1': 'value1',
        'key2': [1, 2, 3, {1: 3, 7: 9}]})
    window.show()
    app.exec_()

The code isn’t a good example for today python coding, though. It’s hard to read, poorly type-hintable, and doesn’t cover all possible combinations. Please don’t use it as-is.

Answered By: frans

I found recursion makes it pretty slow while making a tree structure of a huge directory structure. The below method might help.

  def update_right_dock(self, file_list):   
            obj_list = []
            maps = []
            level = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]         #Stores Maximum and Current Levels For The TreeWidget
            level_name = ["", "", "", "", "", "", "", "", "", "", "", "", "", ""]   #Stores Previous File Path To Compare before adding file or folder to tree
            tree.clear()
            prev = ""
            tot_len = 2

            p = 0
            for file in file_list:
                if(os.path.isdir(file)):
                    is_file = 0
                else:
                    is_file = 1

                tmp_map = []

                file = file[1:]
                abs_path = file.split('/')
                abs_path_len = len(abs_path)
                filename = abs_path[-1]

                if(prev == file[:tot_len - 1]):
                    #print("LOOOOOOOOOOOOP ------ 1")
                    while (i < abs_path_len - 1):
                        level[level_counter + 1] = QTreeWidgetItem(level[level_counter], [abs_path[i]])

                        tmp_map.append(level[level_counter + 1])
                        tmp_map.append(level_counter + 1)
                        tmp_map.append(1)
                        obj_list.append(tmp_map)
                        tmp_map = []

                        level[level_counter + 1].setCheckState(0, Qt.Checked)
                        tree.expandItem(level[level_counter + 1])
                        level_counter = level_counter + 1
                        level_name[i] = abs_path[i]
                        i = i + 1
                    level[level_counter + 1] = QTreeWidgetItem(level[level_counter], [abs_path[i]])

                    tmp_map.append(level[level_counter + 1])
                    tmp_map.append(level_counter + 1)
                    tmp_map.append(0)
                    obj_list.append(tmp_map)
                    tmp_map = []

                    level[level_counter + 1].setCheckState(0, Qt.Checked)
                    tree.expandItem(level[level_counter + 1])

                    file_len = len(filename)
                    tot_len = len(file) - file_len
                    prev = file[:tot_len - 1]
                    continue


                len2 = len(level_name)
                k = 0
                while k < abs_path_len and k < len2:
                    if (level_name[k] == abs_path[k]):
                        k  = k + 1
                        continue
                    break
                level_counter = k + 1
                i = level_counter - 1
                while k < abs_path_len:
                    level_name[k] = abs_path[k]
                    k = k + 1

                if level_counter > 1:
                    #print("LOOOOOOOOOOOOP ------ 2")
                    if(i == abs_path_len - 1):
                        level_counter = level_counter - 1
                    while i < abs_path_len - 1:
                        level[level_counter] = QTreeWidgetItem(level[level_counter - 1], [abs_path[i]])

                        tmp_map.append(level[level_counter])
                        tmp_map.append(level_counter)
                        tmp_map.append(1)
                        obj_list.append(tmp_map)
                        tmp_map = []

                        level[level_counter].setCheckState(0, Qt.Checked)
                        tree.expandItem(level[level_counter])
                        level_counter = level_counter + 1
                        level_name[i] = abs_path[i]

                        i = i + 1
                        if i == abs_path_len - 1:
                                level_counter = level_counter - 1

                    level[level_counter + 1] = QTreeWidgetItem(level[level_counter], [abs_path[i]])

                    tmp_map.append(level[level_counter + 1])
                    tmp_map.append(level_counter + 1)
                    tmp_map.append(0)
                    obj_list.append(tmp_map)
                    tmp_map = []

                    level[level_counter + 1].setCheckState(0, Qt.Checked)
                    tree.expandItem(level[level_counter + 1])

                    file_len = len(filename)
                    tot_len = len(file) - file_len
                    prev = file[:tot_len - 1]
                    continue

                if(abs_path_len == 1):
                    level[level_counter + 1] = QTreeWidgetItem(tree, [abs_path[i]])

                    tmp_map.append(level[level_counter + 1])
                    tmp_map.append(level_counter + 1)
                    tmp_map.append(0)
                    obj_list.append(tmp_map)
                    tmp_map = []

                    level[level_counter + 1].setCheckState(0, Qt.Checked)
                    tree.expandItem(level[level_counter + 1])
                    continue

                i = 1    
                #print("LOOOOOOOOOOOOP ------ 3")
                level[level_counter] = QTreeWidgetItem(tree, [abs_path[0]])

                tmp_map.append(level[level_counter])
                tmp_map.append(level_counter)
                tmp_map.append(1)
                obj_list.append(tmp_map)
                tmp_map = []

                level[level_counter].setCheckState(0, Qt.Checked)
                tree.expandItem(level[level_counter])
                level_name[level_counter - 1] = abs_path[0]       

                while i < abs_path_len - 1:
                    level[level_counter + 1] = QTreeWidgetItem(level[level_counter], [abs_path[i]])
                    tmp_map.append(level[level_counter + 1])
                    tmp_map.append(level_counter + 1)
                    tmp_map.append(1)
                    obj_list.append(tmp_map)
                    tmp_map = []

                    level[level_counter + 1].setCheckState(0, Qt.Checked)
                    tree.expandItem(level[level_counter + 1])
                    level_counter = level_counter + 1
                    level_name[i] = abs_path[i]
                    if i == abs_path_len - 1:
                            level_counter = level_counter - 1
                    i = i + 1

                level[level_counter + 1] = QTreeWidgetItem(level[level_counter], [abs_path[i]])
                tmp_map.append(level[level_counter + 1])
                tmp_map.append(level_counter + 1)
                tmp_map.append(0)
                obj_list.append(tmp_map)
                tmp_map = []

                level[level_counter + 1].setCheckState(0, Qt.Checked)
                tree.expandItem(level[level_counter + 1])
                level_name[i] = abs_path[i]

                file_len = len(filename)
                tot_len = len(file) - file_len
                prev = file[:tot_len - 1]
                p = p + 1
Answered By: Asit Rout

Few days ago i searched such theme, but all i could find – how to fill tree, but not how to extract it. So i write some. Maybe someone still find this useful.
Thanks to the prev author, i used his code with some improvements.
This will load your collection (it may be list or dict or tuple with many children)

def load_tree(self, d):
    self.fill_widget(self.tree, d)

def fill_widget(self, widget, value):
    widget.clear()
    self.fill_item(widget.invisibleRootItem(), value)

def fill_item(self, item, value):
    def new_item(parent, text):
        child = QTreeWidgetItem([str(text)])
        if text not in ("[dict]", "[list]", "[tuple]"):
            child.setFlags(child.flags() | Qt.ItemIsEditable)
        parent.addChild(child)
        child.setExpanded(True)

        return child

    if isinstance(value, dict):
        new_parent = new_item(item, f"[{value.__class__.__name__}]")
        for elem_k, elem_v in value.items():
            sub_parent = new_item(new_parent, elem_k)
            self.fill_item(sub_parent, elem_v)
    elif isinstance(value, (tuple, list)):
        new_parent = new_item(item, f"[{value.__class__.__name__}]")
        for val in value:
            self.fill_item(new_parent, val)
    else:
        new_item(item, f"{value}")

And extract code a little bit more.. complicated. If someone can make it more fancy – pls do.

def get_dict(self):
    result = None

    def unpack(to_unpack, key, source=None):
        for child_index in range(to_unpack.childCount()):
            child = to_unpack.child(child_index)
            child_text = child.text(0)
            try:
                child_text = float(child_text)
            except ValueError:
                try:
                    child_text = int(child_text)
                except ValueError:
                    pass

            if source is None:
                core = result
            else:
                core = source

            if key == "[dict]":
                core.update({child_text: None})
                if child.childCount() > 0:
                    unpack(child, child_text, core)
            elif key == "[list]" or key == "[tuple]":
                if child_text == "[dict]":
                    core.append({})
                elif child_text == "[list]" or child_text == "[tuple]":
                    core.append([])
                else:
                    core.append(child_text)

                if child.childCount() > 0:
                    unpack(child, child_text, core[child_index])
            else:
                if child_text == "[dict]":
                    core.update({key: {}})
                elif child_text == "[list]" or child_text == "[tuple]":
                    core.update({key: []})
                else:
                    core.update({key: child_text})

                if child.childCount() > 0:
                    unpack(child, child_text, core[key])

    for index in range(self.tree.topLevelItemCount()):
        parent = self.tree.topLevelItem(index)
        element_text = parent.text(0)
        if element_text == "[dict]":
            result = {}
            unpack(parent, element_text)
        elif element_text == "[list]" or element_text == "[tuple]":
            result = []
            unpack(parent, element_text)
        else:
            result = element_text

    return result

Where self.tree – QTreeWidget object in your window.

Answered By: Windy

Thanks for providing the above examples.

I recently needed to convert a python list, via a python dictionary, to reside in a PyQt6 TreeWidget. My requirements were not as complex as the above examples cater for, however my simplified code may be of some use…

#!/usr/bin/env python3
from PyQt6.QtWidgets import QApplication, QMainWindow, QTreeWidget, QTreeWidgetItem 
from PyQt6.QtCore import Qt
HEADING = "Food Items"
# Initial messy list. Unsorted, has duplicates, case issues, no food starting with "D"  
food_list = ["apples", "apples","CAUliflower", "BEETroot", "eggplant", 
        "broccoli", "avocados", "CABBAGE", "carrots", "carrots"] 

def convert_list_to_dictionary(a_list):
    #Capitalize to fix case issues
    a_list = list(map(str.capitalize,a_list))            
    # Remove duplicates in the list                    
    a_list = list(dict.fromkeys(a_list))
    # Sort
    a_list.sort()
    # Convert to dictionary       
    a_dict = {}
    for item in a_list:
        start_character = item[:1]
        if not start_character in a_dict:
            a_dict[start_character] = [item]
        else:
            a_dict[start_character].append(item)
    return a_dict
 
    
class MainWindow(QMainWindow):
    def __init__(self, dictionary):
        super().__init__()
        # Setup TreeWidget
        self.tree = QTreeWidget()
        self.setCentralWidget(self.tree)
        self.tree.clear()
        self.tree.setHeaderLabel(HEADING)        
        self.tree.setColumnCount(1)
        self.tree.clicked.connect(self.treewidget_clicked)
        # Add dictionary to Tree
        for keyword, element_list in dictionary.items():
            parent = QTreeWidgetItem([str(keyword)])
            parent.setFlags(parent.flags() & ~ Qt.ItemFlag.ItemIsSelectable) 
            self.tree.invisibleRootItem().addChild(parent)                            
            for element in element_list:
                child = QTreeWidgetItem([str(element)])
                parent.addChild(child)                    

    def treewidget_clicked(self, model_index):
        #print(model_index) # PyQt6.QtCore.QModelIndex object
        #print(model_index.flags().value) # 60 parent or 61 child        
        # Check if ItemIsSelectable exists based on model_index.flags()
        if not model_index.flags() & Qt.ItemFlag.ItemIsSelectable:
            #print("Item is not selectable")
            return
        print("Selected Item: {}".format(model_index.data()))
            # Do stuff with model_index.data()...
            
if __name__ == '__main__':
    food_dict = convert_list_to_dictionary(food_list) 
    app = QApplication([])
    w = MainWindow(food_dict)
    w.show()
    app.exec()
Answered By: irs
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.