What does filterAcceptsRow() exactly do? - python

there are 3 folders in one directory:
f:/root_folder/folder1
f:/root_folder/_folder2
f:/root_folder/folder3.asset
i try to customize QSortFilterProxyModel.filterAcceptsRow() for only show folders that with the .asset suffix.
** i know i can use QFileSystemModel.setNameFilters([*.asset]) do that. but sometimes it doesn't work.
i have Python3.7 + PySide2 5.13.0.
# first inherit a QFileSystemModel instance:
listModel = QFileSystemModel()
# let the instance display only folders, except for '.' and '..':
listModel.setFilter(QDir.NoDotAndDotDot | QDir.Dirs)
# assgin a root path. i just want the model to search the 'f:/root_folder':
listModel.setRootPath("f:/root_folder")
# add a custom QSortFilterProxyModel:
myProxy = myProxyModel()
myProxy.setSourceModel(listModel)
# finally show result in a QListView:
# 'ui' is a QWidget object that contain a listView widget.
ui.listView.setModel(myProxy)
ui.listView.setRootIndex(myProxy.mapFromSource(listModel.index("f:/root_folder")))
here is the custom QSortFilterProxyModel:
# test:
class myProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
return True
at this point, the script works as expected: with 3 folders in the list and no filters.
if i understand correctly, the 'source_parent' should be a QModelIndex to the 'listModel', and it pointing to directory 'f:/root_folder'. and the 'source_row' should be the "ordinal number" of an item in the 'f:/root_folder', one of three folders. right?
then i added my own filters:
# first try:
class myProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel()
# 'source_model' should be the 'listModel', right?
source_index = source_model.index(source_row, 0, source_parent)
# 'source_index' is a QModelIndex, pointing to 'folder1' or '_folder2' or 'folder3.asset'.
# start filtering
filename = source_index.data(Qt.DisplayRole)
print(filename) # check
if filename[-6:] == ".asset": return True
else: return False
it should display 3 folder names on the console, display 1 folder(folder3.asset) in the list. but i got very strange result! here is the result of the console: ** it lists all my hard drives several times
HDD (F:)
root_folder
HDD (F:)
HDD (E:)
HDD (D:)
C:
HDD (F:)
HDD (E:)
HDD (D:)
C:
and the listView is empty.
is the 'source_parent' invalid? then i try this:
class myProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
if not source_parent.isValid():
print("index invalid")
return False
else: return True
got this on console:
index invalid
index invalid
and 3 folders in listView:
now i'm totally confused.
what does filterAcceptsRow() exactly do?

You just have to filter the children of the inde associated with the rootPath() of QFileSystemModel:
from PySide2 import QtCore, QtGui, QtWidgets
class SuffixDirProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._suffix = ""
def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel()
if (
self._suffix
and isinstance(source_model, QtWidgets.QFileSystemModel)
and source_parent == source_model.index(source_model.rootPath())
):
index = source_model.index(source_row, 0, source_parent)
name = index.data(QtWidgets.QFileSystemModel.FileNameRole)
file_info = source_model.fileInfo(index)
return name.split(".")[-1] == self._suffix and file_info.isDir()
return True
#property
def suffix(self):
return self._suffix
#suffix.setter
def suffix(self, s):
self._suffix = s
self.invalidateFilter()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
model = QtWidgets.QFileSystemModel()
model.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
path = # "f:/root_folder"
model.setRootPath(path)
proxy = SuffixDirProxyModel()
proxy.suffix = "asset"
proxy.setSourceModel(model)
w = QtWidgets.QListView()
w.setViewMode(QtWidgets.QListView.IconMode)
w.setModel(proxy)
w.setRootIndex(proxy.mapFromSource(model.index(path)))
w.show()
sys.exit(app.exec_())

Nothing if you don't overload it.
And you only need to overload it if the filter provided in the class you use, usually QSortFilterProxyModel, does not meet your needs.

Related

How to properly remove a row from a QAbstractListModel attached to a QListView without a delay?

