Using QTreeWidgetItemIterator in PyQT4 to return isChecked from QTreeWidget as a dictionary (or...something)

4.1k views Asked by At

Check the edit for final code!

So...I'll admit that I'm drawing an absolute blank here due to lack of knowledget and will just present my code and pray a little bit.

Using this fantastic xml to QTreeWidget generator that ekhumoro coded I've added in checkboxes (tristate for parent nodes) and now I'm attempting to iterate over these checkboxes and return a dictionary or a list of lists or...something with a parent:children (server:services in this tree) relationship so that I can build paths to folders out of the tree's checked results. This would occur on a 'Start' button press as the first function in a series.

The end goal is to have a tool that pulls logs from multiple servers and services-per-server on a platform based on a user provided start and stop time, and deliver these logs to an admin box with identical folder structure.

I've successfully navigated through a test dictionary, searched for and located files based on os.path.getctime and getmtime, zipped them, then copied them to a seperate drive with identical folder structure that's created as part of a function...

However, I've found literally almost no documentation on the TreeWidgetItemIterator (only http://pyqt.sourceforge.net/Docs/PyQt4/qtreewidgetitemiterator.html#details) which wasn't very helpful to me. So the integral (and final) piece to this puzzle has me lost!

The XMLHandler:

from PyQt4 import QtCore, QtGui, QtXml
from PyQt4.QtXml import *


class XmlHandler(QXmlDefaultHandler):
    def __init__(self, root):
        QtXml.QXmlDefaultHandler.__init__(self)
        self._root = root
        self._item = None
        self._text = ''
        self._error = ''

    def startElement(self, namespace, name, qname, attributes):
        if qname == 'Machine' or qname == 'Feature':
            if self._item is not None:
                self._item = QtGui.QTreeWidgetItem(self._item)
            else:
                self._item = QtGui.QTreeWidgetItem(self._root)
            self._item.setData(0, QtCore.Qt.UserRole, qname)
            self._item.setText(0, 'Unknown Machine')
            if qname == 'Machine':
                self._item.setExpanded(False)
                self._item.setCheckState(0, QtCore.Qt.Unchecked)
                self._item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsTristate)
            elif qname == 'FeatureName':
                self._item.setText(1, self._text)
        self._text = ''
        return True

    def endElement(self, namespace, name, qname):
        if qname == 'FeatureName' or qname == 'MachineName':
            if self._item is not None:
                self._item.setText(0, self._text)
                self._item.setCheckState(0, QtCore.Qt.Unchecked)
        elif qname == 'Feature' or qname == 'Machine':
            self._item = self._item.parent()
        return True

    def characters(self, text):
        self._text += text
        return True

    def fatalError(self, exception):
        print('Parse Error: line %d, column %d:\n  %s' % (
              exception.lineNumber(),
              exception.columnNumber(),
              exception.message(),
              ))
        return False

The Class that uses XmlHandler to make the widget:

class MakeWidget(QtGui.QTreeWidget):
    def __init__(self):
        QtGui.QTreeWidget.__init__(self)
        self.header().setResizeMode(QtGui.QHeaderView.Stretch)
        self.setHeaderLabels(['Servers and Services'])
        source = QtXml.QXmlInputSource()
        source.setData(xml)
        handler = XmlHandler(self)
        reader = QtXml.QXmlSimpleReader()
        reader.setContentHandler(handler)
        reader.setErrorHandler(handler)
        reader.parse(source) 

Creation of the widget in my GUI:

        self.treeServiceSelection = MakeWidget(xml, parent=self.ui.groupBoxServiceSelection)

The widget is two level with parents and children, and that's all:

https://i.stack.imgur.com/mqoO5.jpg

And now we've come to where I'm stuck. I can tie the signal to the button press, I can do everything else but get the dang checked items from the QTreeWidget. It seems that I would need to construct an iterator and have the Checked flag in it's init but anything that I've tried has come up with bupkiss. I'm more than happy to work to a solution rather than have one provided to me, but not having a starting point has been depressing.

