I am trying to use QTableView with a custom model and QSortFilterProxyModel.
If I set the base model: self.setModel(self._model) the table displays correctly.
However, if I set the proxy model: self.setModel(self._proxy_model) no rows are displayed.
Not sure why. The docs https://doc.qt.io/qtforpython-5/PySide2/QtCore/QSortFilterProxyModel.html does not give a clue... Nor in general is it possible to find reasonable PyQt resources or examples on the internet.
My code below (python3.9, PyQt5, Windows):
import sys
import typing
from PyQt5.QtCore import QModelIndex, Qt, QSortFilterProxyModel, QAbstractTableModel
from PyQt5.QtWidgets import QTableView, QWidget, QApplication
from typing import Any, List, Dict, Optional, Iterable, Tuple, Union
KeyType = Tuple[Any, ...]
class KeyedTableModel(QAbstractTableModel):
"""Keyed table model."""
def __init__(self, headers: Iterable[str], key_columns: Union[int, Iterable[int]]) -> None:
"""Initialize the class."""
super().__init__()
self._headers = list(headers)
self._key_columns = [key_columns] if isinstance(key_columns, int) else list(key_columns)
self._key_to_row_idx: Dict[KeyType, int] = {}
self._data: List[List[Any]] = []
def _get_row_key(self, row: List[str]) -> KeyType:
return tuple(row[c] for c in self._key_columns)
def columnCount(self, parent: QModelIndex = ...) -> int:
# The length of our headers.
return len(self._headers)
def rowCount(self, parent: QModelIndex = ...) -> int:
return len(self._data)
def insertRow(self, row: int, parent: QModelIndex = ...) -> bool:
self._data.insert(row, [None] * self.columnCount())
return True
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
"""Get cell data."""
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
def setData(self, index: QModelIndex, value: typing.Any, role: int = Qt.DisplayRole) -> bool:
"""Set cell data."""
if role == Qt.DisplayRole:
self._data[index.row()][index.column()] = value
return True
def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole) -> Any:
"""Get header data."""
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._headers[section])
if orientation == Qt.Vertical:
return str(section)
def add_or_update(self, rows: Union[Iterable[List[Any]], List[Any]]) -> None:
"""Add or update row in the model."""
if isinstance(rows, List) and rows and not isinstance(rows[0], List):
rows = [rows]
min_row_idx, max_row_idx = sys.maxsize, -sys.maxsize
for row in rows:
key = self._get_row_key(row)
row_idx = self._key_to_row_idx.get(key)
if row_idx is None:
row_idx = self.rowCount()
self.insertRow(row_idx)
self._key_to_row_idx[key] = row_idx
for c, val in enumerate(row):
self.setData(self.index(row_idx, c), val)
min_row_idx = min(min_row_idx, row_idx)
max_row_idx = max(max_row_idx, row_idx)
top_left = self.index(min_row_idx, 0)
bot_right = self.index(max_row_idx, self.columnCount() - 1)
self.dataChanged.emit(top_left, bot_right)
class TableWidgetPlus2(QTableView):
def __init__(self, headers: Iterable[str],
key_columns: Union[int, Iterable[int]],
parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
self._model = KeyedTableModel(headers, key_columns)
self._proxy_model = QSortFilterProxyModel(self)
self._proxy_model.setSourceModel(self._model)
self.setModel(self._proxy_model)
def add_or_update(self, rows: Union[Iterable[List[Any]], List[Any]]) -> None:
"""Add or update row."""
self._model.add_or_update(rows)
self.repaint()
def table_widget_plus_2_demo_main() -> int:
"""Simulation main function."""
app = QApplication(sys.argv)
win = TableWidgetPlus2(['ID', 'Action', 'Repeat'], [0])
win.add_or_update(['A1', 'walk', 100])
win.add_or_update(['A1', 'stop', 200])
win.resize(1000, 500)
win.showMaximized()
res = app.exec_()
return res
if __name__ == '__main__':
table_widget_plus_2_demo_main()