Skillversitas

The Model/View Framework

The model/view framework is Qt's architecture for displaying structured data: the model holds the data (the rows, the columns, the items), the view displays it (the QTableView, the QTreeView, the QListView), and the proxy (the optional middle layer) transforms the model for the view (the sort, the filter). The separation is the point: the model is the data with no rendering, the view is the rendering with no data, and the same model can back several views while a proxy (the sort, the filter) sits between them without either knowing. The models are the QStandardItemModel (the item-based, the easy) and the QAbstractItemModel (the index-based, the custom), and the views are the table, the tree, and the list. This chapter covers the QStandardItemModel and the views, the model's roles (the display, the edit, the tooltip, the check-state), the selection, and the custom model (the QAbstractItemModel's index, the data, the rowCount, the columnCount).

The QStandardItemModel and the Views

from PyQt5.QtWidgets import (QTableView, QTreeView,
                             QStandardItemModel,
                             QHeaderView, QApplication)
import sys
app = QApplication(sys.argv)

# the item model - the EASY model (the item-based):
model = QStandardItemModel(0, 3)     # 0 rows, 3 columns
model.setHorizontalHeaderLabels(
    ["Name", "Size", "Modified"])

# the appendRow: the row of QStandardItem (the cell)
model.appendRow([
    QStandardItem("report.pdf"),
    QStandardItem("204 KB"),
    QStandardItem("2026-11-01"),
])
model.appendRow([
    QStandardItem("notes.txt"),
    QStandardItem("4 KB"),
    QStandardItem("2026-11-05"),
])

# the view: the QTableView DISPLAYS the model:
view = QTableView()
view.setModel(model)      # the view shows the model
view.setSortingEnabled(True)   # the header-click sort
view.horizontalHeader().setStretchLastSection(True)

# the SAME model, a different view (the separation):
# the QTreeView (the column-less, the row-as-item), the
# QListView (the single-column) - the model is the data,
# the view is the presentation, and the swap is the setModel
view.show()

# the item's properties: the display, the edit, the check
item = QStandardItem("todo")
item.setCheckable(True)
item.setCheckState(Qt.Unchecked)     # the checkbox cell
item.setToolTip("the item's tooltip")
# the item is the cell's DATA + the cell's STATE (the
# checkable, the editable, the enabled)

# the edit: the view's edit triggers the item's edit (the
# setEditable, the default for the text item) - the model
# is updated by the view's edit, the data flows view->model

The QStandardItemModel is the easy model: the item-based, where each cell is a QStandardItem (the data plus the cell's state - the checkable, the editable, the enabled, the tooltip), and the appendRow adds a row of items. The setHorizontalHeaderLabels sets the column headers, and the setVerticalHeaderLabels the row headers. The view is the QTableView (the table), the QTreeView (the tree), or the QListView (the list) - and the setModel is the attachment: the view displays the model, and the same model can back several views (the separation - the data is the model's, the presentation is the view's, and the swap is the setModel). The setSortingEnabled gives the header-click sort (the view's built-in sort over the model), and the edit (the view's double-click, the setEditable) flows the user's change view-to-model (the model is updated by the view's edit). The item model is the standard for the table and the list the app needs, and the QStandardItem is the cell's data plus state in one object.

The Roles: What a Cell Carries

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QColor

# the ROLE is the kind of data a cell carries; the view
# asks the model for the role's value, and renders it:
#   DisplayRole   - the text shown (the default)
#   EditRole      - the text in the editor (the edit's)
#   ToolTipRole   - the tooltip
#   StatusTipRole - the status bar tip
#   TextAlignmentRole - the alignment
#   ForegroundRole / BackgroundRole - the colors (the QBrush)
#   CheckStateRole - the check state (the checkbox cell)
#   DecorationRole - the icon (the QPixmap / the QIcon)
#   FontRole - the font
#   WhatsThisRole - the what's-this help

# the item model: the setXxx map to the roles
item = QStandardItem("the cell")
item.setForeground(QBrush(QColor(200, 0, 0)))   # the ForegroundRole
item.setBackground(QBrush(QColor(255, 250, 220)))  # the BackgroundRole
item.setData(Qt.AlignRight, Qt.TextAlignmentRole)
item.setData(QIcon("folder"), Qt.DecorationRole)  # the icon

# the custom model (below): the data(index, role) returns
# the role's value - the DisplayRole the text, the
# ForegroundRole the color, etc. The role is the contract
# between the model (the data) and the view (the render):
# the model supplies the role's value, the view renders it,
# and the role is the vocabulary they share

The role is the kind of data a cell carries, and it is the contract between the model and the view: the view asks the model for the role's value (data(index, role)), and renders it - the DisplayRole the text shown, the EditRole the text in the editor, the ToolTipRole the tooltip, the ForegroundRole/BackgroundRole the colors (the QBrush), the CheckStateRole the checkbox cell's state, the DecorationRole the icon, the FontRole the font, the TextAlignmentRole the alignment. In the item model, the setForeground/setBackground/setData map to the roles (the setData(value, role) is the general form). In the custom model, the data(index, role) returns the role's value - the DisplayRole the text, the ForegroundRole the color - and the role is the vocabulary the model and the view share: the model supplies, the view renders, and the role is the interface. The role is what makes the model presentation-agnostic - the model carries the data in the roles, and the view decides how each role renders (the QTableView the cell, the QListView the delegate), which is the separation the framework is built on.

The Selection and the Model's Signals

from PyQt5.QtWidgets import QTableView, QApplication
import sys
app = QApplication(sys.argv)
view = QTableView()
# view.setModel(model)   # the model attached

# the selection: the view's selectionModel (the selection
# is the VIEW's, not the model's - the model is the data,
# the selection is the presentation's state)
sel = view.selectionModel()
sel.setSelectionMode(
    QAbstractItemView.ExtendedSelection)   # the multi
sel.setSelectionBehavior(
    QAbstractItemView.SelectRows)          # the row, not cell

# the selection's signals:
def on_selection_changed(sel_range, index_range):
    # the selectedChanged: the selection changed
    indexes = sel.selectedRows()
    for idx in indexes:
        print("selected:", idx.data())
sel.selectionChanged.connect(on_selection_changed)

# the currentChanged: the current index moved (the keyboard)
sel.currentChanged.connect(
    lambda cur, prev: print("current:", cur.data()))

# the model's signals: the data changed - the view updates
#   model.dataChanged(index, index, roles) - the cell changed
#   model.rowsInserted / rowsRemoved - the rows changed
#   model.layoutChanged - the structure changed
# the view OBSERVES the model: the model's signal fires,
# the view re-renders the affected region (the automatic
# sync, the no manual refresh)

# the programmatic selection:
sel.select(model.index(0, 0),
           QItemSelectionModel.ClearAndSelect)
sel.clearSelection()

The selection is the view's, not the model's: the selectionModel (reached through the view) holds the selected indexes, the selection mode (ExtendedSelection the multi, the SingleSelection, the NoSelection), and the selection behavior (SelectRows the row, the SelectItems the cell). The selection's signals are the interaction: the selectionChanged (the selection changed - the selectedRows the selected, the hook for the "show the selected row's details"), and the currentChanged (the current index moved, the keyboard navigation). The model's signals are the data change: the dataChanged (the cell changed), the rowsInserted/rowsRemoved (the rows changed), the layoutChanged (the structure changed) - and the view observes the model: the model's signal fires, and the view re-renders the affected region automatically, which is the no-manual-refresh the framework gives. The programmatic selection - the select with the ClearAndSelect, the clearSelection - is the app selecting on the user's behalf (the search that highlights the match). The selection is the view's state over the model's data, and the signals on both (the selection's, the model's) are the observation the view and the app both use.

The Custom Model: QAbstractItemModel

from PyQt5.QtCore import (QAbstractItemModel, QModelIndex,
                          Qt)
from PyQt5.QtWidgets import QTableView, QApplication
import sys

# the custom model: the index-based, for the data that is
# not a list of items (the database, the tree, the computed)
class NumberModel(QAbstractItemModel):
    def __init__(self, n=5, parent=None):
        super().__init__(parent)
        self.n = n

    def rowCount(self, parent=QModelIndex()):
        # the rows (the parent is the top for the top-level)
        return self.n if not parent.isValid() else 0

    def columnCount(self, parent=QModelIndex()):
        return 3     # the value, the double, the label

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        row, col = index.row(), index.column()
        if role == Qt.DisplayRole:
            if col == 0:
                return row + 1
            if col == 1:
                return (row + 1) * 1.5
            if col == 2:
                return "row %d" % row
        if role == Qt.ForegroundRole and col == 1:
            from PyQt5.QtGui import QBrush, QColor
            return QBrush(QColor(0, 120, 0))
        return None

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            return ["Value", "Double", "Label"][section]
        return None

app = QApplication(sys.argv)
view = QTableView()
view.setModel(NumberModel())
view.show()
sys.exit(app.exec_())

# the FOUR methods the model must implement:
#   rowCount / columnCount - the shape
#   data(index, role)      - the cell's value (the role)
#   index(row, col, parent)- the index for the position
#     (the QAbstractItemModel provides the default)
# the headerData - the header (the optional, the default None)
# the model is the DATA + the SHAPE; the view is the render;
# the QModelIndex is the cell's address (the row, the col,
# the parent) the data / the edit / the selection all use

The custom model is the QAbstractItemModel subclass - the index-based, for the data that is not a flat list of items (the database result, the tree, the computed values). The four methods the model implements are the rowCount and the columnCount (the shape), the data(index, role) (the cell's value for the role - the DisplayRole the text, the ForegroundRole the color), and the index(row, col, parent) (the QModelIndex for the position, the default provided by the base). The headerData is the header (the optional). The QModelIndex is the cell's address (the row, the column, the parent) that the data, the edit, and the selection all use - it is the model's reference to a cell, not a pointer into the data, and the model maps the index to its data. The model is the data plus the shape, the view is the render, and the setModel is the attachment - the same separation as the item model, with the index-based access for the data the QStandardItem does not fit. The model is where the app's data lives, the view is where it is shown, and the QModelIndex is the address they share.

The Proxy: The Sort and the Filter

from PyQt5.QtCore import QSortFilterProxyModel
from PyQt5.QtWidgets import (QTableView, QLineEdit,
                             QStandardItemModel, QApplication)
import sys
app = QApplication(sys.argv)

# the proxy - the middle layer between the model and the
# view: it wraps the model and TRANSFORMS it (the sort,
# the filter) without either the model or the view knowing
model = QStandardItemModel(0, 2)
model.setHorizontalHeaderLabels(["Name", "Size"])
model.appendRow([QStandardItem("a.txt"), QStandardItem("1")])
model.appendRow([QStandardItem("b.pdf"), QStandardItem("2")])
model.appendRow([QStandardItem("c.md"),  QStandardItem("3")])

proxy = QSortFilterProxyModel()
proxy.setSourceModel(model)      # the model the proxy wraps
proxy.setFilterCaseSensitivity(Qt.CaseInsensitive)
proxy.setFilterKeyColumn(0)      # the column the filter on
proxy.setDynamicSortFilter(True) # the re-sort / re-filter
                                 # on the change

view = QTableView()
view.setModel(proxy)             # the VIEW sees the PROXY
view.setSortingEnabled(True)     # the sort is the PROXY's
# the view's header-click sort sorts the PROXY (the model
# is untouched, the sort is the presentation's)

# the filter: the text the user types narrows the rows
edit = QLineEdit()
edit.textChanged.connect(
    lambda t: proxy.setFilterFixedString(t))
# the setFilterFixedString: the substring match (the column
# 0, the case-insensitive) - the rows that do not match
# are HIDDEN (the proxy's filter, the model's data intact)

# the proxy is the SEPARATION: the model is the data (the
# full, the unsorted), the proxy is the transform (the
# sorted, the filtered), the view is the render (the proxy's
# view of the model) - the same model, the different
# presentations (the sorted, the filtered) via the proxy

# the custom proxy: the QProxyModel subclass (the mapProxy
# the index map, the filterAcceptsRow the filter rule) -
# the transform the QSortFilterProxyModel does not cover

The proxy is the middle layer between the model and the view: it wraps the model (the setSourceModel) and transforms it - the sort, the filter - without either the model or the view knowing. The QSortFilterProxyModel is the standard: the setDynamicSortFilter (the re-sort and the re-filter on the change), the setFilterKeyColumn (the column the filter on), the setFilterFixedString (the substring match, the rows that do not match hidden), and the sort (the view's header-click, the setSortingEnabled - the sort is the proxy's, the model untouched). The view sees the proxy (the setModel(proxy)), and the proxy's view of the model is what is rendered - the sorted, the filtered. The separation is the point: the model is the data (the full, the unsorted), the proxy is the transform (the sorted, the filtered), and the view is the render - the same model, the different presentations (the sorted view, the filtered view) via the proxy, and the model's data is intact under the transform. The custom proxy is the QProxyModel subclass (the mapToSource the index map, the filterAcceptsRow the filter rule) for the transform the standard does not cover. The model-view-proxy is the three-layer architecture, and the proxy is the layer that makes the presentation a transform of the data rather than a copy of it.

The Working Rules

  • The model is the data, the view is the presentation, the setModel is the attachment. The same model can back several views (the table, the tree, the list), and the swap is the setModel - the data is the model's, the rendering is the view's, and the separation is the architecture.
  • The QStandardItemModel for the item-based, the QAbstractItemModel for the index-based. The item model is the easy (the QStandardItem the cell's data plus state); the custom model is for the data the item does not fit (the database, the tree, the computed) - the rowCount/columnCount/data/index the four methods.
  • The role is the contract between the model and the view. The model supplies the role's value (data(index, role)), the view renders it - the DisplayRole the text, the ForegroundRole the color, the DecorationRole the icon. The role is the vocabulary they share, and the model is presentation-agnostic because of it.
  • The selection is the view's, not the model's. The selectionModel (through the view) holds the selected indexes and the selection mode/behavior - the selection is the presentation's state over the model's data, and the model's signals (dataChanged, the rowsInserted) are what the view observes to re-render.
  • The proxy is the transform, not a copy. The QSortFilterProxyModel wraps the model (the sort, the filter) and the view sees the proxy - the model's data is intact under the transform, and the different presentations (the sorted, the filtered) are the proxy's views of the same model.

Summary

The model/view framework is Qt's architecture for structured data: the model holds the data (the QStandardItemModel the item-based, the QAbstractItemModel the index-based with the rowCount/columnCount/data/index), the view displays it (the QTableView, the QTreeView, the QListView, the setModel the attachment), and the proxy (the QSortFilterProxyModel the standard, the QProxyModel the custom) transforms the model for the view (the sort, the filter) without either knowing. The role is the contract between the model and the view - the model supplies the role's value (data(index, role)), the view renders it (the DisplayRole the text, the ForegroundRole the color, the DecorationRole the icon) - and the model is presentation-agnostic because of it. The selection is the view's (the selectionModel, the mode and the behavior, the selectionChanged the signal), and the model's signals (dataChanged, the rowsInserted) are what the view observes to re-render automatically. The QModelIndex is the cell's address the data, the edit, and the selection all use. The separation is the point: the data is the model's, the presentation is the view's, the transform is the proxy's, and the same model backs the different views and the different presentations.