Edit: So none of what I actually posted was of any use, but ekhumoro understood the crux of my question and provided his solution (the accepted answer). I added one an elif to handle anything where a parent was checked (as it didn't seem to read that the children were checked as an effect of this):

def exportTree(self):
    mapping = defaultdict(list)
    root = self.tree.invisibleRootItem()
    for index in range(root.childCount()):
        parent = root.child(index)
        if parent.checkState(0) == QtCore.Qt.PartiallyChecked:
            features = mapping[parent.text(0)]
            for row in range(parent.childCount()):
                child = parent.child(row)
                if child.checkState(0) == QtCore.Qt.Checked:
                    features.append(child.text(0))
        elif parent.checkState(0) == QtCore.Qt.Checked:
            features = mapping[parent.text(0)]
            for row in range(parent.childCount()):
                features.append((parent.child(row)).text(0))
    return mapping

and then was able to use this example to select specific children based on another comboBox elsewhere in the gui:

def checkbox2mods(self):
    d = {'DMD2K8COR2VM': ['CC2DCP', 'Centercord'],
         'DMD2K8COR1VM': ['CC2DCP', 'Centercord']}
    root = self.tree.invisibleRootItem()
    if self.checkBox2.checkState() == QtCore.Qt.Checked:
        for index in range(root.childCount()):
            parent = root.child(index)
            for row in range(parent.childCount()):
                child = parent.child(row)
                x = d.values()
                y = d.keys()
                for _ in x:
                    if child.text(0) in _:
                        if parent.text(0) in y:
                            child.setCheckState(0, QtCore.Qt.Checked)
                            parent.setExpanded(True)

I'm sure the code is nasty, but by only selecting the children it partial checks the parents, which allows for the export function to work correctly.

2

There are 2 answers

1
ekhumoro On BEST ANSWER

Im assuming that the core of your question is that you want to iterate over a tree widget and build a dictionary containing the checked items.

The example below assumes the tree is two levels deep, and doesn't attempt any management of check-state:

# not needed for python 3
import sip
sip.setapi('QString', 2)

from collections import defaultdict
from PyQt4 import QtGui, QtCore

class Window(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.tree = QtGui.QTreeWidget(self)
        self.tree.header().hide()
        for index in range(5):
            parent = QtGui.QTreeWidgetItem(self.tree, ['NUS2K%s' % index])
            if index % 3:
                parent.setCheckState(0, QtCore.Qt.PartiallyChecked)
            else:
                parent.setCheckState(0, QtCore.Qt.Unchecked)
            features = 'Loader Reports Logging'.split()
            for count, item in enumerate(features):
                child = QtGui.QTreeWidgetItem(parent, [item])
                if index % 3 and count % 3:
                    child.setCheckState(0, QtCore.Qt.Checked)
                else:
                    child.setCheckState(0, QtCore.Qt.Unchecked)
            parent.setExpanded(True)
        self.button = QtGui.QPushButton('Export', self)
        self.button.clicked.connect(self.handleExport)
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.tree)
        layout.addWidget(self.button)

    def handleExport(self):
        mapping = self.exportTree()
        for machine, features in mapping.items():
            print('%s:' % machine)
            for feature in features:
                print('  %s' % feature)

    def exportTree(self):
        mapping = defaultdict(list)
        root = self.tree.invisibleRootItem()
        for index in range(root.childCount()):
            parent = root.child(index)
            if parent.checkState(0) == QtCore.Qt.PartiallyChecked:
                features = mapping[parent.text(0)]
                for row in range(parent.childCount()):
                    child = parent.child(row)
                    if child.checkState(0) == QtCore.Qt.Checked:
                        features.append(child.text(0))
        return mapping

if __name__ == '__main__':

    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.setGeometry(800, 300, 300, 300)
    window.show()
    sys.exit(app.exec_())
4
Svend Feldt On

I use the below code no iterate through a tree

def updateTreeItems(self):
    for x in range(0,self.ConfigTree.topLevelItemCount()):
        topLvlItem = self.ConfigTree.topLevelItem(x)
        for y in range(numColums):
            self.updateHighlight(topLvlItem,y)
        if self.ConfigTree.isItemExpanded(topLvlItem):
            for i in range(0,topLvlItem.childCount()):
                childItem = topLvlItem.child(i)
                for y in range(numColums):
                    self.updateHighlight(childItem,y)

I use the self.updateHighlight(childItem,y), to highlight a cell in the tree using, but you could add some data to a list or something - if it is checked.

I did have a little trouble reading what your question was, but please let me know if I'm way off? - I think I can help, as I have used some trees by now :-)