Conditional custom rows in pyqt4 QFileSystemModel - qt

I want to add custom rows in QFileSystemModel under QTreeview. The row is only added when the directory contains files with a certain extension. Basically, After starting up the directory listing, the user will click through the folders. As soon as the folder the user clicked contains the target file, I would like to hide these files (which I know how to do), then use custom rows to represent a summary of these files.
For example, if the folder contains files like the following
A.01.dat
A.02.dat
A.03.dat
...
B.01.dat
B.02.dat
B.03.dat
I would like to create custom rows:
A
B
However, if the folder clicked does not contain these .dat files, then no custom rows should be created.
I have also tried to insert rows directly into QFileSystemModel
self.treeivew.model = QtGui.QFileSystemModel()
...
for n, s in enumerate(self.sequence):
self.treeview.model.beginInsertRows(index, 0, 0)
result = self.treeview.model.insertRow(1, index)
print(result)
self.treeview.model.setData(index, QString(s['Name']),role=QtCore.Qt.DisplayRole)
self.treeview.model.endInsertRows()
But the insertion failed.
If reimplementation is necessary, as I have seen many places have suggested, can anyone provide a concrete example on how the reimplementation should be done to allow such conditional custom row insertion?
Thanks in advance.

I would implement an item model with dynamic child insertion. This is just a standard QAbstractItemModel with a few extra methods -
rowCount - you would normally implement this for a tree model anyway. Just make sure that it returns 0 if the node has children that have not been loaded yet.
hasChildren - override to return True for nodes that have children that haven't been loaded yet and return whatever the base class returns in all other cases.
canFetchMore - return True if the node has children that haven't been loaded yet, False otherwise.
fetchMore - this is where you perform whatever logic you need to decide what nodes to create and insert them into the model.
Here's the basic idea - for nodes that you know have children that haven't been loaded, return 0 from rowCount and True from canFetchMore and hasChildren. This tells Qt to show a node with an expander next to it even though it currently has no children. When the expander is clicked, fetchMore is called and you populate the children from the given parent.
One thing to note - you must call beginInsertRows and endInsertRows in the fetchMore method. What's more, you musn't change the underlying datastore before calling beginInsertRows or after endInsertRows. Unfortunately, you need to know how many rows you are inserting when you call beginInsertRows - so you are probably going to want to generate a list of nodes to add, then make the call to beginInsertRows. If you do it this way though, you can't set the new nodes' parent, as it would change the underlying datastore.
You can see in the code below, that I set the parent node in the Node.insert_child method which is called between the beginInsertRows and endInsertRows calls.
The code doesn't do exactly what you are after - it's a basic file system model illustrating dynamic loading, you'll need to insert you custom logic to generate the category nodes you want in the fetchMore call. It also only shows the filename and lacks icons.
If you want the modified date and size to be shown, you'll need to store these in the relevant nodes and set the model columnCount method to return the correct number of columns.
For icons, extend the model data method to check for the Qt.DecorationRole and return the relevant QIcon.
There might be some superfluous stuff in the code as it's a cut down and repurposed model from something else.
import sys
import os
import sip
sip.setapi('QVariant', 2)
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Node(object):
def __init__(self, name, path=None, parent=None):
super(Node, self).__init__()
self.name = name
self.children = []
self.parent = parent
self.is_dir = False
self.path = path
self.is_traversed = False
if parent is not None:
parent.add_child(self)
def add_child(self, child):
self.children.append(child)
child.parent = self
def insert_child(self, position, child):
if position < 0 or position > self.child_count():
return False
self.children.insert(position, child)
child.parent = self
return True
def child(self, row):
return self.children[row]
def child_count(self):
return len(self.children)
def row(self):
if self.parent is not None:
return self.parent.children.index(self)
return 0
class FileSystemTreeModel(QAbstractItemModel):
FLAG_DEFAULT = Qt.ItemIsEnabled | Qt.ItemIsSelectable
def __init__(self, root, path='c:/', parent=None):
super(FileSystemTreeModel, self).__init__()
self.root = root
self.parent = parent
self.path = path
for file in os.listdir(path):
file_path = os.path.join(path, file)
node = Node(file, file_path, parent=self.root)
if os.path.isdir(file_path):
node.is_dir = True
def getNode(self, index):
if index.isValid():
return index.internalPointer()
else:
return self.root
## - dynamic row insertion starts here
def canFetchMore(self, index):
node = self.getNode(index)
if node.is_dir and not node.is_traversed:
return True
return False
## this is where you put custom logic for handling your special nodes
def fetchMore(self, index):
parent = self.getNode(index)
nodes = []
for file in os.listdir(parent.path):
file_path = os.path.join(parent.path, file)
node = Node(file, file_path)
if os.path.isdir(file_path):
node.is_dir = True
nodes.append(node)
self.insertNodes(0, nodes, index)
parent.is_traversed = True
def hasChildren(self, index):
node = self.getNode(index)
if node.is_dir:
return True
return super(FileSystemTreeModel, self).hasChildren(index)
def rowCount(self, parent):
node = self.getNode(parent)
return node.child_count()
## dynamic row insert ends here
def columnCount(self, parent):
return 1
def flags(self, index):
return FileSystemTreeModel.FLAG_DEFAULT
def parent(self, index):
node = self.getNode(index)
parent = node.parent
if parent == self.root:
return QModelIndex()
return self.createIndex(parent.row(), 0, parent)
def index(self, row, column, parent):
node = self.getNode(parent)
child = node.child(row)
if not child:
return QModelIndex()
return self.createIndex(row, column, child)
def headerData(self, section, orientation, role):
return self.root.name
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == Qt.DisplayRole:
return node.name
else:
return None
def insertNodes(self, position, nodes, parent=QModelIndex()):
node = self.getNode(parent)
self.beginInsertRows(parent, position, position + len(nodes) - 1)
for child in nodes:
success = node.insert_child(position, child)
self.endInsertRows()
return success
app = QApplication(sys.argv)
model = FileSystemTreeModel(Node('Filename'), path='c:/')
tree = QTreeView()
tree.setModel(model)
tree.show()
sys.exit(app.exec_())

