Loading...
Loading...
Qt Model/View architecture — QAbstractItemModel, table/list/tree views, item delegates, and proxy models. Use when displaying tabular data, building a list with custom items, implementing a tree, creating a sortable/filterable table, or writing a custom item delegate. Trigger phrases: "QAbstractItemModel", "table view", "list model", "QTableView", "QListView", "tree view", "item delegate", "sort table", "filter model", "QSortFilterProxyModel", "custom model", "model data"
npx skill4agent add l3digital-net/claude-code-plugins qt-model-viewData Source ──→ Model ──→ [Proxy Model] ──→ View ──→ Delegate (renders cells)
↕ ↕
QAbstractItemModel QAbstractItemView| Base class | When to use |
|---|---|
| Simple list of strings |
| Quick prototype or small dataset |
| Custom list with single column |
| Custom table with rows × columns |
| Tree structures with parent/child |
QAbstractTableModelQAbstractListModelQStandardItemModelfrom PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
from PySide6.QtGui import QColor
class PersonTableModel(QAbstractTableModel):
HEADERS = ["Name", "Age", "Email"]
def __init__(self, data: list[dict], parent=None) -> None:
super().__init__(parent)
self._data = data
# --- Required overrides ---
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return 0 if parent.isValid() else len(self._data)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
return 0 if parent.isValid() else len(self.HEADERS)
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> object:
if not index.isValid():
return None
row, col = index.row(), index.column()
item = self._data[row]
match role:
case Qt.ItemDataRole.DisplayRole:
return str(item[self.HEADERS[col].lower()])
case Qt.ItemDataRole.BackgroundRole if item.get("active") is False:
return QColor("#f5f5f5")
case Qt.ItemDataRole.ToolTipRole:
return f"Row {row}: {item}"
case _:
return None
def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> object:
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
return self.HEADERS[section]
return None
# --- Mutation support ---
def setData(self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole) -> bool:
if not index.isValid() or role != Qt.ItemDataRole.EditRole:
return False
self._data[index.row()][self.HEADERS[index.column()].lower()] = value
self.dataChanged.emit(index, index, [role])
return True
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
base = super().flags(index)
return base | Qt.ItemFlag.ItemIsEditable
# --- Batch updates (correct reset pattern) ---
def replace_all(self, new_data: list[dict]) -> None:
self.beginResetModel()
self._data = new_data
self.endResetModel()
def append_row(self, item: dict) -> None:
pos = len(self._data)
self.beginInsertRows(QModelIndex(), pos, pos)
self._data.append(item)
self.endInsertRows()begin*/end*beginInsertRowsbeginRemoveRowsbeginResetModelfrom PySide6.QtWidgets import QTableView
model = PersonTableModel(people_data)
view = QTableView()
view.setModel(model)
# Tuning
view.horizontalHeader().setStretchLastSection(True)
view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
view.setSortingEnabled(True) # requires QSortFilterProxyModel for custom models
view.resizeColumnsToContents()from PySide6.QtCore import QSortFilterProxyModel, Qt
source_model = PersonTableModel(data)
proxy = QSortFilterProxyModel()
proxy.setSourceModel(source_model)
proxy.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
proxy.setFilterKeyColumn(0) # filter on "Name" column
view.setModel(proxy)
view.setSortingEnabled(True)
# Filter dynamically from a search box
# setFilterRegularExpression is preferred for new code (uses QRegularExpression internally)
search_box.textChanged.connect(proxy.setFilterRegularExpression)
# For modifying multiple filter parameters efficiently, use beginFilterChange/endFilterChange
# rather than calling invalidateFilter() after each changeQSortFilterProxyModelfilterAcceptsRowfrom PySide6.QtWidgets import QStyledItemDelegate, QStyleOptionViewItem, QApplication
from PySide6.QtGui import QPainter
from PySide6.QtCore import QRect, Qt
class ProgressDelegate(QStyledItemDelegate):
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None:
value = index.data(Qt.ItemDataRole.DisplayRole)
if not isinstance(value, int):
super().paint(painter, option, index)
return
# Draw progress bar using the style
opt = QStyleOptionProgressBar()
opt.rect = option.rect.adjusted(2, 4, -2, -4)
opt.minimum = 0
opt.maximum = 100
opt.progress = value
opt.text = f"{value}%"
opt.textVisible = True
QApplication.style().drawControl(QStyle.ControlElement.CE_ProgressBar, opt, painter)
view.setItemDelegateForColumn(2, ProgressDelegate(view))self._datarowCount()columnCount()parent.isValid()dataChangedcanFetchMore()fetchMore()