It is probably too late for an answer here, but perhaps it will still benefit someone in the future.
The following is a working example of a list model with the canFetchMore and fetchMore + view with several custom methods:
- The method tries to load more elements from the model if the model has something else not loaded
- A method that can retrieve specific rows from a model if they are not already loaded
The QMainWindow subclass in this example has a timer that is used to call the first of the above methods again, each time forcing a different batch of elements to be loaded from the model into the view. Downloading items in batches for short periods of time allows you to avoid completely blocking the user interface stream and to be able to edit the downloaded items so far with a slight lag. The example contains a progress bar showing part of the loaded items.
The QMainWindow subclass also has a rotation field that allows you to select the specific row displayed in the view. If the corresponding element is already obtained from the model, the view simply scrolls to it. Otherwise, it extracts this row element from the model first, synchronously, for example, with a user interface lock.
Here is the complete solution code tested with python 3.5.2 and PyQt5:
import sys from PyQt5 import QtWidgets, QtCore class DelayedFetchingListModel(QtCore.QAbstractListModel): def __init__(self, batch_size=100, max_num_nodes=1000): QtCore.QAbstractListModel.__init__(self) self.batch_size = batch_size self.nodes = [] for i in range(0, self.batch_size): self.nodes.append('node ' + str(i)) self.max_num_nodes = max(self.batch_size, max_num_nodes) def flags(self, index): if not index.isValid(): return QtCore.Qt.ItemIsEnabled return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable; def rowCount(self, index): if index.isValid(): return 0 return len(self.nodes) def data(self, index, role): if not index.isValid(): return None if role != QtCore.Qt.DisplayRole: return None row = index.row() if row < 0 or row >= len(self.nodes): return None else: return self.nodes[row] def setData(self, index, value, role): if not index.isValid(): return False if role != QtCore.Qt.EditRole: return False row = index.row() if row < 0 or row >= len(self.nodes): return False self.nodes[row] = value self.dataChanged.emit(index, index) return True def headerData(self, section, orientation, role): if section != QtCore.Qt.Horizontal: return None if section != 0: return None if role != QtCore.Qt.DisplayRole: return None return 'node' def canFetchMore(self, index): if index.isValid(): return False return (len(self.nodes) < self.max_num_nodes) def fetchMore(self, index): if index.isValid(): return current_len = len(self.nodes) target_len = min(current_len + self.batch_size, self.max_num_nodes) self.beginInsertRows(index, current_len, target_len - 1) for i in range(current_len, target_len): self.nodes.append('node ' + str(i)) self.endInsertRows() class ListView(QtWidgets.QListView): def __init__(self, parent=None): QtWidgets.QListView.__init__(self, parent) def jumpToRow(self, row): model = self.model() if model == None: return False num_rows = model.rowCount() while(row >= num_rows): res = fetchMoreRows(QtCore.QModelIndex()) if res == False: return False num_rows = model.rowCount() index = model.index(row, 0, QtCore.QModelIndex()) self.scrollTo(index, QtCore.QAbstractItemView.PositionAtCenter) return True def fetchMoreRows(self, index): model = self.model() if model == None: return False if not model.canFetchMore(index): return False model.fetchMore(index) return True class MainForm(QtWidgets.QMainWindow): def __init__(self, parent=None): QtWidgets.QMainWindow.__init__(self, parent) # Setup the model self.max_num_nodes = 10000 self.batch_size = 100 self.model = DelayedFetchingListModel(batch_size=self.batch_size, max_num_nodes=self.max_num_nodes) # Setup the view self.view = ListView() self.view.setModel(self.model) # Update the currently selected row in the spinbox self.view.selectionModel().currentChanged.connect(self.onCurrentItemChanged) # Select the first row in the model index = self.model.index(0, 0, QtCore.QModelIndex()) self.view.selectionModel().clearSelection() self.view.selectionModel().select(index, QtCore.QItemSelectionModel.Select) # Setup the spinbox self.spinBox = QtWidgets.QSpinBox() self.spinBox.setMinimum(0) self.spinBox.setMaximum(self.max_num_nodes-1) self.spinBox.setSingleStep(1) self.spinBox.valueChanged.connect(self.onSpinBoxNewValue) # Setup the progress bar showing the status of model data loading self.progressBar = QtWidgets.QProgressBar() self.progressBar.setRange(0, self.max_num_nodes) self.progressBar.setValue(0) self.progressBar.valueChanged.connect(self.onProgressBarValueChanged) # Add status bar but initially hidden, will only show it if there something to say self.statusBar = QtWidgets.QStatusBar() self.statusBar.hide() # Collect all this stuff into a vertical layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.view) self.layout.addWidget(self.spinBox) self.layout.addWidget(self.progressBar) self.layout.addWidget(self.statusBar) self.window = QtWidgets.QWidget() self.window.setLayout(self.layout) self.setCentralWidget(self.window) # Setup timer to fetch more data from the model over small time intervals self.timer = QtCore.QBasicTimer() self.timerPeriod = 1000 self.timer.start(self.timerPeriod, self) def onCurrentItemChanged(self, current, previous): if not current.isValid(): return row = current.row() self.spinBox.setValue(row) def onSpinBoxNewValue(self, value): try: value_int = int(value) except ValueError: return num_rows = self.model.rowCount(QtCore.QModelIndex()) if value_int >= num_rows: # There is no such row within the model yet, trying to fetch more while(True): res = self.view.fetchMoreRows(QtCore.QModelIndex()) if res == False: # We shouldn't really get here in this example since out # spinbox range is limited by exactly the number of items # possible to fetch but generally it a good idea to handle # cases like this, when someone requests more rows than # the model has self.statusBar.show() self.statusBar.showMessage("Can't jump to row %d, the model has only %d rows" % (value_int, self.model.rowCount(QtCore.QModelIndex()))) return num_rows = self.model.rowCount(QtCore.QModelIndex()) if value_int < num_rows: break; if num_rows < self.max_num_nodes: # If there are still items to fetch more, check if we need to update the progress bar if self.progressBar.value() < value_int: self.progressBar.setValue(value_int) elif num_rows == self.max_num_nodes: # All items are loaded, nothing to fetch more -> no need for the progress bar self.progressBar.hide() # Update the selection accordingly with the new row and scroll to it index = self.model.index(value_int, 0, QtCore.QModelIndex()) selectionModel = self.view.selectionModel() selectionModel.clearSelection() selectionModel.select(index, QtCore.QItemSelectionModel.Select) self.view.scrollTo(index, QtWidgets.QAbstractItemView.PositionAtCenter) # Ensure the status bar is hidden now self.statusBar.hide() def timerEvent(self, event): res = self.view.fetchMoreRows(QtCore.QModelIndex()) if res == False: self.timer.stop() else: self.progressBar.setValue(self.model.rowCount(QtCore.QModelIndex())) if not self.timer.isActive(): self.timer.start(self.timerPeriod, self) def onProgressBarValueChanged(self, value): if value >= self.max_num_nodes: self.progressBar.hide() def main(): app = QtWidgets.QApplication(sys.argv) form = MainForm() form.show() app.exec_() if __name__ == '__main__': main()
Another thing that I would like to point out is that this example expects the fetchMore method to do its job synchronously. But in more complex approaches, fetchMore doesn't really need to do that. If your model loads its elements from, say, a database, then synchronously talking to the database in the user interface stream would be a bad idea. Instead, the implementation of fetchMore can initiate an asynchronous signal / slot exchange sequence with some object that handles the connection to the database occurring in some background thread.