4

I am writing an application that makes use of a custom QWidget in place of regular listitems or delegates in PyQt. I have followed the answer in Render QWidget in paint() method of QWidgetDelegate for a QListView -- among others -- to implement a QTableModel with custom widgets. The resulting sample code is at the bottom of this question. There are some problems with the implementation that I do not know how to solve:

  1. Unloading items when they are not being displayed. I plan to build my application for a list that will have thousands of entries, and I cannot keep that many widgets in memory.
  2. Loading items that are not yet in view or at least loading them asynchronously. Widgets take a moment to render, and the example code below has some obvious lag when scrolling through the list.
  3. When scrolling the list in the implementation below, each newly loaded button when loading shows up at the top left corner of the QListView for a split second before bouncing into position. How can that be avoided?

--

import sys
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt


class TestListModel(QtCore.QAbstractListModel):
    def __init__(self, parent=None):
        QtCore.QAbstractListModel.__init__(self, parent)
        self.list = parent

    def rowCount(self, index):
        return 1000

    def data(self, index, role):
        if role == Qt.DisplayRole:
            if not self.list.indexWidget(index):
                button = QtGui.QPushButton("This is item #%s" % index.row())
                self.list.setIndexWidget(index, button)
            return QtCore.QVariant()

        if role == Qt.SizeHintRole:
            return QtCore.QSize(100, 50)

    def columnCount(self, index):
        pass


def main():
    app = QtGui.QApplication(sys.argv)

    window = QtGui.QWidget()

    list = QtGui.QListView()
    model = TestListModel(list)

    list.setModel(model)
    list.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)

    layout = QtGui.QVBoxLayout(window)
    layout.addWidget(list)

    window.setLayout(layout)
    window.show()

    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
Community
  • 1
  • 1
lyschoening
  • 18,170
  • 11
  • 44
  • 54
  • To address issues 3, have you tried giving the push button a parent at creation time? Namely, the QListView. – Lorenz03Tx Mar 20 '13 at 18:15

2 Answers2

2

You can use a proxy model to avoid to load all widgets. The proxy model can calculate the row count with the heights of the viewport and the widget. He can calculate the index of the items with the scrollbar value.

It is a shaky solution but it should work.

If you modify your data() method with:

button = QtGui.QPushButton("This is item #%s" % index.row())
self.list.setIndexWidget(index, button)
button.setVisible(False)

The items will not display until they are moved at their positions (it works for me).

Dimitry Ernot
  • 6,256
  • 2
  • 25
  • 37
1

QTableView only requests data to the model for items in its viewport, so the size of your data doesn't really affect the speed. Since you already subclassed QAbstractListModel you could reimplement it to return only a small set of rows when it's initialized, and modify its canFetchMore method to return True if the total amount of records hasn't been displayed. Although, with the size of your data, you might want to consider creating a database and using QSqlQueryModel or QSqlTableModel instead, both of them do lazy loading in groups of 256.

To get a smoother load of items you could connect to the valueChanged signal of your QTableView.verticalScrollBar() and depending on the difference between it's value and maximum have something like:

while xCondition:
   if self.model.canFetchMore():
      self.model.fetchMore()

Using setIndexWidget is slowing up your application considerably. You could use a QItemDelegate and customize it's paint method to display a button with something like:

class MyItemDelegate(QtGui.QItemDelegate):
    def __init__(self, parent=None):
        super(MyItemDelegate, self).__init__(parent)

    def paint(self, painter, option, index):
        text = index.model().data(index, QtCore.Qt.DisplayRole).toString()

        pushButton = QtGui.QPushButton()
        pushButton.setText(text)
        pushButton.setGeometry(option.rect)

        painter.save()
        painter.translate(option.rect.x(), option.rect.y())

        pushButton.render(painter)

        painter.restore()

And setting it up with:

myView.setItemDelegateForColumn(columnNumber, myItemDelegate)
  • Lazy loading does not help my problem of wanting to unload items that are no longer in view. A delegate is obviously stateless, but I do need enough state to have the content respond to input. – lyschoening Mar 22 '13 at 18:41
  • @lyschoening I think subclassing [`QSqlQueryModel`](http://doc-snapshot.qt-project.org/4.8/qsqlquerymodel.html) would be a good choice. Using it's `setQuery` method you can select the number of records you would like to display from a database. Instead of creating multiple buttons, you could combine it with [`QDataWidgetMapper`](http://doc-snapshot.qt-project.org/4.8/qdatawidgetmapper.html) and map it to a set of static buttons, and maybe animate them for more eye candy, checkout the code example in [this question](http://stackoverflow.com/a/15582844/1006989) –  Mar 23 '13 at 03:57