qt-model-view

📁 l3digital-net/claude-code-plugins 📅 2 days ago
4
总安装量
3
周安装量
#53696
全站排名
安装命令
npx skills add https://github.com/l3digital-net/claude-code-plugins --skill qt-model-view

Agent 安装分布

opencode 3
gemini-cli 3
antigravity 3
claude-code 3
github-copilot 3
codex 3

Skill 文档

Qt Model/View Architecture

Architecture Overview

Data Source ──→ Model ──→ [Proxy Model] ──→ View ──→ Delegate (renders cells)
                 ↕                            ↕
              QAbstractItemModel         QAbstractItemView

Separate data (model) from presentation (view). The delegate handles painting and editing per-cell. Proxy models layer transformations (sort, filter) without modifying the source model.

Choosing a Model Base Class

Base class When to use
QStringListModel Simple list of strings
QStandardItemModel Quick prototype or small dataset
QAbstractListModel Custom list with single column
QAbstractTableModel Custom table with rows × columns
QAbstractItemModel Tree structures with parent/child

For anything non-trivial, subclass QAbstractTableModel or QAbstractListModel — QStandardItemModel has poor performance with large datasets and poor testability.

Custom Table Model

from 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()

Always bracket mutations with begin*/end* methods (beginInsertRows, beginRemoveRows, beginResetModel). Skipping them causes views to lose sync with the model.

Connecting Model to View

from 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()

Sort and Filter with QSortFilterProxyModel

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 change

For custom filter logic, subclass QSortFilterProxyModel and override filterAcceptsRow.

Custom Item Delegate

Use delegates to render non-text data (progress bars, icons, custom widgets):

from 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))

Key Rules

  • Never access self._data directly from outside the model — always go through the model API
  • rowCount() and columnCount() must return 0 when parent.isValid() (Qt tree contract, even for tables)
  • dataChanged must be emitted with the exact changed index range — emitting the full model unnecessarily forces full view repaint
  • For large datasets (>10k rows), consider lazy loading via canFetchMore() / fetchMore()