pyqt adding column at a tableview using QThread - python

I have some problem adding rows at a qtableview using thread for preventing GUI blocking. The number of rows is quite large. (may be 1800 ~ 3000 and each row has graph consisting of large data plot.) So it is simply predicted that Main GUI Thread is block by adding large number of rows if I don't use QThread. Yeah, it is fine so far.
So I implements above to solve the problem that I mentioned, but the problem still remains.
class ColumnTableView(MyTableView):
HistogramColumnNum = 3
def __init__(self. *args, **kwargs)
MyTableView.__init__(*args, **kwargs)
def beginInsertMode(self, mIdx, sIdx, eIdx):
self.model().beginResetModel()
def endInsertMode(self):
self.model().endResetModel()
def setData(self, data, ):
if data is None:
return
data = data['outputs']
keys = list(data.keys())
try:
data = data[keys[0]]
except:
return
self.wholeData = data
model = self.model()
model.items = []
self.reset()
if data is not None:
dqData = data
model.beginResetModel()
model.endResetModel()
self.worker.requestBeginInsertMode.connect(self.beginInsertMode)
self.worker.requestEndInsertMode.connect(self.endInsertMode)
self.worker.setData(data, model)
self.worker.start()
class Worker(QThread):
requestBeginInsertMode = pyqtSignal()
requestEndInsertMode = pyqtSignal()
def __init__(self, parent=None):
QThread.__init__(self, parent)
self.data = None
self.model = None
def setData(self, data, model):
self.model = model
self.data = data
def run(self):
data = self.data.npData
model = self.model
cols = data.shape[1]
for idx in range(cols):
print(idx)
col = data[:, idx]
item = Column(str(self.data.header[idx]), str(col.dtype), str(countOmissionData(col)), col)
model.addColumn(item)
self.requestEndInsertMode.emit()
class ColumnModel(QAbstractTableModel):
def __init__(self):
super(ColumnModel, self).__init__()
def rowCount(self, index=QModelIndex()):
if not self.items:
return 0
else:
return len(self.items)
def addColumn(self, column):
self.items.append(column)
So I implements multithread that actually add rows(named Column in my code. Sorry for confusing.) but it is not work (adding column still block the main GUI thread.)
How can I solve this problem?

Related

Qtableview is not updating when table model updates

