Checkbox Tree view

Hello, I have a tree with checkboxes (image) and the following code. I would like to add 2 functions. The first function: When I click on "Test", the icon of that checkbox should change + all icons leading to the root of the tree, which also includes "Stromprüfung" and "LX 01 Gruppe635". The second function: When I select "LX 01 Gruppe635", all icons should change to checked, including "Sichtprüfung", "Stromprüfung", "Statistik", "Test", and "Test2".
image

def runAction(self, event):
	"""
	Fired whenever an item is clicked on this tree.

	Arguments:
		self: A reference to the component that is invoking this function.
		event: An object with the following attributes:
			data: The value of the contextual 'data' object on the clicked node
			itemPath (list): A list containing the item indexes leading to the item
			                 that was clicked.
			label (str): Text to display for this option.
	"""
	# Folgende Variabeln dienen zur Überscihtlichkeit des Codes
	Unclicked = "material/check_box_outline_blank"  #Variable für Leere Checkbox
	Clicked = "material/check_box"					#Variable für ausgewählte Checkbox 
	itemsPath = event.itemPath						#Liste, die den Pfad zu dem angeklickten Element enthält
	items =	self.props.items
	
	for index in itemsPath[:-1]: 			#Überprüfe jedes Element in itemsPath außer dem letzten
		items = items[index]["items"]		#Navigiere zu den untergeordneten Elementen basierend auf dem Index
		
	icon = items[itemsPath[-1]]["icon"]["path"] 				#Hole den Pfad des Icons des zuletzt angeklickten Elements
	if icon == Unclicked:										#Ändere den Pfad des Icons je nach aktuellem Zustand der Checkbox
		items[itemsPath[-1]]["icon"]["path"] = Clicked
	else:
		items[itemsPath[-1]]["icon"]["path"] = Unclicked

You'll need to make a recursive function in your project library. To avoid having to walk through the tree structure twice, you'll need to put a boolean key in the data key of each tree item to keep track of the item's check status (checked/unchecked).

You'll also need a recursive function to go through all the tree nodes and turn them into python objects, otherwise you get some weird behavior with setting values

Function to convert to python objects
def sanitizeIgnitionObject(element):
	""" """
	if hasattr(element, '__iter__'):
		if hasattr(element, 'keys'):
			return dict((k, sanitizeIgnitionObject(element[k])) for k in element.keys())
		else:
			return list(sanitizeIgnitionObject(x) for x in element)
	return element

Recursive Function to set children as selected
CHECKED_BOX_ICON = "material/check_box"
UNCHECKED_BOX_ICON = "material/check_box_outline_blank"


def recursiveSetChecked(elements):
	""" """

	for element in elements:
		element['icon']['path'] = CHECKED_BOX_ICON
		element['data']['checked'] = True

		if len(element['items']) == 0:
			continue

		element['items'] = recursiveSetChecked(element['items'])

	return elements

Your event script would then look like something along the lines of:

Selection script in library
def treeItemSelected(items, itemPath, data):
	""" """

	# If item is being deselected, only change the icon and status bit
	if data['checked']:
		for itemIndex in itemPath[:-1]:
			itemIndex = int(itemIndex)
			items = items[itemIndex]['items']
		
		item = items[itemPath[-1]]
		item['data']['checked'] = False
		item['icon']['path'] = UNCHECKED_BOX_ICON

	# If item is being selected, set parents in path to selected, and set any children of the selected node to selected
	else:
		item = items
		for idx, pathIdx in enumerate(itemPath):
			pathIdx = int(pathIdx)

			# First item in a tree is selected from a list
			if idx == 0:
				item = item[pathIdx]

			# Subsequent items are selected from a list in a dict key
			else:
				item = item['items'][pathIdx]

			item["icon"]["path"] = CHECKED_BOX_ICON
			item['data']['checked'] = True

		if len(item['items']) > 0:
			# Set children to checked
			item['items'] = recursiveSetChecked(item['items'])

	return items

and you would call this from the actual runAction with a single line:

