qt-model-view

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Qt Model/View Architecture

Qt Model/View 架构

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.
Data Source ──→ Model ──→ [Proxy Model] ──→ View ──→ Delegate (renders cells)
                 ↕                            ↕
              QAbstractItemModel         QAbstractItemView
将数据(Model)与展示(View)分离。委托负责每个单元格的绘制和编辑。代理模型可在不修改源模型的情况下叠加排序、筛选等转换操作。

Choosing a Model Base Class

选择模型基类

Base classWhen 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.
基类使用场景
QStringListModel
简单的字符串列表
QStandardItemModel
快速原型开发或小型数据集
QAbstractListModel
单列自定义列表
QAbstractTableModel
多行多列的自定义表格
QAbstractItemModel
带有父子结构的树形结构
对于非简单场景,建议继承
QAbstractTableModel
QAbstractListModel
——
QStandardItemModel
在处理大型数据集时性能较差,且可测试性不佳。

Custom Table Model

自定义表格模型

python
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.
python
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()
始终使用
begin*/end*
方法包裹修改操作(如
beginInsertRows
beginRemoveRows
beginResetModel
)。跳过这些方法会导致视图与模型失去同步。

Connecting Model to View

将模型连接到视图

python
from PySide6.QtWidgets import QTableView

model = PersonTableModel(people_data)
view = QTableView()
view.setModel(model)
python
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()
undefined
view.horizontalHeader().setStretchLastSection(True) view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) view.setSortingEnabled(True) # 自定义模型需要QSortFilterProxyModel支持 view.resizeColumnsToContents()
undefined

Sort and Filter with QSortFilterProxyModel

使用QSortFilterProxyModel实现排序与筛选

python
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)
python
from PySide6.QtCore import QSortFilterProxyModel, Qt

source_model = PersonTableModel(data)
proxy = QSortFilterProxyModel()
proxy.setSourceModel(source_model)
proxy.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
proxy.setFilterKeyColumn(0)   # 基于“Name”列筛选

view.setModel(proxy)
view.setSortingEnabled(True)

Filter dynamically from a search box

从搜索框动态筛选

setFilterRegularExpression is preferred for new code (uses QRegularExpression internally)

新代码建议使用setFilterRegularExpression(内部基于QRegularExpression实现)

search_box.textChanged.connect(proxy.setFilterRegularExpression)
search_box.textChanged.connect(proxy.setFilterRegularExpression)

For modifying multiple filter parameters efficiently, use beginFilterChange/endFilterChange

若要高效修改多个筛选参数,使用beginFilterChange/endFilterChange

rather than calling invalidateFilter() after each change

而非每次修改后调用invalidateFilter()


For custom filter logic, subclass `QSortFilterProxyModel` and override `filterAcceptsRow`.

如需自定义筛选逻辑,可继承`QSortFilterProxyModel`并重写`filterAcceptsRow`方法。

Custom Item Delegate

自定义项委托

Use delegates to render non-text data (progress bars, icons, custom widgets):
python
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))
使用委托渲染非文本数据(如进度条、图标、自定义控件):
python
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
        # 使用样式绘制进度条
        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()
  • 永远不要从模型外部直接访问
    self._data
    ——必须通过模型API操作
  • parent.isValid()
    时,
    rowCount()
    columnCount()
    必须返回0(Qt树形结构约定,即使是表格也需遵守)
  • 必须使用精确的变更索引范围触发
    dataChanged
    ——不必要地触发整个模型变更会强制视图完全重绘
  • 对于大型数据集(>10000行),考虑通过
    canFetchMore()
    /
    fetchMore()
    实现懒加载