2016-07-21 48 views
8

Mam QTableView, który dynamicznie ładuje dane z niestandardowego modelu, który dziedziczy QAbstractItemModel. Model implementuje zarówno fetchMore, jak i canFetchMore.Pyre force view to fetchMore from QAbstractItemModel

Problem polega na tym, że chciałbym móc wybrać wszystkie wiersze dla małych zestawów danych, ale jeśli w widoku kliknę ctrl-a, to tylko wybiorą aktualnie załadowane wiersze.

Czy istnieje jakiś mechanizm wymuszający na QTableView pobieranie większej liczby wierszy? Idealnie chciałbym pokazać pasek postępu wskazujący ułamek danych, które zostały załadowane z modelu. Co kilka sekund chciałbym wymusić na modelu załadowanie nieco większej ilości danych, ale nadal chcę pozwolić użytkownikowi na interakcję z danymi, które zostały załadowane do tej pory. W ten sposób po zakończeniu paska postępu użytkownik może nacisnąć ctrl-a i mieć pewność, że wszystkie dane zostały wybrane.


Edytuj: Mam inny motywacyjny przypadek użycia. Chcę przejść do określonego wiersza, ale jeśli ten wiersz nie jest załadowany, mój interfejs nic nie robi.

Jak wymusić QAbstractItemModel, aby pobrać więcej (lub do określonego wiersza), a następnie zmusić QTableView, aby to pokazać?

Jeśli nie zaimplementuję funkcji FetchMore i canFetchMore, poprzednia funkcja działa, ale ładowanie tabel jest bardzo powolne. Kiedy wdrażam te metody, dzieje się odwrotnie. Brak odpowiedzi na ten problem powoduje problemy z użytecznością mojego interfejsu qt, więc otwieram nagrodę za to pytanie.

Oto metoda, której używam do wybrania konkretnego wiersza.

def select_row_from_id(view, _id, scroll=False, collapse=True): 
    """ 
     _id is from the iders function (i.e. 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 

Jeśli użytkownik przewinął ręcznie obok danego wiersza, ta funkcja działa. Jednakże, jeśli użytkownik nie widział określonego wiersza, ta funkcja przewija się z powrotem na górę widoku.

+0

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. –

+0

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

Odpowiedz

4

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:

  1. Metoda próbuje załadować więcej elementów z modelu, jeżeli model ma coś nie załadowany jeszcze
  2. 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.

+0

Nie jest za późno! Co zabawne, znów zacząłem pracować nad tym kawałkiem kodu. Niezły czas. – Erotemic

+0

Pytanie: Dlaczego masz 'if index.isValid(): return' dla' fetchMore' zamiast 'if not index.isValid(): return'? To samo z 'rowCount' i' canFetchMore'. – Erotemic

+1

Indeks we wszystkich trzech metodach jest macierzysty. W tym przykładzie przedstawiono model listy, których elementy są uważane za "pod" niewidocznym elementem nadrzędnym, który ma nieprawidłowy indeks - pomyśl o modelu listy jako modelu drzewa z tylko jednym katalogiem głównym i tylko jednym poziomem głębokim. Tak więc wszystkie trzy metody zgadzają się na robienie rzeczy w przypadku nieprawidłowego indeksu niewidocznego elementu nadrzędnego, nie zgadzają się na pobranie większej liczby pozycji lub podanie liczby wierszy dla pozycji z prawidłowymi indeksami będącymi faktycznymi pozycjami na liście. – Dmitry