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.
![]() |
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:
Create the model. For this exercise, we subclass
QAbstractTableModeland implementrowCount(),columnCount(),data(),setData(),flags()andheaderData()to create an editable table model. The backing data is a two-dimensional Python list assigned to theself.csv_datainstance field.-
Reimplement
sort(). The method signature is:1 sort(column: int, order: Qt.SortOrder=Qt.SortOrder.AscendingOrder) -> None
where
columnis the model column to sort by, andorderis aQt.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_datain place using Python’s listsort(), passing it a lambda that extractsrow[column]as the sort key and settingreversebased on the order argument. - Call
changePersistentIndexList()to remap any liveQPersistentModelIndexobjecs 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.
- Create the view, set its model, and enable sorting. In the main window, create a
CsvModelobject and aQTableView. Assign the model to the view and enable the sorting viaview.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.
![]() |
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:
Create the model. We reuse the model from the previous section with one addition: a
self.row_indiceslist is used to store the current sort order, leavingself.csv_datauntouched.Update
data()to read throughrow_indices. Instead of accessingcsv_data[index.row()]directly, we first look up the mapped row withrow_indices[index.row()]and then use it to access the data.Reimplement
sort(). Instead of sortingcsv_datadirectly, sortrow_indicesinstead.

26.3 Automatic Sorting on Data Changes
At times, you want the data to stay sorted automatically whenever it is modified or deleted.
![]() |
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:
Implement
sort(). The method is identical to section 26.1: emitlayoutAboutToBeChanged(), sort the data, remap the persistent index list, and emitlayoutChanged().Call
sort()fromsetData()whenever the indicator name changes. Since all data modifications go throughsetData(), we check whether the edited column is column 0 and callsort()if so, right before emittingdataChaged().
- Perform the initial sort. The backing data in
csv_datastart in insertion order, so we callsort()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 |
![]() |
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:
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 aQSortFilterProxyModelinstance and callsetSourceModel()to connect it to the source model.Create the view and assign the proxy model to it. Create a
QTableModeland usesetModel()to assign the proxy model to it.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 |
![]() |
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:
Create the proxy model and set its source model. In the main window, create a
CsvModelobject, create aQSortFilterProxyModeland set theCsvModelas it source viasetSourceModel().Set the proxy model options. Filtering will be applied to column 1 (‘Description’) via
setFilterKeyColumn(), and will be case-insensitive viasetFilterCaseSensitivity().Create a
QTableViewand assign the proxy model to it withsetModel().Filter the source model based on the user input. Add a
QLineEditto the main window and connect itstextChangedsignal to a slot that callssetFilterRegularExpression()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.
![]() |
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:

Subclass
QSortFilterProxyModel. In addition toparent, its__init()takeslowandhighparameters, defining the acceptable range.Reimplement
filterAcceptsRow(). For each row, retrieve ‘Value (%)’ from column 1 and ‘Aggregate’ from column 2 using the source model’sindex()anddata(). ReturnTrueonly ifaggregateequals1andvaluefalls outside(self.low, self.high), otherwise returnFalse.

- Create the custom proxy model object and assign it to the view. In the main window, create the source model, then an
OutlierProxyModelwith a range(1.0, 50.0). Create aQTableViewand 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.
![]() |
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:
-
Create the model and the view, and set the initial selection behavior and mode. In this section we use a read-only
CsvModeland a table view. Assign the model to the view and set the initial selection behavior toSelectItems, and the initial selection mode toSingleSelection. Available selecion behavior constants are:Constant Description SelectItemsClicks select individual cells SelectRowsClicks select entire rows SelectColumnsClicks select entire columns Available selection mode constants are:
Constant Description NoSelectionNothing can be selected SingleSelectionOnly one item selected at a time; selecting another deselects the previous ContiguousSelectionOnly a single unbroken block of items can be selected MultiSelectionAny combination of items can be selected; clicking toggles each one independently ExtendedSelectionLike MultiSelectionbut requires Ctrl to toggle individual items and Shift to select a rangeBoth enumerations are
QAbstractItemViewmembers and, by inheritance, alsoQTableViewmembers. 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 withview.setSelectionMode().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 withview.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 |
![]() |
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:
Create the model and the view and assign the model to the view. We use the same read-only
CsvModelas in the previous section and a table view withSingleSelectionmode andSelectRowsbehavior.Create the detail panel. Create a
QGroupBoxwith aQFormLayoutcontaining threeQLabels, one each for indicator name, value, and aggregate flag, and add the group box to the main window.Connect the selection model’s
selectionChange()signal toon_selection_change(). The signal passes twoQItemSelectionarguments to the slot,selected, containingthe items that just became selected, anddeselected, containing the items that just became deselected (Both are deltas). In the slot, we callselected.indexes()to get the newly selected indexes and read the row data from the model to update the detail panel labels. Whenindexesis 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.
![]() |
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:
Create the quick-pick view. Create a
QListView, set the model on it, and configure it to display only the first column viasetModelColumn(). Set the flow toLeftToRight` to lay the items out horizontally.Create the detail view and share the first view selection model. Create a
QTableView, set the same model on it, then callsetSelectionModel()passing the first view’s selection model. From this point both views share a singleQItemSelectionModelinstance.
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.
![]() |
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:
- Create a
QItemSelectionModelsubclass and reimplementselect(). In C++,select()has two overloads, one that takes aQModelIndex, and one that takes aQItemSelection. In Python both overloads map to the same method, so we useisinstanceto check and handle each case separately. ForQModelIndex, we check whether the row is aggregate and return early if not. ForQItemSelection, we iterate over all ranges in the selection, collect the aggregate rows into a newQItemSelectionobject, and pass the filtered selection to the base class. THe helper methodis_aggregate()retrieves the value in column 2 for a given row and returnsTrueif it equals to1.
Replace the view’s default selection model with an
AggregateSelectionModelobject. The view is set toExtendedSelectionmodel so multiple aggregate rows can be selected at once.Let the user select all aggregate rows at once. Add a button and connect its
clicked()signal toview.selectAll(). The view’s selection model is anAggregateSelectionModelsoselectAll()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.

