pythonpyside6

How to acces object in QTreeView


I have a script that contains 4 classes: class Node, class TreeViewModel, class Prj_Collection and class Prj.

First I create Prj objects using Prj class that are added to the dictionary using Prj_Collection class. Then dictionary is converted to a list and attribute "name" of each object is represented in QTreeView using class Node and class TreeViewModel. I have a "selection_changed" function that returns the "number" attribute of selected object when selection is changed.

The way how right now everything is implemented is that I populate QTreeView with strings that doesn't have any reference to a Prj object. And when selection is changed I do a search through all objects to find an object for which "name" atribute and selected item in QTreeView match in order to find object of interest and have access to other attributes of that object, e.g. "number" attribute.

I guess that there should be a way that QTreeView can contain a reference directly to the Prj object and not to the string. But how can I implement it, that TreeViewModel will contain information about Prj objects and not just strings?

Here is the code for aforementioned 4 classes and "selection_changed" function:

import sys, os
from PySide6 import QtCore
from PySide6.QtWidgets import QTreeView, QApplication
from PySide6.QtCore import QAbstractItemModel, QModelIndex
import random
class Node(object):
    def __init__(self, name="", parent=None):
        self._parent = parent
        self._name = name
        self._children = []

    def children(self):
        return self._children

    def hasChildren(self):
        return bool(self.children())

    def parent(self):
        return self._parent

    def name(self):
        return self._name

    def set_name(self, name):
        self._name = name

    def type_info(self):
        return 'NODE'

    def columnCount(self):
        return 1

    def child_count(self):
        return len(self._children)

    def add_child(self, child):
        self._children.append(child)
        child._parent = self

    def insert_child(self, position, child):
        if 0 <= position < self.child_count():
            self._children.insert(position, child)
            child._parent = self
            return True
        return False

    def remove_child(self, position):
        if 0 <= position < len(self._children):
            child = self._children.pop(position)
            child._parent = None
            return True
        return False

    def child(self, row):
        if 0 <= row < self.child_count():
            return self._children[row]

    def row(self):
        if self._parent is not None:
            return self._parent._children.index(self)
        return -1

    def find_child_by_name(self, name):
        for child in self._children:
            if child.name() == name:
                return child
        return None

    def log(self, tab_level=-1):
        output = ''
        tab_level += 1

        for i in range(tab_level):
            output += '\t'

        output += '|____' + self._name + '\n'

        for child in self._children:
            output += child.log(tab_level)

        tab_level -= 1

        return output

    def __repr__(self):
        return self.log()

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class TreeViewModel(QAbstractItemModel):
    def __init__(self, opened_fits=None, parent=None):
        super().__init__(parent)
        self._root_node = Node()
        self.opened_fits = opened_fits or []

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            if section == 0:
                return 'Name'
            else:
                return 'Type'

    def index(self, row, column, parent):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()
        node = parent.internalPointer() if parent.isValid() else self._root_node
        if node.children:
            return self.createIndex(row, column, node.child(row))
        else:
            return QModelIndex()

    def parent(self, child):
        if not child.isValid():
            return QModelIndex()
        node = child.internalPointer()
        if node.row() >= 0:
            return self.createIndex(node.row(), 0, node.parent())
        return QModelIndex()

    def rowCount(self, parent=QModelIndex()):
        node = parent.internalPointer() if parent.isValid() else self._root_node
        return node.child_count()

    def columnCount(self, parent=QModelIndex()):
        return 1

    def hasChildren(self, parent=QModelIndex()):
        node = parent.internalPointer() if parent.isValid() else self._root_node
        return node.hasChildren()

    def data(self, index: QModelIndex(), role=QtCore.Qt.DisplayRole):
        if index.isValid() and role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole,):
            node = index.internalPointer()
            return node.name()

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if role in (QtCore.Qt.EditRole,):
            node = index.internalPointer()
            node.set_name(value)
            self.dataChanged.emit(index, index)
            return True
        return False

    def flags(self, index: QModelIndex()):
        return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable)

    def indexFromItem(self, it):
        root_index = QModelIndex()
        if isinstance(it, Node):
            parents = []
            while it is not self._root_node:
                parents.append(it)
                it = it.parent()
            root = self._root_node
            for parent in reversed(parents):
                root = root.find_child_by_name(parent.name())
                root_index = self.index(root.row(), 0, root_index)
        return root_index

    def item_from_path(self, path, sep):
        depth = path.split(sep)
        root = self._root_node
        for d in depth:
            root = root.find_child_by_name(d)
            if root is None:
                return None
        return root

    def appendRow(self, item, parent=None):
        self.appendRows([item], parent)

    def appendRows(self, items, parent=None):
        if isinstance(items, list):
            ix = self.indexFromItem(parent)
            self.insertRows(self.rowCount(ix), items, parent)

    def insertRows(self, position, items, parent=None):
        parent_index = self.indexFromItem(parent)
        self.beginInsertRows(parent_index, position, position + len(items) - 1)
        if parent is None:
            parent = self._root_node
        for item in items:
            parent.add_child(item)
        self.endInsertRows()
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Prj_Collection(dict):
    def __init__(self, *args, **kwargs):
        super(Prj_Collection, self).__init__(*args, **kwargs)

    def prj(self, *args, **kwargs):
        f = Prj(*args, **kwargs)
        if os.path.basename(f.project_path) in self:
            self[os.path.basename(f.project_path)].append(f)
        else:
            self[os.path.basename(f.project_path)] = [f]
        return f
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Prj(object):
    def __init__(self, name, project_path):
        self.name = name
        self.project_path = project_path
        self.number = random.randint(1, 100)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def selection_changed(selected, deselected):
    # return selected index which is QModelIndex
    model_index = selected.indexes()[0]
    # Return data correcponding to QModelIndex
    print(model.data(model_index))
    # Dictionary of objects to list
    _list = list(map(list, dictionary.items()))
    # Return an object that correcponds to the selected item in a QTreeView
    for item in _list:
        for x in item[1]:
            if x.name == model.data(model_index):
                print("i found object!")
                print(x.number)
                break
            else:
                x = None

