Blog

Using a QAbstractListModel in QML

Using a QAbstractListModel in QML

The QAbstractListModel class provided by Qt can be used to organize data that will be presented visually as a list or table. Standardizing the interface with an abstract class like QAbstractListModel makes it easy to keep your model data completely isolated from your view (a software design principle known as "separation of concerns"). That abstraction makes it a powerful and flexible tool, but it also makes the learning curve steep.

The goal of this post is to provide concrete examples, explanations, and definitions of terms so you can more easily make use of the QAbstractListModel class. For your reference, you can see the complete example code on GitHub.

Example GUI

Let's say we've got a list of devices with which our software interacts. The data we've got for each device is:

  • A human-readable name (a string)
  • A serial number (an integer)
  • Whether or not the device is currently connected (a Boolean)

Our example GUI will look like this:

Example GUI

Part of the appeal of Qt is that you can make extremely slick UIs. We will not be doing that here in order to keep the focus on listmodel concepts. I've resisted the urge to add eye candy for the sake of clarity, and I have crafted the example to make it clear how you could stylize the list if you wanted to.

Additionally, the example uses Qt's Python bindings (PySide6). Everything here is equally applicable to C++, but again, for the sake of simplicity, it is presented as a Python application. The QML is identical in both cases.

The QML Description

First, we'll describe the visualization of our list of devices in QML. The QML ListView class is a great start. We'll set three properties:

  • model: this is what we'll use to bind the QML ListView to a QAbstractListModel class defined in C++ or Python
  • delegate: this is used to define how each item in the list is rendered as a QML object
  • highlight: this is not necessary to use a ListView, but it is generally useful to visualize a selected item in the list

Basic Example

A rough first draft of the QML might look like this (for the final version, see here):

ListView {
    id: deviceList

    model: controller.listmodel
    delegate: Item {
        width: deviceList.width

        Text {
            text: "Placeholder"
        }

        MouseArea {
            anchors.fill: parent
        }
    }
  // Item delegate

    highlight: Rectangle { color: "lightBlue" }
}  // ListView

Using the Model and the Delegate

In my main() function, I set a context property called controller that refers to an instance of my Controller class:

qml_app_engine = QQmlApplicationEngine()
qml_context = qml_app_engine.rootContext()
controller = Controller(parent=app)
qml_context.setContextProperty("controller", controller)

The Controller class exposes a Qt property called listmodel. Note that that property is declared as a QObject in my Python code, and that it does not need a property change signal (i.e., I use constant=True). In the QML above, I bind the ListView's model property to the listmodel property of my controller object:

model: controller.listmodel