I'm trying to build an application with PyQT6 that allows users to browse through a list of images with thumbnails and display the selected image in an image viewer. The application can also add and delete images. Adding images seems to work fine, but when I delete an image from the model the row in the QListView suddenly displays the data from the next row in the list. After a random interval of anywhere between half a second and about five seconds the row will actually be removed, and the list will display the proper file ordering. The fact that this behavior occurs makes me think I'm not removing the item from the model properly, and ideally I'd like the deletion of a row to be instantaneous.
Here is my minimum reproducible example:
import PyQt6 as qt
import PyQt6.QtCore as QtCore
from PyQt6.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
import os
import sys
import traceback
class ImageDataModel(QAbstractListModel):
def __init__(self, images=None):
super(ImageDataModel, self).__init__()
if images is None:
self.images = []
else:
self.images = images
self.thumbnails = []
for img_path in self.images:
icon = QPixmap(img_path).scaledToHeight(20)
self.thumbnails.append(icon)
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole:
img_path = self.images[index.row()]
return img_path
if role == Qt.ItemDataRole.DecorationRole:
thumbnail = self.thumbnails[index.row()]
return thumbnail
def rowCount(self, index):
return len(self.images)
def removeRow(self, index):
self.images.pop(index)
self.thumbnails.pop(index)
class myListView(QListView):
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setSelectionMode(QListView.SelectionMode.ExtendedSelection)
def currentChanged(self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex) -> None:
if (current.row() >= 0):
self.parent.get_selection(current) # this method simply displays the selected image
return super().currentChanged(current, previous)
class MyMenu(QMainWindow):
def __init__(self):
super().__init__()
self.layout = QHBoxLayout()
self.list = myListView(self)
try:
image_file_list = [x for x in os.listdir('path/to/image/directory') if x.lower().endswith(".png")]
except:
image_file_list = []
image_file_list.sort()
self.model = ImageDataModel(image_file_list)
self.list.setModel(self.model)
self.list.clicked.connect(self.get_selection) # this method simply displays the selected image
self.list.setCurrentIndex(self.model.index(0,0))
self.layout.addWidget(self.list)
self.widget = QWidget()
self.widget.setLayout(self.layout)
self.setCentralWidget(self.widget)
# Deletes the currently displayed image and annotation from the dataset
def delete_image(self):
# Determine what to set the new index to after deletion
if self.list.currentIndex().row() != 0:
new_index = self.list.currentIndex().row() - 1
else:
new_index = 0
# Attempt to remove the row and delete the file
try:
self.list.model().removeRow(self.list.currentIndex().row())
os.remove(self.img_path)
# Set index row to the image immediately preceding the deleted image
index = self.model.createIndex(new_index, 0)
self.list.setCurrentIndex(index)
except:
traceback.print_exc()
# Replaced display code for brevity
def get_selection(self, item):
print(item.row())
# Handles keypresses
def keyPressEvent(self, e) -> None:
global model_enabled
if (e.key() == Qt.Key.Key_Escape):
app.quit()
if (e.key() == Qt.Key.Key_Delete):
self.delete_image()
def main():
app = QApplication(sys.argv)
window = MyMenu()
window.show()
app.exec()
main()
Any change in the size, order/layout and data of a model should always be done using the proper function calls so that the views linked to the model get proper notifications about those changes.
For size and layout changes, it's important to always call the begin* and end* functions, which allows the view to be notified about the upcoming change, so they can keep a persistent list of the current items (including selection) and restore it when the change is completed.
Row removal is achieved using beginRemoveRows() and endRemoveRows().
In your case:
def removeRow(self, index):
self.beginRemoveRows(QModelIndex(), index, index)
self.images.pop(index)
self.thumbnails.pop(index)
self.endRemoveRows()
return True # <- the function expects a bool in return
Note that the correct way to implement row removal is done by implementing removeRows(), not removeRow() (singular), which internally calls removeRows anyway. So, you can leave the existing removeRow() call, do not override removeRow() and implement removeRows() instead.
def removeRows(self, row, count, parent=QModelIndex()):
if row + count >= len(self.images) or count < 1:
return False
self.beginRemoveRows(parent, row, row + count - 1)
del self.images[row:row+count]
del self.thumbnails[row:row+count]
self.endRemoveRows()
return True
A similar concept should always be done when adding new items after a view is linked to the model; in that case, implement insertRows() and there you'll call beginInsertRows() insert the new data and finally call endInsertRows().
Note that your code will throw an exception if the images is None, as it doesn't create the thumbnails object.

