PyQt4强制查看来自QAbstractItemModel

时间:2016-07-21 14:20:41

标签: python qt pyqt qtableview 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

如果用户手动滚动过相关行,则此功能有效。但是,如果用户没有看到特定的行,则此功能只会向后滚动到视图的顶部。

2 个答案:

答案 0 :(得分:4)

对于这里的答案来说可能为时已晚,但未来可能会让某人受益。

下面可以找到包含canFetchMorefetchMore方法的列表模型的工作示例+带有几个自定义方法的视图:

  1. 尝试从模型加载更多项目的方法,如果模型尚未加载
  2. 如果尚未加载
  3. ,则能够从模型中获取特定行的方法

    示例中的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实现可以启动异步信号/槽通信序列,其中一些对象处理与某些后台线程中发生的数据库的通信。

答案 1 :(得分:0)

一个自用模型类,基于 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