The delegate property of ListView is like a template that defines how each item in the list is rendered as a QML object. For the sake of demonstration, we'll keep it simple here. I made it a QML Item that is as wide as the ListView itself and contains a Text object and a MouseArea, but you can make it anything you like (you'll generally make it much fancier)! For example, you might instead have something like a RowLayout containing a Checkbox, an Image, and a Text. (Haven't used layouts yet? Start here!) However you want each item in your list to be visualized, you can define it in your delegate. For simplicity, I often start by just rendering it all in a Text item. We'll look at how to access each item of data (name, serial number, and connection status) in the next section.

Note also that the MouseArea in my delegate is used to select an item in the list. Each item in the list is instantiated as a delegate object, so each item in the list has a MouseArea that can handle click events. We'll look at this in more detail later also.

The QAbstractListModel Class

If you are managing a large quantity of data and you want to visualize it on your QML GUI, you have a few options. For simple cases, a Repeater can usually get the job done just fine and is conceptually very easy to grasp. However, for very large lists, a Repeater is not recommended because it instantiates all visual items at once. In cases where you have a lot of data, you often only want to view or update a small section of it. For these cases, QML provides the ListView object, which expects to be bound to a QAbstractListModel object in your C++ or Python application.

QAbstractListModel is an abstract class that cannot be instantiated itself, so you need to create a new class that inherits from it and is specialized for your needs. I would first suggest reading the section of its documentation titled "Subclassing," which states that:

When subclassing QAbstractListModel, you must provide implementations of the rowCount() and data() functions. Well behaved models also provide a headerData() implementation.

If your model is used within QML and requires roles other than the default ones provided by the roleNames() function, you must override it.

For editable list models, you must also provide an implementation of setData() and implement the flags() function so that it returns a value containing Qt::ItemIsEditable.

It's unlikely that those few sentences made it immediately obvious what you need to do. Let's start with what confused me most when I got started: the concept of a "role."

Roles

Think about the data we're presenting. We have a list of devices, and each device in the list has three pieces of data (name, serial number, and connection status). You might think of each device's data as a row in a table:

Serial Number Human-readable name Connected?
123 name1 No
456 name2 Yes
789 name3 No

The simplest analogy is that the "role" is the piece of data that goes in each column. You might also think of it as identifying each piece of data in each object in the list. So, we will define our "roles" as name, serial, and connected.

Notice also that Qt provides a built-in ItemDataRole enum. I initially found this very confusing, because it provides roles with names like Qt::DisplayRole and Qt::EditRole, which don't really sound like individual data items to me. The built-in roles are intended for use with built-in classes like QString and QIcon, and they don't necessarily make sense for this particular custom class, so don't let it throw you off. Consider though, that you might have roles (items of data in your class) that aren't pieces of data that you'd want to render as text but are instead pieces of data that determine how the display of that data behaves (like a background color or an icon).

Roles that you want to define yourself for your own custom class can use enum values starting with Qt::UserRole, which has value 0x0100 = 256.

An Aside on Tables

It's worth mentioning that there is indeed a QAbstractTableModel class as well. As shown above, we can use the role as the "second dimension" of our one-dimensional list, making it look like a table. So, when would you use QAbstractTableModel? You might use it when you have a 2D array of objects, where each object has a set of properties that you identify as "roles."

What makes the most sense as a data model will depend on your specific data, and it may be confusing to think about a list in terms of "rows" if your data doesn't really seem like a table (you may not even arrange items vertically on your UI, which makes the terminology much worse!). We're stuck with the "row" and "column" terminology used by Qt here, but the models can be used in whatever way makes the most sense for the data you need to represent.

In most real-life applications (as well as in the example code here), I use a 1D array with multiple roles, because I find that to be the simplest and most natural data structure. However, both QAbstractTableModel and QAbstractItemModel are available to you if you need a more complex visualization of more complex data. Once you get a handle on QAbstractListModel, the more general classes will make more sense.

How are Roles Used in QML?

As shown above, you will use the QML ListView's model property to specify an object in your C++ or Python code that inherits from QAbstractListModel. The delegate property is then used to define the QML object that will visualize that data. In this case, each item in the list has three roles (name, serial, and connected), and we'll want to access each of those data items in QML independently.

Looking at the GUI again:

GUI example

Each item in the list is visualized as a Text item where the content follows this pattern:

[index of item]: [name role] ([serial role]) - [connection role]

In our delegate, we can access each item of data using the name of the role:

ListView {
    model: controller.listmodel
    delegate: Text {
        text: `${index}: ${name} (${serial}) - ${connected ? "OK" : "NOT FOUND"}`
    }

}

Inside the delegate object we can simply use index, name, serial and connected as if they are bound to the individual data items inside that element of the list. index is provided out-of-the-box by ListView, but name, serial, and connected are the names of roles we define ourselves. Within the delegate object, we can refer to those names, and our child class of QAbstractListModel will provide methods that QML can use to link those names to specific pieces of data.

The index value is also useful in our MouseArea. We added the MouseArea so that the user could click an item in the list and manipulate it. Since the MouseArea is inside the delegate, we have access to the index value. In the MouseArea's signal handler onClicked, we will want to set the currentIndex property of the ListView to the index of the item in the list that was clicked:

ListView {
    id: deviceList

    delegate: Item {
        MouseArea {
            onClicked: deviceList.currentIndex = index
        }   // MouseArea
    }
  // Item delegate

    highlight: Rectangle { color: "lightBlue" }
}  // ListView

Setting the currentIndex of the ListView enables the ListView to automatically animate the highlight object that we defined. When the user clicks an item in the list, it will move the Rectangle to highlight the selected list item.

As an exercise for the reader, try making the delegate more interesting. Instead of indicating the state of the connected role with just a Text, try using the connected role to set the text color of the delegate, or add an icon to each row that indicates whether or not the device is connected.

Setting Up the Roles

Let's circle back to this statement in the documentation:

If your model is used within QML and requires roles other than the default ones provided by the roleNames() function, you must override it.

We want to use our own role names for this, so we can use names that make sense in our delegate (like name, serial, and connected). The mapping from integer role enum values (like Qt::UserRole) to strings of characters is established with the QAbstractItemModel::roleNames() method, which your custom listmodel will inherit. All classes that inherit QAbstractListModel need to implement this method, which returns the map from integers to byte arrays. In C++, this map is a QHash<int, QByteArray>, and in Python it is a basic dict. The integer is the role enum value, and the byte array is the string name used in QML to access that role in each item of the listmodel.

I like to set up my roles by doing two things: creating an enum (starting with the value Qt::UserRole and incrementing from there) that enumerates my custom roles, and then creating a dictionary that maps the role enum values to byte arrays (the names used by QML to access elements of the model). In our example, I might do:

class DeviceItemRoles(IntEnum):
    NAME = Qt.UserRole
    SERIAL = auto()
    CONNECTED = auto()


_role_names = {
    DeviceItemRoles.NAME: b'name',
    DeviceItemRoles.SERIAL: b'serial',
    DeviceItemRoles.CONNECTED: b'connected'
}

Note again that in Python, the values are byte arrays (b''), not strings.

With this setup, the delegate of our ListView can access each piece of data in each list item using the strings name, serial, and connected. QML knows how the role integers (from the enum) map to the names because it knows that a QAbstractListModel must have a roleNames() method, so now we just need to give it a way to access each piece of data given the list index and the role. That is the job of the QAbstractItemModel::data() method, which we will get to shortly.

Subclassing a QAbstractListModel

Recall from the documentation that subclasses of QAbstractListModel need to implement the rowCount() and data() methods, plus roleNames() if the listmodel is used in QML. We'll cover these one-by-one, but first let's define how we'll store our data.

Data Storage

I find that the easiest way to store the data (for a Python application) is with a list of dictionaries, where each dictionary uses the role enum as the key for each data value. This is by no means the only way, but it is very simple and often sufficient. So, you might start developing your custom listmodel class like this:

class DeviceListModel(QAbstractListModel):
    def __init__(self):
        super().__init__()
        self._data = []

    def add_device(self, name, serial, connected):
        new_row = {
            DeviceItemRoles.NAME: name,
            DeviceItemRoles.SERIAL: serial,
            DeviceItemRoles.CONNECTED: connected
        }

        self._data.append(new_row)

When we create a new listmodel, the list of data, self._data, is just an empty list. We can then add device data to the list with the add_device() method, which takes the name, serial number, and connection status, puts them in a dictionary with the appropriate role enum values as keys, and then appends that dictionary to the data list.

Now that we've established how the data is stored, we can fill out the required methods.

The roleNames() Method

roleNames() is the easiest to implement, because it's already done! The _role_names dictionary from above is exactly what roleNames() should return, so this one's a no-brainer:

def roleNames(self):
    return _role_names

That's it!

The rowCount() Method

rowCount() is similarly straightforward. The number of rows is just the number of elements in our self._data list. We don't need to do much here either:

def rowCount(self, parent=QModelIndex()):
    return len(self._data)

The only thing to address is that weird parent argument. What's that about?

It comes from the base class, QAbstractItemModel. The base class is more general. Whereas QAbstractListModel represents a one-dimensional list of items that all have the same type of elements, QAbstractItemModel can describe trees and other complex hierarchical structures. In those cases, you need to provide the index of a parent object in the tree so the rowCount() method can return the number of children of that parent. Once you get your bearings with the QAbstractListModel, you can dig into the QAbstractItemModel, but for now, let's just ignore parent, because it doesn't apply to a one-dimensional list. Just give it a default QModelIndex.

The data() Method

Finally, we need to implement a method that will return data values when QML asks for them. The C++ signature of this method is:

QVariant QAbstractItemModel::data(const QModelIndex &index, int role = Qt::DisplayRole) const

So, our implementation of the method needs to take the index of the row we want (as a QModelIndex object) and the role of the individual data item we want (as an integer, like our convenient DeviceItemRoles enum), and it will return the data as a QVariant. With the PySide6 bindings, there is no QVariant. We can return whatever Python object we want, and if there's no data at that index or with that role, we can just return None. A simple implementation in Python looks like:

def data(self, index, role):
    if role not in list(DeviceItemRoles):
        return None

    try:
        device = self._data[index.row()]
    except IndexError:
        return None

    if role in device:
        return device[role]
    return None

There's a little more meat here than in our roleNames() and rowCount() methods. First, we check that the role integer that was passed in is an item in our DeviceItemRoles enum. If it isn't, then something is looking for a role we aren't providing, so we'll just return None.

Next, we'll try to get the index of the item in the list. Note that you can't index the self._data list using the index argument directly. You need to call index.row(), which is a consequence of the fact that QAbstractListModel is a child of the more general QAbstractItemModel class, which is not necessarily a 1D list. If you look at the QModelIndex class, you'll see that, in addition to row(), it also provides column(), as well as various other methods that only apply to more complex structures.

Anyway, if the given index is out of bounds, then something is looking for an invalid row, and we return None. Beyond that, both the index and the role are ok, so we return the appropriate value by indexing the list to get a dictionary, and then looking up the value of the role key in that dictionary. Whatever data was stored there gets returned.

Updating, Inserting, and Removing Data

What was implemented above is sufficient for a listmodel that will never change, but that's probably in the minority of use cases. If you only have a small-ish amount of static data, it would probably be easier to use a Repeater. More likely, you'll want to add data to your list, remove data, or change data at runtime, and QAbstractListModel is a much better fit in these situations. In order to manipulate our list contents, we need to understand a few additional concepts.

Signaling Changes to Existing Row Data from the Application

In general, when we bind properties to QML, we provide a signal that we emit when the property changes. QML listens for that signal, and when it gets emitted, it calls the property getter to refresh the value. This is done by a QAbstractListModel by using the dataChanged signal provided by its parent, QAbstractItemModel, which specifies which elements of the model changed (in terms of rows, columns, and roles).

In some cases, you need to emit this signal yourself. For example, we might want our listmodel class to have a method that sets all devices to "disconnected." That would look like:

def set_all_disconnected(self):
    for d in self._data:
        d[DeviceItemRoles.CONNECTED] = False
    self.dataChanged.emit(self.index(0), self.index(self.rowCount() - 1), [])

In this method, we first loop over all items in the data list and set the connected value to False. Then, we only need to emit a single signal that says that all items in the list have changed (i.e., every index from 0 to rowCount() - 1). The empty list in the last parameter of the signal is a list of roles that changed, which can be left empty to indicate that all roles have changed. In this case, you can specify [DeviceItemRoles.CONNECTED] if you prefer. This only makes a difference if you have many roles.

Signaling Changes to the Collection of Rows

Even if you don't change any existing data, you might add or remove entire rows, and QML will need to know what to update when that happens. In this case, we use a pair of methods, beginInsertRows() and endInsertRows(), to specify that we're adding data (and how many rows we're adding).

Let's say we want to add a new element to the list after the selected index. We can do that with a method like:

def add_device_after_index(self, idx, name, serial, connected):
    index_of_new_device = idx + 1
    new_device = {
        DeviceItemRoles.NAME: name,
        DeviceItemRoles.SERIAL: serial,
        DeviceItemRoles.CONNECTED: connected
    }

    self.beginInsertRows(QModelIndex(), index_of_new_device, index_of_new_device)
    self._data.insert(index_of_new_device, new_device)
    self.endInsertRows()

beginInsertRows() needs three things: the QModelIndex of the parent into which rows are inserted (for 1D listmodels that we're talking about here, just give it a default one), the row number that the first new row will have after insertion, and the row number that the last new row will have after insertion. I'm never able to remember this, so I almost always consult the helpful diagrams in the documentation for this method.

After that, we insert the new data into our list, and then we call endInsertRows(). The beginInsertRows() method handles emitting a signal for you (rowsAboutToBeInserted), and endInsertRows() handles emitting a different signal for you (rowsInserted) so you don’t have to emit any signals yourself! These signals are used to notify QML that it’s time to refresh the ListView with new rows, and which ones need to be updated (if the model contains large quantities of data, we obviously only want to update as few as possible).

Signaling Changes to Existing Row Data from the GUI

The final scenario we'll discuss addresses the last part of the QAbstractListModel documentation on subclassing:

For editable list models, you must also provide an implementation of setData() and implement the flags() function so that it returns a value containing Qt::ItemIsEditable.

As you can see above, if the application manipulates data in the QAbstractListModel, it simply needs to emit a signal (dataChanged) to notify QML that there's something new. The setData() method is used when information goes the opposite direction, from the UI to the application. For example, say the delegate contains a checkbox. If the user clicks the checkbox in a particular row, QML needs to tell the QAbstractListModel that there is a new value for the checkbox's role at a particular list index. It does this by calling the setData() method, which takes three arguments: the index, the new value, and the role. It will look very similar to the data() method above, perhaps like:

def setData(self, index, value, role):
    if role != MyRoleEnum.SOME_EDITABLE_ROLE:
        return False

    try:
        data_row = self._data_list[index.row()]
    except IndexError:
        return False

    data_row[MyRoleEnum.SOME_EDITABLE_ROLE] = value
    self.dataChanged.emit(index, index, [MyRoleEnum.SOME_EDITABLE_ROLE])
    return True

In short, you use the index and role arguments to find the data you’re looking for in the model, you set that data to the new value, and then you emit dataChanged.

Summary

The QAbstractListModel (and its base class, QAbstractItemModel) is a powerful way to present a list of data to a user interface, but the extensive abstraction can make the documentation hard to parse. A simple example should help clarify, as well as a small number of important concepts:

  • Many of QAbstractListModel's methods are inherited from its base classes, and consequently involve a parent QModelIndex that doesn't apply to a simple list and can be very confusing.
  • A "role" is simply a way to specify individual pieces of data in a list item.
  • Roles can be used to make a list seem like a table, and that's fine... you can still use QAbstractListModel!
  • When adding items to the list or removing items from it, call the beginInsertRows() and endInsertRows() methods before making your changes, and the correct signals will be emitted for you at the right times.
  • If your application updates the model by changing data in an existing item in the list (or multiple existing items in the list), make sure you emit the dataChanged signal after the changes are made to notify QML that it needs to update its views.
  • If the user interacts with the QML UI and modifies data in the model, you will need to implement setData() to store the new information in the model object, and then you will need to emit dataChanged.

Building a Qt app?

I'd love to help! Give us a call or send us an email to discuss! 

Learn more about our Application Development expertise and contact us for your next project.

Comments

There are currently no comments, be the first to post one.

Post a comment

Name (required)

Email (required)

CAPTCHA image
Enter the code shown above:

Related Blog Posts