How to retrieve current filtered elements inner filterAcceptsRow method in QSortFilterProxyModel

In order to filter a TreeView with two levels (see image below), I used the class QSortFilterProxyModel which has a QStandardItemModel as source model.
to ignore the filtering of the root nodes, I overloaded the filterAcceptsRow() method as follows.
this is my main project working with copy/paste a code below model class, i replaced data import from database with a dummy data dictionary
import sys
from PyQt5 import QtWidgets,QtGui
from PyQt5.QtCore import QSortFilterProxyModel
services =[{'id':1,'chanel_name':'France 24','satId':0},
{'id':2,'chanel_name':'Eurosport 02','satId':0},
{'id':3,'chanel_name':'CANAL +','satId':0},
{'id':4,'chanel_name':'Eurosport 02','satId':0},
{'id':5,'chanel_name':'TV 5','satId':0},
{'id':6,'chanel_name':'BBC World','satId':0},
{'id':7,'chanel_name':'M6','satId':0},
{'id':15,'chanel_name':'TEVA','satId':0},
{'id':8,'chanel_name':'PLANETTE','satId':1},
{'id':9,'chanel_name':'France 2','satId':1},
{'id':10,'chanel_name':'France 3','satId':1},
{'id':11,'chanel_name':'MBC 1','satId':1},
{'id':12,'chanel_name':'NIL TV','satId':1},
{'id':13,'chanel_name':'M2','satId':1},
{'id':14,'chanel_name':'CANAL 02','satId':1},
{'id':15,'chanel_name':'CHANEL 1','satId':2},
{'id':8,'chanel_name':'CINE+ FAMILY','satId':2},
{'id':9,'chanel_name':'OCS CITY','satId':2},
{'id':10,'chanel_name':'OCS CHOCS','satId':2},
{'id':11,'chanel_name':'ACTION','satId':2},
{'id':12,'chanel_name':'NATIONAL GEO','satId':2},
{'id':13,'chanel_name':'TVEA','satId':2},
{'id':14,'chanel_name':'CANAL 02','satId':2},
]
class ProxyServiceBySatModel(QSortFilterProxyModel):
def __init__(self,):
super().__init__()
def filterAcceptsRow(self, source_row, source_parent):
index = self.sourceModel().index(source_row,0,source_parent)
if not source_parent.isValid():
return True
else:
return QSortFilterProxyModel.filterAcceptsRow(self,source_row, source_parent)
class ServicesBySat(QtGui.QStandardItemModel):
def __init__(self):
super(ServicesBySat,self).__init__()
self.setHorizontalHeaderLabels(['Sat name'])
self.satellite = ['ASTRA 19° E','HotBird 13°','NILSAT 7° W']
def importData(self, root=None):
sats= {}
root = self.invisibleRootItem()
for service in services:
sat = service['satId']
if sat not in sats:
parent = root
parent.appendRow(QtGui.QStandardItem(self.satellite[sat]))
sats[sat] = parent.child(parent.rowCount() - 1)
else:
parent = sats[sat]
parent.appendRow(QtGui.QStandardItem(service['chanel_name']))
class MainWindowISE(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('Tree search')
self.setGeometry(100,100,300,300)
self.setupUi()
self.model = ServicesBySat()
self.model.importData()
self.proxyModel = ProxyServiceBySatModel()
self.proxyModel.setSourceModel(self.model)
self.proxyModel.setFilterKeyColumn(-1)
self.treeView_services.setModel(self.proxyModel)
self.lineEdit_Search.textEdited.connect(self.on_lineEditSearch_textEdited)
def setupUi(self):
self.gridLayout = QtWidgets.QGridLayout(self)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.label = QtWidgets.QLabel("Search : ", self)
self.horizontalLayout.addWidget(self.label)
self.lineEdit_Search = QtWidgets.QLineEdit(self)
self.horizontalLayout.addWidget(self.lineEdit_Search)
self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
self.treeView_services = QtWidgets.QTreeView(self)
self.gridLayout.addWidget(self.treeView_services, 1, 0, 1, 1)
def on_lineEditSearch_textEdited(self):
self.proxyModel.setFilterRegularExpression(self.lineEdit_Search.text())
def main():
app = QtWidgets.QApplication(sys.argv)
window =MainWindowISE()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
the filtering works well, but now to improve the behavior of my filter, i want to hide the root nodes that have not a childrens in filtered items by adding a condition like this considering that self is my proxy model.
For example,as shown in the representation below, i don't want the NILESAT element be displayed since it has no children who match the filter pattern
I modified my method as follows, to achieve the expected result but without success.
class ProxyServiceBySatModel(QSortFilterProxyModel):
def init(self,):
super().init()
def filterAcceptsRow(self, source_row, source_parent):
index = self.sourceModel().index(source_row,0,source_parent)
if not source_parent.isValid() and self.hasChildren(index) :
return True
else:
return QSortFilterProxyModel.filterAcceptsRow(self,source_row, source_parent)
or i can use this condition but a get the same result.
if not source_parent.isValid() and self.rowCount(index) > 0 :
but i get an exeption
QSortFilterProxyModel: index from wrong model passed to mapToSource
how is it possible to get the current row Count or hasChildren value inner filterAcceptsRow method, or another way to achieve the expected result.

PySide: QFileSystemModel - Display/Show Root Item

i am using QFileSystemModel to display subdirectories of a set root path in a QTreeView. Works all fine but it would be very nice to also see the Root item as it is hidden right now.
model = QtGui.QFileSystemModel()
model.setRootPath(path)
treeview.setModel(model)
treeview.setRootIndex(model.index(path))
treeview.show()
EDIT: OS is Windows 7
The idea is to use as root the parent directory and filter the sibling directories, for this I created a QSortFilterProxyModel that receives an index from the desired directory but you must pass it a QPersistentModelIndex since the latter is permanent unlike the QModelIndex that can be changed in any moment.
import os
from PySide import QtCore, QtGui
class FileProxyModel(QtGui.QSortFilterProxyModel):
def setIndexPath(self, index):
self._index_path = index
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent):
if hasattr(self, "_index_path"):
ix = self.sourceModel().index(sourceRow, 0, sourceParent)
if self._index_path.parent() == sourceParent and self._index_path != ix:
return False
return super(FileProxyModel, self).filterAcceptsRow(sourceRow, sourceParent)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
path = # ...
parent_dir = os.path.abspath(os.path.join(path, os.pardir))
treeview = QtGui.QTreeView()
model = QtGui.QFileSystemModel(treeview)
model.setRootPath(QtCore.QDir.rootPath())
proxy = FileProxyModel(treeview)
proxy.setSourceModel(model)
proxy.setIndexPath(QtCore.QPersistentModelIndex(model.index(path)))
treeview.setModel(proxy)
treeview.setRootIndex(proxy.mapFromSource(model.index(parent_dir)))
treeview.expandAll()
treeview.show()
sys.exit(app.exec_())

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

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:
http://i.imgur.com/npPhG41.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.
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_())
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 :-)

