26. Model-View Programming - Sorting, Filtering and Selection

There are two ways to sort data in the model-view architecture:

  • Implement the sorting directly in the model by overriding the sort() method. Any view connected to that model can then sort programmatically.

  • Use a proxy model. The proxy sits between the model and the view and handles sorting transparently, with no changes to the source model. You can use a proxy when working with a model you do not own, or when using a list view, which has no headers to click.

26.1 Implementing In-Place Sorting in Custom Models

You may want to implement in-place (destructive) sorting when:

  • Your model owns the data.
  • The original order never needs to be restored.
An icon of a clipboard-list1

You are building a financial dashboard where users sort economic indicators by clicking column headers, and the sorted order should become the new permanent order of the data.

To implement in-place sorting in custom models:

  1. Create the model. For this exercise, we subclass QAbstractTableModel and implement rowCount(), columnCount(), data(), setData(), flags() and headerData() to create an editable table model. The backing data is a two-dimensional Python list assigned to the self.csv_data instance field.

  2. Reimplement sort(). The method signature is:

    1 sort(column: int, order: Qt.SortOrder=Qt.SortOrder.AscendingOrder) -> None
    

    where column is the model column to sort by, and order is a Qt.SortOrder, ascending by default. In the method, we:

    • Emit layoutAboutToBeChanged() to notify connected views to cache their current state before the layout changes,
    • Back up the data before sorting. Sort csv_data in place using Python’s list sort(), passing it a lambda that extracts row[column] as the sort key and setting reverse based on the order argument.
    • Call changePersistentIndexList() to remap any live QPersistentModelIndex objecs held by views or external code. We build a lookup from the sorted list and use the pre-sort backup to map each old row position to its new position.
    • Emit layoutChanged() to notify connected views that the layout change is complete.
  1. Create the view, set its model, and enable sorting. In the main window, create a CsvModel object and a QTableView. Assign the model to the view and enable the sorting via view.setSortingEnabled().

When you run the application, you will be able to sort the view by clicking the header of any of its columns. The model is sorted as well.

Note that when you change any cell value, the sort order does not automatically change.

26.2 Non-Destructive Sorting in Custom Models

You would use non-destructive sorting when:

  • You must preserve the original data order.
  • The data is read-only or shared between multiple models or views.
An icon of a clipboard-list1

You are developing an analytics tool where users can sort economic indicators by value or aggregete for analysis, while the original indicator order is preserved for reporting.

To implement non-destructive sorting in custom models:

  1. Create the model. We reuse the model from the previous section with one addition: a self.row_indices list is used to store the current sort order, leaving self.csv_data untouched.

  2. Update data() to read through row_indices. Instead of accessing csv_data[index.row()] directly, we first look up the mapped row with row_indices[index.row()] and then use it to access the data.

  3. Reimplement sort(). Instead of sorting csv_data directly, sort row_indices instead.

26.3 Automatic Sorting on Data Changes

At times, you want the data to stay sorted automatically whenever it is modified or deleted.

An icon of a clipboard-list1

You are creating a portfolio management app where the list of economic indicators must stay alphabetically sorted by name whenever an indicator is renamed.

To sort the data automatically on data changes:

  1. Implement sort(). The method is identical to section 26.1: emit layoutAboutToBeChanged(), sort the data, remap the persistent index list, and emit layoutChanged().

  2. Call sort() from setData() whenever the indicator name changes. Since all data modifications go through setData(), we check whether the edited column is column 0 and call sort() if so, right before emitting dataChaged().

  1. Perform the initial sort. The backing data in csv_data start in insertion order, so we call sort() once on the model before showing the main window.

Now, when the user edits an indicator name and confirms the change, the list re-sorts automatically. Edits to other columns leave the sort order unchanged.

26.4 Sorting with Proxy Models

QSortFilterProxyModel sits between a source model and a view, mapping the source model’s indexes to its own indexes without modifying the underlying data. This makes it possible to sort or filter the view without touching the source model at all.

You can fine-tune the way sorting is performed with these methods:

