Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

PyQt TreeView model from YAML file



  • I'm trying to write a GUI editor for a tree data structure in the YAML format and I'm having some issues with creating the model for PyQt.

    My test code for the data model (below) seems to run but the data is not showing up in the QTreeView widget. I'm totally lost at this point. Right now I'm just trying to get the data from the YAML file (see bottom of post) to show up in the window. My objective in the end is to have that data be editable in the window with import and export functionality.

    Code so far

    from anytree.importer import DictImporter
    from PyQt5 import QtWidgets as qtw
    from PyQt5 import QtCore as qtc
    from anytree import AnyNode
    from PyQt5.QtCore import Qt
    from pathlib import Path
    import yaml
    import sys
    
    
    class TreeItem:
    	def __init__(self, node):
    		self._node = node
    		self._parent = getattr(node, 'parent', None)
    		self._children = getattr(node, 'children', list())
    		self._name = getattr(node, 'name', '')
    
    	@property
    	def parent(self):
    		return self._parent
    
    	@property
    	def children(self):
    		return self._children
    
    	@children.setter
    	def children(self, child_node):
    		"""Add a child to the list of children"""
    		self._children.append(child_node)
    
    	def childCount(self):
    		"""Return the number of children"""
    		return len(self._children)
    
    	def child(self, row):
    		return self._children[row]
    
    	def columnCount(self):
    		return 1
    
    	def data(self, column):
    		try:
    			return self._node.children[column]
    		except TypeError:
    			return self._node.root
    
    	def row(self):
    		if self._parent:
    			return self._parent._children.index(self)  # noqa
    		return 0
    
    
    class TreeModel(qtc.QAbstractItemModel):
    	"""Custom model"""
    	def __init__(self, data=None, parent=None, fn=None):
    		super().__init__(parent)
    		self._root_item = TreeItem(AnyNode(name='rootItem', id=0))
    		self._parent = parent
    		self._data = data
    		self._fn = fn
    
    	@property
    	def root_item(self):
    		return self._root_item
    
    	@root_item.setter
    	def root_item(self, r):
    		self._root_item = r
    
    	@property
    	def model_data(self):
    		return self._data
    
    	@model_data.setter
    	def model_data(self, d):
    		self._data = d
    
    	@property
    	def fn(self):
    		return self._fn
    
    	@fn.setter
    	def fn(self, f):
    		self._fn = f
    
    	def setupTreeModel(self):
    		if self._data:
    			self.load_from_dict()
    		elif self._fn and Path(self._fn).exists():
    			self.load_from_dict(data_dict=self.load_from_file())
    		return True
    
    	def load_from_dict(self, data_dict=None):
    		"""Load data from a dictionary produced by anytree"""
    		assert isinstance(data_dict, AnyNode), ('data_dict must be of type '
    												'`AnyNode`')
    		self._data = data_dict
    		self._root_item = TreeItem(self._data.root)
    		return
    
    	def load_from_file(self, fn=None):
    		"""Load data from YAML file
    
    			:param fn: The filepath to the input YAML file
    			:type fn: Pathlike, str, None
    		"""
    		if self._fn is None:
    			self.fn = fn
    		with open(self.fn, 'r') as fin:
    			data = yaml.load(fin, Loader=yaml.FullLoader)
    		return DictImporter().import_(data)
    
    	def columnCount(self, parent):  # noqa
    		if parent.isValid():
    			return parent.internalPointer().columnCount()
    		else:
    			return -1
    
    	def data(self, index, role):  # noqa
    		if not index.isValid():
    			return qtc.QVariant()
    		if role != Qt.DisplayRole:
    			return qtc.QVariant()
    		item = index.internalPointer()
    		return item.data(index.column())
    
    	def flags(self, index):
    		"""Ensure that views know that the model is read-only"""
    		if not index.isValid():
    			return Qt.NoItemFlags
    		return Qt.ItemIsEnabled | Qt.ItemIsSelectable
    
    	def headerData(self, section, orientation, role):  # noqa
    		if orientation == Qt.Horizontal and role == Qt.DisplayRole:
    			return self.rootItem.data(section)
    		return qtc.QVariant()
    
    	def index(self, row, column, parent):  # noqa
    		if not self.hasIndex(row, column, parent):
    			return qtc.QModelIndex()
    		if not parent.isValid():
    			parentItem = self.rootItem
    		else:
    			parentItem = parent.internalPointer()
    		childItem = parentItem.child(row)
    		if childItem:
    			return self.createIndex(row, column, childItem)
    		else:
    			return qtc.QModelIndex()
    
    	def parent(self, index):
    		"""TODO: What is this method for?"""
    		if not index.isValid():
    			return qtc.QModelIndex()
    		childItem = index.internalPointer()
    		parentItem = childItem.parent()
    		if parentItem == self.rootItem:
    			return qtc.QModelIndex()
    		return self.createIndex(parentItem.row(), 0, parentItem)
    
    	def rowCount(self, parent):  # noqa
    		if parent.column() > 0:
    			return 0
    		if not parent.isValid():
    			parentItem = self.root_item
    		else:
    			parentItem = parent.internalPointer()
    		return parentItem.childCount()
    
    
    if __name__ == "__main__":
    	test_fn = 'test_data/YAML/test_file_2.yaml'
    	app = qtw.QApplication(sys.argv)
    	window = qtw.QMainWindow()
    	view = qtw.QTreeView()
    	model = TreeModel(data=None, parent=None, fn=None)
    	model.fn = test_fn
    	model.setupTreeModel()
    	view.setModel(model)
    	window.show()
    	sys.exit(app.exec_())
    

    Test Data

    name: Top Level Node
    id: 1
    children: 
    
    - name: 3326622-1 Node
      id: 3146
      children:
    
      - name: 342273-37
        id: 3147
        children:
    
        - name: 342273-39
          id: 3148
          children:
    
          - name: 342273-35
            id: 3000403
    
      - name: 342273-38
        id: 3149
        children:
    
        - name: 342273-41
          id:  # This is blank sometimes
    
        - name: 342273-40 RIB ASSY
          id: 3150
          children:
          
          - name: 342273-36
            id: 3000432
    
          - name: 347506R
            id: 
    
        - name: 357169-1_005
          id: 3000441
    
    

    Edit 1: Adding some details I left off initially

    My main issue is I don't understand all the parts that go into making a custom Model for a Tree-style data file. I feel like I don't understand what Qt is expecting for a custom Tree Model or how it handles navigating/building the hierarchical relationships.

    I'm also not fluent in C++ so translating some of the C++ Qt examples is a bit difficult.

    In short:

    1. What steps do I need to follow to build a general TreeModel-like model?
    2. Am I missing something when trying to display my data in the user-facing code? I can't get even sample data from Simple Tree Model Example to show up when 'translating' that C++ code to Python
    3. My data isn't being read in line-by-line like the Simple Tree Model Example
      • It's coming into the TreeModel as a hierarchical nodal relationship (an anytree.Node object) that just needs to be converted to TreeItems to make them compatible with PyQt.
      • Eventually the data will be edited in the View by the user so I need the PyQt functionality that lets me edit things like that. Those are far off details for the moment though but it's worth mentioning.
    4. I'm having trouble converting the incoming data from anytree.Node objects to be compatible with PyQt
      • I want to keep the functionality of anytree.Node in my TreeItem
      • That allows me to use methods such as anytree.Node.parent, anytree.Node.siblings, anytree.Node.root, etc. and gives me easy access to a pretty-printer with nice unicode branch characters.
    5. An important point is the YAML format is a hard requirement. I'll be reading that data in from a file with tens of thousands of nodes and writing to the same format eventually.

    Is the following correct? This is what I've understood thus far.

    • I need a Model and a View
      • The model handles my data
      • The view handles the interaction with my user and tells the model when to change the data
    • The model will use TreeItem objects to represent each node on the tree
    • The TreeModel is the entire tree. That gets passed to the View for display to the user.
      • The TreeModel will (recursively?) iterate through my incoming data (after being read by anytree.importer.DictImporter) to create each TreeItem node and link those parent-child relationships in a way that PyQt can understand.

  • Lifetime Qt Champion

    Hi and welcome to devnet,

    Did you already went through the Simple Tree Model example ?

    It should be a good base to translate to Python for your use case.



  • Hi @SGaist thanks for the reply.

    I've updated the post with some more details that I should have included initially. Sorry if it wasn't clear in my original post. I've been working on this off-and-on for a few weeks and have a dozen different model codes I'm trying. I guess I'm struggling with the fundamentals of how PyQt handles this sort of task.


Log in to reply