23. Model-View Programming with QAbstractTableModel

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

List models organize the data as a one-dimenzional 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 fictional company client. Here, we use a comma-separated (CSV) file where each row has four fields: client id, first name, last name, and profession. We 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-list1

You need to show data from a CSV file (data.csv) in a table view. The file contains company clients data, each row containing client id, first name, last name and profession. You decide to implement a QAbstractTableModel subclass using the CSV file as the 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 csv reader’s rows are also lists which effectively makes csv_data a two-dimensional list suitable for use with QAbstractTableModel subclasses.

  2. Implement the rowCount() and the 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() as the data has two dimensions. With this you have a functional QAbstracttableModel subclass.

  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 headerData() method but implementing headerData() is still not mandatory.

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

You need to make your CSV file-backed model, that contains company clients data, editable.

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 (ie. fields) are flagged as selectable, enabled and editable. This does not have to be the case. For instance, if you had a model based on a SQL table you would flag the primary key fields as selectable only, making only the primary keys read-only.

  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 cells.

An icon of a clipboard-list1

You add a form to the GUI to make editing your CSV file-backed model user-friendly.

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 column in our tabele model (first name, last name and occupation) contains string data so we create three line edits, one for each column. We also add a Submit button to let the user submit changes to the current table row and add all widgets to a form layout.

  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 widgets with the table view currently selected row.

Now, when the user changes the data in the data-widget mapper 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-list1

You need to let the users append, insert, and remove rows from your CSV file-backed model.

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 and 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 guarder 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 client 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 current index row and call insertRows() with it to insert and 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 row is pressed, get the current view index and and call removeRows() with its row to remove the current row from the model.

    Note that both in insertRows() and removeRows() we cheat a bit and 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 and that 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 beginInsertRow() or beginRemoveRows() to notify other components that the model has changed.