23. Model-View Programming with QAbstractTableModel

QAbstractTableModel enables you to display and edit tabular data in Qt applications.

List models organize data as a one-dimensional sequence of items, where each row represents a single, atomic element. In contrast, table models structure data in a two-dimensional grid, where each row represents a composite record or entity, and the columns within it hold individual fields or attributes.

In the previous chapter we used a simple text file as the data source: each file row was a single element representing a client from a bank’s client roster. In this chapter we use a comma-separated (CSV) file where each row has four fields: date, payee, category, and amount, together representing a single transaction. The sections follow the same progression as in the previous chapter:

  • read-only model
  • editable model
  • data mapping
  • resizable model

but focusing on columnar aspects.

23.1 Basic Read-Only Table Model

An icon of a clipboard-list

You need to show a user’s recent transactions from a CSV file (‘data.csv’) in a QTableView. Each row contains a transaction’s date, payee, category, and amount. You decide to implement a QAbstractTableModel subclass using the CSV file as its data source.

To create a read-only table model:

  1. Create a subclass of QAbstractTableModel named CsvModel and make external data available to it. We read the data from data.csv using Python’s csv reader and store it in a member variable named self.csv_data. csv_data is a list and each row returned by the csv reader is also a list, which effectively makes csv_data a two-dimensional list suitable for use with QAbstractTableModel subclasses. CsvModel treats every field, including the amount column, as plain text - Chapter 25 shows how returning a properly-typed value from data(Qt.ItemDataRole.EditRole) lets Qt automatically select the right editor for it.

  2. Implement the rowCount() and columnCount() methods. rowCount() is the length of the self.csv_data list and never changes since the model is read-only. columnCount() is hard-coded to return 4, the number of columns in the CSV file.

  3. Implement the data() method. data() expects two arguments: index, a QModelIndex instance, and role, a member of the DisplayRole enumeration. In the method body we check if role is equal to DisplayRole and, if it is, we return the data that the index points to. Note that we need to use both index.row() and index.column(), since the data has two dimensions.

  1. In your main class (Window), create a CsvModel object, create a QTableView object, and set its model using QTableView.setModel().

QAbstractTableModel subclasses are commonly used with QTableView but not necessarily so. QTableView is able to display table headers using the header data you provide to it in the model’s headerData() method, but implementing headerData() is still not mandatory.

Just as list models address items with row() alone, table models address every cell with the pair row() and column() - the two-dimensional counterpart of the one-column shape from the previous chapter:

23.2 Making the Table Model Editable

Just as for a list model, to make a table model editable you need to implement its setData() method and modify its flags() method to return the ItemIsEditable flag for each item.

An icon of a clipboard-list

You need to make your CSV file-backed transaction log editable, so users can correct a mis-categorized transaction or fix a typo in a payee name.

To make a table model editable:

  1. Create a QAbstractTableModel subclass.

  2. Implement the rowCount(), columnCount(), and data() methods. The implementation is the same as in the read-only model example.

  3. Implement the setData() method. setData() accepts three arguments:

    • index, a QModelIndex object that you will use to get the coordinates of the data point to be changed,
    • value, which is the new data value, and
    • role, one of the Qt.ItemDataRole enumeration members, in most cases EditRole.

    In the method body check if role is equal to EditRole, check if the new data is actually different from the current data, change the data, and emit the dataChanged() signal.

  4. Implement the flags() method. In this method you return the item flags given its index. In the example all items (i.e. fields) are flagged as selectable, enabled, and editable. This does not have to be the case - for instance, if you wanted to protect the transaction date from casual edits, you would flag that column as selectable only, leaving the rest editable.

  1. Then, in the main class, create a model instance, create a QTableView instance, and use QTableView.setModel() to connect the two.

23.3 Using Data Widget Mapper with Table Models

We have already seen how to use a data-widget mapper in the previous chapter, where we mapped a single line edit to a list model row. Here, we expand the example to map several widgets to a table model’s cells.

An icon of a clipboard-list

You need to add a form to the GUI so users can review and correct a transaction’s payee, category, and amount without editing table cells directly.

To use a data-widget mapper with a table model:

  1. Create the model class. It is the same editable table model as in the previous section.

  2. Create the widgets. Each mapped column in our table model (payee, category, and amount) contains string data, so we create three line edits, one for each column.

  3. Create a data-widget mapper object and add the widgets to it. Create a QDataWidgetMapper object and set the CsvModel as its model. Map each line edit to a CsvModel column using addMapping(), and synchronize the data in the mapper’s widgets with the table view’s currently selected row.

Now, when the user changes the data in the data-widget mapper’s widgets and presses the Submit button, the model is updated and the changes are propagated to the view.

23.4 Resizable Table Model

Just as with a list model, to make a table model resizable you need to implement two methods:

  • insertRows()
  • removeRows()
An icon of a clipboard-list

You need to let users record new transactions or delete existing ones from the transaction log.

To create a resizable table model:

  1. Create the model. Create a subclass of QAbstractTableModel and store the CSV data in the self.csv_data instance field. You already implemented rowCount(), columnCount(), and data() for your read-only model, and setData() and flags() to make it editable. To make the model resizable:
    • Implement insertRows(): check if row is within acceptable range, and insert an empty row in self.csv_data, guarding the insertion with beginInsertRows() and endInsertRows() calls. Return True on success and False otherwise.
    • Implement removeRows(). The line self.csv_data[row:row + 1] = [] removes the single element at index row from the list by assigning an empty list to that one-element slice, also guarded with beginRemoveRows() and endRemoveRows(). Return True on success and False otherwise.

Then, in the main window:

  1. Create the model and view objects. Create a CsvModel object, initializing it with the file that contains the transaction data. Create a QTableView object and set the CSV model as its model.

  2. Add the Insert, Append, and Remove buttons:

    • When the Insert button is pressed, get the view’s current index row and call insertRows() with it to insert an empty row above the current row.
    • When the Append button is pressed, get the next available row number and call insertRows() with it to append an empty row to the model.
    • When the Remove button is pressed, get the current view index and call removeRows() with its row to remove the current row from the model.

    Note that both insertRows() and removeRows() here assume that only one row can be inserted or removed at a time. To allow for multiple rows insertion and deletion, you need to adjust the range passed to beginInsertRows() and beginRemoveRows() and update the insert and remove logic.

    Also note that insertRows() does not let you pass initial data to it, so we just insert empty strings as the data. If you need to insert some initial data, use setData() after the insertion completes, or extend the model with a custom method (e.g. insertRowsWithData()) that accepts data parameters and calls the standard insertRows() internally.

    Lastly, the documentation mentions that you can provide your own API for altering or removing the data, as long as you call beginInsertRows() or beginRemoveRows() to notify other components that the model has changed.

Similar to the previous chapter, CsvModel never reimplements parent(). Every row in a QAbstractTableModel is conceptually a child of the model’s invisible root index (an invalid QModelIndex()), exactly as it was for QAbstractListModel in chapter 22:

QAbstractTableModel can technically supply data to a QListView too. The view defaults to displaying column 0 of whatever model it’s given (setModelColumn() lets you point it at a different one), so nothing stops you from subclassing QAbstractTableModel, hardcoding columnCount() to return 1, and getting the same result as a proper list model. The difference is that QAbstractListModel already assumes a single column and implements columnCount() for you, so a genuinely one-dimensional data source needs one less method and signals that intent clearly to anyone reading the class later. You would use QAbstractTableModel if your data is inherently tabular or might grow more columns later. You will see all three model base classes compared side by side in section 24.5.