Related

Adapting a PyQt QSqlTableModel to color rows, if they include a specific entry in a column, while retaining editing functionality

I am currently working a small database visualization using PyQt. As the visualization should include edit functionality on some columns, I wanted to use PyQts QSqlTableModel.
Basically a new dataset is given and compared to the existing database.
The entries not yet in the database should be displayed as well as the entries, which have a corresponding entry in the database already.
The new entries with changes are flaged as 'new' and the current entries are flaged as 'old'.
The Table is setup as follows:
Show_Again
Annotation
Status(flag)
...
1 or null
text or null
old, new, null
...
1 or null
text or null
old, new, null
...
Now I was asked to highlight the entries with Status='new' in red and if possible highlight where the old and new entries differ.
The regular background coloring I tried to implent by adapting a solution a found on stackoverflow.
While the background is now colored red, the programm crashes as soon as I try to enter any value in the first three columns.
Traceback (most recent call last):
File "h:\Workspace_Arbeit\Pruefungsdb_Visualizer\minimum_viable_code.py", line 400, in data
if 'new' in QSqlTableModel.data(self, self.index(index.row(), 2), QtCore.Qt.DisplayRole):
TypeError: argument of type 'NoneType' is not iterable
This the current code:
class SqlTableModel(QSqlTableModel):
ExecuteRole = QtCore.Qt.UserRole + 1
def __init__(self, parent=None, db = QSqlDatabase()):
super(QSqlTableModel,self).__init__(parent, db)
def data(self, index, role):
if role == QtCore.Qt.BackgroundRole:
if 'new' in QSqlTableModel.data(self, self.index(index.row(), 2), QtCore.Qt.DisplayRole):
return QtGui.QBrush(QtCore.Qt.red)
if role==QtCore.Qt.DisplayRole:
return QSqlTableModel.data(self, index, role)
if role==QtCore.Qt.EditRole:
return super(SqlTableModel, self).data(index, role)
return super(SqlTableModel, self).data(index, role)
def setData(self, index, value, role):
return QSqlTableModel.setData(self, index, value, role)
I have also tried other solutions, that suggested to use an ItemStyleDelegate, which I unfortunately did not get to work.
Although I suspect it would have created different problems as I currently use an ItemDelegate to prevent editing on any column beyond the third.
The error is clear, you're trying to do find new in the result of data().
But, as you also reported in your scheme, that column could also have null values, which in python terms corresponds to None, that is clearly not an iterable type.
First you need to get the value, then you check if the value is a string and it contains "new". Note that the whole data() implementation you did is almost unnecessary, since you're just returning the default behavior. Override only what you actually need to change, and leave the rest as it is.
class SqlTableModel(QSqlTableModel):
def data(self, index, role):
if role == QtCore.Qt.BackgroundRole:
value = super().data(index.siblingAtColumn(2), Qt.DisplayRole)
if isinstance(str, value) and 'new' in value:
return QtGui.QBrush(QtCore.Qt.red)
return super().data(index, role)
Note that you could do the same even with a delegate:
class MyDelegate(QStyledItemDelegate):
def initStyleOption(self, opt, index):
super().initStyleOption(opt, index)
state = index.siblingAtColumn(2).data()
if isinstance(state, str) and 'new' in state:
opt.backgroundBrush = QtGui.QBrush(QtCore.Qt.red)
This has absolutely no relation with the fact that you want to prevent editing, which you could do independently even at model level:
class SqlTableModel(QSqlTableModel):
# ...
def flags(self, index):
flags = super().flags(index)
if index.column() >= 3:
flags &= ~QtCore.Qt.ItemIsEditable
return flags

