QTableView performance - qt

I'm using a QTableView with PyQt5. As soon as I start to display a few thousand rows, the performance is abysmal. I tried the suggestions from this question but the Qt Graphics View framework and QTreeView are not viable options for me. Does anybody have other ideas for optimizing the performance of a QTableView?

You can mirror the effect of QTreeView's setUniformRowHeights by implementing sizeHintForRow:
class UniformRowHeights(QTableView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._row_height = None
def sizeHintForRow(self, row):
model = self.model()
if row < 0 or row >= model.rowCount():
# Mirror super implementation.
return -1
return self.get_row_height()
def get_row_height(self):
if self._row_height is None:
self._row_height = max(self._get_cell_heights())
return self._row_height
def changeEvent(self, event):
# This for instance happens when the style sheet changed. It may affect
# the calculated row height. So invalidate:
self._row_height = None
super().changeEvent(event)
def _get_cell_heights(self, row=0):
self.ensurePolished()
option = self.viewOptions()
model = self.model()
for column in range(model.columnCount()):
index = model.index(row, column)
delegate = self.itemDelegate(index)
if delegate:
yield delegate.sizeHint(option, index).height()

Related

Iterate through a pyqt pyside QtreeView with a filesystem model and format value based on a condition [duplicate]

I'm having a QListView with a QFileSystemModel. Based on a selection in a QTreeView, the QListView shows the content of the folder.
Now I need to change the color of the filenames depending on some condition.
The initial idea would be to iterate over the items in the QListView and set the color for each item depending on whether the condition is fulfilled. However this seems to be impossible, since the setData() method of QFileSystemModel only accepts changes to the EditRole, ignoring something like [see this]
self.FileModel.setData(index, QtGui.QBrush(QtCore.Qt.red), role=QtCore.Qt.ForegroundRole)
This has also been pointed out here
and the suggestion in the latter was to subclass QItemDelegate for the purpose of colorizing items in the QListView.
I therefore subclassed QStyledItemDelegate and reimplemented its paint() method to show the filename in green, if the condition is fulfilled - which works fine. However it now looks kind of ugly: File icons are lost and the "mouse_over" effect is not working anymore.
While this subclassing is anyway a messy work-around, my top-level question would be
Is there a way to colorize items in a QListView connected to a QFileSystemModel based on a condition?
Now provided that this might not be the case and sticking to the subclassing of QItemDelegate,
Is there a way to get the original behaviour with nice selections and icons back?
Does anyone know which ItemDelegate is originally used for QFileSystemModel in a QListView and how to use it?
Is it possible to get its source code and copy the paint method from there ?
Here is a minimal code that uses subclassing and shows the descibed behaviour. It uses a QLineEdit where one can type in a string, such that all files containing that string are highlighted in green.
import sys
from PyQt4 import QtGui, QtCore
class MyFileViewDelegate(QtGui.QStyledItemDelegate ):
def __init__(self, parent=None, *args, **kwargs):
QtGui.QItemDelegate.__init__(self, parent, *args)
self.condition = None
self.isMatch = False
self.brush_active = QtGui.QBrush(QtGui.QColor("#79b9ed"))
self.brush_active_matched = QtGui.QBrush(QtGui.QColor("#58cd1c"))
self.pen = QtGui.QPen(QtGui.QColor("#414141") )
self.pen_matched = QtGui.QPen(QtGui.QColor("#39c819") )
self.pen_active = QtGui.QPen(QtGui.QColor("#eef2fd") )
self.pen_active_matched = QtGui.QPen(QtGui.QColor("#e7fade") )
def paint(self, painter, option, index):
text = index.data(QtCore.Qt.DisplayRole)
self.matchText(text)
painter.save()
######## set background
painter.setPen(QtGui.QPen(QtCore.Qt.NoPen))
if option.state & QtGui.QStyle.State_Selected:
if self.isMatch:
painter.setBrush(self.brush_active_matched)
else:
painter.setBrush(self.brush_active)
painter.drawRect(option.rect)
######## set font color
if option.state & QtGui.QStyle.State_Selected:
if self.isMatch:
painter.setPen(self.pen_active_matched)
else:
painter.setPen(self.pen_active)
else:
if self.isMatch:
painter.setPen(self.pen_matched)
else:
painter.setPen(self.pen)
painter.drawText(option.rect, QtCore.Qt.AlignLeft, text)
painter.restore()
def matchText(self, filename):
# testing condition. In the real case this is much more complicated
if (self.condition != None) and (self.condition != "") and (self.condition in filename):
self.isMatch = True
else:
self.isMatch = False
def setCondition(self, condition):
self.condition = condition
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None, useDelegate = False):
super(MainWidget, self).__init__(parent)
self.setLayout(QtGui.QVBoxLayout())
self.FolderModel = QtGui.QFileSystemModel()
self.FolderModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.AllDirs)
self.FolderModel.setRootPath("")
self.FolderView = QtGui.QTreeView(parent=self)
self.FolderView.setModel(self.FolderModel)
self.FolderView.setHeaderHidden(True)
self.FolderView.hideColumn(1)
self.FolderView.hideColumn(2)
self.FolderView.hideColumn(3)
self.FolderView.expanded.connect(self.FolderView.scrollTo)
self.FolderView.clicked[QtCore.QModelIndex].connect(self.browserClicked)
self.FileModel = QtGui.QFileSystemModel()
self.FileModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Files)
self.FileView = QtGui.QListView(parent=self)
self.FileView.setModel(self.FileModel)
self.FileViewDelegate = None
if useDelegate:
self.FileViewDelegate = MyFileViewDelegate()
self.FileView.setItemDelegate(self.FileViewDelegate)
self.FileView.setSelectionMode( QtGui.QAbstractItemView.ExtendedSelection )
self.LineEdit = QtGui.QLineEdit()
self.LineEdit.textChanged.connect(self.changeCondition)
# Add Widgets to layout
self.layout().addWidget(self.FolderView)
self.layout().addWidget(self.FileView)
self.layout().addWidget(self.LineEdit)
def changeCondition(self, text):
if self.FileViewDelegate:
self.FileViewDelegate.setCondition(text)
def browserClicked(self, index):
# the signal passes the index of the clicked item
# set the FileView's root_index to the clicked index
dir_path = self.FileModel.filePath(index)
root_index = self.FileModel.setRootPath(dir_path)
self.FileView.setRootIndex(root_index)
class App(QtGui.QMainWindow):
def __init__(self, parent=None, useDelegate=False):
super(App, self).__init__(parent)
self.central = MainWidget(parent =self, useDelegate=useDelegate)
self.setCentralWidget(self.central)
if __name__=='__main__':
app = QtGui.QApplication(sys.argv)
thisapp = App(None, True) # set False to view App without custom FileViewDelegate
thisapp.show()
sys.exit(app.exec_())
This is the comparison of how it looks with and without subclassing QItemDelegate:
just to mention, another problem with this code is, that once the condition is changed, one needs to move the mouse into the QFileView to initiate the repainting. I wonder which slot I could use to connect to the LineEdit.textChange signal to do that directly.
There's no need for an item-delegate. It can be achieved much more simply by reimplementing the data method of the QFileSystemModel:
class FileSystemModel(QtGui.QFileSystemModel):
def __init__(self, *args, **kwargs):
super(FileSystemModel, self).__init__(*args, **kwargs)
self.condition = None
def setCondition(self, condition):
self.condition = condition
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
def data(self, index, role=QtCore.Qt.DisplayRole):
if self.condition and role == QtCore.Qt.TextColorRole:
text = index.data(QtCore.Qt.DisplayRole)
if self.condition in text:
return QtGui.QColor("#58cd1c")
return super(FileSystemModel, self).data(index, role)
class MainWidget(QtGui.QWidget):
def __init__(self, parent=None, useDelegate = False):
super(MainWidget, self).__init__(parent)
...
self.FileModel = FileSystemModel(self)
...
def changeCondition(self, text):
self.FileModel.setCondition(text)

