Create Proper Hierarchical Data for TreeView?
-
Hi,
All the examples I found so far for TreeView uses the "objects" in C4D such as the objects and materials.
I needed to create a TreeView that would represent directories and subdirectories (much the same way with the Content Browser) .
I can access the directories properly using this code based on this thread
import os def list_files(startpath): for root, dirs, files in os.walk(startpath): level = root.replace(startpath, '').count(os.sep) indent = ' ' * 4 * (level) print('{}{}/'.format(indent, os.path.basename(root))) subindent = ' ' * 4 * (level + 1)
Result: folder1/ sec_folder1_A/ ter_folder1_A/ sec_folder1_B/ folder2/ subfolder2_A/
My problem, for now, is creating Proper Hierarchical Data for Tree View so that I can execute
GetFirst
,GetDown
,GetNext
function properly (although I have recreate those functions, for another thread problem maybe haha).1) What would be the proper format? list or dictionary? It seems like either are insufficient.
Dictionary: dir_dict = { 'folder1': {'sec_folder1_A':'ter_folder1_A', 'sec_folder1_B': 'None' }, 'folder2': {'sec_folder2_A': 'None'} } List: dir_list = [ [folder1, [ [sec_folder1_A, [ter_folder1_A]], sec_folder1_B ], [folder2, [subfolder2_A] ] ]
2) Even if its List or Dictionary, I'm actually at lost how to populate it as hierarchical data. Do you have any tips?
Thank you for looking at my problem
Regards,
Ben -
Hi,
- Technically the
dict
(i.e. a hash map) solution would be better due to the constant access time, but that will hardly be of any real consequence in your case. But if you are going for an unoptimised graph representation (i.e. do not use something like an adjacency matrix or an inverted index) - which is the right choice here IMHO, I do not really see the point of handling the "raw data". Why not write some node type with which you can both represent and access/modify your graph? - Well,
TreeviewFunctions
is the interface between theTreeViewCustomGui
and whatever is representing the data of your graph. You have to implement the graph traversal methods you mentioned. Using adict
or an inverted index this can get a bit messy. For an adjacency matrix things would be a bit more straight forward, but the easiest solution is probably implementing your own type, since you have only to return the corresponding attribute of your custom node type then.
Cheers,
zipit - Technically the
-
Hi @zipit
Thanks for the response. Just a heads up, I'm still a novice Python user so forgive me if I'm asking the obvious below.
1 ==================
RE: do not use something like an adjacency matrix or an inverted index
I haven't encountered both but what you should I use other than those two? I didn't know there were "types" of dictionary.RE:Why not write some node type with which you can both represent and access/modify your graph?
I don't know how to write a node type. That's also the first time I have heard of it.
This is what you are referring to right?
https://developers.maxon.net/docs/py/2023_2/modules/c4d.plugins/BaseData/NodeData/index.html2 ==================
Since creating node type is out of my league. I guess using dict will be the way to go. Need to read up on inverted index and adjacency matrix. Basically, both can help me "crawl" on my hierarchical data right? Such as performing
GetNext
,GetDown
functions. -
Hi,
- These are both just concepts to store and access graphs (i.e. also a tree). An adjacency matrix is just a square matrix for n nodes, where you describe the relation between two nodes in each cell. An inverted index flattens any edge relation of a graph to a list by inverting the edge relation (i.e. if you have the node
a
with some kind of relation tob,c, d
, the inverted index would beb: a, c: a, d: a
). There are fancier concepts to do this, but they are not really needed here, since a file tree is so small. In fact I would not use any optimisation here as stated in my previous post. - Don't sell yourself short. I think you overestimate the difficulty of the problem. You have just to write a class that can somehow represent a graph (or a trie in your case). Below is a minimal example. The advantage of this approach is that is easy to understand and work with, the disadvantage is it is slow (doesn't matter here) and expressing more than one relation can be difficult (doesn't matter here either):
class Node(object): """A very simple node type for a tree/trie graph. """ def __init__(self, **kwargs): """The constructor for ``Node``. Args: **kwargs: Any non-graph related attributes of the node. """ # You might want to encapsulate your attributes in properties, so # that you can validate / process them, I took the lazy route. if "name" not in kwargs: kwargs["name"] = "Node" self.__dict__.update(kwargs) self.parent = None self.children = [] self.prev = None self.next = None self.down = None def __repr__(self): """The representation is: class, name, memory location. """ msg = "<{} named {} at {}>" hid = "0x{:0>16X}".format(id(self)) return msg.format(self.__class__.__name__, self.name, hid) def add(self, nodes): """Adds one or multiple nodes to the instance as children. Args: nodes (list[Node] or Node): The nodes to add. Raises: TypeError: When nodes contains non-Node elements. """ nodes = [nodes] if not isinstance(nodes, list) else nodes # last child of the instance, needed for linked list logic prev = self.children[-1] if self.children else None for node in nodes: if not isinstance(node, Node): raise TypeError(node) node.parent = self node.prev = prev if prev is not None: prev.next = node else: self.down = node self.children.append(node) prev = node def pretty_print(self, indent=0): """Pretty print the instance and its descendants. Args: indent (int, optional): Private. """ tab="\t" * indent a = self.prev.name if self.prev else None b = self.next.name if self.next else None c = self.down.name if self.down else None msg = "{tab}{node} (prev: {prev}, next: {next}, down: {down})" print msg.format(tab=tab, node=self, prev=a, next=b, down=c) for child in self.children: child.pretty_print(indent+1) def build_example_tree(): """ """ root = Node(name="root") node_0 = Node(name="node_0") node_00 = Node(name="node_00") node_01 = Node(name="node_01") node_02 = Node(name="node_02") node_1 = Node(name="node_1") node_10 = Node(name="node_10") root.add(nodes=[node_0, node_1]) node_0.add(nodes=[node_00, node_01, node_02]) node_1.add(nodes=node_10) return root root = build_example_tree() root.pretty_print()
Cheers,
zipit - These are both just concepts to store and access graphs (i.e. also a tree). An adjacency matrix is just a square matrix for n nodes, where you describe the relation between two nodes in each cell. An inverted index flattens any edge relation of a graph to a list by inverting the edge relation (i.e. if you have the node
-
@zipit
Thanks for the response and the sample code. I appreciate it a lot.
So I guess, when you mentioned "Node Type", it's not necessarily specific to Cinema4D but a general concept.So now, I just need to repopulate the Node Class with the directories.
Will keep you updated on the progress.This should keep me busy for the weekend.
Thanks again!
-
@zipit
DISCLAIMER: If you think, the question below merits a separate thread, let me know, and I'll create another thread.
Thanks again for the reply. The code still not completed yet.
I populated the class with the directories but I'm having a problem with "parenting" the nodes.
Specifically, I'm having problem accessing the Node class by its name attribute.This is the snippet of the problem:
current_index = folder_path_list.index(folder) parent_folder = folder_path_list[current_index-1] parent_node = #PROBLEM #get_class_node_base_on_its_name new_node = Node(name=folder) parent_node.add(nodes=new_node)
You can check the whole code below:
import os start_path = '.' # written by zipit class Node(object): """A very simple node type for a tree/trie graph. """ baseId = 90000 dct = {} def __init__(self, **kwargs): """The constructor for ``Node``. Args: **kwargs: Any non-graph related attributes of the node. """ # You might want to encapsulate your attributes in properties, so # that you can validate / process them, I took the lazy route. if "name" not in kwargs: kwargs["name"] = "Node" self.__dict__.update(kwargs) self.parent = None self.children = [] self.prev = None self.next = None self.down = None def __repr__(self): """The representation is: class, name, memory location. """ msg = "<{} named {} at {}>" hid = "0x{:0>16X}".format(id(self)) return msg.format(self.__class__.__name__, self.name, hid) def add(self, nodes): """Adds one or multiple nodes to the instance as children. Args: nodes (list[Node] or Node): The nodes to add. Raises: TypeError: When nodes contains non-Node elements. """ nodes = [nodes] if not isinstance(nodes, list) else nodes # last child of the instance, needed for linked list logic prev = self.children[-1] if self.children else None for node in nodes: if not isinstance(node, Node): raise TypeError(node) node.parent = self node.prev = prev if prev is not None: prev.next = node else: self.down = node self.children.append(node) prev = node def pretty_print(self, indent=0): """Pretty print the instance and its descendants. Args: indent (int, optional): Private. """ tab="\t" * indent a = self.prev.name if self.prev else None b = self.next.name if self.next else None c = self.down.name if self.down else None msg = "{tab}{node} (prev: {prev}, next: {next}, down: {down})" print (msg.format(tab=tab, node=self, prev=a, next=b, down=c)) for child in self.children: child.pretty_print(indent+1) created_dir = [] root_node = Node(name="images") for root, dirs, files in os.walk(start_path): root_normalized = root.replace(start_path, '') for dir in dirs: folder_path = os.path.join(root_normalized, dir) folder_path_list = folder_path.split(os.sep) folder_path_list = list(filter(None, folder_path_list)) # Remove empty strings for folder in folder_path_list: # Images node is already created before this loop. It is set as the root node if folder == "images": continue # Prevent creation of node if it was already created beforehand if folder in created_dir: continue current_index = folder_path_list.index(folder) parent_folder = folder_path_list[current_index-1] parent_node = #get_class_node_base_on_its_name new_node = Node(name=folder) parent_node.add(nodes=new_node) created_dir.append(folder) root_node.pretty_print()
The directory is still as above:
images/ folder1/ sec_folder1_A/ ter_folder1_A/ sec_folder1_B/ folder2/ subfolder2_A/
-
Hi,
I am not quite sure what your actual question is. However, I think that you somehow missed the point of writing a node type. The advantage of it is that you can encapsulate any logic into your nodes and can operate on local and small scale. When you write some external functions which operate more or less non-object oriented and on the scale of the whole graph, you could also use a builtin data structure like a dictionary and ignore the whole OO-stuff. I do not want to start a discussion about functional vs. OO programming, use whatever you are comfortable with, but if you want to benefit from some custom node type, you have to implement a custom node type
Below you will find an example for how I would go about what you are probably trying to do. Please note that this is example code and not by any means something that should be used
Cheers,
zipitimport os class BaseNode(object): """The base node type. """ def __init__(self, **kwargs): """The constructor for BaseNode. Args: **kwargs: Any non-graph related attributes of the node. """ # You might want to encapsulate your attributes in properties, so # that you can validate / process them, I took the lazy route. if "name" not in kwargs: kwargs["name"] = "Node" self.__dict__.update(kwargs) self.children = [] self.up = None self.down = None self.prev = None self.next = None def __iter__(self): """Yields all nodes attached to the node. Yields: BaseNode: A node attached to this node. """ for child in self.children: yield child def __repr__(self): """The string representation of the node. """ msg = "<{} named {} at {}>" hid = "0x{:0>16X}".format(id(self)) return msg.format(self.__class__.__name__, self.name, hid) def _link_nodes(self): """Builds the links between the children of the instance and the instance. """ prev = None for node in self: node.up = self node.prev = prev if prev is not None: prev.next = node prev = node self.down = self.children[0] if self.children else None def add(self, nodes): """Adds one or multiple nodes to the instance as children. Args: nodes (list[BaseNode] or BaseNode): The nodes to add. Raises: TypeError: When nodes contains non-BaseNode elements. """ nodes = [nodes] if not isinstance(nodes, list) else nodes # last child of the instance, needed for linked list logic prev = self.children[-1] if self.children else None for node in nodes: if not isinstance(node, BaseNode): raise TypeError(node) self.children.append(node) self._link_nodes() def pretty_print(self, indent=0): """Pretty print the instance and its descendants. """ tab = "\t" * indent a = self.prev.name if self.prev else None b = self.next.name if self.next else None c = self.down.name if self.down else None msg = "{tab}{node} (prev: {prev}, next: {next}, down: {down})" print(msg.format(tab=tab, node=self, prev=a, next=b, down=c)) for child in self.children: child.pretty_print(indent+1) class PathNode(BaseNode): """The file-path specialization of a BaseNode. """ def __init__(self, path, **kwargs): """The constructor of PathNode. Args: path (str): The file/folder path of the node. **kwargs: Any other non-graph related attributes to be attached to the node. """ BaseNode.__init__(self, path=path) self._initialize_path_tree() def _initialize_path_tree(self): """Initializes a path tree with the path attribute of the instance. Recursively builds a PathNode tree for the folders and files that are descendants of the path attribute of the instance. """ # Validate the path attribute. if not "path" in self.__dict__: msg = "The node hasn't been initialized with a 'path' attribute." raise AttributeError(msg) if not os.path.exists(self.path): msg = "The path '{path}' does not exist or cannot be accessed." raise OSError(msg.format(path=self.path)) path = self.path root_path, element = os.path.split(path) # Node is a terminal node / file if os.path.isfile(path): name, extension = os.path.splitext(element) self.name = name self.extension = extension self.is_file = True # Node is a non-terminal node / a folder else: self.name = element self.extension = None self.is_file = False # Build the children paths = [os.path.join(path, item) for item in os.listdir(path)] for item in paths: self.add(PathNode(path=item)) self.sort() def get(self, name, inclusive=False): """Yields all nodes which have a matching name attribute. Note: This your "get_class_node_base_on_its_name" method. I am not quite sure, what you are trying to accomplish with it, so there are multiple things one could do differently. Args: name (str): The name to match against. Yields: BaseNode: A node that is a descendant of the instance and which fulfills the criteria. """ if self.name == name: yield self for item in self: for result in item.get(name=name): yield result def sort(self): """Sorts the children by folder/files, file types and names. """ if not self.children: return key_lambda = lambda x: (x.is_file, x.extension, x.name) self.children = sorted(self.children, key=key_lambda) self._link_nodes() for node in self: node.sort() def pretty_print(self, indent=0): """Pretty print the directory structure attached to this node. """ tab = "\t" * indent if self.is_file: msg = "{tab}{name}{ext}".format(tab=tab, name=self.name, ext=self.extension) else: msg = "{tab}[{name}]".format(tab=tab, name=self.name) print msg for child in self.children: child.pretty_print(indent+1) def demo(): """ """ path = os.path.dirname(__file__) root = PathNode(path=path) print "root:", root root.pretty_print() print "\nIterate the root node:" for node in root: print node print "\nGet node by name:" for item in root.get("plots"): print item print "\nGet node by name:" for item in root.get("todo"): print item if __name__ == "__main__": demo()
root: <PathNode named misc at 0x000000000309C0C8> [misc] [plots] plots_interpolation.py plots_projection.py plotters.py [scripts] svg_colors.py todo.md scribbles_1.py scribbles_2.py scribbles_3.py scribbles_4.py scribbles_5.py Iterate the root node: <PathNode named plots at 0x000000000309C208> <PathNode named scripts at 0x000000000309C548> <PathNode named todo at 0x000000000309C5C8> <PathNode named scribbles_1 at 0x000000000309C308> <PathNode named scribbles_2 at 0x000000000309C448> <PathNode named scribbles_3 at 0x000000000309C488> <PathNode named scribbles_4 at 0x000000000309C4C8> <PathNode named scribbles_5 at 0x000000000309C508> Get node by name: <PathNode named plots at 0x000000000309C208> Get node by name: <PathNode named todo at 0x000000000309C5C8> [Finished in 0.1s]
-
Hi sorry for the delay I completely missed the topic, you can find valuable information about how TreeView is working on using customgui listview, then for more advice especially about TreeViewFunctions.GetDown usage Insert object in Treeview, and in No multiple selection in Treeview not working?, you can find a more versatile "node" object which can be used as a root, while previously I used a list as a root.
Cheers,
Maxime. -
Thank you for your response.
@zipit
With the previous help, I was able to represent the folder hierarchy in the treeview.
You can see it here (image)
Source file is here.In summary, I didn't use your recent with the
PathNode
class but it helped me how to create theGet Parent Node
function which was to store the nodes in a list, and compare the node name and the folder name.===================================
The code is working now but may I ask about this line.
for item in self:
This is my first seeing a for loop for the
self
. I understand if it would beself.list_of_names
orself.list_of_nodes
but iterating for theself
itself? How is it even possible?I couldn't see any
self.append(item)
in the code.Please enlighten me sensei.
No worries. Thanks for the thread links. Will add them as reference.
Regards,
Ben -
This works, because I did implement
__iter__
inBaseNode
, which will be called when you try to iterate an object. For the same reason alsofor node in root
does work. You can learn more about operator overloading in Python here. In a pythonic context they are called spcieal methods, important methods or dunder methods, but practically it's just operator overloading if you are willing to be a bit fuzzy with the term operator.Cheers,
zipit -
Thanks for the clarification.
Have a great day ahead!