PyQt4 forcibly displays selection from QAbstractItemModel

I have a QTableView that dynamically loads data from a user model that inherits QAbstractItemModel. The model implements both fetchMore and canFetchMore.

The problem is that I would like to be able to select all rows for small datasets, but if I press ctrl-a in the view, it will only select the rows that are currently loaded.

Is there any mechanism to get QTableView to retrieve more rows? Ideally, I would like to show a progress bar showing the proportion of data loaded from the model. Every few seconds, I would like to make the model load a little more data, but I still want to allow the user to interact with the data that has been loaded so far. Thus, when the progress bar is complete, the user can press ctrl-a and be sure that all data is selected.


Edit: I have another motivating use case. I want to go to a specific line, but if that line is not loaded, my interface does nothing.

How can I get QAbstractItemModel to get more (or to a specific row) and then get QTableView to show it?

If I do not implement fetchMore and canFetchMore, the previous functionality works, but loading tables is very slow. When I implement these methods, the opposite happens. The lack of an answer to this problem causes usability problems with my QT interface, so I am opening a bounty to this question.

Here is the method that I use to select a specific row.

def select_row_from_id(view, _id, scroll=False, collapse=True): """ _id is from the iders function (ie an ibeis rowid) selects the row in that view if it exists """ with ut.Timer('[api_item_view] select_row_from_id(id=%r, scroll=%r, collapse=%r)' % (_id, scroll, collapse)): qtindex, row = view.get_row_and_qtindex_from_id(_id) if row is not None: if isinstance(view, QtWidgets.QTreeView): if collapse: view.collapseAll() select_model = view.selectionModel() select_flag = QtCore.QItemSelectionModel.ClearAndSelect #select_flag = QtCore.QItemSelectionModel.Select #select_flag = QtCore.QItemSelectionModel.NoUpdate with ut.Timer('[api_item_view] selecting name. qtindex=%r' % (qtindex,)): select_model.select(qtindex, select_flag) with ut.Timer('[api_item_view] expanding'): view.setExpanded(qtindex, True) else: # For Table Views view.selectRow(row) # Scroll to selection if scroll: with ut.Timer('scrolling'): view.scrollTo(qtindex) return row return None 

If the user manually scrolled the line, this function works. However, if the user has not seen a specific line, this function simply scrolls back to the top of the view.

+8
python qt pyqt qtableview qabstractitemmodel
source share
1 answer

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.

+4
source share

All Articles