Search code examples
pythonpyside2qtableviewqabstracttablemodelsizehint

QTableView dynamic row heigh for large QAbstractTableModel


I know there have been a lot of times question was answered on stackoverflow about how to set row height for QTableView. I'm asking one more time but my question is not exactly about "how", at least not so simple. I'm setting row height successfully with help of Qt.SizeHintRole in data method of my custom model derived from QAbstractTableModel - see code below. (Also tried very similar example but with help of sizeHint() method of QStyledItemDelegate - the result is exactly the same.)

It works pretty good when I have MODEL_ROW_COUNT about 100 as in example below. But my dataset has ~30-40 thousands of rows. As result this simple application starts about 30 seconds with MODEL_ROW_COUNT=35000 for example.
The reason of this big delay is this line of code:
self.table_view.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
Everything works really fast with MODEL_ROW_COUNT=35000 if I would comment this line. But in this case data() method is not called with Qt.SizeHintRole and I can't manipulate row height.

So, my question is - how to set row height on a per row basis for dataset with thousands of rows? Below example works but takes 30 seconds to start with 35 000 rows (after window is shown everything is fluent)...

At the same time if I use QSqlTableModel it doesn't have this problem and I may use sizeHint() of QStyledItemDelegate without big problems. But it's a mess to have too many delegates... May I subclass QStyledItemDelegate instead of QAbstractTableModel to implement my custom model? (I'm not sure that it will work as every source recomment to subclass QAbstractTableModel for custom models...) Or I did something wrong and there is a better way than usage of QHeaderView.ResizeToContents?

P.S. I really need different heights. Some rows in database have less data and I may show them in a couple of cells. But others have more data and I need extra space to display it. The same height for all rows will mean either waste of space (a lot of white space on a screen) or lack of essential details for some data rows. I'm using contant CUSTOM_ROW_HEIGHT only too keep example as much simple as possible and reproducible with ease - you may use any DB with any large table (I think I may re-create it even without DB... will try soon)

from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QTableView, QHeaderView
from PySide2.QtSql import QSqlDatabase, QSqlQuery
from PySide2.QtCore import Qt, QAbstractTableModel, QSize


class MyWindow(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        self.db = QSqlDatabase.addDatabase("QSQLITE")
        self.db.setDatabaseName("/home/db.sqlite")
        self.db.open()

        self.table_model = MyModel(parent=self, db=self.db)
        self.table_view = QTableView()
        self.table_view.setModel(self.table_model)

        # SizeHint is not triggered without this line but it causes delay
        self.table_view.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)

        layout = QVBoxLayout(self)
        layout.addWidget(self.table_view)
        self.setLayout(layout)

class MyModel(QAbstractTableModel):
    CUSTOM_ROW_HEIGHT = 300
    MODEL_ROW_COUNT = 100
    MODEL_COL_COUNT = 5

    def __init__(self, parent, db):
        QAbstractTableModel.__init__(self, parent)
        self.query = QSqlQuery(db)
        self.query.prepare("SELECT * FROM big_table")
        self.query.exec_()

    def rowCount(self, parent=None):
        return self.MODEL_ROW_COUNT

    def columnCount(self, parent=None):
        return self.MODEL_COL_COUNT

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        if role == Qt.DisplayRole:
            if self.query.seek(index.row()):
                return str(self.query.value(index.column()))
        if role == Qt.SizeHintRole:
            return QSize(0, self.CUSTOM_ROW_HEIGHT)
        return None


def main():
    app = QApplication([])
    win = MyWindow()
    win.show()
    app.exec_()

if __name__ == "__main__":
    main()

Solution

  • Ok, Thanks to @musicamante I realized that I missed canFetchMore() and fetchMore() methods. So, I implemented dynamic size property and these methods in MyModel class. It was not hard at all and now I have better performance than QSqlTableModel and identical visual behavior with direct conrol of visible buffer size. Below is new code of MyModel class:

    class MyModel(QAbstractTableModel):
        CUSTOM_ROW_HEIGHT = 300
        MODEL_ROW_COUNT = 37000
        MODEL_COL_COUNT = 5
        PAGE_SIZE = 500
    
        def __init__(self, parent, db):
            QAbstractTableModel.__init__(self, parent)
            self.query = QSqlQuery(db)
            self.query.prepare("SELECT * FROM big_table")
            self.query.exec_()
    
            self._current_size = self.PAGE_SIZE
    
        def rowCount(self, parent=None):
            return self._current_size
    
        def columnCount(self, parent=None):
            return self.MODEL_COL_COUNT
    
        def data(self, index, role=Qt.DisplayRole):
            if not index.isValid():
                return None
            if role == Qt.DisplayRole:
                if self.query.seek(index.row()):
                    return str(self.query.value(index.column()))
            if role == Qt.SizeHintRole:
                return QSize(0, self.CUSTOM_ROW_HEIGHT)
            return None
    
        def canFetchMore(self, index):
            return self._current_size < self.MODEL_ROW_COUNT
    
        def fetchMore(self, index):
            self.beginInsertRows(index, self._current_size, self._current_size + self.PAGE_SIZE - 1)
            self._current_size += self.PAGE_SIZE
            self.endInsertRows()