How do I change the colour of a specific branch within QTreeView?

I have a QTreeView, and I've figured out how to set its style by using setStyleSheet in my main class:
self.view.setStyleSheet("""
QTreeView::item {
margin: 2px;
}
""")
That will style the entire QTreeView. But I want certain items in the tree to be bolded. When I create the branches (using [the parent widget].appendRow("the name of the item")), is there a way to 'tag' or isolate specific items so it can be styled the same way? I think the answer has something to do with the 'AccessibleName' or 'ObjectName' properties, but I'm having trouble finding documentation on it.
Update: This is what I have so far:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from future_builtins import *
import os, sys
from PySide.QtCore import *
from PySide.QtGui import *
path_to_media = '/Volumes/show/vfx/webisodes/%d1%/seq/%d2%/%d3%/renders/2d/comp/'
class FileTree(QTreeView):
"""Main file tree element"""
def __init__(self):
QTreeView.__init__(self)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Space or event.key() == Qt.Key_Return:
index = self.selectedIndexes()[0]
crawler = index.model().itemFromIndex(index)
if crawler.uri:
print("launching", crawler.uri)
p = os.popen(('open -a "RV.app" "'+ crawler.uri) +'"', "r")
QTreeView.keyPressEvent(self, event)
class Branch(QStandardItem):
"""Branch element"""
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
class AppForm(QMainWindow):
def __init__(self, parent = None):
super(AppForm, self).__init__(parent)
self.model = QStandardItemModel()
self.view = FileTree()
self.view.setStyleSheet("""
QTreeView::item {
margin: 2px;
}
""")
self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.view.setModel(self.model)
self.setCentralWidget(self.view)
self.Grow()
# self.view.setSortingEnabled(True)
def Grow(self):
"""Populates FileTree (using BuildBranch())"""
global path_to_media
self.path = {}
self.path['source'] = path_to_media
self.path['parts'] = []
self.path['depth'] = 0
self.path['crawl'] = {}
for i in self.path['source'].split('%'):
if i[0] == "d" and i[1].isdigit():
self.path['depth'] += 1
else:
self.path['parts'].append(i)
self.BuildBranch(self.path['parts'], self.path['depth'], parentWidget = self.model.invisibleRootItem())
def BuildBranch(self, parts, depth, uri = '', count = 0, parent = '', parentWidget = ''):
"""Recursively crawls folder structure and adds appropriate branches"""
if not uri: uri = parts[0]
else: uri += parent + parts[count]
try:
if os.listdir(uri):
for i in os.listdir(uri):
if i[0] != '.':
if count != depth:
if os.path.isdir(uri):
thisWidget = Branch(i)
parentWidget.appendRow(thisWidget)
self.BuildBranch(parts, depth, uri, count + 1, i, parentWidget = thisWidget)
else:
thisWidget = Branch(i)
parentWidget.appendRow(thisWidget)
elif count == depth:
thisWidget = Branch(i, uri + i, 'media')
parentWidget.appendRow(thisWidget)
else:
print("nothing here; nuking " + parent)
# Need to add code to nuke unused branch
except OSError:
print("Folder structure error... nuking the branch")
# Need to add code to nuke unused branch
def main():
app = QApplication(sys.argv)
form = AppForm()
form.resize(800, 600)
form.setWindowTitle('Qt Dailies')
form.show()
app.exec_()
if __name__ == "__main__":
main()
Update 2: Okay, I modified my Branch class so that if 'bold' is passed to it, it makes the branch bold (in theory)...
class Branch(QStandardItem):
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
if tag == 'bold':
self.setData(self.createBoldFont(), Qt.FontRole)
def createBoldFont(self):
if self.font: return self.font
self.font = QFont()
self.font.setWeight(QFont.Bold)
return self.font
... but while the code runs, nothing seems to happen. What am I still not getting?
Qt's model-view architecture allows for data that describes the different roles being performed. For example, there is a role for editing data, displaying data, etc. You're interested in the font role (i.e. Qt::FontRole) as the font has a weight enum of which bold is one value.
When you build your branches you first need to identify which items should be bolded. I'll assume you have a method like such that can identify whether or not they should be bold:
def should_be_bolded(self, item):
return 1 # your condition checks
Now just set the weight on the font and set the font role of the item using its setData method:
def BuildBranch(...):
thisWidget = Branch(i)
if self.should_be_bolded(thisWidget):
thisWidget.setData(self.createBoldFont(), Qt.FontRole)
def createFont(self):
if self.font: return self.font
self.font = QFont()
self.font.setWeight(QFont.Bold)
return self.font
But wait... you already have a subclass of QtandardItem, so you can use that:
class Branch(QStandardItem):
"""Branch element"""
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
if self.should_be_bolded():
self.bold_myself()
You'll have to fix the should_be_bolded and bold_myself methods, cleaning up accordingly, but hopefully you get the point.
Stephen pointed out that you can also subclass one of the QAbstractItemModels, like the QStandardItemModel you're using, and return a specific Qt.FontRole. His way makes that knowledge implicit in the model. Decide where that knowledge best belongs and place it in the most appropriate place, whether it's in the item, the tree-creation algorithm, the model, or even a view model.
In your model's data() method you can add code to set the font depending on the content of the item. For example, if you wanted to bold everything in one particular row,
def data(self, index, role):
if role == QtCore.Qt.FontRole:
if index.row() == 1:
boldFont = QtGui.QFont()
boldFont.setBold(True)
return boldFont
You just need a way to retrieve the name of your branch when given an index for it. That depends on the implementation of your tree model.
The Qt Model/View tutorial has a good example, although it's in C++. Look at section 2.2 (Extending the Read Only Example with Roles).

Categories

Resources