QGraphicsView shows a white screen after the update [duplicate]

This question already has answers here:
Updating GUI elements in MultiThreaded PyQT
(2 answers)
Closed 1 year ago.
I'm writing a program that should display a chessboard on request from the console, here's skeleton QGraphicsView
class TableView(QGraphicsView):
def __init__(self, item):
super().__init__(item)
self.num = 1
self.Size = self.size() - QtCore.QSize(2, 2)
scene = QtWidgets.QGraphicsScene()
self.setScene(scene)
self.RenderBoard()
def UpdateTable(self, **kwargs):
for key, val in kwargs.items():
setattr(self, key, val)
self.RenderBoard()
def LoadImage(self, path, size, pos=None, flip=False):
print(path, size, pos, flip)
picture = QtGui.QImage(path)
picture = picture.scaled(size)
if flip:
my_transform = QtGui.QTransform()
my_transform.rotate(180)
picture = picture.transformed(my_transform)
pic = QtWidgets.QGraphicsPixmapItem()
pic.setPixmap(QtGui.QPixmap.fromImage(picture))
if pos is not None:
pic.setPos(*pos)
self.scene().addItem(pic)
def RenderBoard(self):
self.scene().clear()
self.LoadImage('img/board.png', self.Size, flip=(self.num % 2 == 0))
def resizeEvent(self, event):
self.Size = self.size() - QtCore.QSize(2, 2)
self.RenderBoard()
super().resizeEvent(event)
The program works and outputs the board, but as soon as I call UpdateTable from QtCore.QThread, which accepts requests from the console, the screen turns white. But, if i change the size of the program, the resizeEvent event is called and the program comes to life again and shows exactly what I wanted.
Do you have any idea what the problem might be?
P.S. the update that I call changes one of the arrays from which the reading occurs during rendering, I printing it during the Update call and during the Resize call, in both cases, it is the same.
Other functions
class UpdaterThread(QtCore.QThread):
def __init__(self, table):
QtCore.QThread.__init__(self)
self.table = table
def run(self):
sleep(1)
self.table.UpdateTable(**{'num': 2})
class Ui_ChessHelper(object):
def setupUi(self, ChessHelper):
self.table = TableView(self.centralwidget)
self.table.setMinimumSize(QtCore.QSize(350, 350))
self.table.setObjectName("table")
self.thread = UpdaterThread(self.table)
self.thread.start()
QGraphicsView has a method scene() that returns the current scene. When you write
self.scene = QtWidgets.QGraphicsScene()
self.setScene(self.scene)
you are clobbering that method's name with your new scene object. Instead
scene = QtWidgets.QGraphicsScene()
self.setScene(scene)
then use self.scene() to access it later when needed.