def runAction(self, event):
	"""
	Fired whenever an item is clicked on this tree.

	Arguments:
		self: A reference to the component that is invoking this function.
		event: An object with the following attributes:
			data: The value of the contextual 'data' object on the clicked node
			itemPath (list): A list containing the item indexes leading to the item
			                 that was clicked.
			label (str): Text to display for this option.
	"""

	self.props.items = LibNameHere.treeItemSelected(
		sanitizeIgnitionObject(self.props.items), event.itemPath, event.data)

DISCLAIMER: Untested but based loosely off my own functions for interacting with tree items

Edit: Corrected the iteration through the event.itemPath and setting of values when unchecking an item.

I have written the functions "sanitizeIgnitionObject," "recursiveSetChecked," and "treeItemSelected" in my project library named "fkt_Komponentenbaum." I have created the Boolean key with false and true as shown in the image.
image

However, I am getting an error. Can you help me with that?

image

Here is the full script for the functions in "fkt_Komponentenbaum":

def sanitizeIgnitionObject(element):
	if hasattr(element, '__iter__'):
		if hasattr(element, 'keys'):
			return dict((k, sanitizeIgnitionObject(element[k])) for k in element.keys())
		else:
			return list(sanitizeIgnitionObject(x) for x in element)
	return element

CHECKED_BOX_ICON = "material/check_box"
UNCHECKED_BOX_ICON = "material/check_box_outline_blank"
	
	
def recursiveSetChecked(elements):
	""" """
	
	for element in elements:
		element['icon']['path'] = CHECKED_BOX_ICON
		element['data']['checked'] = True
	
		if len(element['items']) == 0:
			continue
	
		element['items'] = recursiveSetChecked(element['items'])
	
	return elements	

def treeItemSelected(items, itemPath, data):	
	# If item is being deselected, only change the icon and status bit
	if data['checked']:
		for itemIndex in itemPath.split("/")[:-1]:
			itemIndex = int(itemIndex)
			items = items[itemIndex]['items']
	
		items['data']['checked'] = False
		items['icon']['path'] = UNCHECKED_BOX_ICON
	
	# If item is being selected, set parents in path to selected, and set any children of the selected node to selected
	else:
		item = items
		for idx, pathIdx in enumerate(itemPath.split("/")):
			pathIdx = int(pathIdx)
	
			# First item in a tree is selected from a list
			if idx == 0:
				item = item[pathIdx]
	
			# Subsequent items are selected from a list in a dict key
			else:
				item = item['items'][pathIdx]
	
			item["icon"]["path"] = CHECKED_BOX_ICON
			item['data']['checked'] = True
	
		if len(item['items']) > 0:
			# Set children to checked
			item['items'] = recursiveSetChecked(item['items'])
		
	return items

for the ClickedOnItem event:

def runAction(self, event):
	"""
	Fired whenever an item is clicked on this tree.

	Arguments:
		self: A reference to the component that is invoking this function.
		event: An object with the following attributes:
			data: The value of the contextual 'data' object on the clicked node
			itemPath (list): A list containing the item indexes leading to the item
			                 that was clicked.
			label (str): Text to display for this option.
	"""
	self.props.items = fkt_Komponentenbaum.treeItemSelected(fkt_Komponentenbaum.sanitizeIgnitionObject(self.props.items), event.itemPath, event.data)

It looks like itemPath is a list, not a string, so I imagine you will need to split the string in the next line:

# Iterate through the tagpaths
for idx, pathIdx in enumerate(itemPath):
	# Get the last part of the tag path and cast it to an integer
	pathIdx = int(pathIdx.split("/")[-1])

It looks like you are doing the same thing in line 31, but you're probably not faulting on it because data['checked'] isn't true.

1 Like

I have changed it as you have.
image

Now I get this error:
image

