qt-model-view
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseQt Model/View Architecture
Qt Model/View 架构
Architecture Overview
架构概述
Data Source ──→ Model ──→ [Proxy Model] ──→ View ──→ Delegate (renders cells)
↕ ↕
QAbstractItemModel QAbstractItemViewSeparate 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 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 |
For anything non-trivial, subclass or — has poor performance with large datasets and poor testability.
QAbstractTableModelQAbstractListModelQStandardItemModel| 基类 | 使用场景 |
|---|---|
| 简单的字符串列表 |
| 快速原型开发或小型数据集 |
| 单列自定义列表 |
| 多行多列的自定义表格 |
| 带有父子结构的树形结构 |
对于非简单场景,建议继承或——在处理大型数据集时性能较差,且可测试性不佳。
QAbstractTableModelQAbstractListModelQStandardItemModelCustom 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 methods (, , ). Skipping them causes views to lose sync with the model.
begin*/end*beginInsertRowsbeginRemoveRowsbeginResetModelpython
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*beginInsertRowsbeginRemoveRowsbeginResetModelConnecting 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()
undefinedview.horizontalHeader().setStretchLastSection(True)
view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
view.setSortingEnabled(True) # 自定义模型需要QSortFilterProxyModel支持
view.resizeColumnsToContents()
undefinedSort 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 directly from outside the model — always go through the model API
self._data - and
rowCount()must return 0 whencolumnCount()(Qt tree contract, even for tables)parent.isValid() - must be emitted with the exact changed index range — emitting the full model unnecessarily forces full view repaint
dataChanged - For large datasets (>10k rows), consider lazy loading via /
canFetchMore()fetchMore()
- 永远不要从模型外部直接访问——必须通过模型API操作
self._data - 当时,
parent.isValid()和rowCount()必须返回0(Qt树形结构约定,即使是表格也需遵守)columnCount() - 必须使用精确的变更索引范围触发——不必要地触发整个模型变更会强制视图完全重绘
dataChanged - 对于大型数据集(>10000行),考虑通过/
canFetchMore()实现懒加载fetchMore()