In index, why check if item already has an index? (Qt)

Short version
When reimplementing index for a tree model, why would we first check to see if the relevant item already has an index (with hasIndex), returning the root index if it does not? Isn't the whole point to create an index for that item?
Detailed version
Consider the reimplemention of index in PySide's simpletreemodel example (full code is below). My understanding is that the index method is meant to take in the row, column, and parent index of a particular item, and return an index for that item using createIndex. But the index method opens with the following lines:
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
I am a bit confused here. If the item does not already have a valid index, why return the root index? How does this method ever create an index in the first place? When I cut out the above lines, I see no deleterious effects in the application.
Since I am trying to simplify the PySide example as much as possible, I want to just remove those two lines. Will this have bad unforeseen consequences?
Relevant code
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
The hasIndex function just performs bounds-checking. If the row or column arguments are less than zero, or outside of the range of the row or column count of the parent index, it will return False; otherwise, it will return True.
Also, in the example implementation, the index method does not return the root-index when hasIndex returns false: it returns an invalid index (the no-argument constructor of QModelIndex always creates an invalid index). The model should always return an invalid index when there is no corresponding item available in the data that is being modelled.

QT: internal drag and drop of rows in QTableView, that changes order of rows in QTableModel

I want to perform a sort of rows in QTableView, so that the underlying TableModel would have its data sorted, too:
If I'm not mistaken, built-in sorts in QTableView don't affect the order of rows in underlying TableModel, so I had to write a custom QTableView and custom QAbstractTableModel implementation of internal drag and drop.
To test, if it works at all, I respond to any drag of cell by reordering first and second rows:
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class Model(QAbstractTableModel):
def __init__(self):
QAbstractTableModel.__init__(self, parent=None)
self.data = [("elem1", "ACDC"), ("elem2", "GUNSNROSES"), ("elem3", "UFO")]
self.setSupportedDragActions(Qt.MoveAction)
def flags(self, index):
if index.isValid():
return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
else:
return Qt.ItemIsDropEnabled | Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def rowCount(self, parent=QModelIndex()):
return len(self.data)
def columnCount(self, parent=QModelIndex()):
return 1
def data(self, index, role):
if role == Qt.DisplayRole:
print "row = %s" % int(index.row())
return QVariant(self.data[int(index.row())][1])
return QVariant()
def headerData(self, index, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(str(index))
elif orientation == Qt.Vertical and role == Qt.DisplayRole:
return self.data[index][0]
def dragMoveEvent(self, event):
event.setDropAction(QtCore.Qt.MoveAction)
event.accept()
def moveRows(self, parent, source_first, source_last, parent2, dest):
print "moveRows called, self.data = %s" % self.data
self.beginMoveRows(parent, source_first, source_last, parent2, dest)
self.data = self.data[1] + self.data[0] + self.data[2]
self.endMoveRows()
print "moveRows finished, self.data = %s" % self.data
class View(QTableView):
def __init__(self, parent=None):
QTableView.__init__(self, parent=None)
self.setSelectionMode(self.ExtendedSelection)
self.setDragEnabled(True)
self.acceptDrops()
self.setDragDropMode(self.InternalMove)
self.setDropIndicatorShown(True)
def dragEnterEvent(self, event):
event.accept()
def dragMoveEvent(self, event):
event.accept()
def dropEvent(self, event):
print "dropEvent called"
point = event.pos()
self.model().moveRows(QModelIndex(), 0, 0, QModelIndex(), 1)
event.accept()
def mousePressEvent(self, event):
print "mousePressEvent called"
self.startDrag(event)
def startDrag(self, event):
print "startDrag called"
index = self.indexAt(event.pos())
if not index.isValid():
return
self.moved_data = self.model().data[index.row()]
drag = QDrag(self)
mimeData = QMimeData()
mimeData.setData("application/blabla", "")
drag.setMimeData(mimeData)
pixmap = QPixmap()
pixmap = pixmap.grabWidget(self, self.visualRect(index))
drag.setPixmap(pixmap)
result = drag.start(Qt.MoveAction)
class Application(object):
def __init__(self):
app = QApplication(sys.argv)
self.window = QWidget()
self.window.show()
layout = QVBoxLayout(self.window)
self.view = View()
self.view.setModel(Model())
layout.addWidget(self.view)
sys.exit(app.exec_())
For some reason, this code doesn't work. It successfully starts the drag (well, almost successfully, cause it shows the previous row, instead of the current one as the drag icon), invokes mousePressEvent, startDrag, dropEvent and moveRows function, but then dies within moveRows with message:
Qt has caught an exception thrown from an event handler. Throwing
exceptions from an event handler is not supported in Qt. You must
reimplement QApplication::notify() and catch all exceptions there.
Qt has caught an exception thrown from an event handler. Throwing
exceptions from an event handler is not supported in Qt. You must
reimplement QApplication::notify() and catch all exceptions there.
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
Aborted
(Duplication of paragraph in error message is intentional - that's what it outputs verbatim).
How do I debug this error? (inserting try-except in moveRows doesn't help)
Do you have a better recipe for performing internal drag-and-drops, affecting the model in tableviews?
You have several problems in your code I will address just two here:
You are calling Model.moveRows with the wrong arguments:
change self.model().moveRows(QModelIndex(), 0, 0, QModelIndex(), 1)
by self.model().moveRows(QModelIndex(), 1, 1, QModelIndex(), 0)
You are changing your data in the wrong way:
change self.data = self.data[1] + self.data[0] + self.data[2]
by self.data = [self.data[1], self.data[0] , self.data[2]]
Note: problem 1 is the one who is provoking the exception on your code. Also note that is a bad idea naming an instance variable and a function the same (Model.data)

PyQT: QListWidget with infinite scrolling

I have a QListWidget with few elements. Moving through the list is done with the arrow keys.
How to make the "infinite loop", meaning that whan the last item is reached and you go down, the selection jumps to the first item, and reverse from first to last if you want to go up?
Here is part of the code for creating list widget:
self.listWidget = QtGui.QListWidget(Form)
self.listWidget.setFont(font)
self.listWidget.setFocusPolicy(QtCore.Qt.StrongFocus)
self.listWidget.setAutoFillBackground(True)
self.listWidget.setAlternatingRowColors(True)
self.listWidget.setWordWrap(True)
self.listWidget.setSelectionRectVisible(True)
self.listWidget.setObjectName("listWidget")
self.listWidget.hasFocus()
self.listWidget.itemActivated.connect(self.klik)
self.gridLayout.addWidget(self.listWidget, 0, 0, 1, 1)
In order to do such a circular list, you can subclass QListWidget, override its keyPressEvent() method and check whether you are in the first/last row before moving via setCurrentRow() if needed.
class CircularListWidget(QtGui.QListWidget):
"""
Circular ListWidget.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Down:
if self.currentRow() == self.count()-1:
self.setCurrentRow(0)
return
elif event.key() == QtCore.Qt.Key_Up:
if self.currentRow() == 0:
self.setCurrentRow(self.count()-1)
return
# Otherwise, parent behavior
super().keyPressEvent(event)
Just replace your QListWidget by this CircularListWidget and it should work. I did not find any built-in method to do what you ask for, hence this workaround.

QSortFilterProxyModel and filter by integers, booleans

I have a QSortFilterProxyModel which is connected to a QSqlQueryModel. In the underlying query there are boolean and integer fields. I would like to filter by these boolean, integers, etc. values. Surprisingly enough (or maybe I'm wrong) QSortFilterProxyModel only filters by strings. This is for instance a "problem" if you want to filter IDs (which are normally integers). If you try for instance to filter an ID=22, you get all the IDs with "22" inside (122, 222, 322, etc.). See this link for a non very elegant solution.
But how would you filter by boolean fields? Can someone give some hint? I suppose I have to subclass QSortFilterProxyModel, or is there another method?
A bit late, but it could be useful to others (and maybe some true expert might add precision/corrections) !
A QSortFilterProxyModel instance uses the method
bool QSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex & source_parent)
to determine if the given row should be kept or not.
By default it will simply test that the string returned by
myProxyModel.data(source_row, QtCore.Qt.DisplayRole)
matches the regular expression you might have set earlier using
myProxyModel.setFilterRegExp(myRegex)
Since you are subclassing this QSortFilterProxyModel you can easily define a new way to filter your items.
Add a method to set the ID you want to check
myProxyModel.setFilterID(myRefId)
then override the filterAcceptsRow to do the test against the ID instead of the regular expression !
You could also want to keep both method (or more) available. To do so, in your filterAcceptsRow method read the data from
myProxyModel.data(source_row, QtCore.Qt.UserRole)
instead of DisplayRole. When setting the UserRole you can store any data, not just string.
Here is an exemple (in python, it's shorter to write, but it works the same in any language) where we store a custom proxy object into the model:
from PyQt4 import QtGui
from PyQt4 import QtCore
class MyDummyObj(object):
def __init__(self, objLabel, objID, hidden=False)
self.__label = objLabel
self.__id = objLabel
self.__hidden = hidden
def getLabel(self):
return self.__label
def getID(self):
return self.__id
def isSecret(self):
return self.__hidden
class MyProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self):
super(MyProxyModel, self).__init__()
self.__testID = None
self.__showHidden = False
def setFilterID(self, filterID):
self.__testID = filterID
def showHiddenRows(self, showHidden=False)
self.__showHidden = showHidden
def filterAcceptsRow(self, sourceRow, sourceParent):
model = self.sourceModel()
myObject = model.data(model.index(sourceRow, 0, sourceParent),
QtCore.Qt.UserRole).toPyObject()
if not self.__showHidden:
if myObject.isSecret():
return False
if self.__testID is not None:
return self.__testID == myObject.getID()
return self.getFilterRegExp().exactMatch(myObject.getLabel())
You can quite explicitly filter boolean roles with setFilterFixedString: filterModel->setFilterFixedString("false")
or filterModel->setFilterFixedString("true") as needed.
a simple solution is to use regexp with start and end anchor :
proxyModel->setFilterRegExp(QString("^%1$").arg(id));

Resources