Prawdopodobnie jest za późno na odpowiedź, ale może i tak przyniesie to korzyść komuś w przyszłości.
Poniżej można znaleźć pracy przykład modelu lista ze canFetchMore
i fetchMore
metod + widok z kilku metod niestandardowych:
- Metoda próbuje załadować więcej elementów z modelu, jeżeli model ma coś nie załadowany jeszcze
- sposób odpowiedni do pobierania konkretnych wierszy od modelu, jeśli nie zostały one jeszcze załadowany
QMainWindow
podklasa na przykład posiada stoper, który jest używany do wielokrotnie wywoływać pierwszą z wyżej wymienionych metod, za każdym razem wymuszając ładunek innej partii elementów z modelu do widoku. Ładowanie elementów w partiach w małych odstępach czasu pozwala uniknąć całkowicie zablokowania wątku interfejsu użytkownika i umożliwia edycję elementów wczytanych dotychczas z niewielkim opóźnieniem lub bez opóźnienia. Przykład zawiera pasek postępu pokazujący część załadowanych do tej pory elementów.
Podklasa QMainWindow
ma również pudełko spinowe, które pozwala wybrać konkretny wiersz do wyświetlenia w widoku. Jeśli odpowiedni element został już pobrany z modelu, widok po prostu przewija się do niego. W przeciwnym razie pobiera element tego wiersza z modelu w pierwszej kolejności, w sposób synchroniczny, tj. Blokujący interfejs użytkownika.
Oto pełny kod rozwiązania, przetestowane z Pythona 3.5.2 i 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's 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's range is limited by exactly the number of items
# possible to fetch but generally it's 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()
Jeszcze jedno chciałbym zwrócić uwagę na to, że ten przykład oczekuje, że metoda fetchMore
zrobić swoją pracę synchronicznie . Ale w bardziej wyrafinowanym podejściu fetchMore
tak naprawdę nie musi tak działać. Jeśli twój model ładuje swoje przedmioty z, powiedzmy, bazy danych, to synchroniczne rozmawianie z bazą danych w wątku UI byłoby złym pomysłem. Zamiast tego implementacja fetchMore
mogłaby rozpocząć asynchroniczną sekwencję komunikacji sygnał/gniazdo z jakimś obiektem obsługującym komunikację z bazą danych występującą w pewnym wątku tła.
Nie mam czasu na pełną odpowiedź, ale jednorazowy zegar powiązany z gniazdem, który aktualizuje pasek postępu i sprawdza, czy jest dostępna większa ilość danych. Za każdym razem, gdy wykonywany jest porcja, pętla zdarzeń ponownie wchodzi, przetwarza zdarzenia użytkownika, a następnie ponownie ją odtworza. Wystrzeliwanie pojedynczego strzału, 0 interwałów czasowych, aż wszystkie dane zostaną pobrane. –
Co musi zrobić gniazdo, aby sprawdzić, czy jest więcej danych? To jest ta część, której nie rozumiem. Nie martwię się tak bardzo o pasek postępu, ale nie jestem w stanie określić, do czego zadzwonić, aby wymusić na nim sprawdzenie, czy jest więcej danych i odpowiednio zaktualizowanych. – Erotemic