PyQt4强制视图从QAbstractItemModel获取更多



我有一个QTableView,它可以从继承QAbstractItemModel的自定义模型中动态加载数据。该模型同时实现了fetchMore和canFetchMore。

问题是,我希望能够为小型数据集选择所有行,但如果我在视图中点击ctrl-a,它只会选择当前加载的行。

是否有某种机制可以强制QTableView获取更多行?理想情况下,我想显示一个进度条,指示已从模型加载的数据部分。每隔几秒钟,我想强制模型加载更多的数据,但我仍然想让用户与迄今为止加载的数据进行交互。这样,当进度条完成时,用户可以按下ctrl-a并确信所有数据都已选择。


编辑:我有另一个激励性的用例。我想跳到一个特定的行,但如果该行没有加载,我的接口将不起任何作用。

如何强制QAbstractItemModel获取更多(或最多获取一行),然后强制QTableView显示它?

如果我不实现fetchMore和canFetchMore,前面的功能可以工作,但加载表非常慢。当我实现这些方法时,情况正好相反。没有这个问题的答案会导致我的qt接口的可用性出现问题,所以我愿意回答这个问题。

下面是我用来选择一个特定行的方法。

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

如果用户手动滚动过有问题的行,则此功能有效。但是,如果用户没有看到特定的行,则此功能将滚动回视图的顶部。

现在给出答案可能已经太晚了,但也许这对未来的某个人仍然有益。

下面可以找到一个具有canFetchMorefetchMore方法的列表模型的工作示例+一个具有两个自定义方法的视图:

  1. 方法,尝试从模型加载更多项(如果模型尚未加载某些项)
  2. 方法,该方法能够在尚未加载特定行的情况下从模型中获取这些行

示例中的QMainWindow子类有一个计时器,用于重复调用上述第一个方法,每次都强制将另一批项目从模型加载到视图中。在小的时间间隔内批量加载项目可以避免完全阻塞UI线程,并且能够在几乎没有延迟的情况下编辑到目前为止加载的项目。该示例包含一个进度条,显示到目前为止已加载的部分项目。

QMainWindow子类还有一个旋转框,可以选择要在视图中显示的特定行。如果已经从模型中提取了相应的项,则视图只需滚动到它。否则,它将首先从模型中获取此行的项,以同步的方式,即UI阻塞方式。

以下是用python 3.5.2和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()

我还想注意的一件事是,这个例子期望fetchMore方法同步完成它的工作。但在更复杂的方法中,fetchMore实际上不必这么做。如果你的模型从数据库加载项目,那么在UI线程中与数据库同步对话将是个坏主意。相反,fetchMore实现可以启动信号/时隙通信的异步序列,其中某个对象处理在某个后台线程中发生的与数据库的通信。

一个基于Dmitry答案的自用模型类。

class EzQListModel(QAbstractListModel):
    items_changed = Signal()
    an_item_changed = Signal(int)
    def __init__(self, batch_size=100, items_header='Items', parent=None):
        super().__init__(parent)
        self._batch_size = batch_size
        self._current_size = 0
        self._items = []
        self.items_header = items_header
        self.data_getter_mapping = {Qt.DisplayRole: self.get_display_data, Qt.BackgroundRole: self.get_background_data}
    @property
    def items_size(self):
        return len(self._items)
    def update_fetch_more(self):
        if self.canFetchMore():
            self.fetchMore()
        return self
    @contextlib.contextmanager
    def ctx_change_items(self):
        yield
        self.items_changed.emit()
    @contextlib.contextmanager
    def ctx_change_an_item(self, index):
        yield
        self.an_item_changed.emit(index)
    def clear_items(self):
        with self.ctx_change_items():
            self._items.clear()
            self._current_size = 0
        return self
    def append_item(self, x):
        with self.ctx_change_items():
            self._items.append(x)
        return self
    def insert_item(self, index, x):
        with self.ctx_change_items():
            self._items.insert(index, x)
        return self
    def extend_items(self, items):
        with self.ctx_change_items():
            self._items.extend(items)
        return self
    def get_item(self, index):
        return self._items[index]
    def set_item(self, index, value):
        with self.ctx_change_items():
            with self.ctx_change_an_item(index):
                self._items[index] = value
        return self
    def flags(self, index):
        if not index.isValid():
            return Qt.ItemIsEnabled
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
    def rowCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0
        n = self._current_size
        if n <= self.items_size:
            return n
        else:
            self._current_size = self.items_size
            return self.items_size
    @staticmethod
    def get_none_data(index):
        return None
    def get_display_data(self, index: QModelIndex):
        return self._items[index.row()]
    @staticmethod
    def get_background_data(index: QModelIndex):
        palette = QApplication.palette()
        return palette.alternateBase() if index.row() % 2 else palette.base()
    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        if self.items_size <= index.row() < 0:
            return None
        return self.data_getter_mapping.get(role, self.get_none_data)(index)
    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            return False
        if role != Qt.EditRole:
            return False
        row = index.row()
        if self.items_size <= row < 0:
            return False
        self._items[row] = value
        self.dataChanged.emit(index, index)
        # print(self.setData.__name__, row, self._items[row], self.data(index))
        return True
    def headerData(self, section, orientation, role=None):
        if orientation != Qt.Horizontal:
            return None
        if section != 0:
            return None
        if role != Qt.DisplayRole:
            return None
        return self.items_header
    def canFetchMore(self, parent: QModelIndex = QModelIndex()):
        if parent.isValid():
            return False
        return self._current_size < self.items_size
    def fetchMore(self, parent: QModelIndex = QModelIndex()):
        if parent.isValid():
            return
        fcls = FirstCountLastStop().set_first_and_total(self._current_size,
                                                        min(self.items_size - self._current_size, self._batch_size))
        self.beginInsertRows(parent, fcls.first, fcls.last)
        self.endInsertRows()
        self._current_size += fcls.total

class FirstCountLastStop:
    def __init__(self):
        self.first = 0
        self.total = 0
        self.last = 0
        self.stop = 1
    def set_first_and_total(self, first, count):
        self.first = first
        self.total = count
        self.stop = first + count
        self.last = self.stop - 1
        return self

相关内容

  • 没有找到相关文章

最新更新