22. Model-View Programming with QAbstractListModel

This chapter introduces custom Qt models using QAbstractListModel through a series of simple demonstrations manipulating a bank’s client roster.

The Qt model/view architecture is Qt’s variant of the model-view-controller (MVC) design pattern where, per the documentation, the controller and view are combined. The model and view are decoupled, allowing the same model to be used with multiple views.

Qt provides several abstract classes you can subclass for custom models, such as:

  • QAbstractItemModel
  • QAbstractTableModel
  • QAbstractListModel

Qt also offers ready-to-use concrete models, including:

  • QStandardItemModel
  • QFileSystemModel
  • QSqlQueryModel

While QStandardItemModel requires you to copy your data into QStandardItem objects one by one, a custom QAbstractListModel subclass takes a different approach. It wraps data that already exists somewhere else and exposes it to Qt’s views without duplicating it into a parallel set of item objects. This distinction matters if your data set is large or already has its own representation you don’t want to mirror in the model.

22.1 Read-only List Model

Of the three abstract model classes above, QAbstractListModel is the easiest to subclass. It provides default implementations for several QAbstractItemModel methods (most notably columnCount(), which always returns 1), and offers a specialized interface for simple, non-hierarchical sequences of items (lists).

Like other model classes, a QAbstractListModel subclass acts as an intermediary between a data source and a view:

A data source can range from a simple Python list or structured text file to relational database data. The model’s role is to supply data in a format that a Qt view can read and display.

To create a read-only list model you must implement at least two methods:

  • rowCount(): Returns the number of rows in the model. (e.g., len(lst) for a Python list, line count for a file, or count(*) for a SQL query). The parent parameter is for hierarchical models and unused here.

  • data(index, role): Returns the data for the given role and item referenced by the index. For list models, index is a QModelIndex object with column() as zero and row() indicating the position in the underlying data. The role is one of the Qt.ItemDataRole enumeration values(default: DisplayRole).

An icon of a clipboard-list

You have a text file listing your bank’s client roster (‘data.txt’) and you need to display it in a list view.

To use a QAbstractListModel subclass in your application:

  1. Subclass QAbstractListModel and provide access to the data source. In the example we read the text file entirely in __init__() and store its lines in a Python list (self.txt_data), where each element is a single string (a client’s name and account type). For other types of data sources you can retrieve data dynamically instead.

  2. Implement rowCount(). View classes call this method to determine the model’s length. Here we return the data list length with len(self.txt_data).

  3. Implement data(). This method returns model data for a given index and data role. Use index.row() to access txt_data. Return data only for DisplayRole (the text for display) and return None otherwise.

    A view will always ask for data associated with a specific data role. Qt has several built-in roles and you can define your own:

    Constant Value Description
    DisplayRole 0 The key data to be rendered in the form of text. (QString)
    DecorationRole 1 The data to be rendered as a decoration in the form of an icon. (QColor, QIcon or QPixmap)
    EditRole 2 The data in a form suitable for editing in an editor. (QString)
    ToolTipRole 3 The data displayed in the item’s tooltip. (QString)
    StatusTipRole 4 The data displayed in the status bar. (QString)
    WhatsThisRole 5 The data displayed for the item in “What’s This?” mode. (QString)
    FontRole 6 The font used for items rendered with the default delegate. (QFont)
    TextAlignmentRole 7 The alignment of the text for items rendered with the default delegate. (Qt::Alignment)
    BackgroundRole 8 The background brush used for items rendered with the default delegate. (QBrush)
    ForegroundRole 9 The foreground brush (text color, typically) used for items rendered with the default delegate. (QBrush)
    CheckStateRole 10 This role is used to obtain the checked state of an item. (CheckState)
    AccessibleTextRole 11 The text to be used by accessibility extensions and plugins, such as screen readers. (QString)
    AccessibleDescriptionRole 12 A description of the item for accessibility purposes. (QString)
    SizeHintRole 13 The size hint for the item that will be supplied to views. (QSize)
    InitialSortOrderRole 14 This role is used to obtain the initial sort order of a header view section. (Qt::SortOrder)
    UserRole 0x0100 The first role that can be used for application-specific purposes.

    It might be tempting to skip the role check and simply return the txt_data element for all roles, but this will lead to invalid/zero-sized items. SizeHintRole expects a QSize value. Sending a text value to the view for this role will break the view’s display.

  4. Optionally, implement headerData() for row or column headers. Here, the single column header is ‘Clients’. QListView does not display headers but QTableView would.

The QModelIndex class helps views locate model items. All Qt models are table-based: you can think of the model in this example as a one-column table with n rows. You can use row() and column() to reference this table’s cells, and in this chapter column() will always be zero, since a list model has only one column.

In the main window code we import TxtFileModel and create an instance of it, create a QListView() instance and assign the model to it.

QAbstractItemModelTester aids model development. If you pass your model instance to its constructor it logs implementation errors to the console, helping catch issues early.

This demo is read-only. Next sections cover editable and resizable models.

22.2 Editable List Model

Creating a basic list model requires implementing at least rowCount() and data(). To make it editable, you need to implement two more:

  • setData(index, value, role): Sets the data for the given role (typically EditRole) and index, and returns True if successful; otherwise returns False. If set successfully, emit the dataChanged() signal.
  • flags(index): Returns the item flags for the index. The base implementation enables and selects items. To allow editing, add Qt.ItemFlags.ItemIsEditable.
An icon of a clipboard-list

You have implemented a read-only model of your bank’s client roster, backed by a text file (‘data.txt’) and you need to make it editable.

