QListView中自定义IndexWidget的平滑懒加载与卸载

4 投票
2 回答
2876 浏览
提问于 2025-04-17 19:25

我正在写一个应用程序,里面用自定义的QWidget来替代PyQt中的普通列表项或代理。我参考了在QListView的QWidgetDelegate的paint()方法中渲染QWidget等答案,来实现一个带有自定义小部件的QTableModel。下面的问题是我在实现过程中遇到的一些困难,我不知道该怎么解决:

  1. 当某些项目不显示时,如何卸载它们。我打算为一个有成千上万条目的列表构建我的应用程序,而我不能在内存中保留那么多小部件。
  2. 如何加载那些还没有在视图中的项目,或者至少是异步加载它们。小部件渲染需要一点时间,下面的示例代码在滚动列表时明显有延迟。
  3. 在下面的实现中,当我滚动列表时,每次加载的新按钮在加载时会在QListView的左上角闪现一下,然后才会跳到正确的位置。怎么才能避免这种情况呢?

--

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()

2 个回答

1

QTableView 只会向模型请求视口内的数据显示,所以你的数据量大小其实对速度影响不大。既然你已经创建了一个 QAbstractListModel 的子类,你可以重新实现它,让它在初始化时只返回少量的行数据,并修改它的 canFetchMore 方法,当总记录数还没显示完时返回 True。不过,考虑到你的数据量,可能需要考虑创建一个数据库,并使用 QSqlQueryModelQSqlTableModel,这两个模型都支持分批加载,每次加载256条数据。

为了让加载项目更顺畅,你可以连接到 valueChanged 信号,来自你的 QTableView.verticalScrollBar(),根据它的 valuemaximum 之间的差值来做一些处理,比如:

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

使用 setIndexWidget 会显著降低你的应用程序速度。你可以使用 QItemDelegate,并自定义它的 paint 方法来显示一个按钮,像这样:

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()

然后用以下方式进行设置:

myView.setItemDelegateForColumn(columnNumber, myItemDelegate)
3

你可以使用一个代理模型来避免加载所有的控件。这个代理模型可以根据视口的高度和控件的高度来计算行数。它还可以根据滚动条的值来计算项目的索引。

这个方法可能不太稳定,但应该能奏效。

如果你修改你的 data() 方法如下:

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

那么这些项目在被移动到它们的位置之前是不会显示的(对我来说是有效的)。

撰写回答