PyQt6: Find element by path in QTreewidget

184 Views Asked by At

How to select this element in Qtreewidget using the path to an element?

The path is written as an example string: parent/parent/parent/parent/element

It is important to search for elements along the entire path and not only by the name of this element, because there may be many elements in the tree with the same name, but each one will certainly have a different path.

I tried to solve it in ten ways:

def find_and_select_item(self):
    path = self.comboBox_Scene.currentText()
    path_elements = path.split('/')
    current_item = self.treeWidget.invisibleRootItem()
    for element in path_elements:
        items = self.treeWidget.findItems(element, Qt.MatchFlag, 0)
        if items:
            current_item = items[0]
        else:
            # Element not found in the current level of the tree
            return None

    # Select the found item
    current_item.setSelected(True)
    return current_item

Unfortunately, this function returns an error::
TypeError: findItems(self, text: str, flags: Qt.MatchFlag, column: int = 0): argument 2 has unexpected type 'EnumType'

3

There are 3 best solutions below

5
ekhumoro On BEST ANSWER

This is most easily implemented using recursion, which can provide a more general solution that is capable of finding multiple matching pathways in the tree (if necessary).

Below is a basic working demo that shows how to achieve this. Since the question states that each element has a different path, this demo only looks for the first matching item. However, the searchByPath method returns a generator that can iterate over all matches if necessary (and it can also start searching from any point in the tree).

enter image description here

from PyQt6 import QtCore, QtGui, QtWidgets

class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.button = QtWidgets.QPushButton('Test')
        self.button.clicked.connect(self.handleButton)
        self.edit = QtWidgets.QLineEdit()
        self.tree = QtWidgets.QTreeWidget()
        self.tree.setHeaderHidden(True)
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.tree)
        layout.addWidget(self.edit)
        layout.addWidget(self.button)
        for text in ('Red', 'Blue', 'Green'):
            level0 = QtWidgets.QTreeWidgetItem(self.tree, [text])
            for text in ('Cyan', 'Violet', 'Yellow'):
                level1 = QtWidgets.QTreeWidgetItem(level0, [text])
                for text in ('Orange', 'Brown', 'Purple'):
                    level2 = QtWidgets.QTreeWidgetItem(level1, [text])
                    for text in ('Gold', 'Silver', 'Bronze'):
                        level3 = QtWidgets.QTreeWidgetItem(level2, [text])
        self.edit.setText('Green/Violet/Orange/Bronze')

    def handleButton(self):
        self.tree.clearSelection()
        self.tree.collapseAll()
        path = self.edit.text()
        if path:
            item = next(self.searchByPath(path), None)
            if item is not None:
                self.tree.scrollToItem(item)
                item.setSelected(True)

    def searchByPath(self, path, root=None):
        if root is None:
            root = self.tree.invisibleRootItem()
        elements = list(map(str.strip, path.split('/')))
        last = len(elements) - 1
        def search(parent, level=0):
            if level <= last:
                target = elements[level]
                for index in range(parent.childCount()):
                    child = parent.child(index)
                    if child.text(0) != target:
                        continue
                    if level == last:
                        yield child
                    else:
                        yield from search(child, level + 1)
        return search(root)


if __name__ == '__main__':

    app = QtWidgets.QApplication(['Test'])
    window = Window()
    window.setGeometry(600, 100, 300, 300)
    window.show()
    app.exec()
16
TheHungryCub On

In PyQt6, the usage of enumerations might be slightly different from PyQt5. In PyQt6, Qt.MatchFlag might be replaced with a specific enumeration, such as Qt.MatchFlag.MatchExactly.

Here’s an updated version of your function:

from PyQt6.QtCore import QDir, Qt  # Make sure to include the specific Qt modules you need

def find_and_select_item(self):
    path = self.comboBox_Scene.currentText()
    path_elements = path.split('/')
    current_item = self.treeWidget.invisibleRootItem()
    
    for element in path_elements:
        items = self.treeWidget.findItems(element, Qt.MatchFlag.MatchExactly, 0)
        if items:
            current_item = items[0]
        else:
            # Element not found in the current level of the tree
            return None

    # Select the found item
    current_item.setSelected(True)
    return current_item

Approch 2:

It seems that the issue may be related to the differences in PyQt6 or how the findItems method is being used. ensuring that the element you are searching for matches exactly and is case-sensitive:

from PyQt6.QtCore import Qt

def find_and_select_item(self):
    path = self.comboBox_Scene.currentText()
    path_elements = path.split('/')
    current_item = self.treeWidget.invisibleRootItem()

    for element in path_elements:
        items = self.treeWidget.findItems(element, Qt.MatchExactly | Qt.MatchCaseSensitive, 0)
        if items:
            current_item = items[0]
            print("Found item:", current_item.text(0))  # Print the text content of the found item
            # Move the current_item reference to the found item
            current_item = items[0]
        else:
            print(f"Element '{element}' not found in the current level of the tree.")
            return None

    # Select the last found item
    current_item.setSelected(True)
    print("Selected item:", current_item.text(0))  # Print the text content of the selected item
    return current_item
0
Marcin On

I'm trying to solve it in new ways:

def iterator_treewiget(self, widget, content):
    for i in range(widget.childCount()):
        item = widget.child(i)
        text = item.text(0)

        if content in text:
            self.treeWidget.setCurrentItem(item)
            return
        self.iterator_treewiget(item, content)

def find_and_select_item(self):
    content = self.comboBox_Scene.currentText()
    items = content.split('/')
    n = len(items)
    item = items[n-1]
    if content:
        root = self.treeWidget.invisibleRootItem()
        self.iterator_treewiget(root, item)

I found a solution to my problem, but it has some drawbacks. Firstly, it hangs when there are two elements that are the same but in different locations. For example:

parent/parent/parent/parent/element
parent1/parent1/element
paren2/parent2/parent2/element1
parent/parent/parent/parent/element2
parent1/parent1/element3
paren2/parent2/parent2/element4
parent/parent/parent/parent/element5
parent1/parent1/element6
paren2/parent2/parent2/element7
parent/parent/parent/parent/element8
parent1/parent1/element9
paren2/parent2/parent2/element10

When I point to "element1", "element2", "element3"... 4,5,6,7,8,9,10, everything works. When I select one of the "element", the program hangs. Secondly, with a larger database of elements after several different element selections, the error "RecursionError: maximum recursion depth exceeded while calling a Python object"