To create an editable list model:

  1. Create a QAbstractListModel subclass and provide access to the data source.

  2. Implement the rowCount() and data() methods as you did for the read-only model.

  3. Implement the setData() method. setData() accepts three arguments: index, value and role. In the method we check if the role is equal to Qt.ItemDataRole.EditRole and, if it is, we set the model data for the index.row() to value using self.txt_data[index.row()] = value. If the data is set successfully we emit the dataChanged signal and the method returns True. Otherwise it returns False. Take care that all code paths in this method return a boolean value.

  4. Implement the flags() method. In this method we signal to views that the model data is editable by adding Qt.ItemFlags.ItemIsEditable to the flags. Note that we can make the model items editable selectively by using the index parameter. In the example we ignore index which means that all the model items are editable.

    The Qt.ItemFlags enum has these values:

    Constant Value Description
    Qt::NoItemFlags 0 It does not have any properties set.
    Qt::ItemIsSelectable 1 It can be selected.
    Qt::ItemIsEditable 2 It can be edited.
    Qt::ItemIsDragEnabled 4 It can be dragged.
    Qt::ItemIsDropEnabled 8 It can be used as a drop target.
    Qt::ItemIsUserCheckable 16 It can be checked or unchecked by the user.
    Qt::ItemIsEnabled 32 The user can interact with the item.
    Qt::ItemIsAutoTristate 64 The item’s state depends on the state of its children.
    Qt::ItemNeverHasChildren 128 The item never has child items.
    Qt::ItemIsUserTristate 256 The user can cycle through three separate states.

    Note that in our flags() implementation we return NoItemFlags for an invalid index (return Qt.ItemFlags()). If we didn’t, the model tester would print a warning:

    qt.modeltest: FAIL! flags == Qt::ItemIsDropEnabled || flags == 0 () returned FALSE (C:\Users\qt\work\qt\qtbase\src\testlib\qabstractitemmodeltester.cpp:377)

    The model tester calls flags() with an invalid index and expects the result to be either 0/NoItemFlags or exactly the drop-enabled flag. If you always add ItemIsEditable (even for that invalid index) the test fails.

    An invalid index does not point to a real item, so it can not be edited. The tester is simply checking that the model does not claim that something that does not exist can be edited.

The main window code is the same as in the previous section, except that we add a method to handle the model’s dataChanged signals to show that they are indeed emitted in setData().

Now if you double-click any of the list view lines you are able to edit it and the changes are saved in the model (ie. to the TxtFileModel.txt_data list and signaled by the dataChanged signal. You can also implement the logic to update the text file from the txt_data values which we omit in this example.

22.3 Data-Widget Mapping

The QDataWidgetMapper class lets you map a data model row (or column) to a set of widgets, making them data-aware. When the mapper’s current index changes, mapped widgets are automatically updated with data from the model. This is useful for creating forms that enhance user experience when viewing and editing data.

An icon of a clipboard-list

You have an editable model of your bank’s client roster. Editing is enabled by double-clicking a client row in the list view, but your staff wants to edit a client’s details through a dedicated form field instead of double-clicking into the list.

  1. Create a QAbstractListModel subclass to represent your model. Then, in the main window class:
  1. Create your model object and the widgets for displaying and editing your model data.

  2. Create the mapper object and use QDataWidgetMapper.setModel() to connect your model to it.

  3. Use QDataWidgetMapper.addMapping() to map your model’s columns with the widgets. The example model has only one column, which we map to a QLineEdit widget.

  4. Synchronize the view’s current item with the mapper’s current index so both update when the user changes the view’s selection. Connect the view’s selection model currentChanged signal directly to the mapper’s setCurrentModelIndex() slot.

The mapper updates its widgets automatically whenever its own current index changes. It does not watch the view’s selection, so you must tell it which row to display.

In the example, we use manual submit policy, updating the model via a ‘Submit’ button. This keeps the line edit in sync with the current view item, allowing easy updates by editing the line edit value and clicking ‘Submit’.

22.4 Resizable List Model

For a basic QAbstractListModel subclass, you need to implement at least two methods: rowCount() and data(). To make the model editable, you need to implement two more: setData() and flags(). To be able to add or remove rows, you also need to implement insertRows() and removeRows().

An icon of a clipboard-list

You have an editable model of your bank’s client roster. You need to let staff add new clients to the roster or remove existing ones.

To make a resizable QAbstractListModel subclass:

  1. Create a subclass of the QAbstractListModel class. As in previous examples, read the data from a text file and store it in a Python list named self.txt_data.

  2. Implement the insertRows() method. For simplicity, the example inserts rows filled with template text (“<insert row data>”). Guard the data insertion with beginInsertRows() (to signal connected views that rows are about to be inserted) and endInsertRows(). This pair of methods ensures that views remain in a valid state. beginInsertRows() takes three arguments: parent (an invalid QModelIndex() in our case), first (the starting row number post-insertion) and last (the ending row number post-insertion). insertRows() returns True on success or False otherwise.

  3. Implement the removeRows() method. This removes rows from the self.txt_data, enclosed by beginRemoveRows() and endRemoveRows(), and returns True on success or False otherwise.

Then, in your main class:

  1. Create three QPushButtons: insert_button, append_button and remove_button. Handle the insert button’s clicked() signal with a slot named on_insert() calling insertRow() to insert a single row by invoking insertRows(). Similarly, handle the append button’s clicked() signal with on_append() to insert a row at the end.

Throughout this chapter, TxtFileModel went from a two-method read-only stub to a model that a user can edit through the view or a mapped form field, and resize with insert and remove buttons, all without ever reimplementing the parent() method. Every item in a QAbstractListModel is conceptually a child of the model’s invisible root index (an invalid QModelIndex()):