I am writing a gui application for processing multiple Excel files into a single file. The main window displays a table to which the user can add or remove files to be collected in the larger file. Each row will represent a file with columns for different parameters (data to be pulled from that file.) I've been working on implementing a QAbstractTableModel for this purpose, which works great, although I've not been able to update the table view. When a new row is added to my array of data, a new row is added to the table view but each column is empty. I'm not sure why this is as I've confirmed that the data array is updating as it should. Example:
class _tableModel(QAbstractTableModel):
def __init__(self, data=None):
QAbstractTableModel.__init__(self)
self.data = data
self.load_data(data)
def load_data(self, data):
self.input_files = data[:,0]
self.input_sheets = data[:,1]
self.column_count = 2
self.row_count = len(self.input_sheets)
def rowCount(self, parent=QModelIndex()):
return self.row_count
def columnCount(self, parent=QModelIndex()):
return self.column_count
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return None
if orientation == Qt.Horizontal:
return ("File", "Sheet")[section]
else:
return "{}".format(section)
def data(self, index, role=Qt.DisplayRole):
column = index.column()
row = index.row()
if role == Qt.DisplayRole:
if column == 0:
file = str(self.input_files[row])
return file
elif column == 1:
return str(self.input_sheets[row])
elif role == Qt.BackgroundRole:
return QColor(Qt.white)
elif role == Qt.TextAlignmentRole:
return Qt.AlignRight
return None
def appendRowData(self, data):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.data = np.concatenate((self.data, data), axis=0)
self.endInsertRows()
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
self.data[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
class Widget(QWidget):
def __init__(self, data=None):
QWidget.__init__(self)
self.main_layout = QVBoxLayout()
self.addFileButton = QPushButton('Add File')
self.addFileButton.clicked.connect(self.addFileDialog)
self.main_layout.addWidget(self.addFileButton)
self.model = _tableModel(data)
self.table_view = QTableView()
self.table_view.setModel(self.model)
self.horizontal_header = self.table_view.horizontalHeader()
self.vertical_header = self.table_view.verticalHeader()
self.horizontal_header.setSectionResizeMode(
QHeaderView.ResizeToContents
)
self.vertical_header.setSectionResizeMode(
QHeaderView.ResizeToContents
)
self.horizontal_header.setStretchLastSection(True)
size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
size.setHorizontalStretch(1)
self.table_view.setSizePolicy(size)
self.main_layout.addWidget(self.table_view)
self.setLayout(self.main_layout)
def addFileDialog(self):
self.fileWizard = QDialog()
self.wiz_layout = QFormLayout()
self.fileWizard.setLayout(self.wiz_layout)
self.selectExcelFile = QPushButton('Select Excel File')
self.selectExcelFile.clicked.connect(self.selectexcel)
self.wiz_layout.addRow(self.selectExcelFile)
self.selectedFileDisplay = QTextEdit()
self.wiz_layout.addRow(self.selectedFileDisplay)
self.sheet = QLineEdit()
self.wiz_layout.addRow('Sheet Name: ', self.sheet)
self.add_to_table = QPushButton('Add to File Table')
self.wiz_layout.addWidget(self.add_to_table)
self.add_to_table.clicked.connect(self._addTableEntry)
self.fileWizard.show()
def selectexcel(self):
self.filename = QFileDialog.getOpenFileName(self)
self.filename = self.filename[0]
self.selectedFileDisplay.setText(self.filename)
pass
def _addTableEntry(self):
row = self.model.rowCount()
data = np.array([[self.filename, self.sheet.text()]])
self.model.appendRowData(data)
class _mainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.generalLayout = QGridLayout()
data = np.array([['File name here','Sheet name here']])
self._centralWidget = Widget(data)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = _mainWindow()
window.show()
sys.exit(app.exec_())
Both rowCount and columnCount must be dynamic, since they return the current extent of the model.
You're setting those values as static, so, while the model "accepts" the insertion of rows (as shown in the view), it's not able to access them because the row and column count don't reflect the updated model size.
def rowCount(self, parent=QModelIndex()):
return len(self.data[:,1])
Note that you're also not updating the self.data but in fact replacing it. Since you're using self.input_files for data(), it will probably result in an exception, as those arrays are not actually updated.
So, you either always use self.data anywhere with the correct slices, or use python properties to access input_files and input_sheets.
class _tableModel(QAbstractTableModel):
# ...
#property
def input_files(self):
return self.data[:,0]
# ...

Custom proxy model crashes when `removeRows()` from the source model

I am learning how to use proxy models recently, And I want to create a custom proxy model that can flat nodes in a source tree model to a list model, I have found a good solution to this:
How to create a proxy model that would flatten nodes of a QAbstractItemModel into a list in PySide?
However, when I try to removeRows() (remove tree node) from the source tree model, the proxy model crashes, I guess it's because the source model didn't emit layoutChanged signal to the proxy model to refresh the self.m_rowMap and self.m_indexMap?
【question1】: How to fix the crash?
【question2】:For QSortFilterProxyModel, removeRows() from the source model won't crash the proxy model, so I also want to know the underlying mechanism of QSortFilterProxyModel, especially the implementation of the following methods:
setSourceModel(),
mapFromSource(),
mapToSource(),
mapSelectionFromSource(),
mapSelectionToSource()
Especially how it emits signals between the sourceModel and the QSortFilterProxyModel?
Reproduce example:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import (QAbstractProxyModel, QModelIndex, pyqtSlot)
class FlatProxyModel(QAbstractProxyModel):
def __init__(self, parent=None):
# super(FlatProxyModel, self).__init__(parent)
super().__init__(parent)
def buildMap(self, model, parent=QModelIndex(), row=0):
"""
Usage:
* to build the rowMap and indexMap of the treeModel
"""
if row == 0:
self.m_rowMap = {}
self.m_indexMap = {}
rows = model.rowCount(parent)
for r in range(rows):
index = model.index(r, 0, parent)
print('row', row, 'item', model.data(index))
self.m_rowMap[index] = row
self.m_indexMap[row] = index
row = row + 1
if model.hasChildren(index):
row = self.buildMap(model, index, row)
return row
def setSourceModel(self, model):
QAbstractProxyModel.setSourceModel(self, model)
self.buildMap(model)
print(flush=True)
model.dataChanged.connect(self.sourceDataChanged)
def mapFromSource(self, index):
if index not in self.m_rowMap:
return QModelIndex()
# print('mapping to row', self.m_rowMap[index], flush = True)
return self.createIndex(self.m_rowMap[index], index.column())
def mapToSource(self, index):
if not index.isValid() or (index.row() not in self.m_indexMap):
return QModelIndex()
# print('mapping from row', index.row(), flush = True)
return self.m_indexMap[index.row()]
def columnCount(self, parent):
return QAbstractProxyModel.sourceModel(self).columnCount(self.mapToSource(parent))
def rowCount(self, parent):
# print('rows:', len(self.m_rowMap), flush=True)
return len(self.m_rowMap) if not parent.isValid() else 0
def index(self, row, column, parent):
# print('index for:', row, column, flush=True)
if parent.isValid():
return QModelIndex()
return self.createIndex(row, column)
def parent(self, index):
return QModelIndex()
#pyqtSlot(QModelIndex, QModelIndex)
def sourceDataChanged(self, topLeft, bottomRight):
self.dataChanged.emit(self.mapFromSource(topLeft),
self.mapFromSource(bottomRight))
class myWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.model = QtGui.QStandardItemModel()
names = ['Foo', 'Bar', 'Baz']
for first in names:
row = QtGui.QStandardItem(first)
for second in names:
row.appendRow(QtGui.QStandardItem(first + second))
self.model.appendRow(row)
self.proxy = FlatProxyModel()
self.proxy.setSourceModel(self.model)
self.nestedProxy = FlatProxyModel()
self.nestedProxy.setSourceModel(self.proxy)
vLayout = QtWidgets.QVBoxLayout(self)
hLayout = QtWidgets.QHBoxLayout()
self.treeView = QtWidgets.QTreeView()
self.treeView.setModel(self.model)
self.treeView.expandAll()
self.treeView.header().hide()
hLayout.addWidget(self.treeView)
self.listView1 = QtWidgets.QListView()
self.listView1.setModel(self.proxy)
hLayout.addWidget(self.listView1)
self.listView2 = QtWidgets.QListView()
self.listView2.setModel(self.nestedProxy)
hLayout.addWidget(self.listView2)
vLayout.addLayout(hLayout)
removeButton = QtWidgets.QPushButton('Remove')
removeButton.clicked.connect(self.removeItems)
vLayout.addWidget(removeButton)
def removeItems(self):
index = self.treeView.currentIndex()
model = index.model()
model.removeRows(index.row(), 1, index.parent())
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = myWidget()
w.show()
sys.exit(app.exec_())
The problem is that when removing an item from the source model the proxy is not notified and the "map" is not updated. One possible solution is to connect the rowsRemoved signal to the buildMap.
class FlatProxyModel(QAbstractProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self.connections = []
def buildMap(self, model, parent=QModelIndex(), row=0):
# ...
def setSourceModel(self, model):
if self.sourceModel() is not None:
for connection in self.connections:
self.sourceModel().disconnect(connection)
QAbstractProxyModel.setSourceModel(self, model)
if self.sourceModel() is None:
self.connections = []
return
self.connections = [
self.sourceModel().dataChanged.connect(self.sourceDataChanged),
self.sourceModel().rowsRemoved.connect(self.reload_model),
self.sourceModel().modelReset.connect(self.reload_model),
self.sourceModel().rowsInserted.connect(self.reload_model)
]
self.reload_model()
def reload_model(self):
self.beginResetModel()
self.buildMap(self.sourceModel())
self.endResetModel()
# ...
def removeItems(self):
index = self.treeView.currentIndex()
if not index.isValid():
return
model = index.model()
model.removeRows(index.row(), 1, index.parent())

How to have multiple columns in a QComboBox with a QAbstractTableModel

I've seen questions similar to this one but they are aimed at QTableView. This is not using that,, this is just for a dropdown (QComboBox) with a custom QAbstractTableModel, which needs to have 2 columns.
BIG UPDATE
(Note: Legacy code has been deleted as this is a better approach on the same question, and legacy code was confusing as hell).
Okay, so trying to catch up with what #eyllanesc explained, I changed this from a QAbstractListModel to a QAbstractTableModel. The result is:
class ModelForComboboxesWithID(QAbstractTableModel):
"""Create our basic model"""
def __init__(self, program, records):
super(ModelForComboboxesWithID, self).__init__()
self._data = records
self.program = program
self.path_images = program.PATH_IMAGES
def rowCount(self, index: int = 0) -> int:
"""The length of the outer list. Structure: [row, row, row]"""
if not self._data:
return 0 # Doubt: Do we need to return this if self._data is empty?
return len(self._data)
def columnCount(self, index: int = 0) -> int:
"""The length of the sub-list inside the outer list. Meaning that Columns are inside rows
Structure: [row [column], row [column], row [column]]"""
if not self._data:
return 0 # Doubt: Do we need to return this if self._data is empty?
return len(self._data[0])
def data(self, index, role=None):
"""return the data on this index as data[row][column]"""
# 1 - Display data based on its content (this edits the text that you visually see)
if role == Qt.DisplayRole:
value = self._data[index.row()][index.column()]
return value
# 2 - Tooltip displayed when hovering on it
elif role == Qt.ToolTipRole:
return f"ID: {self._data[index.row()][1]}"
Which I set this way:
def eventFilter(self, target, event: QEvent):
if event.type() == QEvent.MouseButtonPress:
if target == self.Buscadorcombo_cliente:
records = ... # my query to the database
set_combo_records_with_ids(self.program, target, records)
target.currentIndexChanged.connect(self.test)
def set_combo_records_with_ids(program, combobox: QComboBox, records):
"""Clear combobox, set model/data and sort it"""
combobox.clear()
model = ModelForComboboxesWithID(program, records)
combobox.setModel(model)
combobox.model().sort(0, Qt.AscendingOrder)
combobox.setModelColumn(0)
The result of this works almost perfect:
On the dropdown(Combobox) it displays the name.
If you hover on an item, it displays the ID.
Now I am able to get any data of it this way.
def test(self, index):
data_id = self.Buscadorcombo_cliente.model().index(index, 1).data()
data_name = self.Buscadorcombo_cliente.model().index(index, 0).data()
print(data_id)
print(data_name)
You have to set a QTableView as a view:
from PySide2 import QtGui, QtWidgets
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
combo = QtWidgets.QComboBox()
model = QtGui.QStandardItemModel(0, 2)
for i in range(10):
items = []
for j in range(model.columnCount()):
it = QtGui.QStandardItem(f"it-{i}{j}")
items.append(it)
model.appendRow(items)
combo.setModel(model)
view = QtWidgets.QTableView(
combo, selectionBehavior=QtWidgets.QAbstractItemView.SelectRows
)
combo.setView(view)
view.verticalHeader().hide()
view.horizontalHeader().hide()
header = view.horizontalHeader()
for i in range(header.count()):
header.setSectionResizeMode(i, QtWidgets.QHeaderView.Stretch)
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(combo)
lay.addStretch()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

Sorting two rows using QSortFilterProxyModel in QTableView

I am having issues trying to get my filter working for my QTableView; currently it only works for the first column, however I am trying to filter the first two columns using a QLineEdit. It should match either the first or second column.
I am working on making a minimal example, but was just seeing if anyone can see if i'm just making simple mistakes in my code.
Changing 'i' in the for loop to a single column (0 or 1) works, but doesn't work as expected because it just filters that specific column.
class SortFilterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
QtCore.QSortFilterProxyModel.__init__(self, *args, **kwargs)
self.filters = {}
def setFilterByColumn(self, regex, column):
self.filters[column] = regex
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
for key, regex in self.filters.items():
ix = self.sourceModel().index(source_row, key, source_parent)
if ix.isValid():
text = self.sourceModel().data(ix)
if regex.indexIn(text) == -1:
return False
return True
class Database(QtWidgets.QMainWindow, Ui_databaseWindow):
def __init__(self,parent=None):
super().__init__()
self.setupUi(self)
self.mainTableView.clicked.connect(self.tableInfo)
self.radioGroup = QtWidgets.QButtonGroup()
self.radioGroup.addButton(self.frameView) # below are radio buttons
self.radioGroup.addButton(self.cylView)
self.radioGroup.addButton(self.driversView)
self.radioGroup.addButton(self.valView)
self.radioGroup.addButton(self.fixedView)
self.radioGroup.addButton(self.vvcpView)
self.frameView.setChecked(True)
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("C:\\Workspace\\Database\\data.db")
self.db.open()
self.projectModel = QtSql.QSqlQueryModel()
self.proxyModel = SortFilterProxyModel(self)
self.proxyModel.setSourceModel(self.projectModel)
self.radioGroup.buttonClicked.connect(self.checkState)
self.projectModel.setQuery("select * from tblBasic_Frame",self.db)
self.mainTableView.setModel(self.proxyModel)
self.mainTableView.setSortingEnabled(True)
self.sortBox.textChanged.connect(self.onTextChanged) #QLineEdit
#QtCore.pyqtSlot(str)
def onTextChanged(self, text):
if self.valveView.isChecked():
for i in range(0,2):
self.proxyModel.setFilterByColumn(QtCore.QRegExp(text, QtCore.Qt.CaseInsensitive),i)
else:
self.proxyModel.setFilterByColumn(QtCore.QRegExp(text, QtCore.Qt.CaseInsensitive),0)
Your code has the following errors:
If the text in the first column does not match the text then you return False indicating that the row is not displayed without considering that there may be a match with the second column.
Let's say you have filtered the 2 columns and change the state of valveView, this will only update the regex of the first column so it will still filter the second column with the previous regex.
If there is no text in the QLineEdit then you must clean the filters.
You must also update the filter status when the valveView status is changed.
class SortFilterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
QtCore.QSortFilterProxyModel.__init__(self, *args, **kwargs)
self.filters = {}
def setFilterByColumn(self, regex, column):
self.filters[column] = regex
self.invalidateFilter()
def clear_filter(self):
self.filters = {}
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
values = []
if self.filters:
for key, regex in self.filters.items():
text = self.sourceModel().index(source_row, key, source_parent).data()
values.append(regex.indexIn(text) != -1)
return any(values)
return True
class Database(QtWidgets.QMainWindow, Ui_databaseWindow):
def __init__(self,parent=None):
super().__init__()
self.setupUi(self)
self.mainTableView.clicked.connect(self.tableInfo)
self.radioGroup = QtWidgets.QButtonGroup()
self.radioGroup.addButton(self.frameView) # below are radio buttons
self.radioGroup.addButton(self.cylView)
self.radioGroup.addButton(self.driversView)
self.radioGroup.addButton(self.valView)
self.radioGroup.addButton(self.fixedView)
self.radioGroup.addButton(self.vvcpView)
self.frameView.setChecked(True)
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("C:\\Workspace\\Database\\data.db")
self.db.open()
self.projectModel = QtSql.QSqlQueryModel()
self.proxyModel = SortFilterProxyModel(self)
self.proxyModel.setSourceModel(self.projectModel)
self.radioGroup.buttonClicked.connect(self.checkState)
self.projectModel.setQuery("select * from tblBasic_Frame",self.db)
self.mainTableView.setModel(self.proxyModel)
self.mainTableView.setSortingEnabled(True)
self.sortBox.textChanged.connect(self.update_filter) #QLineEdit
self.valveView.toggled.connect(self.update_filter)
#QtCore.pyqtSlot()
def update_filter(self):
text = self.sortBox.text()
self.proxyModel.clear_filter()
if text:
if self.valveView.isChecked():
for i in range(2):
self.proxyModel.setFilterByColumn(
QtCore.QRegExp(text, QtCore.Qt.CaseInsensitive), i
)
else:
self.proxyModel.setFilterByColumn(
QtCore.QRegExp(text, QtCore.Qt.CaseInsensitive), 0
)

PyQt4 force view to fetchMore from QAbstractItemModel

I have a QTableView that dynamically loads data from a custom 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 hit ctrl-a in the view it only will select the rows that are currently loaded.
Is there some mechanism to force the QTableView to fetch more rows? Ideally I would like to show a progress bar indicating the fraction of data that has been loaded from the model. Every few seconds I would like to force the model to load a bit more of the data, but I still want to let the user interact with the data that has been loaded so far. This way when the progress bar is complete the user can press ctrl-a and be confident that all data is selected.
Edit: I have another motivating use case. I want to jump to a specific row, but if that row is not loaded my interface does nothing.
How can I force a QAbstractItemModel to fetch more (or up to a specific row) and then force the QTableView to show it?
If I don't implement fetchMore and canFetchMore, the previous functionality works, but loading the tables is very slow. When I implement those methods the opposite happens. Not having an answer to this problem is causing issues with the usability of my qt interface, so I'm opening a bounty for this question.
Here is a method I'm using to select a specific row.
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
If the user has manually scrolled past the row in question then this function works. However, if the user has not seen the specific row this function just scrolls back to the top of the view.
It's probably too late for the answer here but maybe it would still benefit someone in future.
Below one can find a working example of a list model with canFetchMore and fetchMore methods + a view with a couple of custom methods:
Method trying to load more items from the model, if the model has something not loaded yet
Method capable of fetching the specific rows from the model if they haven't been loaded yet
The QMainWindow subclass in the example has a timer which is used to repeatedly call the first of the above mentioned methods, each time forcing the load of another batch of items from the model into the view. The loading of items in batches over small time intervals allows one to avoid blocking the UI thread completely and be able to edit the items loaded so far with little to no lag. The example contains a progress bar showing the part of items loaded so far.
The QMainWindow subclass also has a spin box which allows one to pick a particular row to show in the view. If the corresponding item has already been fetched from the model, the view simply scrolls to it. Otherwise it fetches this row's item from the model first, in a synchronous i.e. UI blocking fashion.
Here's the full code of the solution, 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'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()
One more thing I'd like to note is that this example expects the fetchMore method to do its work synchronously. But in more sophisticated approaches fetchMore doesn't actually have to act so. If your model loads its items from, say, a database then talking with the database synchronously in the UI thread would be a bad idea. Instead fetchMore implementation could start the asynchronous sequence of signal/slot communications with some object handling the communication with the database occurring in some background thread.
a self-using model class, based on Dmitry's answer.
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

Categories

Resources