Method Description
setSortCaseSensitivity(sensitivity) Control case sensitivity when sorting strings
setSortLocaleAware(bool) Use locale-aware collation when sorting strings
setSortRole(role) Data role used when comparing items for sorting
setDynamicSortFilter(bool) Re-sort and re-filter automatically when source data changes
An icon of a clipboard-list1

You are building a financial dashboard where a fixed reference view and a sortable analysis view display the same economic indicators side by side, and sorting in one must not affect the other.

To use a proxy model to enable sorting:

  1. Create the proxy model and set its source model. The source model is a standard editable table model with no sort() implemented. In the main window, create a QSortFilterProxyModel instance and call setSourceModel() to connect it to the source model.

  2. Create the view and assign the proxy model to it. Create a QTableModel and use setModel() to assign the proxy model to it.

  3. Enable sorting in the proxy view. Call setSortingEnabled() on the proxy view to allow sorting by clicking its column headers.

For comparison, this example places a source model view above the proxy model view. The source view has no sorting enabled. When you sort the proxy view, the source, the source view is not affected. Both views are editable and any change made through one is immediatelly reflected in the other.

Note that because the proxy keeps its sort order active, editing an cell value through the proxy view will cause the row to immediately jump to its new sorted position once the edit is confirmed.

26.5 Basic Filtering with Proxy Models

In addition to sorting, QSortFilterProxyModel supports filtering its source model with these methods:

Method Description
setFilterFixedString(pattern) Filter by exact string match
setFilterRegularExpression(pattern) Filter by regular expression pattern string
setFilterRegularExpression(regularExpression) Filter by a QRegularExpression object
setFilterWildcard(pattern) Filter by wildcard pattern string

You can fine-tune the search with these methods:

Method Description
setDynamicSortFilter(bool) Re-sort and re-filter automatically when source data changes
setFilterCaseSensitivity(sensitivity) Control case sensitivity of string-based filters
setFilterKeyColumn(column) Column to apply the filter to; -1 filters across all columns
setFilterRole(role) Data role used when evaluating each item against the filter
invalidateFilter() Force the proxy to re-evaluate the filter without changing filter parameters
An icon of a clipboard-list1

You are building a financial research tool where users need to locate economic indicators by searching through their descriptions.

The source model in this section is the same as in the previous section, except that the data has only two columns, ‘Indicator’ and ‘Description’.

To use a proxy model for filtering:

  1. Create the proxy model and set its source model. In the main window, create a CsvModel object, create a QSortFilterProxyModel and set the CsvModel as it source via setSourceModel().

  2. Set the proxy model options. Filtering will be applied to column 1 (‘Description’) via setFilterKeyColumn(), and will be case-insensitive via setFilterCaseSensitivity().

  3. Create a QTableView and assign the proxy model to it with setModel().

  4. Filter the source model based on the user input. Add a QLineEdit to the main window and connect its textChanged signal to a slot that calls setFilterRegularExpression() with the current text.

Because dynamicSortFilter is enabled by default, the filter is live - if you edit a row’s description so that it no longer matches the current filter text, the row disappears from the view immediately.

26.6 Custom Filtering in Proxy Models

In the previous section we saw that QSortFilterProxyModel provides built-in methods for filtering by string, wildcard, and regular expression. For more complex filters you can subclass it and reimplement filterAcceptsRow(). The method signature is:

1 def filterAcceptsRow(source_row: int, source_parent: QModelIndex) -> bool:

where source_row is the row number in the source model and source_parent is the parent index, which is always an invalud QModelIndex for flat table models and is only relevant for tree models.

An icon of a clipboard-list1

You are building an economic monitoring tool that highlights aggregate indicators outside a normal operating range, flagging both unusually low and unusually high values among aggregated data for analyst review.

The source model is the same as in the section 26.4, except the data has been updated:

 1 self.header = ['Indicator', 'Value (%)',
 2                'Aggregate', 'Include in report']
 3 self.csv_data = [
 4    ['GDP',       2.4,  1, True],
 5    ['CPI',       8.7,  1, True],
 6    ['Jobs',      0.3,  0, True],
 7    ['Confidence',74.0, 0, True],
 8    ['Industry',  91.5, 1, True],
 9    ['Retail',    3.1,  1, True],
10    ['Inflation', 0.1,  1, True],
11    ['PMI',       62.3, 0, True],
12    ['Trade',     88.0, 1, True],
13 ]

