22. Model-View Programming with QAbstractListModel

This chapter introduces custom Qt models using QAbstractListModel through a series of simple demonstrations manipulating a list of company clients.

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

Earlier examples demonstrated some of these with Qt view classes. Here, we focus on creating custom models.

22.1 Read-only List Model

[TODO: QAbstractListModel inheritance tree]

Of the three abstract model classes above, QAbstractListModel is the easiest to subclass. It provides default implementations for several QAbstractItemModel methods 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:

[TODO: DATA-MODEL-VIEW DIAGRAM]

A data source can range from a simple Python list or structured text file, relational database data, and anything in between. 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 model 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-list1

You have a text file listing company clients (‘data.txt’) and you need to display them in a list view.

To achieve this:

 1 from PySide6.QtCore import QAbstractListModel, Qt
 2 
 3 # 1. Create a QAbstractListModel subclass.
 4 #    The data is read from a text file
 5 #    and stored in a Python list.
 6 
 7 class TxtFileModel(QAbstractListModel):
 8     
 9     def __init__(self, source, parent=None):
10         
11         super().__init__(parent)
12 
13         self.txt_data = []
14         self.header = 'Clients'
15         with open(source) as txt_file:
16             for line in txt_file:
17                 self.txt_data.append(line.strip())
18     
19     # 2. Implement the rowCount() method
20     
21     def rowCount(self, parent) -> int:
22         return len(self.txt_data)
23     
24     # 3. Implement the data() method
25     
26     def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> object|None:
27         if role == Qt.ItemDataRole.DisplayRole:
28             return self.txt_data[index.row()]
29         return None
30     
31     # 4. Optionally, implement the headerData() method
32     #    QListView does not have a header
33     #    so this is never executed!
34 
35     def headerData(self, section, orientation, role) -> object | None:
36         if orientation == Qt.Orientation.Horizontal:
37             if role == Qt.ItemDataRole.DisplayRole:
38                 return self.header
 1 import sys
 2 from PySide6.QtWidgets import (QApplication,
 3     QWidget, QListView, QVBoxLayout)
 4 from PySide6.QtTest import QAbstractItemModelTester
 5 from txtfilemodel import TxtFileModel
 6 
 7 
 8 class Window(QWidget):
 9     
10     def __init__(self):
11 
12         super().__init__()
13         layout = QVBoxLayout()
14         self.setLayout(layout)
15 
16         model = TxtFileModel('data.txt')
17         QAbstractItemModelTester(model)
18         view = QListView()
19         view.setModel(model)
20         layout.addWidget(view)
21 
22 
23 if __name__ == '__main__':
24 
25     app = QApplication(sys.argv)
26     main_window = Window()
27     main_window.show()
28     sys.exit(app.exec())
  1. Subclass QAbstractListModel and provide access to the data source. Here, read the text file entirely in __init__() and store lines in a Python list (txt_data), where each element is a single string (a client’s name and profession). 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, return len(self.txt_data). Qt list models default to one column.

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

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

QModelIndex:

This class helps views locate model items. Qt models are table-based - think of the model in this example as a one-column table with n rows. Use row() and column() to reference cells.

QAbstractItemModelTester

This class aids model development. 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

As shown in the read-only list model example, creating a basic list model requires implementing at least rowCount() and data(). To make it editable, add two more methods:

  • 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-list1

You have implemented a read-only model of a company clients backed by a text file (‘data.txt’) and you need to make it editable.

To create an editable list model:

 1 from PySide6.QtCore import QAbstractListModel, Qt
 2 
 3 # 1. Create a QAbstractListModel subclass
 4 #    and make the data available to it.
 5 
 6 class TxtFileModel(QAbstractListModel):
 7     
 8     def __init__(self, source, parent=None):
 9         