Here is the code for populating data and representing in a QTreeView:

# populate data
folder = ["project1", "project2", "project3"]

dictionary = Prj_Collection()
for item in folder:
    dictionary.prj(item, "folder")
#represent data in a QTreeView
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
app = QApplication(sys.argv)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
view = QTreeView()
model = TreeViewModel()
view.setModel(model)
model.appendRow(Node("folder"))
_list = list(map(list, dictionary.items())) # make a list from dictionary
for root, dirs in _list:
    depth = root.split(os.sep)
    if not root.startswith("folder"):
        root = "folder"
    it = model.item_from_path(root, os.sep)
    (model.appendRows([Node(_dir.name) for _dir in dirs], it))
view.show()
view.selectionModel().selectionChanged.connect(selection_changed)
sys.exit(app.exec())

Solution

  • I found an example that used QAbstractItemModel with QTreeView for accessing objects. I adjust code from the example to my need with PySide5:

    from PySide6 import QtGui, QtCore
    from PySide6.QtWidgets import QTreeView, QApplication, QDialog, QVBoxLayout, QLabel, QFrame, QHBoxLayout, QPushButton
    import random
    
    HORIZONTAL_HEADERS = ("Project", "Folder")
    
    
    class PrjCollection(list):
        def __init__(self, *args, **kwargs):
            super(PrjCollection, self).__init__(*args, **kwargs)
    
        def create_prj(self, *args, **kwargs):
            p = Prj(*args, **kwargs)
            self.append(p)
            return p
    
    class Prj(object):
        def __init__(self, name, folder):
            self.folder = folder
            self.name = name
            self.number = random.randint(0, 100)
    
        def __repr__(self):
            return "PROJECT - %s %s" % (self.name, self.folder)
    
    
    class TreeItem(object):
        def __init__(self, project, header, parent_item):
            self.project = project
            self.parent_item = parent_item
            self.header = header
            self.children = []
    
        def add_child(self, child):
            self.children.append(child)
    
        def child(self, row):
            return self.children[row]
    
        def child_count(self):
            return len(self.children)
    
        def column_count(self):
            return 1
    
        def data(self, column):
            if self.project == None:
                if column == 0:
                    # return QtCore.QVariant(self.header)
                    return self.header
                if column == 1:
                    # return QtCore.QVariant("")
                    return ""
            else:
                if column == 0:
                    # return QtCore.QVariant(self.project.name)
                    return self.project.name
                if column == 1:
                    # return QtCore.QVariant(self.project.folder)
                    return self.project.folder
            # return QtCore.QVariant()
            return None
    
        def parent(self):
            return self.parent_item
    
        def row(self):
            if self.parent_item:
                return self.parent_item.children.index(self)
            return 0
    
    
    class TreeModel(QtCore.QAbstractItemModel):
        def __init__(self, projects, parent=None):
            super(TreeModel, self).__init__(parent)
            self.projects = projects or []
            self.root_item = TreeItem(None, "All", None)
            self.parents = {0: self.root_item}
            self.setup_model_data()
    
        def columnCount(self, parent=None):
            if parent and parent.isValid():
                return parent.internalPointer().column_count()
            else:
                return 1
    
        def data(self, index, role):
            if not index.isValid():
                # return QtCore.QVariant()
                return None
            item = index.internalPointer()
            if role == QtCore.Qt.DisplayRole:
                return item.data(index.column())
            if role == QtCore.Qt.UserRole:
                if item:
                    return item.project
    
            # return QtCore.QVariant()
            return None
    
        def headerData(self, column, orientation, role):
            if (orientation == QtCore.Qt.Horizontal and
                    role == QtCore.Qt.DisplayRole):
                try:
                    # return QtCore.QVariant(HORIZONTAL_HEADERS[column])
                    return HORIZONTAL_HEADERS[column]
                except IndexError:
                    pass
            # return QtCore.QVariant()
            return None
    
        def index(self, row, column, parent):
            if not self.hasIndex(row, column, parent):
                return QtCore.QModelIndex()
    
            if not parent.isValid():
                parentItem = self.root_item
            else:
                parentItem = parent.internalPointer()
    
            childItem = parentItem.child(row)
            if childItem:
                return self.createIndex(row, column, childItem)
            else:
                return QtCore.QModelIndex()
    
        def parent(self, index):
            if not index.isValid():
                return QtCore.QModelIndex()
    
            childItem = index.internalPointer()
            if not childItem:
                return QtCore.QModelIndex()
    
            parentItem = childItem.parent()
    
            if parentItem == self.root_item:
                return QtCore.QModelIndex()
    
            return self.createIndex(parentItem.row(), 0, parentItem)
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            if parent.column() > 0:
                return 0
            if not parent.isValid():
                p_Item = self.root_item
            else:
                p_Item = parent.internalPointer()
            return p_Item.child_count()
    
        def setup_model_data(self):
            for project in self.projects:
                folder = project.folder
                if folder not in self.parents:
                    newparent = TreeItem(None, folder, self.root_item)
                    self.root_item.add_child(newparent)
                    self.parents[folder] = newparent
    
                parentItem = self.parents[folder]
                newItem = TreeItem(project, "", parentItem)
                parentItem.add_child(newItem)
    
        def searchModel(self, project):
            def searchNode(node):
                for child in node.children:
                    if project == child.project:
                        index = self.createIndex(child.row(), 0, child)
                        return index
    
                    if child.child_count() > 0:
                        result = searchNode(child)
                        if result:
                            return result
    
            retarg = searchNode(self.parents[0])
            return retarg
    
        def find_GivenName(self, name):
            app = None
            for project in self.projects:
                if project.name == name:
                    app = project
                    break
            if app != None:
                index = self.searchModel(app)
                return (True, index)
            return (False, None)
    
    
    if __name__ == "__main__":
        def row_clicked(index):
            print(tv.model().data(index, QtCore.Qt.UserRole).number)
    
        app = QApplication([])
    
        prj_col = PrjCollection()
    
        for name, folder in (("Project1", "Folder1"),
                             ("Project2", "Folder1"), ("Project11", "Folder2")):
            prj_col.create_prj(name, folder)
    
        model = TreeModel(prj_col)
    
        dialog = QDialog()
    
        dialog.setMinimumSize(300, 150)
        layout = QVBoxLayout(dialog)
    
        tv = QTreeView(dialog)
        tv.setModel(model)
        tv.setAlternatingRowColors(True)
        layout.addWidget(tv)
    
        tv.clicked[QtCore.QModelIndex].connect(row_clicked)
        dialog.exec()
    
        app.closeAllWindows()