The goal show only rows where the ‘Aggregate’ 1, and ‘Value (%)’ falls outside the range (1.0, 50.0).

To apply a custom filter in a proxy model:

![](code/26_MV_programming_sorting_filtering_selection/06_custom proxy_model_filter/proxymodels.py)

  1. Subclass QSortFilterProxyModel. In addition to parent, its __init() takes low and high parameters, defining the acceptable range.

  2. Reimplement filterAcceptsRow(). For each row, retrieve ‘Value (%)’ from column 1 and ‘Aggregate’ from column 2 using the source model’s index() and data(). Return True only if aggregate equals 1 and value falls outside (self.low, self.high), otherwise return False.

![](code/26_MV_programming_sorting_filtering_selection/06_custom proxy_model_filter/main.py)

  1. Create the custom proxy model object and assign it to the view. In the main window, create the source model, then an OutlierProxyModel with a range (1.0, 50.0). Create a QTableView and assign the proxy model to it.

The table view shows only aggregate indicators with values outside the (1.0, 50.0) range: Industry, Inflation, and Trade.

Note that the filter range is fixed at construction time. To make it adjustable at runtime, you would expose a method that updates self.low and self.high and calls invalidateFilter() to trigger re-evaluation of all rows.

26.7 Selection Modes and Behaviors

Every view in the model/view architecture owns a QItemSelectionModel that holds the selection state and handles all selection operations. You rarely interact with it directly - instead, you configure how user actions translate into selection commands by setting the view’s selection mode and selection behavior. The selection mode controls how many items can be selected at once, while the selection behavior controls whether clicks select individual cells, entire rows, or entire columns.

An icon of a clipboard-list1

You are building an economic indicators dashboard and need to understand how selection modes and behaviors affect user interaction. Configure a table view to switch between different selection modes and behaviors at runtime to explore their differences.

To change a view’s selection mode or behavior:

  1. Create the model and the view, and set the initial selection behavior and mode. In this section we use a read-only CsvModel and a table view. Assign the model to the view and set the initial selection behavior to SelectItems, and the initial selection mode to SingleSelection. Available selecion behavior constants are:

    Constant Description
    SelectItems Clicks select individual cells
    SelectRows Clicks select entire rows
    SelectColumns Clicks select entire columns

    Available selection mode constants are:

    Constant Description
    NoSelection Nothing can be selected
    SingleSelection Only one item selected at a time; selecting another deselects the previous
    ContiguousSelection Only a single unbroken block of items can be selected
    MultiSelection Any combination of items can be selected; clicking toggles each one independently
    ExtendedSelection Like MultiSelection but requires Ctrl to toggle individual items and Shift to select a range

    Both enumerations are QAbstractItemView members and, by inheritance, also QTableView members.

  2. Add a combobox to switch between selection modes. Populate it with all five mode constants as item data. When the user picks a mode, retrieve it with itemData() and apply it with view.setSelectionMode().

  3. Add a combobox to switch between selection behaviors. Populate it with all three behavior constants as item data. When the user picks a behavior, retrieve it with itemData() and apply it with view.setSelectionBehavior()

This lets the user run the application and try different combinations of modes and behaviors to see how they interact.

26.8 Responding to Selection Changes

Qt Widgets views use QItemSelectionModel to keep track of their selected items. The selected items are stored as a list of ranges retrievable via its selection() method, which returns a QItemSelection object:

[TODO insert QItemSelectionModel-QItemSelection diagram]

You can respont to selection changes using QItemSelectionModel’s signals:

Signal Description
currentChanged(current, previous) Emitted when the current item changes
currentRowChanged(current, previous) Emitted when the current item changes to an item in a different row
currentColumnChanged(current, previous) Emitted when the current item changes to an item in a different column
selectionChanged(selected, deselected) Emitted when the selection changes, with the newly selected and deselected items as deltas
modelChanged(model) Emitted when the underlying model is replaced
An icon of a clipboard-list1

