Related
I use QTableView to display and edit a Pandas DataFrame.
I use this method in the TableModel class to remove rows:
def removeRows(self, position, rows, QModelIndex):
start, end = position, rows
self.beginRemoveRows(QModelIndex, start, end) #
self._data.drop(position,inplace=True)
self._data.reset_index(drop=True,inplace=True)
self.endRemoveRows() #
self.layoutChanged.emit()
return True
It works fine until I add comboBox to some cells on the TableView. I use the following codes to add combobox (in the Main class), but when I delete a row it shows the error message (Python 3.10, Pandas 1.4.1):
IndexError: index 2 is out of bounds for axis 0 with size 2 or (Python 3.9, Pandas 1.3.5) : 'IndexError: single positional indexer is out-of-bounds'
count=len(combo_type)
for type in combo_type:
for row_num in range(self.model._data.shape[0]):
# print(i)
combo = CheckableComboBox(dept_list,self.model._data,row_num,type,count)
self.tableView.setIndexWidget(self.model.index(row_num, self.model._data.shape[1] - 2*count), combo)
count=count-1
But if I comment out the two lines: self.beginRemoveRows(QModelIndex, start, end) and self.endRemoveRows() from removeRows method, it works and there are no more error messages. But according to the Qt documents, these two methods must be called.
A removeRows() implementation must call beginRemoveRows() before the
rows are removed from the data structure, and it must call
endRemoveRows() immediately afterwards.
def removeRows(self, position, rows, QModelIndex):
start, end = position, rows
#self.beginRemoveRows(QModelIndex, start, end) # remove
self._data.drop(position,inplace=True)
self._data.reset_index(drop=True,inplace=True)
#self.endRemoveRows() # remove
self.layoutChanged.emit()
return True
I have tried for hours, but I cannot figure this out. Can anyone help me and explain what is wrong with my code, please?
This is my class for Table Model:
from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from datetime import datetime
import pandas as pd
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
# See below for the nested-list data structure.
# .row() indexes into the outer list,
# .column() indexes into the sub-list
print(index.row(), index.column())
value = self._data.iloc[index.row(), index.column()]
# Perform per-type checks and render accordingly.
if isinstance(value, datetime):
# Render time to YYY-MM-DD.
if pd.isnull(value):
value=datetime.min
return value.strftime("%Y-%m-%d")
if isinstance(value, float):
# Render float to 2 dp
return "%.2f" % value
if isinstance(value, str):
# Render strings with quotes
# return '"%s"' % value
return value
# Default (anything not captured above: e.g. int)
return value
# implement rowCount
def rowCount(self, index):
# The length of the outer list.
return self._data.shape[0]
# implement columnCount
def columnCount(self, index):
# The following takes the first sub-list, and returns
# the length (only works if all rows are an equal length)
return self._data.shape[1]
# implement flags
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
# implement setData
def setData(self, index, value, role):
if role == Qt.EditRole:
self._data.iloc[index.row(), index.column()] = value
# self._data.iat[index.row(), self._data.shape[1]-1] = value
self.dataChanged.emit(index, index)
return True
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
def insertRows(self, position, rows, QModelIndex, parent):
self.beginInsertRows(QModelIndex, position, position+rows-1)
default_row=[[None] for _ in range(self._data.shape[1])]
new_df=pd.DataFrame(dict(zip(list(self._data.columns),default_row)))
self._data=pd.concat([self._data,new_df])
self._data=self._data.reset_index(drop=True)
self.endInsertRows()
self.layoutChanged.emit()
return True
def removeRows(self, position, rows, QModelIndex):
start, end = position, rows
self.beginRemoveRows(QModelIndex, start, end) # if remove these 02 lines, it works
self._data.drop(position,inplace=True)
self._data.reset_index(drop=True,inplace=True)
self.endRemoveRows() # if remove these 02 lines, it works
self.layoutChanged.emit()
return True
Class for checkable combobox:
from PyQt5.QtWidgets import QComboBox
from PyQt5.QtCore import Qt
import CONSTANT
class CheckableComboBox(QComboBox):
def __init__(self,item_list, df,number,type,col_offset_value):
super().__init__()
self._changed = False
self.view().pressed.connect(self.handleItemPressed)
self.view().pressed.connect(self.set_df_value)
# Store checked item
self.checked_item=[]
self.checked_item_index=[]
self.type=type
self.col_offset_value=col_offset_value
# DataFrame to be modified
self.df=df
# Order number of the combobox
self.number=number
for i in range(len(item_list)):
self.addItem(item_list[i])
self.setItemChecked(i, False)
# self.activated.connect(self.set_df_value)
def set_df_value(self):
print(self.number)
self.df.iat[self.number,self.df.shape[1]-self.col_offset_value*2+1]=','.join(self.checked_item)
print(self.df)
def setItemChecked(self, index, checked=False):
item = self.model().item(index, self.modelColumn()) # QStandardItem object
if checked:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
def set_item_checked_from_list(self,checked_item_index_list):
for i in range(self.count()):
item = self.model().item(i, 0)
if i in checked_item_index_list:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
def get_item_checked_from_list(self,checked_item_index_list):
self.checked_item.clear()
self.checked_item.extend(checked_item_index_list)
def handleItemPressed(self, index):
item = self.model().itemFromIndex(index)
if item.checkState() == Qt.Checked:
item.setCheckState(Qt.Unchecked)
if item.text() in self.checked_item:
self.checked_item.remove(item.text())
self.checked_item_index.remove(index.row())
print(self.checked_item)
print(self.checked_item_index)
else:
if item.text()!=CONSTANT.ALL \
and CONSTANT.ALL not in self.checked_item \
and item.text()!=CONSTANT.GWP \
and CONSTANT.GWP not in self.checked_item \
and item.text()!=CONSTANT.NO_ALLOCATION \
and CONSTANT.NO_ALLOCATION not in self.checked_item :
item.setCheckState(Qt.Checked)
self.checked_item.append(item.text())
self.checked_item_index.append(index.row())
print(self.checked_item)
print(self.checked_item_index)
else:
self.checked_item.clear()
self.checked_item_index.clear()
self.checked_item.append(item.text())
self.checked_item_index.append(index.row())
self.set_item_checked_from_list(self.checked_item_index)
self._changed = True
self.check_items()
def hidePopup(self):
if not self._changed:
super().hidePopup()
self._changed = False
def item_checked(self, index):
# getting item at index
item = self.model().item(index, 0)
# return true if checked else false
return item.checkState() == Qt.Checked
def check_items(self):
# traversing the items
checkedItems=[]
for i in range(self.count()):
# if item is checked add it to the list
if self.item_checked(i):
checkedItems.append(self.model().item(i, 0).text())
Main class:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt,QDate,QThread
from net_comm_ui import Ui_MainWindow
from PyQt5.QtWidgets import QApplication, QMainWindow
from pathlib import Path
import multiprocessing
from TableModel import TableModel
from CheckableComboBox import CheckableComboBox
import copy
import datetime
import re
import json
from pathlib import Path
import pandas as pd
import os
from net_comm_worker import Worker
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtCore import pyqtSlot
dept_list = ['A','B','C','D','E','F','G','H']
combo_type=['METHOD','LOB','DEPT','CHANNEL']
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.tableView = QtWidgets.QTableView()
import pandas as pd
mydict = [{'a': 1, 'b': 2, 'c': 3, 'd': 4},
{'a': 100, 'b': 200, 'c': 300, 'd': 400},
{'a': 1000, 'b': 2000, 'c': 3000, 'd': 4000 }]
self.data=pd.DataFrame(mydict)
print('initial self.data')
print(self.data)
self.data['Allocation Method'] = ''
self.data['Allocation Method Selected']=''
self.data['Allocation LOB'] = ''
self.data['Allocation LOB Selected']=''
self.data['Allocation DEPT'] = ''
self.data['Allocation DEPT Selected']=''
self.data['Allocation CHANNEL'] = ''
self.data['Allocation CHANNEL Selected']=''
self.model = TableModel(self.data)
self.tableView.setModel(self.model)
self.setCentralWidget(self.tableView)
self.setGeometry(600, 200, 500, 300)
count=len(combo_type)
# Set ComboBox to cells
for type in combo_type:
for row_num in range(self.model._data.shape[0]):
# print(i)
combo = CheckableComboBox(dept_list,self.model._data,row_num,type,count)
self.tableView.setIndexWidget(self.model.index(row_num, self.model._data.shape[1] - 2*count), combo)
count=count-1
button = QPushButton('Delete row', self)
button.move(100,200)
button.clicked.connect(self.delete_row)
def delete_row(self):
index = self.tableView.currentIndex()
if index.row()<self.model._data.shape[0]:
self.model.removeRows(index.row(), 1, index)
print('self.model._data')
print(self.model._data)
print('self.data')
print(self.data)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
I add one method to add row. Is the self.layoutChanged.emit() is mandatory to update TableView or there is a more efficient way?:
def insertRows(self, position, rows, QModelIndex, parent):
self.beginInsertRows(QModelIndex, position, position+rows-1)
default_row=[[None] for _ in range(self._data.shape[1])]
new_df=pd.DataFrame(dict(zip(list(self._data.columns),default_row)))
self._data=pd.concat([self._data,new_df])
self._data.reset_index(drop=True, inplace=True)
self.endInsertRows()
self.layoutChanged.emit() # ==> is this mandatory?
return True
Your example passes the wrong index to removeRows, which also does not calculate the start and end values correctly. It can be fixed like this:
class MainWindow(QtWidgets.QMainWindow):
...
def delete_row(self):
index = self.tableView.currentIndex()
self.model.removeRows(index.row(), 1)
def insert_row(self):
self.model.insertRows(self.model.rowCount(), 1)
class TableModel(QtCore.QAbstractTableModel):
...
def rowCount(self, parent=QModelIndex()):
...
def columnCount(self, parent=QModelIndex()):
...
def insertRows(self, position, rows, parent=QModelIndex()):
start, end = position, position + rows - 1
if 0 <= start <= end:
self.beginInsertRows(parent, start, end)
for index in range(start, end + 1):
default_row = [[None] for _ in range(self._data.shape[1])]
new_df = pd.DataFrame(dict(zip(list(self._data.columns), default_row)))
self._data = pd.concat([self._data, new_df])
self._data = self._data.reset_index(drop=True)
self.endInsertRows()
return True
return False
def removeRows(self, position, rows, parent=QModelIndex()):
start, end = position, position + rows - 1
if 0 <= start <= end and end < self.rowCount(parent):
self.beginRemoveRows(parent, start, end)
for index in range(start, end + 1):
self._data.drop(index, inplace=True)
self._data.reset_index(drop=True, inplace=True)
self.endRemoveRows()
return True
return False
How do I properly remove items from my custom QAbstractTableModel? Do i need to change this to QStandardItemModel instead?
This is the before:
This is the after...it leaves empty rows and the selection doesn't seem to clear either.
import os
import sys
from PySide import QtCore, QtGui
import random
class CustomJobs(object):
def __init__(self, **kwargs):
super(CustomJobs, self).__init__()
# instance properties
self.name = ''
self.status = ''
# initialize attribute values
for k, v in kwargs.items():
if hasattr(self, k):
setattr(self, k, v)
class PlayblastTableModel(QtCore.QAbstractTableModel):
HEADERS = ['Name', 'Status']
def __init__(self):
super(PlayblastTableModel, self).__init__()
self.items = []
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal:
if role == QtCore.Qt.DisplayRole:
return self.HEADERS[section]
return None
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self.HEADERS)
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def addItem(self, *items):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount() + len(items) - 1)
for item in items:
assert isinstance(item, CustomJobs)
self.items.append(item)
self.endInsertRows()
def removeItems(self, items):
self.beginRemoveRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self.items = [x for x in self.items if x not in items]
self.endRemoveRows()
def clear(self):
self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount())
self.items = []
self.endRemoveRows()
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
row = index.row()
col = index.column()
if 0 <= row < self.rowCount():
item = self.items[row]
if role == QtCore.Qt.DisplayRole:
if col == 0:
return item.name
elif col == 1:
return item.status.title()
elif role == QtCore.Qt.UserRole:
return item
return None
class CustomJobsQueue(QtGui.QWidget):
'''
Description:
Widget that manages the Jobs Queue
'''
def __init__(self):
super(CustomJobsQueue, self).__init__()
self.resize(400,600)
# controls
self.uiAddNewJob = QtGui.QPushButton('Add')
self.uiRemoveSelectedJobs = QtGui.QPushButton('Remove')
self.playblastJobModel = PlayblastTableModel()
self.uiJobTableView = QtGui.QTableView()
self.uiJobTableView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.uiJobTableView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.uiJobTableView.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.uiJobTableView.setModel(self.playblastJobModel)
self.jobSelection = self.uiJobTableView.selectionModel()
# sub layouts
self.jobQueueToolsLayout = QtGui.QHBoxLayout()
self.jobQueueToolsLayout.addWidget(self.uiAddNewJob)
self.jobQueueToolsLayout.addWidget(self.uiRemoveSelectedJobs)
self.jobQueueToolsLayout.addStretch()
# layout
self.mainLayout = QtGui.QVBoxLayout()
self.mainLayout.addLayout(self.jobQueueToolsLayout)
self.mainLayout.addWidget(self.uiJobTableView)
self.setLayout(self.mainLayout)
# connections
self.uiAddNewJob.clicked.connect(self.addNewJob)
self.uiRemoveSelectedJobs.clicked.connect(self.removeSelectedJobs)
# methods
def addNewJob(self):
name = random.choice(['Kevin','Suzie','Melissa'])
status = random.choice(['error','warning','successa'])
job = CustomJobs(name=name, status=status)
self.playblastJobModel.addItem(job)
def removeSelectedJobs(self):
jobs = self.getSelectedJobs()
self.playblastJobModel.removeItems(jobs)
def getSelectedJobs(self):
jobs = [x.data(QtCore.Qt.UserRole) for x in self.jobSelection.selectedRows()]
return jobs
def main():
app = QtGui.QApplication(sys.argv)
window = CustomJobsQueue()
window.show()
app.exec_()
if __name__ == '__main__':
main()
The reason for this behavior is that you're using the wrong row in beginRemoveRows(): you should use the row number you're removing, and since you're using rowCount() that row index is invalid.
def removeItems(self, items):
self.beginRemoveRows(QtCore.QModelIndex(), self.rowCount() - 1, self.rowCount() - 1)
self.items = [x for x in self.items if x not in items]
self.endRemoveRows()
To be more correct, you should remove the actual rows in the model. In your simple case it won't matter that much, but in case your model becomes more complex, keep in mind this.
def removeItems(self, items):
removeRows = []
for row, item in enumerate(self.items):
if item in items:
removeRows.append(row)
for row in sorted(removeRows, reverse=True):
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.items.pop(row)
self.endRemoveRows()
The reason for the reversed row order in the for cycle is that for list consistency reasons the row removal should always begin from the bottom. This can be important if you want to remove rows arbitrarily while keeping the current selection in case the removed items are not selected.
That said, as already suggested in the comments, if you don't need specific behavior and implementation, creating a QAbstractItemModel (or any abstract model) subclass is unnecessary, as QStandardItemModel will usually be enough, as it already provides all required features (including drag and drop support, which can be rather complex if you don't know how the Qt data model works).
Well, unless it's for learning purposes, obviously.
I wanna concatenate the items of a column in tablewidget_1 and the items of a column in tablewidget_2, and put the result in a listwidget or a listview. I m using pyqt5. I know how to set a model to a specific column of a table then affect this model to a listview, but I didn't find a way how to concatenate 2 tablewidgets into the model.
here is the code for a model for just one tablewidget:
model = QtCore.QSortFilterProxyModel()
model.setSourceModel(self.tableWidget.model())
listview.setModel(model)
listview.setModelColumn(0)
The first is to convert the table type model to a list type using a custom QAbstractProxyModel, and then concatenate it using QConcatenateTablesProxyModel(PyQt5>=5.13):
from PyQt5 import QtCore, QtGui, QtWidgets
class Table2ListProxyModel(QtCore.QIdentityProxyModel):
def columnCount(self, parent=QtCore.QModelIndex()):
return 0 if self.sourceModel() is None else 1
def rowCount(self, parent=QtCore.QModelIndex()):
if self.sourceModel() is None:
return 0
return self.sourceModel().rowCount() * self.sourceModel().columnCount()
def index(self, row, column, parent=QtCore.QModelIndex()):
if row < 0 or column < 0 or self.sourceModel() is None:
return QtCore.QModelIndex()
source_parent = self.mapToSource(parent)
r = row // self.sourceModel().columnCount()
c = row % self.sourceModel().columnCount()
sourceIndex = self.sourceModel().index(r, c, source_parent)
return self.mapFromSource(sourceIndex)
def mapToSource(self, proxyIndex):
if self.sourceModel() is None or not proxyIndex.isValid():
return QtCore.QModelIndex()
r = proxyIndex.row() // self.sourceModel().columnCount()
c = proxyIndex.row() % self.sourceModel().columnCount()
return self.sourceModel().index(r, c)
def mapFromSource(self, sourceIndex):
if self.sourceModel() is None or not sourceIndex.isValid():
return QtCore.QModelIndex()
r = sourceIndex.row() * self.sourceModel().columnCount() + sourceIndex.column()
c = 0
return self.createIndex(r, c, sourceIndex.internalPointer())
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._table1 = QtWidgets.QTableWidget(3, 2)
self._table2 = QtWidgets.QTableWidget(4, 3)
self._table3 = QtWidgets.QTableWidget(5, 4)
self._table4 = QtWidgets.QTableWidget(6, 5)
tables = (self._table1, self._table2, self._table3, self._table4)
for i, table in enumerate(tables):
for row in range(table.rowCount()):
for column in range(table.columnCount()):
it = QtWidgets.QTableWidgetItem("{}: {}-{}".format(i, row, column))
table.setItem(row, column, it)
concatenate_proxy_model = QtCore.QConcatenateTablesProxyModel(self)
concatenate_list_view = QtWidgets.QListView()
concatenate_list_view.setModel(concatenate_proxy_model)
grid_layout = QtWidgets.QGridLayout(self)
grid_layout.addWidget(QtWidgets.QLabel("Table to List:"), 1, 0, 1, len(tables))
grid_layout.addWidget(QtWidgets.QLabel("Results:"), 3, 0, 1, len(tables))
grid_layout.addWidget(concatenate_list_view, 4, 0, 1, len(tables))
for i, table in enumerate(tables):
list_view = QtWidgets.QListView()
proxy = Table2ListProxyModel(self)
proxy.setSourceModel(table.model())
list_view.setModel(proxy)
concatenate_proxy_model.addSourceModel(proxy)
grid_layout.addWidget(table, 0, i)
grid_layout.addWidget(list_view, 2, i)
self.resize(640, 480)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
How do i retrieve the correctly selected item from my custom QAbstractListModel which contains a custom sorting algorithm?
You can test the tool by simply making selections in the UI and looking at the console. You can see it's printing the wrong information for the selected item.
I'm assuming the issue relates to how i use the selection indexes to get the item in the Model.
complete code:
import os, sys
from PySide import QtGui, QtCore
class ExplorerItem(object):
def __init__(self, name, tags):
self.name = name
self.tags = tags
class ElementModel(QtCore.QAbstractListModel):
TagsRole = QtCore.Qt.UserRole + 1
NameRole = QtCore.Qt.UserRole + 2
def __init__(self, *args, **kwargs):
QtCore.QAbstractListModel.__init__(self, *args, **kwargs)
self._items = []
self._icons = {}
def rowCount(self, index=QtCore.QModelIndex()):
return len(self._items)
def addItem(self, item):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self._items.append(item)
self.endInsertRows()
def getItem(self, index):
row = index.row()
if index.isValid() and 0 <= row < self.rowCount():
return self._items[row]
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if 0 <= index.row() < self.rowCount():
item = self._items[index.row()]
if role == ElementModel.TagsRole:
return item.tags
elif role == ElementModel.NameRole:
return item.colors
elif role == QtCore.Qt.DisplayRole:
return item.name
elif role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignCenter
class ExplorerSortModel(QtGui.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(ExplorerSortModel, self).__init__(*args, **kwargs)
self._patterns = {}
self.setDynamicSortFilter(True)
self.setSourceModel(ElementModel())
self.sort(0, QtCore.Qt.AscendingOrder)
def set_pattern(self, role, value):
self._patterns[role] = value
def lessThan(self, left, right):
leftData = self.sourceModel()._items[left.row()]
rightData = self.sourceModel()._items[right.row()]
if leftData and rightData:
l = getattr(leftData, 'name', '')
r = getattr(rightData, 'name', '')
return l > r
return True
def filterAcceptsRow(self, sourceRow, sourceParent):
sm = self.sourceModel()
ix = sm.index(sourceRow)
if ix.isValid():
val = True
for role, fvalue in self._patterns.items():
value = ix.data(role)
val = val and self.filter(value, fvalue, role)
return val
return False
#staticmethod
def filter(value, fvalue, role):
'''
fvalue: search value
value: properties value being tested
'''
if role == ElementModel.TagsRole:
if fvalue == []:
return True
else:
return all(any(x in y for y in value) for x in fvalue)
elif role == ElementModel.NameRole:
return True
else:
return False
class QExplorerWidget(QtGui.QWidget):
def __init__(self, *args, **kwargs):
super(QExplorerWidget, self).__init__(*args, **kwargs)
self.resize(400,400)
# control
self.ui_explorer = QtGui.QListView()
self.ui_explorer.setResizeMode(QtGui.QListView.Adjust)
self.ui_explorer.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.ui_explorer.setMovement(QtGui.QListView.Static)
self.ui_explorer.setSpacing(10)
self.explorer_model = ExplorerSortModel()
self.ui_explorer.setModel(self.explorer_model)
self.ui_explorer_selection = self.ui_explorer.selectionModel()
lay = QtGui.QVBoxLayout()
lay.addWidget(self.ui_explorer)
self.setLayout(lay)
# connections
self.ui_explorer_selection.selectionChanged.connect(self.changed_selection)
# test data
self.explorer_model.sourceModel().addItem(ExplorerItem('John',['john','sports']))
self.explorer_model.sourceModel().addItem(ExplorerItem('Apple',['apple','fruit']))
self.explorer_model.sourceModel().addItem(ExplorerItem('Kevin',['kevin','money']))
self.explorer_model.sourceModel().addItem(ExplorerItem('Zoo',['zoo','animals']))
def changed_selection(self):
indexes = self.ui_explorer_selection.selectedIndexes()
for index in indexes:
item = self.explorer_model.sourceModel().getItem(index)
print item.name, item.tags, index
if __name__ == '__main__':
''
app = QtGui.QApplication(sys.argv)
ex = QExplorerWidget()
ex.show()
sys.exit(app.exec_())
The QModelIndex of the selectedIndexes belong to the model that was established in the view, and in this case it is the ExplorerSortModel, so these indexes can not be passed directly to the getItem() method of ElementModel since that method expects that the QModelIndex belongs to ElementModel.
In your case you must convert that QModelIndex belonging to ExplorerSortModel to the corresponding QModelIndex that belongs to ElementModel using the mapSource() method.
def changed_selection(self):
indexes = self.ui_explorer_selection.selectedIndexes()
for index in indexes:
ix_source = self.explorer_model.mapToSource(index)
item = self.explorer_model.sourceModel().getItem(ix_source)
print(item.name, item.tags)
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