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.
Related
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
Starting with complex reduce sample
I have trimmed it down to a single chart and I am trying to understand how the reduce works
I have made comments in the code that were not in the example denoting what I think is happening based on how I read the docs.
function groupArrayAdd(keyfn) {
var bisect = d3.bisector(keyfn); //set the bisector value function
//elements is the group that we are reducing,item is the current item
//this is a the reduce function being supplied to the reduce call on the group runAvgGroup for add below
return function(elements, item) {
//get the position of the key value for this element in the sorted array and put it there
var pos = bisect.right(elements, keyfn(item));
elements.splice(pos, 0, item);
return elements;
};
}
function groupArrayRemove(keyfn) {
var bisect = d3.bisector(keyfn);//set the bisector value function
//elements is the group that we are reducing,item is the current item
//this is a the reduce function being supplied to the reduce call on the group runAvgGroup for remove below
return function(elements, item) {
//get the position of the key value for this element in the sorted array and splice it out
var pos = bisect.left(elements, keyfn(item));
if(keyfn(elements[pos])===keyfn(item))
elements.splice(pos, 1);
return elements;
};
}
function groupArrayInit() {
//for each key found by the key function return this array?
return []; //the result array for where the data is being inserted in sorted order?
}
I am not quite sure my perception of how this is working is quite right. Some of the magic isn't showing itself. Am I correct that elements is the group the reduce function is being called on ? also the array in groupArrayInit() how is it being indirectly populated?
Part of me feels that the functions supplied to the reduce call are really array.map functions not array.reduce functions but I just can't quite put my finger on why. having read the docs I am just not making a connection here.
Any help would be appreciated.
Also have I missed Pens/Fiddles that are created for all these examples? like this one
http://dc-js.github.io/dc.js/examples/complex-reduce.html which is where I started with this but had to download the csv and manually convert to Json.
--------------Update
I added some print statements to try to clarify how the add function is working
function groupArrayAdd(keyfn) {
var bisect = d3.bisector(keyfn); //set the bisector value function
//elements is the group that we are reducing,item is the current item
//this is a the reduce function being supplied to the reduce call on the group runAvgGroup for add below
return function(elements, item) {
console.log("---Start Elements and Item and keyfn(item)----")
console.log(elements) //elements grouped by run?
console.log(item) //not seeing the pattern on what this is on each run
console.log(keyfn(item))
console.log("---End----")
//get the position of the key value for this element in the sorted array and put it there
var pos = bisect.right(elements, keyfn(item));
elements.splice(pos, 0, item);
return elements;
};
}
and to print out the group's contents
console.log("RunAvgGroup")
console.log(runAvgGroup.top(Infinity))
which results in
Which appears to be incorrect b/c the values are not sorted by key (the run number)?
And looking at the results of the print statements doesn't seem to help either.
This looks basically right to me. The issues are just conceptual.
Crossfilter’s group.reduce is not exactly like either Array.reduce or Array.map. Group.reduce defines methods for handling adding new records to a group or removing records from a group. So it is conceptually similar to an incremental Array.reduce that supports an reversal operation. This allows filters to be applied and removed.
Group.top returns your list of groups. The value property of these groups should be the elements value that your reduce functions return. The key of the group is the value returned by your group accessor (defined in the dimension.group call that creates your group) or your dimension accessor if you didn’t define a group accessor. Reduce functions work only on the group values and do not have direct access to the group key.
So check those values in the group.top output and hopefully you’ll see the lists of elements you expect.
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_())
Is it permissible to implement below removal algorithm for QTreeView, where QTreeView::setSelectionMode(QAbstractItemView::ExtendedSelection);, i.e. multiple items is selectable?
QModelIndexList indexList = treeView->selectionModel()->selectedRows();
QList< QPersistentModelIndex > persistentIndexList;
for (QModelIndex const & index : indexList) {
persistentIndexList.append(index);
}
for (QPersistentModelIndex const & persistentIndex : persistentIndexList) {
if (!treeModel->removeRow(persistentIndex.row(), persistentIndex.parent())) {
qWarning() << "Can't remove row" << persistentIndex;
}
}
I think, it is possible the situation, when parent removed before the child and even persistent indexes are not valid at that moment. Am I wrong?
Must the model to check hasIndex in removeRows?
Every implementation of QAbstractItemModel does its best (at least it has to) to keep QPersistentModelIndexs valid when the model is changed. If the model can't calculate new location of the index it invalidates the QPersistentModelIndex. It can happen when the index is removed or a model layout or whole data was changed.
That is why it's always neccessary to check if QPersistentModelIndex valid or not before using it.
But QAbstractItemModel::removeRows returns bool. It means that wrong arguments can be passed to this method. If the model can't remove rows due to wrong arguments it returns false.
So the answer to your question is yes, you should check an index in removeRows and return correct result.
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));