You are adding a detail panel to your indicators dashboard that shows additional information about whichever indicator the user clicks. Connect to the view’s selection model to read the currently selected row and update the panel accordingly.

To respond to selection changes:

  1. Create the model and the view and assign the model to the view. We use the same read-only CsvModel as in the previous section and a table view with SingleSelection mode and SelectRows behavior.

  2. Create the detail panel. Create a QGroupBox with a QFormLayout containing three QLabels, one each for indicator name, value, and aggregate flag, and add the group box to the main window.

  3. Connect the selection model’s selectionChange() signal to on_selection_change(). The signal passes two QItemSelection arguments to the slot, selected, containingthe items that just became selected, and deselected, containing the items that just became deselected (Both are deltas). In the slot, we call selected.indexes() to get the newly selected indexes and read the row data from the model to update the detail panel labels. When indexes is empty, reset the labels to -.

Note that using selected.indexes() works correctly here because the view is in SingleSelection mode, so selected always contains exacty the one row that was just picked. In multi-selection modes you would use view.selectionModel().selectedRows() instead, which returns the complete current selection.

When you click any row, the detail panel updates immediately with that indicator’s data. To enable clearing the view selection, call clearSelection() on the selection model programatically, for example from a button or a keyboard shortcut handler.

26.9 Sharing Selection Models Between Views

Sometimes you want multiple views to share the same selection state. For example, a compact navigation widget and a full detail view showing different aspects of the same data. In Qt, you can achieve this by attaching a view’s QItemSelectionModel to another view.

An icon of a clipboard-list1

You are building an indicator dashboard with a detail table and a quick-pick list showing indicator names. Share a single selection model between them so that selecting a row in the table highlights the corresponding item in the list and vice versa.

Every view creates its own QItemSelectionModel by default. To synchronize selection between two views, you simply replace one view’s selection model with the other’s using setSelectionModel().

To share a selection model between two views:

  1. Create the quick-pick view. Create a QListView, set the model on it, and configure it to display only the first column via setModelColumn(). Set the flow to LeftToRight` to lay the items out horizontally.

  2. Create the detail view and share the first view selection model. Create a QTableView, set the same model on it, then call setSelectionModel() passing the first view’s selection model. From this point both views share a single QItemSelectionModel instance.

Note that both views also use the same source model.

When you run the application, clicking an indicator name in the quick-pick view highlights the corresponding row in the detail table, and clicking a row in the detail table highlights the corresponding item in the quick-pick view.

26.10 Custom Selection Handling

QItemSelectionModel can be subclassed to enforce custom selection rules. All selection operations pass through its select() method and you need to override it to implement those rules.

An icon of a clipboard-list1

You are building an indicators review tool where only rows marked as aggregate may be selected for batch processing. Restrict selection so that clicking a non-aggregate row has no effect and allow the user to select all aggregate rows at once.

To implement custom selection handling:

  1. Create a QItemSelectionModel subclass and reimplement select(). In C++, select() has two overloads, one that takes a QModelIndex, and one that takes a QItemSelection. In Python both overloads map to the same method, so we use isinstance to check and handle each case separately. For QModelIndex, we check whether the row is aggregate and return early if not. For QItemSelection, we iterate over all ranges in the selection, collect the aggregate rows into a new QItemSelection object, and pass the filtered selection to the base class. THe helper method is_aggregate() retrieves the value in column 2 for a given row and returns True if it equals to 1.
  1. Replace the view’s default selection model with an AggregateSelectionModel object. The view is set to ExtendedSelection model so multiple aggregate rows can be selected at once.

  2. Let the user select all aggregate rows at once. Add a button and connect its clicked() signal to view.selectAll(). The view’s selection model is an AggregateSelectionModel so selectAll() selects only aggregate rows.

When you run the application, clicking on a non-aggregate row has no effect. Clicking aggregate rows selects them individually or in combination using Ctrl. Clicking the button selects all aggregate rows at once.