10         super().__init__(parent)
11 
12         self.txt_data = []
13         self.header = 'Clients'
14         with open(source) as txt_file:
15             for line in txt_file:
16                 self.txt_data.append(line.strip())
17     
18     # 2. Implement the rowCount() and data() methods
19     
20     def rowCount(self, parent) -> int:
21         return len(self.txt_data)
22     
23     def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> object|None:
24         if role in [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole]:
25             return self.txt_data[index.row()]
26         return None
27     
28     # 3. Implement the setData() method
29     
30     def setData(self, index, value, role) -> bool:
31         if not index.isValid():
32             return False
33         if role == Qt.ItemDataRole.EditRole:
34             if self.txt_data[index.row()] != value:
35                 self.txt_data[index.row()] = value
36                 self.dataChanged.emit(index, index, [role])
37                 return True
38             return False
39         return False
40     
41     # 4. Implement the flags() method
42     
43     def flags(self, index) -> Qt.ItemFlags:
44         return super().flags(index) | Qt.ItemFlags.ItemIsEditable
45 
46     def headerData(self, section, orientation, role) -> object | None:
47         if orientation == Qt.Orientation.Horizontal:
48             if role == Qt.ItemDataRole.DisplayRole:
49                 return self.header
  1. Create a QAbstractListModel subclass and the access to the data source.

  2. Implement the rowCount() and data() methods just 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.

  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.

 1 import sys
 2 from PySide6.QtWidgets import (QApplication,
 3     QWidget, QListView, QVBoxLayout)
 4 from PySide6.QtTest import QAbstractItemModelTester
 5 from txtfilemodel import TxtFileModel
 6 
 7 
 8 class Window(QWidget):
 9     
10     def __init__(self, parent=None):
11 
12         super().__init__(parent)
13         layout = QVBoxLayout()
14         self.setLayout(layout)
15 
16         model = TxtFileModel('data.txt')
17         QAbstractItemModelTester(model)
18         view = QListView()
19         view.setModel(model)
20         layout.addWidget(view)
21         
22         model.dataChanged.connect(self.on_data_changed)
23     
24     # Handle the dataChanged signals
25     
26     def on_data_changed(self, topLeft, bottomRight, roles):
27         print('Model changed, row:', topLeft.row())
28 
29 
30 if __name__ == '__main__':
31 
32     app = QApplication(sys.argv)
33     main_window = Window()
34     main_window.show()
35     sys.exit(app.exec())

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 Editable List Model with 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 model’s current index changes, mapped widgets are updated with data from the model. This is useful for creating forms enhancing user experience in viewing and editing data.

An icon of a clipboard-list1

You have an editable model of a list of company clients. Editing is enabled by double-clicking a client row in the list view. Your task is to make editing more user-friendly.

 1 import csv
 2 from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt
 3 
 4 # 1. Create the model class
 5 
 6 class TxtFileModel(QAbstractListModel):
 7     
 8     def __init__(self, source, parent=None):
 9         
10         super().__init__(parent)
11         
12         self.txt_data = []
13         self.header = 'Clients'
14         with open(source) as txt_file:
15             for line in txt_file:
16                 self.txt_data.append(line.strip())
17                 
18     def rowCount(self, parent=QModelIndex()) -> int:
19         return len(self.txt_data)
20     
21     def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> object|None:
22         if role == Qt.ItemDataRole.DisplayRole \
23         or role == Qt.ItemDataRole.EditRole:
24             return self.txt_data[index.row()]
25 
26     def setData(self, index, value, role) -> bool:
27         if not index.isValid():
28             return False
29         if role == Qt.ItemDataRole.EditRole:
30             if self.txt_data[index.row()] != value:
31                 self.txt_data[index.row()] = value
32                 self.dataChanged.emit(index, index, [role])
33                 return True
34             return False
35         return False
36     
37     def flags(self, index) -> Qt.ItemFlags:
38         return super().flags(index) | Qt.ItemFlags.ItemIsEditable
39 
40     def headerData(self, section, orientation, role) -> object|None:
41         if orientation == Qt.Orientation.Horizontal:
42             if role == Qt.ItemDataRole.DisplayRole:
43                 return self.header
  1. Create a QAbstractListModel subclass to represent your model. Then, in the main window class:
 1 import sys
 2 from PySide6.QtWidgets import (QApplication,
 3     QWidget, QListView, QLineEdit, QPushButton,
 4     QHBoxLayout, QVBoxLayout, QDataWidgetMapper)
 5 from PySide6.QtTest import QAbstractItemModelTester
 6 from txtfilemodel import TxtFileModel
 7 
 8 
 9 class Window(QWidget):