Implementing a delegate for wordwrap in a QTreeView (Qt/PySide/PyQt)?

I have a tree view with a custom delegate to which I am trying to add word wrap functionality. The word wrapping is working fine, but the sizeHint() seems to not work, so when the text wraps, the relevant row does not expand to include it.
I thought I was taking care of it in sizeHint() by returning document.size().height().
def sizeHint(self, option, index):
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setHtml(text)
document.setTextWidth(option.rect.width())
return QtCore.QSize(document.idealWidth(), document.size().height())
However, when I print out document.size().height() it is the same for every item.
Also, even if I manually set the height (say, to 75) just to check that things will look reasonable, the tree looks like a goldfish got shot by a bazooka (that is, it's a mess):
As you can see, the text in each row is not aligned properly in the tree.
Similar posts
Similar issues have come up before, but no solutions to my problem (people usually say to reimplement sizeHint(), and that's what I am trying):
QTreeWidget set height of each row depending on content
QTreeView custom row height of individual rows
http://www.qtcentre.org/threads/1289-QT4-QTreeView-and-rows-with-multiple-lines
SSCCE
import sys
from PySide import QtGui, QtCore
class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self, parent)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setGeometry(500,200, 400, 300)
self.setUniformRowHeights(False) #optimize: but for word wrap, we don't want this!
print "uniform heights in tree?", self.uniformRowHeights()
self.model = QtGui.QStandardItemModel()
self.model.setHorizontalHeaderLabels(['Task', 'Description'])
self.setModel(self.model)
self.rootItem = self.model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('You have been blocked up')]
item00 = [QtGui.QStandardItem('Tickle nose, this is a very long entry. Row should resize.'), QtGui.QStandardItem('Key first step')]
item1 = [QtGui.QStandardItem('<b>Get a job</b>'), QtGui.QStandardItem('Do not blow it')]
self.rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.rootItem.appendRow(item1)
self.setColumnWidth(0,150)
self.expandAll()
self.setWordWrap(True)
self.setItemDelegate(ItemWordWrap(self))
class ItemWordWrap(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
def paint(self, painter, option, index):
text = index.model().data(index)
document = QtGui.QTextDocument() # #print "dir(document)", dir(document)
document.setHtml(text)
document.setTextWidth(option.rect.width()) #keeps text from spilling over into adjacent rect
painter.save()
painter.translate(option.rect.x(), option.rect.y())
document.drawContents(painter) #draw the document with the painter
painter.restore()
def sizeHint(self, option, index):
#Size should depend on number of lines wrapped
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setHtml(text)
document.setTextWidth(option.rect.width())
return QtCore.QSize(document.idealWidth() + 10, document.size().height())
def main():
app = QtGui.QApplication(sys.argv)
myTree = SimpleTree()
myTree.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
The issue seems to stem from the fact that the value for option.rect.width() passed into QStyledItemDelegate.sizeHint() is -1. This is obviously bogus!
I've solved this by storing the width in the model from within the paint() method and accessing this from sizeHint().
So in your paint() method add the line:
index.model().setData(index, option.rect.width(), QtCore.Qt.UserRole+1)
and in your sizeHint() method, replace document.setTextWidth(option.rect.width()) with:
width = index.model().data(index, QtCore.Qt.UserRole+1)
if not width:
width = 20
document.setTextWidth(width)

PyQt: How Can I set row heights of QTreeView

In PyQt, I am looking for a way to set the height of rows in a QTreeView (similarly to QTableView.setRowHeight(row, row_height), but QTreeView does not have this function). QAbstractItemModel is used to set the tree model. I read some suggestions here using and sub-classing QAbstractItemDelegate.sizeHint(option, index) but I don't know exactly how to call them correctly within my tree model.
Any minimal code or suggestion would be greatly appreciated. Thanks.
The QTreeView works out a height for each row based on the data and size hints returned for each item. I think you probably just need to return size hints, either for all your items, or at least the first row (if you have setUniformRowHeights(True)). Incidentally this can significantly improve performance and so you should set it if you can.
So you just need to implement your AbstractItemModel.data() method to return a size hint in the SizeHintRole. Something like this:
def data(self, index, role = QtCore.Qt.DisplayRole):
# Check the index, possibly return None
if role == QtCore.Qt.DisplayRole:
# Return the data
elif role == QtCore.Qt.SizeHintRole:
return QtCore.QSize(item_width,item_height)
# Other roles - maybe return None if you don't use them.
EDIT: Big example
You say you are still having trouble so here is a complete working example, based on the standard QT itemviews example. Try varying the QSize returned in the data method to see how the view changes:
import sys
from PySide import QtCore,QtGui
class TreeItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.data = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self.rootItem = TreeItem(None)
for i,c in enumerate("abcdefg"):
child = TreeItem([i,c],self.rootItem)
self.rootItem.appendChild(child)
parent = self.rootItem.childItems[1]
child = TreeItem(["down","down"],parent)
parent.appendChild(child)
def columnCount(self, parent):
return 2
def data(self, index, role):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole:
item = index.internalPointer()
return item.data[index.column()]
elif role == QtCore.Qt.SizeHintRole:
print "giving size hint"
return QtCore.QSize(40,40)
return None
def flags(self, index):
if not index.isValid():
return QtCore.Qt.NoItemFlags
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return ["A","B"][section]
return None
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.childItems[row]
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
parentItem = index.internalPointer().parentItem
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return len(parentItem.childItems)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
model = TreeModel()
view = QtGui.QTreeView()
view.setModel(model)
view.setWindowTitle("Simple Tree Model")
view.show()
sys.exit(app.exec_())
It's pretty simple to do this with stylesheet :
self.setStyleSheet("QTreeView::item { padding: 10px }")
Don't know about Python, but C++ code would be like this:
model->setData(model->index(/*your index*/), QSize(20, 20), Qt::SizeHintRole);
And you need to set it for all items in your tree.
If you want use QItemDelegate - you no need to call this function, you just set your delegate to view, like this (C++ code again, but main idea is the same):
treeView->setItemDelegate(new MyDelegate(this));
Than view will use it when it need it.
For the PySide bindings, here's the Python code for the custom delegate on the AbstractItemModel.
from PySide import QtCore, QtGui
tree = QTreeView()
tree.model = QtGui.QAbstractItemModel()
tree.setModel(tree.model)
size = QtCore.QSize(20, 20)
index = tree.model.index(row, col) # row, col are your own
tree.model.setData(index, size, QtCore.Qt.SizeHintRole)
delegate = MyDelegate()
tree.setItemDelegate(delegate)
This is just translating the code from #RazrFalcon to Python, and was requested by another user.
With Qt 5.9.2, I had trouble with simultaneously getting QTreeView auto-sizing columns and custom row heights. Using SizeHintRole worked for height but seemingly wrecked auto-width. The solution for me was to set the rows to have uniform heights and then do something like this in Python 3.7:
self._tree = QTreeView()
self._tree.setAlternatingRowColors(True)
self._tree.setUniformRowHeights(True)
...
fi = QFontInfo(self.font())
self._tree.setStyleSheet(
f'''
QTreeView {{
alternate-background-color: #E6FFFF;
}}
QTreeView::Item{{
height:{fi.pixelSize() * 2}px;
}}
''')
I'll speculate that setting colors and heights this way provides a tiny bit of a performance improvement.

pyqt qt4 QTableView how to disable sorting for certain columns?

So I have a QTableView and I only want to let column sorting on column 1 but not column2.
Naturally I tried to installEventFilter on QHeaderView or QTableView, but MouseButtonPress event is not being passed unless you installEventFilter on QApplication
Now if when eventFilter is called, the target object is always the top level widget although event.pos() is actually relative to the header or tablecell depending on where you click.
So we cannot use QHeaderView.rect().contains(event.pos()) to find out if the user clicks on the header because you get false positive when you click on the top edge of the very first table cell.
You can still however calculate this using globalPos but then your eventFilter's logic has to change when you change layout or add more widgets above the tableview.
I believe it is a bug that event.pos() returns the relative pos even the object argument always refer to the same top level widget.
A more logical API would be that there is a event.target() method to return the target where it calculates the relative pos.
But I don't see a target() method or a way to find the target in this event filter.
Maybe I'm missing something?
# -*- coding: utf-8 -*-
# pyqt windows 4.10.3
# python 2.7.5 32 bits
from PyQt4.QtCore import *
from PyQt4.QtGui import *
app = None
tableHeader = None
class MyModel(QAbstractTableModel):
def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
return 2
def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
return 2
def data(self, modelIndex, role=None):
if modelIndex.isValid():
row = modelIndex.row()
col = modelIndex.column()
if role == Qt.DisplayRole:
return "%02d,%02d" % (row, col)
def flags(self, index):
if index.isValid():
return Qt.ItemIsEnabled
def headerData(self, section, Qt_Orientation, role=None):
if role == Qt.DisplayRole and Qt_Orientation == Qt.Horizontal:
return "Column " + str(section+1)
class MyEventFilter(QObject):
def eventFilter(self, object, event):
if event.type() == QEvent.MouseButtonPress:
# object is always app/top level widget
print 'MouseButtonPress target :' + repr(object)
# even though event.pos() gives pos relative to the header when you click on header,
# and pos relative to table cells when you click on table cell
print repr(event.pos())
# however we can get the mouse's global position
print repr(event.globalPos())
# given the top level widget's geometry
print repr(app.activeWindow().geometry())
# and the table header's left, top and height
print repr(tableHeader.rect())
# we can find out whether mouse click is targeted at the header
print repr(event.globalPos().y() - app.activeWindow().geometry().y())
# BUT WHAT IF THE LAYOUT CHANGE OR WE ADD MORE WIDGETS ABOVE THE TABLEVIEW?
# WE HAVE TO ADJUST THE CALCULATION ABOVE!
return False
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = QMainWindow()
t = QTableView()
tableHeader = t.horizontalHeader()
t.setModel(MyModel())
w.setCentralWidget(t)
ef = MyEventFilter()
# installing in QMainWindow or QTableView won't catch MouseButtonPress
# https://qt-project.org/forums/viewthread/9347
#w.installEventFilter(ef)
#t.installEventFilter(ef)
app.installEventFilter(ef)
w.show()
sys.exit(app.exec_())
There's a much easier solution: reimplement the sort method of the model, and only permit sorting for the appropriate column.
Also, as an added refinement, use the sortIndicatorChanged signal of the header to restore the current sort indicator when appropriate.
Here's a demo script:
from PyQt4 import QtGui, QtCore
class TableModel(QtGui.QStandardItemModel):
_sort_order = QtCore.Qt.AscendingOrder
def sortOrder(self):
return self._sort_order
def sort(self, column, order):
if column == 0:
self._sort_order = order
QtGui.QStandardItemModel.sort(self, column, order)
class Window(QtGui.QWidget):
def __init__(self, rows, columns):
QtGui.QWidget.__init__(self)
self.table = QtGui.QTableView(self)
model = TableModel(rows, columns, self.table)
for row in range(rows):
for column in range(columns):
item = QtGui.QStandardItem('(%d, %d)' % (row, column))
item.setTextAlignment(QtCore.Qt.AlignCenter)
model.setItem(row, column, item)
self.table.setModel(model)
self.table.setSortingEnabled(True)
self.table.horizontalHeader().sortIndicatorChanged.connect(
self.handleSortIndicatorChanged)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.table)
def handleSortIndicatorChanged(self, index, order):
if index != 0:
self.table.horizontalHeader().setSortIndicator(
0, self.table.model().sortOrder())
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window(5, 5)
window.show()
window.setGeometry(600, 300, 600, 250)
sys.exit(app.exec_())

Resources