Get QTreeview index from text slug - python

I'm trying to retrieve the model index for the QTreeView item using the given slug - which is a single string representing the treeview item's hierarchy separated by hyphens. In this case, I want to get the model index for the given slug 'Vegetable-Carrot-Blackbean':
My current function always returns "Vegetable" and I feel like the way it's written I'd expect it to continually loop through a given index's children until it fails, returning the last found tree item:
import os, sys
from Qt import QtWidgets, QtGui, QtCore
class CategoryView(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.resize(250,400)
self.categoryModel = QtGui.QStandardItemModel()
self.categoryModel.setHorizontalHeaderLabels(['Items'])
self.categoryProxyModel = QtCore.QSortFilterProxyModel()
self.categoryProxyModel.setSourceModel(self.categoryModel)
self.categoryProxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.categoryProxyModel.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.categoryProxyModel.setDynamicSortFilter(True)
self.uiTreeView = QtWidgets.QTreeView()
self.uiTreeView.setModel(self.categoryProxyModel)
self.uiTreeView.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.uiTreeView)
self.setLayout(self.layout)
def appendCategorySlug(self, slug):
parts = slug.split('-')
parent = self.categoryModel.invisibleRootItem()
for name in parts:
for row in range(parent.rowCount()):
child = parent.child(row)
if child.text() == name:
parent = child
break
else:
item = QtGui.QStandardItem(name)
parent.appendRow(item)
parent = item
def getIndexBySlug(self, slug):
parts = slug.split('-')
index = QtCore.QModelIndex()
if not parts:
return index
root = self.categoryModel.index(0, 0)
for x in parts:
indexes = self.categoryModel.match(root, QtCore.Qt.DisplayRole, x, 1, QtCore.Qt.MatchExactly)
if indexes:
index = indexes[0]
root = index
print index, index.data()
return index
def test_CategoryView():
app = QtWidgets.QApplication(sys.argv)
ex = CategoryView()
ex.appendCategorySlug('Fruit-Apple')
ex.appendCategorySlug('Fruit-Orange')
ex.appendCategorySlug('Vegetable-Lettuce')
ex.appendCategorySlug('Fruit-Kiwi')
ex.appendCategorySlug('Vegetable-Carrot')
ex.appendCategorySlug('Vegetable-Carrot-Blackbean')
ex.appendCategorySlug('Vegan-Meat-Blackbean')
ex.getIndexBySlug('Vegetable-Carrot-Blackbean')
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
pass
test_CategoryView()

The reason your current implementation doesn't work is that the start argument of match() needs to be valid index. The invisisble root item can never be valid because its row and column will always be -1. So, instead, you must use the the model's index() function to try to get the first child index of the current parent. You also need to make sure that an invalid index is returned when any part of the slug cannot be matched, otherwise you could wrongly end up returning an ancestor index.
Here's a method that implements all that:
def getIndexBySlug(self, slug):
parts = slug.split('-')
indexes = [self.categoryModel.invisibleRootItem().index()]
for name in parts:
indexes = self.categoryModel.match(
self.categoryModel.index(0, 0, indexes[0]),
QtCore.Qt.DisplayRole, name, 1,
QtCore.Qt.MatchExactly)
if not indexes:
return QtCore.QModelIndex()
return indexes[0]
As an alternative, you might want to consider returning an item instead, as that will then give you access to the whole QStandardItem API (and the index can be still be easily obtained via item.index()):
def itemFromSlug(self, slug):
item = None
parent = self.categoryModel.invisibleRootItem()
for name in slug.split('-'):
for row in range(parent.rowCount()):
item = parent.child(row)
if item.text() == name:
parent = item
break
else:
item = None
break
return item
But note that this returns None if the slug cannot be found (although it could easily be tweaked to return the invisible root item instead).