10     
11     def __init__(self, parent=None):
12 
13         super().__init__(parent)
14 
15         layout = QVBoxLayout()
16         self.setLayout(layout)
17 
18         self.model = TxtFileModel('data.txt')
19         QAbstractItemModelTester(self.model)
20         
21         self.view = QListView()
22         self.view.setModel(self.model)
23         self.view.selectionModel().currentChanged.connect(
24             self.sync_model_with_mapper)
25         
26         # 2. Create the widgets for displaying
27         #    and editing the data
28         
29         self.lineedit = QLineEdit()
30         self.lineedit.returnPressed.connect(self.submit_new_value)
31         self.view.activated.connect(self.lineedit.setFocus)
32         
33         self.submit_button = QPushButton('Submit')
34         self.submit_button.clicked.connect(self.submit_new_value)
35         
36         # 3. Create the mapper object and set its model
37         
38         self.mapper = QDataWidgetMapper()
39         self.mapper.setModel(self.model)
40         
41         # 4. Add the mappings
42 
43         self.mapper.addMapping(self.lineedit, 0)
44         
45         self.mapper.setSubmitPolicy(
46             QDataWidgetMapper.SubmitPolicy.ManualSubmit)
47         self.mapper.toFirst()
48         
49         horizontal_layout = QHBoxLayout()
50         horizontal_layout.addWidget(self.lineedit)
51         horizontal_layout.addWidget(self.submit_button)
52         
53         layout.addWidget(self.view)
54         layout.addLayout(horizontal_layout)
55     
56     # 5. Synchronize the model with the mapper
57     
58     def sync_model_with_mapper(self, current, previous):
59         self.mapper.setCurrentIndex(current.row())
60         
61     def submit_new_value(self):
62         self.mapper.submit()
63         self.view.setFocus()
64 
65 
66 if __name__ == '__main__':
67 
68     app = QApplication(sys.argv)
69     main_window = Window()
70     main_window.show()
71     sys.exit(app.exec())
  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 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 model’s current index so both update when the user changes the view’s selection.