I guess item indexes are a list of long datatypes. The word path and split('\') made me envision something like path\to\someIndex.

This is probably good enough then:

for idx, pathIdx in enumerate(itemPath):
	# First item in a tree is selected from a list
	if idx == 0:
		item = item[pathIdx]
	#[...]

It seems that there is no need to split or cast anything.

1 Like

You are right! I have omitted .split and commented out pathIdx = int(pathIdx), and in line 31 there were the same issues, so I did the same there. It now works from unchecked to checked, but not the other way. However, I now have a new error in line 35.

Ah yes, the script I had modified was looking a what is returned from a tree's props.selection property, which is a path/to/item format. I made the bad assumption/didn't read that item.path was a list.

change lines 35-36 to, include all three lines:

item = items[itemPath[-1]]
item['data']['checked'] = False
item['icon']['path'] = UNCHECKED_BOX_ICON

I forgot that in the first if section I was assigning the list of items to a variable instead of a single item to a variable.

2 Likes

When I click on 'LX01 Group 635,' all lower points are checked, so the function is correct. However, if I click on a lower point like 'Test1,' then the entire tree disappears up to the clicked item.

image

image

Can this be fixed so that the tree remains visible?

This is the Code:

Please post formatted code - not pictures of code. Anyone who wants to test this or edit it for you would have to type it all out again or use OCR. (Use a picture as well if it provides context.) Thanks!

thanks for the tip. Here is the code for all the functions:

def sanitizeIgnitionObject(element):
	if hasattr(element, '__iter__'):
		if hasattr(element, 'keys'):
			return dict((k, sanitizeIgnitionObject(element[k])) for k in element.keys())
		else:
			return list(sanitizeIgnitionObject(x) for x in element)
	return element

CHECKED_BOX_ICON = "material/check_box"
UNCHECKED_BOX_ICON = "material/check_box_outline_blank"
	
	
def recursiveSetChecked(elements):
	""" """
	
	for element in elements:
		element['icon']['path'] = CHECKED_BOX_ICON
		element['data']['checked'] = True
	
		if len(element['items']) == 0:
			continue
	
		element['items'] = recursiveSetChecked(element['items'])
	
	return elements	

def treeItemSelected(items, itemPath, data):	
	# If item is being deselected, only change the icon and status bit
	if data['checked']:
		for itemIndex in itemPath[:-1]:                 ########[:-1]
			itemIndex = int(itemIndex)
			items = items[itemIndex]['items']
	
		item = items[itemPath[-1]]
		item['data']['checked'] = False
		item['icon']['path'] = UNCHECKED_BOX_ICON

	# If item is being selected, set parents in path to selected, and set any children of the selected node to selected
	else:
		item = items
		for idx, pathIdx in enumerate(itemPath):     ########
			#pathIdx = int(pathIdx)
	
			# First item in a tree is selected from a list
			if idx == 0:
				item = item[pathIdx]
	
			# Subsequent items are selected from a list in a dict key
			else:
				item = item['items'][pathIdx]
	
			item["icon"]["path"] = CHECKED_BOX_ICON
			item['data']['checked'] = True
	
		if len(item['items']) > 0:
			# Set children to checked
			item['items'] = recursiveSetChecked(item['items'])
		
	return items

The Code for the onItemClicked event:

def runAction(self, event):
	"""
	Fired whenever an item is clicked on this tree.

	Arguments:
		self: A reference to the component that is invoking this function.
		event: An object with the following attributes:
			data: The value of the contextual 'data' object on the clicked node
			itemPath (list): A list containing the item indexes leading to the item
			                 that was clicked.
			label (str): Text to display for this option.
	"""
	self.props.items = fkt_Komponentenbaum.treeItemSelected(fkt_Komponentenbaum.sanitizeIgnitionObject(self.props.items), event.itemPath, event.data)

For fun, try changing

	else:
		item = items
		for idx, pathIdx in enumerate(itemPath):     ########
			#pathIdx = int(pathIdx)

			# First item in a tree is selected from a list
			if idx == 0:
				item = item[pathIdx]

to this:

	else:
		for idx, pathIdx in enumerate(itemPath):
			# First item in a tree is selected from a list
			if idx == 0:
				item = items[pathIdx]

Might be running into something with how python does pass by reference for variables.

Also, for science, what happens if you select something like 'Sichtprûfung'?

1 Like

Thank you for all your help!
I have solved the problem, I needed a variable to navigate deeper into the tree without actually changing items. In the end, only the current icons and the checked flag in the items need to be changed.

I have replaced this part of the code:

if data['checked']:
		for itemIndex in itemPath[:-1]:                 ########[:-1]
			itemIndex = int(itemIndex)
			items = items[itemIndex]['items']
	
		item = items[itemPath[-1]]
		item['data']['checked'] = False
		item['icon']['path'] = UNCHECKED_BOX_ICON

with that part.

if data['checked']:								#Wenn Icon checked
		NavItems = items 							#Variable um tiefer in die Baumstruktur zu kommen ohne die Struktur zu verändern
		
		for itemIndex in itemPath[:-1]:         	#durchlaufen des Pfad zum angeklicktem Item
			itemIndex = int(itemIndex)				#konventiert Index in eine ganze Zahl
			NavItems = NavItems[itemIndex]['items']	#Aktualisiert Items

		item = NavItems[itemPath[-1]]				#Auswahl des angeklicktem Item
		item['data']['checked'] = False				#Setzt #Setzt checked flag auf false
		item['icon']['path'] = UNCHECKED_BOX_ICON	#Ändert Icon von Element auf nicht checked

Here is the full code in case someone needs a tree with checkboxes:

def sanitizeIgnitionObject(element):	 
	"""Funktion zur Säuberung von Verschachtellungen"""	
	if hasattr(element, '__iter__'):	#Prüfen, ob Element iteratierbar ist
		if hasattr(element, 'keys'):	#Prüfen, ob Element Keys besitzt
			return dict((k, sanitizeIgnitionObject(element[k])) for k in element.keys()) #Dictuinary wird gesäubert
		else:
			return list(sanitizeIgnitionObject(x) for x in element)	#Liste wird gesäubert		
	return element 


CHECKED_BOX_ICON = "material/check_box"					#Variable wenn Box Cheked
UNCHECKED_BOX_ICON = "material/check_box_outline_blank"	#Variable wenn Box nicht Cheked

	
def recursiveSetChecked(elements): 	
	"""Funktion um alle Icons unterhalb des angeklicktem Icon auf checked zu ändern. 
	Diese Funktion wird so oft durchlaufen, wie viele Items sich unterhalb des angeklicktem befinden """	
	for element in elements:
		element['icon']['path'] = CHECKED_BOX_ICON 	#Ändert Icon von Element auf checked
		element['data']['checked'] = True 			#Setzt checked flag auf true
	
		if len(element['items']) == 0:				#Schaut ob weiter Liste weiter verschachtelt ist
			continue
	
		element['items'] = recursiveSetChecked(element['items']) #durchläuft die Funktion nochmal
	
	return elements	

#system.perspective.print
def treeItemSelected(items, itemPath, data): 	
	"""Funktion zur Rückverfolgung der Items. Es werden alle Icons bis zur wurzel auf Checked gesetzt"""
	if data['checked']:								#Wenn Icon checked
		NavItems = items 							#Variable um tiefer in die Baumstruktur zu kommen ohne die Struktur zu verändern
		
		for itemIndex in itemPath[:-1]:         	#durchlaufen des Pfad zum angeklicktem Item
			itemIndex = int(itemIndex)				#konventiert Index in eine ganze Zahl
			NavItems = NavItems[itemIndex]['items']	#Aktualisiert Items

		item = NavItems[itemPath[-1]]				#Auswahl des angeklicktem Item
		item['data']['checked'] = False				#Setzt #Setzt checked flag auf false
		item['icon']['path'] = UNCHECKED_BOX_ICON	#Ändert Icon von Element auf nicht checked
	
	else: #Wenn Icon nicht checked
		item = items
		for idx, pathIdx in enumerate(itemPath):	#Enumerate: Nummeriert die Elemente in der Liste		
			if idx == 0:							#Bedingung für das erste Element im Pfad
				item = items[pathIdx]				#Wählt das erste Element im Pfad aus
	
			else:
				item = item['items'][pathIdx] 		#Wählt das Element aus einer verschachtelten Ebene aus
	
			item["icon"]["path"] = CHECKED_BOX_ICON	#Ändert Icon von Element auf checked
			item['data']['checked'] = True			#Setzt checked flag auf true
	
		if len(item['items']) > 0: #Prüft, ob das Element untergeordnete Elemente hat.
			item['items'] = recursiveSetChecked(item['items']) #Ruft die Funktion recursiveSetChecked() rekursiv auf, um untergeordnete Elemente auszuwählen
		
	return items