In this case the convenient way is to iterate over the children recursively:
def getIndexBySlug(self, slug):
parts = slug.split("-")
index = QtCore.QModelIndex()
if not parts:
return index
for part in parts:
found = False
for i in range(self.categoryModel.rowCount(index)):
ix = self.categoryModel.index(i, 0, index)
if ix.data() == part:
index = ix
found = True
if not found:
return QtCore.QModelIndex()
return index

Related

QTreeview iterating nodes

I use PyQt5 and the QTreeview to show an xml tree (and some of its attributes as columns) loaded from a file. This works as expected. But I'm now struggling with two things:
The itemChange event. The tree node should be editable and I need to do some checks after editing. The itemChange is called but it is called N times when editing one node.
Starting at the edited item I need to go up and down in the tree and check the child and parent nodes. I expected this to be simple like getParent() for one and a recursive getChildren() for the other direction. But how do I get form the itemChange events QStandardItem to the parent and child items?
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
import xml.etree.ElementTree as ET
tree = ET.ElementTree(file='data.xml')
class MainFrame(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MainFrame, self).__init__(parent)
self.tree = QtWidgets.QTreeView(self)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(self.tree)
root_model = QtGui.QStandardItemModel()
root_model.setHorizontalHeaderLabels(['Label', 'privilege', 'uid', 'docId'])
self.tree.setModel(root_model)
self.tree.setUniformRowHeights(True)
root = tree.getroot()
self._populateTree(root, root_model.invisibleRootItem())
self.tree.expandAll()
self.tree.resizeColumnToContents(0)
self.tree.resizeColumnToContents(1)
self.tree.resizeColumnToContents(2)
self.move(0, 0)
self.resize(800, 1000)
#self.tree.itemChanged.connect(self.edit)
def _populateTree(self, root, parent):
child_item = QtGui.QStandardItem(root.tag)
child_item.setData(root)
privilege = ''
uid = ''
docId = ''
privilege_item = QtGui.QStandardItem(privilege)
uid_item = QtGui.QStandardItem(uid)
docId_item = QtGui.QStandardItem(docId)
parent.appendRow([child_item, privilege_item, uid_item, docId_item])
privilege_item.model().itemChanged.connect(self.change_privilege)
child_item.model().itemChanged.connect(self.change_privilege)
for elem in root.getchildren():
self._populateTree(elem, child_item)
def change_privilege(self, item):
print(item)
print(item.row(), item.column())
def check_parent(self, item):
# get parent of item and check value
# check_parent(...)
def check_child(self, item):
# get children of item and check value
# for child in item.children():
# check_child(child)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main = MainFrame()
main.show()
sys.exit(app.exec_())
To answer the first question: every time you call MainFrame._populateTree you create a signal-slot connection between the signals child_item.model().itemChanged and privilege_item.model().itemChanged and the slot MainFrame.change_privilege. However, since all child items and privelege item belong to the same model (i.e. root_model) you are effectively creating multiple connections between the same signal and slot. This means that when any item in the model is changed, the slot is called multiple times as well. Easiest way around this is to create one connection in __init__ or so.
To access the parent of an item you can indeed use item.parent(). The children of an item can be accessed one-by-one via item.child(row, column) where row is in the range range(item.rowCount()) and column in the range range(item.columnCount()), e.g.
def change_privilege(self, item):
print(item.row(), item.column(), item.text())
self.check_parent(item)
self.check_child(item)
def check_parent(self, item):
if item.parent():
print('Parent:', item.parent().text())
else:
print('No parent')
def check_child(self, item):
if item.hasChildren():
print('Children:')
for row in range(item.rowCount()):
print('\t', row, end='')
for col in range(item.columnCount()):
print('\t', item.child(row, col).text(), end='')
print()
else:
print('No children')

Crashes when moving cellWidgets around in a TableWidget