In the example, we use manual submit policy, updating the model via a ’Submit` button. This syncs the line edit 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 need to implement insertRows() and removeRows().

An icon of a clipboard-list1

You have an editable model of a list of company clients. You need to enable the user to insert or remove clients from it.

To make a resizable QAbstractListModel subclass:

 1 import csv
 2 from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt
 3 
 4 # 1. Create a QAbstractListModel subclass
 5 
 6 class TxtFileModel(QAbstractListModel):
 7     
 8     def __init__(self, source, parent=None):
 9         
10         super().__init__(parent)
11         
12         self.txt_data = []
13         self.header = 'Clients'
14         with open(source) as txt_file:
15             for line in txt_file:
16                 self.txt_data.append(line.strip())
17                 
18     def rowCount(self, parent=QModelIndex()) -> int:
19         return len(self.txt_data)
20     
21     def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> object|None:
22         if role == Qt.ItemDataRole.DisplayRole \
23         or role == Qt.ItemDataRole.EditRole:
24             return self.txt_data[index.row()]
25     
26     def setData(self, index, value, role) -> bool:
27         if not index.isValid():
28             return False
29         if role == Qt.ItemDataRole.EditRole:
30             if self.txt_data[index.row()] != value:
31                 self.txt_data[index.row()] = value
32                 self.dataChanged.emit(index, index, [role])
33                 return True
34             return False
35         return False
36     
37     def flags(self, index) -> bool:
38         return super().flags(index) | Qt.ItemFlags.ItemIsEditable
39     
40     # 2. Implement the insertRows() method
41     
42     def insertRows(self, row, count, parent=QModelIndex()):
43         if 0 <= row <= self.rowCount():
44             self.beginInsertRows(parent, row, row )
45             self.txt_data.insert(row, '<insert row data>')
46             self.endInsertRows()
47             return True
48         return False
49     
50     # 3. Implement the removeRows() method
51     
52     def removeRows(self, row, count, parent=QModelIndex()):
53         if 0 <= row < len(self.txt_data):
54             self.beginRemoveRows(parent, row, row)
55             self.txt_data[row:row + 1] = []
56             self.endRemoveRows()
57             return True
58         return False
59 
60     # QListView does not have a header
61     # so this is never executed!
62 
63     def headerData(self, section, orientation, role):
64         if orientation == Qt.Orientation.Horizontal:
65             if role == Qt.ItemDataRole.DisplayRole:
66                 return self.header
  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). The method 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 import sys
  2 from PySide6.QtWidgets import (QApplication,
  3     QWidget, QListView, QLineEdit, QPushButton,
  4     QHBoxLayout, QVBoxLayout, QDataWidgetMapper)
  5 from PySide6.QtTest import QAbstractItemModelTester
  6 from txtfilemodel import TxtFileModel
  7 
  8 
  9 class Window(QWidget):
 10     
 11     def __init__(self):
 12 
 13         super().__init__()
 14 
 15         layout = QVBoxLayout()
 16         self.setLayout(layout)
 17 
 18         self.model = TxtFileModel('data.txt')
 19         QAbstractItemModelTester(self.model)
 20         self.model.rowsInserted.connect(self.on_rows_inserted)
 21         
 22         self.view = QListView()
 23         self.view.setModel(self.model)
 24         self.view.selectionModel().currentChanged.connect(
 25             self.on_current_changed)
 26         
 27         self.lineedit = QLineEdit()
 28         self.lineedit.returnPressed.connect(self.submit_new_value)
 29         self.view.activated.connect(self.lineedit.setFocus)
 30 
 31         self.mapper = QDataWidgetMapper()
 32         self.mapper.setModel(self.model)
 33         self.mapper.setSubmitPolicy(
 34             QDataWidgetMapper.SubmitPolicy.ManualSubmit)
 35         self.mapper.addMapping(self.lineedit, 0)
 36         self.mapper.toFirst()
 37         
 38         self.submit_button = QPushButton('Submit')
 39         self.submit_button.clicked.connect(self.submit_new_value)
 40         
 41         self.insert_button = QPushButton('Insert new')
 42         self.insert_button.clicked.connect(self.on_insert)
 43         
 44         self.append_button = QPushButton('Append new')
 45         self.append_button.clicked.connect(self.on_append)
 46         
 47         self.remove_button = QPushButton('Remove current')
 48         self.remove_button.clicked.connect(self.on_remove)
 49 
 50         input_layout = QVBoxLayout()
 51         input_layout.addWidget(self.lineedit)
 52         input_layout.addWidget(self.submit_button)
 53 
 54         buttons_layout = QVBoxLayout()
 55         buttons_layout.addWidget(self.insert_button)
 56         buttons_layout.addWidget(self.append_button)
 57         buttons_layout.addWidget(self.remove_button)
 58 
 59         controls_layout = QHBoxLayout()
 60         controls_layout.addLayout(input_layout)
 61         controls_layout.addLayout(buttons_layout)
 62 
 63         layout.addWidget(self.view)
 64         layout.addLayout(controls_layout)
 65         
 66     def on_current_changed(self, current, previous):
 67         self.mapper.setCurrentIndex(current.row())
 68         
 69     def submit_new_value(self):
 70         self.mapper.submit()
 71         self.view.setFocus()
 72         
 73     def on_insert(self):
 74         row = self.view.selectionModel().currentIndex().row()
 75         if self.model.insertRows(row, 1):
 76             self.mapper.setCurrentIndex(row)
 77             self.lineedit.setFocus()
 78         
 79     def on_append(self):
 80         row = self.model.rowCount()
 81         self.model.insertRows(row, 1)
 82         index = self.model.index(row, 0)
 83         self.view.scrollTo(index)
 84         self.lineedit.setFocus()
 85     
 86     def on_remove(self):
 87         index = self.view.currentIndex()
 88         self.model.removeRows(index.row(), 1)
 89     
 90     def on_rows_inserted(self, parent, first, last):
 91         index = self.model.index(first, 0)
 92         if index.isValid():
 93             self.view.setCurrentIndex(index)
 94             self.lineedit.setFocus()
 95 
 96 
 97 if __name__ == '__main__':
 98 
 99     app = QApplication(sys.argv)
100     main_window = Window()
101     main_window.show()
102     sys.exit(app.exec())
  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.