Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sort the data of a Qml ListView by using a QSortFilterProxyModel.

Tags:

c++

qt

qml

I'm making a chat client, and I am using a qml ListView to display all the messages in a "chat room".

I am using my own model class that derives from QAbstractListModel for storing all the messages in all the rooms that the user is a member of.

I want all the messages sorted based on the timestamp they where originally sent, the newest message should appear on the bottom of the view, and the oldest should be at the top.

I also want the user to be able to filter the messages based on which room they where sent in. Which I've already solved.

Here is the declaration of my custom model

class MessageModel : public QAbstractListModel
{
public:
    enum MessageRoles {
            Id = Qt::UserRole + 1,
            RoomId = Qt::UserRole + 2,
            PersonId = Qt::UserRole + 3,
            PersonEmail = Qt::UserRole + 4,
            Created = Qt::UserRole + 5,
            Text = Qt::UserRole + 6
        };
    explicit MessageModel(QObject * parent = nullptr);
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    QHash<int, QByteArray> roleNames() const;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 
    void reset_data(QJsonArray new_data);
    void insert_unique_data(QJsonArray new_data);
private:
    QList<LocalData::Message> m_data;

};

Here is the implementation of my model

MessageModel::MessageModel(QObject *parent) : QAbstractListModel(parent)
{}

int MessageModel::rowCount(const QModelIndex &parent) const
{
    return m_data.size();
}

QHash<int, QByteArray> MessageModel::roleNames() const
{
    QHash<int, QByteArray> roles;
    roles[Id] = "id";
    roles[RoomId] = "roomId";
    roles[PersonId] = "personId";
    roles[PersonEmail] = "personEmail";
    roles[Created] = "created";
    roles[Text] = "messageText";
    return roles;
}

QVariant MessageModel::data(const QModelIndex &index, int role) const {

    if(!index.isValid())
        return QVariant();

    if(index.row() >= m_data.size())
        return QVariant();

    switch (role) {
        case Id:
            return m_data.at(index.row()).id;
        case RoomId:
            return m_data.at(index.row()).roomId;
        case PersonId:
            return m_data.at(index.row()).personId;
        case PersonEmail:
            return m_data.at(index.row()).personEmail;
        case Text:
            return m_data.at(index.row()).text;
        case Created:
            return m_data.at(index.row()).created.toString(Qt::ISODateWithMs);
    }

    return QVariant();
}

void MessageModel::update_data(QJsonArray new_data)
{
    beginResetModel();
    m_data.clear();

    foreach (const QJsonValue & val, new_data) {
        m_data.push_back(LocalData::Message(val.toObject()));
    }
    endResetModel();
}

void MessageModel::insert_unique_data(QJsonArray new_data)
{
    QList<LocalData::Message> temp;
    foreach (const QJsonValue & val, new_data) {
        auto obj_to_insert = LocalData::Message(val.toObject());
        if(!m_data.contains(obj_to_insert))
            temp.push_back(obj_to_insert);
    }
    int begin = rowCount();
    int end = rowCount() + temp.size() - 1;

    beginInsertRows(QModelIndex(), begin, end);
    foreach (const LocalData::Message & msg, temp) {
        m_data.push_back(msg);
    }
    endInsertRows();
}

I want to use a QSortFilterProxyModel to both sort and filter the messages. I've managed to make it filter the messages correctly based on the RoomId role, but I am having troubles with sorting the messages correctly on the Created role.

The proxy model that I'm using is very simple, I've just tried to override the lessThan() function like so:

bool SortFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
    {
        QVariant leftData = sourceModel()->data(source_left);
        QVariant rightData = sourceModel()->data(source_right);

        if(leftData.type() == QVariant::DateTime)
        {
            return leftData.toDateTime() < rightData.toDateTime();
        }
        else {
            return leftData.toString() < rightData.toString();
        }
    }

Othervise it is just a plain QSortFilterProxyModel. When it's being initialised I am calling the following functions:

m_messageModel = new MessageModel;
m_messageProxyModel = new SortFilterProxyModel;

m_messageProxyModel->setSourceModel(qobject_cast<QAbstractListModel *>(m_messageModel));

m_engine.rootContext()->setContextProperty("messages", m_messageProxyModel);

m_messageProxyModel.setSortRole(MessageModel::Created);   

m_messageProxyModel.setDynamicSortFilter(true); 

m_messageProxyModel.sort(0, Qt::AscendingOrder);

m_messageModel, m_messageProxyModel, and m_engine is all member variables (pointers) declared in a class which is instantiated in main(). m_engine is a QQmlApplicationEngine which exposes the variable to qml.

Here is the qml file containing the ListView with all the messages.

import QtQuick 2.0
import QtQuick.Controls 1.4

