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:
- What steps do I need to follow to build a general
TreeModel
-like model? - 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
- 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 (ananytree.Node
object) that just needs to be converted toTreeItem
s 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.
- It's coming into the
- 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 myTreeItem
- 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.
- I want to keep the functionality of
- 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 byanytree.importer.DictImporter
) to create eachTreeItem
node and link those parent-child relationships in a way that PyQt can understand.
- The
- What steps do I need to follow to build a general
-
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.