0

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()
StarterKit
  • 488
  • 4
  • 15
  • If all rows are going to have the same height, can't you just use [`setDefaultSectionSize()`](https://doc.qt.io/qt-5/qheaderview.html#defaultSectionSize-prop) on the vertical header instead? – musicamante Feb 10 '21 at 13:07
  • @musicamante, no, this is exactly the reason - I need different row heigts based on data in this row. Some rows in database have less data and I may show it in ine row. But others have a lot of data and I need extra space to show it. The only way forward I see - is to multiply rows somehow and make some custom drawing in order to allow 2-3 consequent rows appear as 1 solid row. I.e. all rows will be the same height but some "rows" will actually consist of several consequent rows... Not sure that I'll be able to make it good looking... – StarterKit Feb 10 '21 at 13:09
  • Custom drawing on an item view is usually dangerous if not done with *extreme* care, and that's the reason item delegates exist. If row heights are based on the contents, I'm afraid that there's no solution for a data model that big: laying out of items requires *a lot* of computations if based on the contents: each item is positioned at different coordinates based on the size of **all** items before it, and the viewport must know the whole area occupied (also in order to correctly update the scroll bars), so it's pretty natural that it takes a lot of time for tens of thousands of rows. – musicamante Feb 10 '21 at 13:16
  • @musicamante, but `QSqlTableModel` handles is successfully with the same dataset as it loads data in portions. But I see no way how to load only part of data with `QAbstractTableModel`. I just want to have more customizations and it's easy to maintain one class instead of 20 different delegates... – StarterKit Feb 10 '21 at 13:20
  • Your question about using a delegate opposed to subclassing the model is not clear. Yes, sql based models handle this successfully because they use [`canFetchMore()`](https://doc.qt.io/qt-5/qabstractitemmodel.html#canFetchMore) and [`fetchMore()`](https://doc.qt.io/qt-5/qabstractitemmodel.html#fetchMore), so you could implement that too. "every source recomment to subclass QAbstractTableModel for custom models": subclass of an abstract model should only be done when required. Why do you need a *custom* model? What kind of "customization" do you require? – musicamante Feb 10 '21 at 13:23
  • @muscamante, really nothing special - different data decorations, formats, fonts, colors, size...(but based on different pieces of underlying data) So, subclassing `QSqlTableModel` might be an option for me. I'll thing about it. But `fetchMore()` looks like a good option that I overlooked. Do you know any good example of its usage?... I may go this way to have practice and becase I have part of code already written. – StarterKit Feb 10 '21 at 13:29
  • Then, you should subclass QSqlTableModel and override its `data()` (or use [QIdentityProxyModel](https://doc.qt.io/qt-5/qidentityproxymodel.html) and set the sql model as its source, which is almost the same). If you only need to alter some of the returned data, I strongly advise you against implementing your own abstract model: Qt implementation of Sql models is pretty reliable and much faster (since it's done on the C++ side), and when dealing with models so big the less you use python functions the better (faster) it is: let C++ handle everything possible. – musicamante Feb 10 '21 at 13:39
  • Ok, then only one onclear thing for me - how to use underlying sql row in `data()` if I will subclass `QSqlTableModel()` (i.e. I need a way how to access to all fields of particular row and I'm not fully clear about how to get this row...)? May you give some small example? And I think your comments all together will work as an answer... you may put it this way and I'll accept. – StarterKit Feb 10 '21 at 13:41
  • The row is the same in the model as it is in the data source, so `index.row()` of the `index` argument of `data(index, role)`, which is the row of the record in the current query (the `SELECT`), but remember that sql doesn't have a "real permanent row" concept. Anyway, if you have any other question that is not related to this subject (row heights), please create a new post, as extended discussions should be avoided especially if they're off topic. I also suggest you to study the docs about QSqlTableModel and all inherited classes (QSqlQueryModel, etc..) and avoid unnecessary implementations. – musicamante Feb 10 '21 at 13:50

1 Answers1

0

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()
StarterKit
  • 488
  • 4
  • 15