Rectangle{
    property alias messageView: _messagePanelView
    Component {
        id: _messagePanelDelegate
        Item{
            id: _messagePanelDelegateItem;

            width: root.width * 0.8
            height: _messagePanelMessageColumn.height
            Column{
                id: _messagePanelMessageColumn;
                height: children.height;
                spacing: 20
                Text{ text: "<b>" + personEmail + "</b> <t />" + created; font.pointSize: 7 + Math.log(root.width) }
                Text{ text: messageText }
                Rectangle{
                    width: parent.width
                    height: 20
                    color: "#FFFFFF"
                }
            }
        }
    }
    ScrollView{
        anchors.fill: parent
        ListView {
            id: _messagePanelView
            height: root.height - 100
            model: messages
            delegate: _messagePanelDelegate
            interactive: true
        }
    }
}

The source model is currently populated with data when the user asks for all messages in a specific "chat room". A function similar to this is called when the user presses a button.

void sync_messages_and_filter_based_on_roomId(QString roomId)
{
    m_messageProxyModel->setFilterRole(MessageModel::RoomId);
    m_messageProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
    m_messageProxyModel->setFilterRegExp(QRegExp::escape(roomId));

    fetch_messages_in_room_from_server_async(roomId); 
}

fetch_messages_in_room_from_server_async(roomId); is a function that asks the server for new messages, receives a JSON formatted response payload, creates a QJsonArray object and calls void MessageModel::insert_unique_data(QJsonArray new_data) All of which happens in a seperate worker thread.

When this is called the first time, the data actually gets sorted in an descending order (oldest at the bottom, most recent at the top). But when it's called a second time, when the server has more data to provide, the client is supposed to insert all the new messages as rows in the source model, and have the proxy model sort it in an ascending order (most recent at the bottom, oldest at the top) once the new data has been inserted.

Currently the model is only being sorted in an descending order, the first time data is inserted into the model. But once the model has been updated for the first time, it's no longer sorting the data, and the new messages appear on the bottom of the ListView instead of the top, which means that the proxy model has stopped sorting the model.

Here is an image of the ListView, after inserting a second time. Notice the timestamps This is what happens when I terminate the program, and insert all the messages at once

The result that I want is to have the proxy model sort the messages in an ascending order (most recent at the botton, oldest at the top) every time the source model is updated/changed. Meaning when void MessageModel::insert_unique_data(QJsonArray new_data) is called, I'm expecting the new messages to appear on the bottom, with the most recent message as the last message.

Thank you for taking your time to read through my insanely long post. I just wanted to cover all the details since exposing c++ models to qml requires a lot of steps.

like image 236
Hurlevent Avatar asked Aug 08 '17 08:08

Hurlevent


2 Answers

My guess would be that your lessThan function is doing nothing.

When you are calling QVariant leftData = sourceModel()->data(source_left); it calls the data function with the role Qt::DisplayRole which returns an invalid QVariant for your model. Your if(leftData.type() == QVariant::DateTime) is never true.

What you should do is explicitly getting the timestamp with QDateTime leftTimestamp = m_data.at(source_left.row()).created; and the same for source_right. Your if is then useless and you can just do return leftTimestamp < rightTimeStamp;


Alternatively, and if you want to do the sorting and filtering from QML and not c++, you could use my SortFilterProxyModel like so :

import QtQuick 2.0
import QtQuick.Controls 1.4
import SortFilterProxyModel 0.2

Rectangle{
    property alias messageView: _messagePanelView

    SortFilterProxyModel {
         id: proxyMessageModel
         sourceModel: sourceMessageModel // a context property you exposed
         filters: ValueFilter {
             roleName: "roomId"
             value: currentRoomId // a property you could add somewhere
         }
         sorters : RoleSorter {
             roleName: "created"
         }
    }
    Component {
        id: _messagePanelDelegate
        Item{
            id: _messagePanelDelegateItem;

            width: root.width * 0.8
            height: _messagePanelMessageColumn.height
            Column{
                id: _messagePanelMessageColumn;
                height: children.height;
                spacing: 20
                Text{ text: "<b>" + personEmail + "</b> <t />" + created; font.pointSize: 7 + Math.log(root.width) }
                Text{ text: messageText }
                Rectangle{
                    width: parent.width
                    height: 20
                    color: "#FFFFFF"
                }
            }
        }
    }
    ScrollView{
        anchors.fill: parent
        ListView {
            id: _messagePanelView
            height: root.height - 100
            model: proxyMessageModel
            delegate: _messagePanelDelegate
            interactive: true
        }
    }
}
like image 59
GrecKo Avatar answered Oct 04 '22 12:10

GrecKo


I've figured it out!

In the SortProxyModel::lessThan() function, I called the QAbstractProxyModel::data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) functions without changing the default role parameter. Meaning that my implementation of the lessThan() function doesn't compare on the Created role.

How I fixed it was to just use the normal QSortProxyModel type, instead of my own derived version. Subclassing QSortProxyModel was a mistake.

like image 26
Hurlevent Avatar answered Oct 04 '22 11:10

Hurlevent