I am writing a tool that allows me to track some tasks along a path of predifined stages, from something on a backlog, to ToDo, through WIP, Review and finally to done.
I created a custom widget, that will eventually be yellow, not unlike a postit note and perhaps with a bit of formatting it to give it a nice frame, etc... but stopped before getting far enough to make it look right because of this issue.
The idea is that each of these yellow Task widgets will have a stage they are at, and that I can select them in a Table Widget, and move them onto the next or previous stage, which will update taht objects stage, then refresh the TableWidget, read all the widget and where thay should be and set them in their new place.
So I have it kind of working to some degree (below), where I can move the tasks forward and they update location, but I noticed when I click the cells that the widget was previously in, print statement still says that the cell still has a widget there (which kind of makes sense, as code below isn't removing the previous one, but I'd expect to visually still see it). And I can move them forward and backwards, and the information on the tasks does update correctly, but the table won't refresh unless the task moves to a cell that never had a cellWidget in it. Test this by moving it backwards. It works, movnig forward visually does nothing, but moving again, does show up.
I tried clearing the TableWidget and rebuilding from scratch and that crashes. The main issue I am having is that with all these crashes, which is an issue in itself as it makes debugging very tough... When I try and clear the TableWidget (with .clear()) before repopulating, I get this.
Process finished with exit code -1073741819 (0xC0000005)
Same error code if I try removing the old cells by setting the Table Widget to 0 rows before adding the correct number of rows.
A known issue that is less important is when I select a cell without a widget and try and move it, gies me this, but don't worry too much about that fix, as it's known issue.
Process finished with exit code -1073740791 (0xC0000409)
Also tried cleaning up by iterating every cell and if it has a cell widget, remove cell widget before re-setting them to correct place and it still crashes. I'm out of ideas.
Task Widget
import sys
from PyQt5.QtWidgets import (QApplication, QTableWidget, QWidget, QFrame, QHBoxLayout, QLabel,
QPushButton,QVBoxLayout)
class Task(QWidget):
def __init__(self, ID, name, est):
super(Task, self).__init__()
# Creates a small widget that will be added to a table widget
self.ID = ID
self.name = name
self.est = est
# These cell widgets represent tasks. So each task has a particular 'stage' it is at
self.stage = 'ToDo'
self.stages = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
self.objects_labels = {}
self.initUI()
def initUI(self):
# adds a bunch of labels to the widget
layout = QVBoxLayout()
frame = QFrame()
frame.setFrameShape(QFrame.StyledPanel)
frame.setStyleSheet('background-color: red')
frame.setLineWidth(2)
layout.addWidget(frame)
info = [self.ID, self.name, self.est]
for section in info:
self.objects_labels[section] = QLabel(str(section))
layout.addWidget(self.objects_labels[section])
self.setLayout(layout)
self.setStyleSheet('background-color: yellow')
def task_move(self, forward = True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
print(self.stages)
print(index)
if forward:
print('--->')
if self.stage == self.stages[-1]:
print('Already at the end of process')
return
self.stage = self.stages[index + 1]
else:
print('<---')
if self.stage == self.stages[0]:
print('Already at the start of process')
return
self.stage = self.stages[index - 1]
MainWidget
class MainWidget(QWidget):
def __init__(self):
super().__init__()
self.tasks = self.make_tasks()
self.init_ui()
self.update_tw()
def make_tasks(self):
# Create a few tasks
a = Task(0, 'Name_A', 44)
b = Task(0, 'Name_B', 22)
c = Task(0, 'Name_C', 66)
d = Task(0, 'Name_D', 90)
return [a, b, c, d]
def init_ui(self):
layout_main = QVBoxLayout()
self.tw = QTableWidget()
self.tw.cellClicked.connect(self.cell_clicked)
self.tw.horizontalHeader().setDefaultSectionSize(120)
self.tw.verticalHeader().setDefaultSectionSize(120)
layout_main.addWidget(self.tw)
layout_bottom_button_bar = QHBoxLayout()
self.btn_task_backward = QPushButton('<--- Task')
self.btn_task_backward.clicked.connect(lambda: self.move_task(forward=False))
self.btn_task_forward = QPushButton('Task --->')
self.btn_task_forward.clicked.connect(lambda: self.move_task())
for widget in [self.btn_task_backward, self.btn_task_forward]:
layout_bottom_button_bar.addWidget(widget)
layout_main.addLayout(layout_bottom_button_bar)
self.setLayout(layout_main)
self.setGeometry(300, 300, 800, 600)
self.setWindowTitle('MainWidget')
self.show()
#property
def tw_header(self):
return {'Backlog': 0, 'ToDo': 1, 'WIP': 2, 'Review': 3, 'Done': 4}
#property
def selected_indices(self):
return [(x.row(), x.column()) for x in self.tw.selectedIndexes()]
#property
def selected_widgets(self):
selected_widgets = [self.tw.cellWidget(x[0], x[1]) for x in self.selected_indices]
print(selected_widgets)
return selected_widgets
def move_task(self, forward=True):
# Crashes if you select a non-widget cell, but thats a known issue
# Moves the task forward or backward and then prompts to update the TableWidget
for object in self.selected_widgets:
object.task_move(forward=forward)
self.tw.clearSelection()
self.update_tw()
def cell_clicked(self, row, column):
if self.tw.cellWidget(row, column):
print(self.selected_indices)
print(self.selected_widgets)
else:
print('No Cell Widget here')
def update_tw(self):
#I wanted to clear the Table widget and rebuild, but this crashes
# self.tw.clear()
self.tw.setHorizontalHeaderLabels(self.tw_header.keys())
rows = len(self.tasks)
columns = len(self.tw_header)
self.tw.setRowCount(rows)
self.tw.setColumnCount(columns)
# Looks through each task, and then gets it's stage, and then adds the widget to the correct column
for index, object in enumerate(self.tasks):
column = self.tw_header[object.stage]
print('Setting stage {} for {}\n...to r={}, c={}\n***'.format(object.stage, object, index, column))
self.tw.setCellWidget(index, column, object)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWidget()
sys.exit(app.exec_())
From my previous experience, I always found using setCellWidget clunky, underperforming and buggy.
Most of the times my Widgets were lost or misplaced, while refreshing the table similarly to the way you are doing it.
In addition, I guess you would want to use this "Task Mover" on a larger scale, and from what I could see, setting separate Widgets inside QWidgetItems becomes quite slow when done on loads of items.
My suggestion would be to use style delegates, so that you can customize the look of your items to your liking, without having to deal with the setCellWidget stuff which is giving you problem.
Once you have your own delegate, and paint the items the way you want, you can just keep updating that item data and moving the items around the table by using "take" and "set".
I am not sure if this would be the best way of executing this specific task, but moving towards this direction would probably give you greater flexibility and customisation power in the long run.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class TaskProperty():
properties = ["ID", "name", "est", "stage"]
count = 4
ID, Name, Est, Stage = [Qt.UserRole + x for x in range(count)]
STAGES = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
class MainWidget(QWidget):
def __init__(self):
super(MainWidget, self).__init__()
self.tasks = self.make_tasks()
self.init_ui()
self.update_tw()
def make_tasks(self):
# Create a few tasks
a = Task(0, 'Name_A', 44)
b = Task(0, 'Name_B', 22)
c = Task(0, 'Name_C', 66)
d = Task(0, 'Name_D', 90)
return [a, b, c, d]
def init_ui(self):
layout_main = QVBoxLayout()
self.tw = QTableWidget()
# create and set the delegate to the TableWidget
self.delegate = TaskDelegate(self.tw )
self.tw.setItemDelegate(self.delegate)
self.tw.cellClicked.connect(self.cell_clicked)
self.tw.horizontalHeader().setDefaultSectionSize(120)
self.tw.verticalHeader().setDefaultSectionSize(120)
layout_main.addWidget(self.tw)
layout_bottom_button_bar = QHBoxLayout()
self.btn_task_backward = QPushButton('<--- Task')
self.btn_task_backward.clicked.connect(lambda: self.move_task(forward=False))
self.btn_task_forward = QPushButton('Task --->')
self.btn_task_forward.clicked.connect(lambda: self.move_task())
for widget in [self.btn_task_backward, self.btn_task_forward]:
layout_bottom_button_bar.addWidget(widget)
layout_main.addLayout(layout_bottom_button_bar)
self.setLayout(layout_main)
self.setGeometry(300, 300, 800, 600)
self.setWindowTitle('MainWidget')
self.show()
#property
def tw_header(self):
return {'Backlog': 0, 'ToDo': 1, 'WIP': 2, 'Review': 3, 'Done': 4}
#property
def selected_indices(self):
return [(x.row(), x.column()) for x in self.tw.selectedIndexes()]
def move_task(self, forward=True):
'''
To move the task to the next step, we iterate all the items selected.
If the task can be moved, we take the corresponding item from its current cell and move it to the destination.
:param forward:
:return:
'''
selected =self.tw.selectedItems()
for item in selected:
item.setSelected(False)
result = item.task_move(forward=forward)
if result:
next = 1 if forward else -1
row = item.row()
column = item.column()
moveItem = self.tw.takeItem(row, column)
self.tw.setItem(row, column + next, moveItem)
moveItem.setSelected(True)
def cell_clicked(self, row, column):
item = self.tw.item(row, column)
if not isinstance(item, TaskItem):
print "No Task Item Here"
def update_tw(self):
# I wanted to clear the Table widget and rebuild, but this crashes
# self.tw.clear()
self.tw.clear()
self.tw.setHorizontalHeaderLabels(self.tw_header.keys())
rows = len(self.tasks)
columns = len(self.tw_header)
self.tw.setRowCount(rows)
self.tw.setColumnCount(columns)
# Looks through each task, and then gets it's stage, and then adds the widget to the correct column
for row, object in enumerate(self.tasks):
# create items of our custom type only for the column that need to be filled.
# the other cells will be filled with null items.
column = STAGES.index(object.stage)
print('Setting stage {} for {}\n...to r={}, c={}\n***'.format(object.stage, object, row, column))
item = TaskItem(object)
self.tw.setItem(row, column, item)
class TaskDelegate(QStyledItemDelegate):
'''
This delegate take care of Drawing our cells the way we want it to be.
'''
def paint(self, painter, option, index):
'''
Override the Paint function to draw our own cell.
If the QTableWidgetItem does not have our Data stored in it, we do a default paint
:param painter:
:param option:
:param index:
:return:
'''
painter.save()
rect = option.rect
status = index.data(TaskProperty.Stage)
if status is None:
return super(TaskDelegate, self).paint(painter, option, index)
else:
id = STAGES.index(status)
pen = painter.pen()
pen.setBrush(Qt.black)
painter.setPen(pen)
if id == index.column():
rect.translate(3, 3)
newRect = QRect(rect.x(), rect.y(), rect.width() - 6, 20)
infos = [index.data(TaskProperty.ID), index.data(TaskProperty.Name), index.data(TaskProperty.Est)]
painter.setBrush(Qt.red)
painter.drawRect(newRect)
painter.setBrush(Qt.yellow)
for info in infos:
newRect.translate(0, 25)
painter.drawRect(newRect)
painter.drawText(newRect, Qt.AlignHCenter | Qt.AlignVCenter,
str(info))
class TaskItem(QTableWidgetItem):
'''
Subclass QTableWidgetItem.
Probably not needed, since we can set the property when we create the item instead of in the init,
and keep track of which item is attached to which task object using the Column Index of the table.
However, this can be useful if you want to attach more specific procedures to your items
'''
def __init__(self, task):
super(TaskItem, self).__init__()
self._task = task
self.setData(TaskProperty.ID, task.ID)
self.setData(TaskProperty.Name, task.name)
self.setData(TaskProperty.Est, task.est)
self.setData(TaskProperty.Stage, task.stage)
self.objects_labels = {}
def task_move(self, forward=True):
result = self._task.task_move(forward=forward)
self.setData(TaskProperty.Stage, self._task.stage)
return result
class Task(object):
'''
The Task class is now just an object, not a widget.
'''
def __init__(self, ID, name, est):
# Creates a small widget that will be added to a table widget
self.ID = ID
self.name = name
self.est = est
# These cell widgets represent tasks. So each task has a particular 'stage' it is at
self.stage = 'ToDo'
self.stages = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
self.objects_labels = {}
def task_move(self, forward=True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
if forward:
print('--->')
if self.stage == self.stages[-1]:
#print('Already at the end of process')
return False
self.stage = self.stages[index + 1]
else:
print('<---')
if self.stage == self.stages[0]:
#print('Already at the start of process')
return False
self.stage = self.stages[index - 1]
return True
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWidget()
sys.exit(app.exec_())
It is not necessary to clean and create everything again, instead just move the widget for it we must know if it can be moved or not and for that task_move must indicate if the movement is valid or not. Considering the above, the solution is:
def task_move(self, forward=True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
print(self.stages)
print(index)
if forward:
print("---&gt")
if self.stage == self.stages[-1]:
print("Already at the end of process")
return False
self.stage = self.stages[index + 1]
else:
print("&lt---")
if self.stage == self.stages[0]:
print("Already at the start of process")
return False
self.stage = self.stages[index - 1]
return True
def move_task(self, forward=True):
for row, column in self.selected_indices:
widget = self.tw.cellWidget(row, column)
if isinstance(widget, Task) and widget.task_move(forward):
next_column = column + (1 if forward else -1)
# create new task widget
task = Task(widget.ID, widget.name, widget.est)
# remove all task widget
self.tw.removeCellWidget(row, column)
# move task widget
self.tw.setCellWidget(row, next_column, task)
self.tw.clearSelection()
The crashed is because when using clear you are also removing the Task widget so "self.tasks" has objects deleted from C++ that you should not use.

QSortFilterProxyModel Filter Issue

Using a QSortFilterProxyModel I need to filter the data based on the value of a specific column; however, the column may contain multiple values. I need to NOT show the row if the column contains a specific value. Do I need to subclass the QSortFilterProxyModel and override the filterAcceptsRow() method or should I use a setFilterRegExp?
The column can contain integers: 0,1,2,3. If the column contains a 2 then I need to not show the row.
If you store data as a QList or list you can easily subclass QSortFilterProxyModel to check this list in every row
Here is a simple example:
import sys
from PyQt5.QtCore import QSortFilterProxyModel,Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QListView, QApplication
app = QApplication(sys.argv)
list = QListView()
list.setWindowTitle('sample')
list.setMinimumSize(600, 400)
model = QStandardItemModel(list)
for i in range(1, 10):
# create each item with a list attached to the Qt::UserRole + 1
item = QStandardItem(str(i))
item.setData([i, i*2], Qt.UserRole + 1)
model.appendRow(item)
class MyFilterModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
i = self.sourceModel().index(source_row, 0, source_parent)
data = self.sourceModel().data(i, Qt.UserRole + 1)
print(data)
return 2 not in data
filter_model = MyFilterModel()
filter_model.setSourceModel(model)
list.setModel(filter_model)
list.show()
app.exec()
You can even customize your filter model to accepts a lambda filter function
According to your description, you should subclass the QSortFilterProxyModel and override the filterAcceptsRow(), here bellow is a simple proxy model for table model:
class DemoProxyModel(QSortFilterProxyModel):
"""
"""
def __init__(self, parent=None):
super().__init__(parent)
def filterAcceptsRow(self, sourceRow, sourceParent):
"""custom filtering"""
if not self.filterRegExp().isEmpty():
model_source = self.sourceModel()
if model_source is None:
return False
# pick up the column that you want to filter, e.g., column 3
index0 = model_source.index(sourceRow, 3, sourceParent)
data0 = model_source.data(index0)
# return self.filterRegExp() == data_type # equal match, the tight binding
return self.filterRegExp().indexIn(data0) >= 0 # include match, the loose binding
# return data_type.__contains__(self.filterRegExp()) # include match, the loose binding
# parent call for initial behaviour
return super().filterAcceptsRow(sourceRow, sourceParent)

Strange behaviour when appending columns in QStandardItemModel with nested QStandardItems

I am having a setup, where I need a QStandardItemModel with nested QStandardItems to be displayed via a QTreeView. I need to add and remove columns which I cannot seem to figure out how to do properly.
To insert the initial nested rows I use appendRow([item, item, ...]) and always use the previous item as new parent.
I get an unexpected row and column count of 0 for the last QStandardItem as can be seen in the logging output.
After an appendColumn() the column count of the items does not increase from 3 to 4. Instead a new item is inserted at an index with row 0 and column 1.
Before appendColumn() - column count 3
After appendColumn() - new column appears but column count is still 3
Example Code
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import sys
import uuid
from PySide import QtCore
from PySide import QtGui
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class TestModel(QtGui.QStandardItemModel):
def __init__(self, *args, **kwargs):
super(TestModel, self).__init__(*args, **kwargs)
self.logger = logging.getLogger(type(self).__name__)
def set_data(self):
"""Fill with 3 nested rows with 3 columns each for testing."""
# cleanup
self.clear()
self.setHorizontalHeaderLabels(["header_1", "header_2", "header_3"])
# fill nested rows
parent = None
for index in range(3):
row = [QtGui.QStandardItem(str(uuid.uuid4().hex[:4])) for _ in range(3)]
self.appendRow(row) if (parent is None) else parent.appendRow(row)
parent = row[0]
def print_rows(self):
"""Print information about all rows of the model."""
for index in self.all_indices():
item = self.itemFromIndex(index)
msg = "QModelIndex: row {0} column {1} "
msg += "QStandardItem: row count {2} column count {3} "
msg += "value: {4}"
msg = msg.format(
index.row(), index.column(),
item.rowCount(), item.columnCount(), item.text())
self.logger.info(msg)
def all_indices(self, indices=None, index=None):
"""Recurse all indices under index in the first column and return
them as flat list.
"""
indices = indices if (isinstance(indices, list)) else []
index = index if (isinstance(index, QtCore.QModelIndex)) else QtCore.QModelIndex()
for row in range(self.rowCount(index)):
current_index = self.index(row, 0, index)
indices.append(current_index)
if (self.hasChildren(current_index)):
self.all_indices(indices, current_index)
return indices
def append_column(self):
self.logger.info("appendColumn()")
self.appendColumn([QtGui.QStandardItem(str(uuid.uuid4().hex[:4])),])
# test
if (__name__ == "__main__"):
app = QtGui.QApplication(sys.argv)
# widget
widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
widget.setLayout(layout)
widget.show()
# model
model = TestModel()
model.set_data()
# view
view = QtGui.QTreeView()
view.setModel(model)
layout.addWidget(view)
# btn_print_rows
btn_print_rows = QtGui.QPushButton("Prints rows")
btn_print_rows.clicked.connect(model.print_rows)
layout.addWidget(btn_print_rows)
# btn_append_column
btn_append_column = QtGui.QPushButton("Append column")
btn_append_column.clicked.connect(model.append_column)
layout.addWidget(btn_append_column)
sys.exit(app.exec_())
Questions
I get an unexpected row and column count for the last QStandardItem as can be seen in the logging output. Why has the last item a row and column count of 0 when the model clearly displays what seems to be correct data?
How can i correctly facilitate appending/removing of columns in a QStandardItemModel with nested QStandardItems (displayed in a QTreeView)?
Should I have QStandardItems with a column count equal to the header item count? (de/incrementing item.columnCount() for each new column). Or should all QStandardItems have a column count of 1?

PyQt4 Qtreewidget - get hierarchy text if child checkbox is checked

What I am currently trying to do is take a populated tree (qtreewidget) that has checkboxes at the bottom child level, and return the text of the path to the child if the box is checked. The reason I want to do this, is if a child is checked, it will then change a value in a key in a dictionary. (The "raw" dictionary the tree was created from). Here's a visual example of what I mean:
From user input and server directory crawling, we have populated a tree that looks something like this:
(Only the lowest level child items have checkboxes.) And sorry for the horrible tree diagram!! Hopefully it makes sense...
edited
study
-subject 1
--date
---[]c
---[]d
---[]e
-subject 2
--date
---[]g
---[]h
If someone checks (for example) the "g" levle child, is there anyway to then get the path to "g" in a form something like [1, B, g] or 1-B-g or 1/B/g, etc.?
One of the children levels (let's say in the example A and B) are also set to be user editable. So I'd need the info from the tree, not the info the tree was originally populated from.
I have tried printing self.ui.treeWidget indexes with no real luck in getting what I want. I feel as though there is an easy solution for this, but I can't seem to find it. Hopefully someone can help!
Actual Code Snippet:
for h,study in enumerate(tree_dict['study']):
study_name = study['study_name']
treeSTUDY = QtGui.QTreeWidgetItem(self.ui.treeWidget, [study_name])
treeSTUDY.setFlags(QtCore.Qt.ItemIsEnabled)
self.ui.treeWidget.expandItem(treeSTUDY)
for i,subject in enumerate(study['subject']):
subject = subject['ID']
treeSUBJECT = QtGui.QTreeWidgetItem(treeSTUDY, [subject_id])
treeSUBJECT.setFlags(QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled)
for j,visit in enumerate(subject['visit']):
scan_date = visit['date']
treeDATE = QtGui.QTreeWidgetItem(treeSUBJECT, [scan_date[4:6])
treeDATE.setFlags(QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled)
for k,ser in enumerate(visit['series']):
s_name = ser['name'] + '-' + ser['description']
count = str(ser['count'])
treeSCAN = QtGui.QTreeWidgetItem(treeDATE)
treeSCAN.setFlags(QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable)
treeSCAN.setCheckState(0, QtCore.Qt.Unchecked)
treeSCAN.setText(0, s_name)
treeSCAN.setText(1, ser['time'])
treeSCAN.setText(2, ser['number'])
treeSCAN.setText(3, 'count')
All you need is a method that walks up the parent/child chain grabbing the text of each item until the parent is None:
def getTreePath(self, item):
path = []
while item is not None:
path.append(str(item.text(0)))
item = item.parent()
return '/'.join(reversed(path))
UPDATE:
Here is a demo script that shows how to get the checked item and retrieve its path:
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.tree = QtGui.QTreeWidget(self)
self.tree.setHeaderHidden(True)
for index in range(2):
parent = self.addItem(self.tree, 'Item%d' % index)
for color in 'Red Green Blue'.split():
subitem = self.addItem(parent, color)
for letter in 'ABC':
self.addItem(subitem, letter, True, False)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.tree)
self.tree.itemChanged.connect(self.handleItemChanged)
def addItem(self, parent, text, checkable=False, expanded=True):
item = QtGui.QTreeWidgetItem(parent, [text])
if checkable:
item.setCheckState(0, QtCore.Qt.Unchecked)
else:
item.setFlags(
item.flags() & ~QtCore.Qt.ItemIsUserCheckable)
item.setExpanded(expanded)
return item
def handleItemChanged(self, item, column):
if item.flags() & QtCore.Qt.ItemIsUserCheckable:
path = self.getTreePath(item)
if item.checkState(0) == QtCore.Qt.Checked:
print('%s: Checked' % path)
else:
print('%s: UnChecked' % path)
def getTreePath(self, item):
path = []
while item is not None:
path.append(str(item.text(0)))
item = item.parent()
return '/'.join(reversed(path))
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 250, 450)
window.show()
sys.exit(app.exec_())

Categories