QML ListView is not updated on model reset - qt

Qt 5.8, Windows 10.
Quick Controls 2 application. In QML I have a ListView with a model derived from QAbstractListModel.
In the model I have the following code:
void MediaPlaylistModel::update()
{
beginResetModel();
{
std::unique_lock<std::mutex> lock(m_mutex);
m_ids = m_playlist->itemsIds();
}
endResetModel();
}
Nothing happens in QML view after calling this method: list is not updated.
If I go back and then forward (to the page with the ListView) - it'll contain updated data. Model object instance is the same always.
Am I doing something wrong?
Update #1:
The only methods I override are:
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
Update #2:
C++ model code:
MediaPlaylistModel::MediaPlaylistModel(
QSharedPointer<AbstractMediaPlaylist> playlist,
QObject *parent) :
base_t(parent),
m_playlist(playlist)
{
Q_ASSERT(m_playlist);
connect(playlist.data(), &AbstractMediaPlaylist::changed,
this, &MediaPlaylistModel::update);
update();
}
QHash<int, QByteArray> MediaPlaylistModel::roleNames() const
{
QHash<int, QByteArray> result;
result[IdRole] = "id";
result[TitleRole] = "title";
result[DurationRole] = "duration";
return result;
}
void MediaPlaylistModel::update()
{
Q_ASSERT_SAME_THREAD;
beginResetModel();
{
std::unique_lock<std::mutex> lock(m_mutex);
m_ids = m_playlist->itemsIds();
}
endResetModel();
}
int MediaPlaylistModel::rowCount(
const QModelIndex &parent) const
{
Q_UNUSED(parent);
std::unique_lock<std::mutex> lock(m_mutex);
return static_cast<int>(m_ids.size());
}
QVariant MediaPlaylistModel::data(
const QModelIndex &index,
int role) const
{
auto row = static_cast<size_t>(index.row());
int id = 0;
{
std::unique_lock<std::mutex> lock(m_mutex);
if (row >= m_ids.size())
return QVariant();
id = m_ids[row];
}
if (role == IdRole)
return id;
QVariant result;
auto item = m_playlist->item(id);
switch(role)
{
case Qt::DisplayRole:
case TitleRole:
result = item.title;
break;
case DurationRole:
result = item.duration;
break;
}
return result;
}
QML code:
import QtQuick 2.7
import QtQuick.Controls 2.0
import com.company.application 1.0
Page
{
id : root
property int playlistId
property var playlistApi: App.playlists.playlist(playlistId)
ListView
{
id : playlist
anchors.fill: parent
model: playlistApi.model
delegate: ItemDelegate
{
text: model.title
width: parent.width
onClicked: App.player.play(playlistId, model.id)
}
ScrollIndicator.vertical: ScrollIndicator {}
}
}
Update #3:
When the model is updated (one item added), something strange happens with QML ListView: in addition to fact that it's not updated (and it does not call
MediaPlaylistModel::data to retrieve new items), existing items got damaged. When I click on existed item, it's model.id property is always 0. E.g. at app start its model.id was 24, after one item added its model.id became 0.
Update #4:
App.playlists.playlist(playlistId) returns pointer to this class instance:
class CppQmlPlaylistApi :
public QObject
{
Q_OBJECT
Q_PROPERTY(QObject* model READ model NOTIFY modelChanged)
Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged)
public:
explicit CppQmlPlaylistApi(
int playlistId,
QWeakPointer<CorePlaylistsManager> playlistsMgr,
QObject *parent = 0);
QObject* model() const;
QString title() const;
void setTitle(const QString &title);
signals:
void modelChanged();
void titleChanged();
void loadPlaylistRequested(int id);
protected slots:
void onPlaylistLoaded(int id);
void onPlaylistRemoved(int id);
protected:
int m_playlistId = 0;
QWeakPointer<CorePlaylistsManager> m_playlistsMgr;
QSharedPointer<QAbstractItemModel> m_model;
};

The model was in non-GUI thread.
I was getting these debug messages (thanks to AlexanderVX for pointing me out):
QObject::connect: Cannot queue arguments of type 'QQmlChangeSet'
(Make sure 'QQmlChangeSet' is registered using qRegisterMetaType().)
Moving the model object to GUI thread fixed the problem.

Your code, as provided, is good. On the QML side, as long as your model is bound, and not dynamically re-created in JS, you should be good too.
ListView {
model: mediaPlaylistModel
}
Problems can arise if you overloaded beginResetModel or endResetModel by accident. For tests purposes, you can try to emit the QAbstractItemModel::modelReset() signal, and see if it changes anything.
It's quite easy to miss something with QAbstractItemModel, resulting in nothing working anymore !

Related

How to properly link C++ model to QML front end

here is my setup:
First is the model class, this stores a pointer to the data of type CompetitionsList. We can see that it implements the necessary basic functions when deriving from QAbstractListModel, and it uses the QML_ELEMENT macro to expose the model to the QML type system:
class CompetitionsListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(CompetitionsList* list READ list WRITE setList)
QML_ELEMENT
public:
explicit CompetitionsListModel(QObject *parent = nullptr);
enum {
NameRole = Qt::ItemDataRole(),
IdRole
};
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
CompetitionsList *list() const;
void setList(CompetitionsList *list);
virtual QHash<int, QByteArray> roleNames() const;
private:
CompetitionsList* m_list; \\ <--- ptr to data
};
Then we have the data class. This class is also a QML_ELEMENT, and it has a userid property. This is used to populate the QVector<listItem> by requesting data from a RESTful http server.
The two signals preItemAppended() and postItemAppended() are emitted when data is added to the QVector<listItem>. These signals are then connected to the Model as a way of notifying the model it must create a new row in the list. The important function here is populateCompetitions, which I explain below.
struct listItem {
QString competitionName;
QString competitionId;
};
class CompetitionsList : public QObject
{
Q_OBJECT
Q_PROPERTY(QString userId READ getUserId WRITE setUserId)
QML_ELEMENT
public:
explicit CompetitionsList(QObject *parent = nullptr);
void populateCompetitions(const QString& userId);
QVector<listItem> items();
QString getUserId() const;
void setUserId(const QString &value);
signals:
void preItemAppended();
void postItemAppended();
void errorPopulatingData();
private:
QVector<listItem> m_competitions;
QString userId;
};
The populateCompetitions function looks as so:
void CompetitionsList::populateCompetitions(const QString &userId)
{
qDebug() << "initialising data";
HttpClient* client = new HttpClient();
client->getUserCompetitions(userId);
connect(client, &HttpClient::getUserCompetitionsResult, [=](const QByteArray& reply){
QJsonDocument _reply = QJsonDocument::fromJson(reply);
if(_reply["data"].isNull()) {
emit errorPopulatingData();
}
else {
const QJsonArray competitions = _reply["data"]["competitions"].toArray();
for(const QJsonValue& competition : competitions) {
emit preItemAppended();
listItem item{competition["competition-name"].toString(), competition["competition-id"].toString()};
m_competitions.append(std::move(item));
emit postItemAppended();
}
}
});
}
It requests data from the database, and then stores it locally.
Then finally, the QML model which ties it all together:
ListView {
id: view
implicitHeight: 1920
implicitWidth: 1080
clip: true
model: CompetitionsListModel {
list: CompetitionsList {
id: compList
}
Component.onCompleted: { compList.userId = "6033f377257e8630ed13299e" } //<-- calls the populateCompetitions function
}
delegate: RowLayout {
Text {
text: qsTr(model.name + ":::" + model.id)
}
}
}
We can see that it has a ListView element, with a model of type CompetitionsListModel and a list of type CompetitionsList. Once the model component is created, we set the userId of the list, this in turn calls the populateCompetitions function which sets up the data for the model to use as seen above.
Unfortunately this doesnt anything when I run the code. Blank screen. Nada. I was wondering if anyone has an insight as to what might be causing this based on the code provided. Ive been at it for so long and it just inst being nice.
I think you need to call qmlRegisterType or UncreatableType on your CompetitionsListModel :
https://doc.qt.io/qt-5/qqmlengine.html#qmlRegisterType

How to apply and view user roles?

I'm trying to create custom model, and want it to work with custom roles. But I dont really understand how to do it. Also, i want to use my model with a qt widget, not with QML View. How roles are applying to certain items?
How to setup ListView, so that could work with my custom roles?
I know that I need to create enum, and reimplement roleNames function
my model .h file
class ListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
ListModel();
virtual ~ListModel() override;
enum CustomRoles{
RoleType=Qt::UserRole+1,
ButtonRole,
CheckboxRole,
};
protected:
QList<BaseItems*> itemList;
QHash<int, QByteArray> _roles;
// int _RowCount = 0;
public:
void Add(BaseItems* item);
BaseItems* getItem(int index);
void clear();
int count() const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
Q_SIGNALS:
void countChanged();
};
my model .cpp file
ListModel::ListModel() : QAbstractListModel()
{
}
ListModel::~ListModel()
{
itemList.clear();
}
void ListModel::Add(BaseItems *item)
{
beginInsertRows(QModelIndex(),itemList.count(),itemList.count());
itemList.append(item);
endInsertRows();
Q_EMIT countChanged();
}
BaseItems* ListModel::getItem(int index)
{
return itemList.at(index);
}
void ListModel::clear()
{
qDeleteAll(itemList);
itemList.clear();
}
int ListModel::count() const
{
return rowCount();
}
int ListModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return itemList.count();
}
QVariant ListModel::data(const QModelIndex &index, int role) const
{
ItemButton *button = dynamic_cast<ItemButton*>(itemList.at(index.row()));
if (!index.isValid())
return QVariant();
if (index.row() >= itemList.count())
return QVariant();
switch (role)
{
case Qt::DisplayRole:{
return QVariant::fromValue(button->Text);}
case ButtonRole:{
return QVariant::fromValue(button->Text);}
}
return QVariant();
}
QHash<int, QByteArray> ListModel::roleNames() const {
QHash<int, QByteArray> role;
role[RoleType] = "first";
role[ButtonRole] = "last";
return role;
}
Instead of "first" and "last" better you name the roles meaningfully:
QHash<int, QByteArray> ListModel::roleNames() const {
QHash<int, QByteArray> role;
role[RoleType] = "roleType";
role[ButtonRole] = "buttonRole";
return role;
}
So these quoted names will be used. If you want to display the data from this model in QML you can do something like this:
ListView {
width: 100
height: 500
model: listModel
delegate: Text {
text: model.roleType + model.buttonRole
}
}
listModel object can be initialized in C++ and can be passed to QML using
view->rootContext()->setContextProperty("listModel", listModel);
or you can make an instance of the ListModel in QML, but in the cpp file you will have to register your ListModel type
qmlRegisterType<ListModel>("ListModel", 1, 0, "ListModel");
then in the qml file:
import ListModel 1.0
finally creating an instance of the model by
ListModel {
id: listModel
}

Dynamic translation of combobox qml

I've added translation to my qt/qml app using this tutorial
https://retifrav.github.io/blog/2017/01/04/translating-qml-app/
https://github.com/retifrav/translating-qml
And most seems to work well except that the values of the combobox dont get update with dynamic translate.
Im using qt 5.11.2.
By a combobox i mean this:
ComboBox {
textRole: "text"
Layout.fillWidth: true
model: ListModel {
Component.onCompleted: {
append({text: qsTr("None")})
append({text: qsTr("Subpanel")})
append({text: qsTr("All")})
}
}
}
ComboBox {
textRole: "text"
Layout.fillWidth: true
model: ListModel {
ListElement{text: qsTr("None")}
ListElement{text: qsTr("Subpanel")}
ListElement{text: qsTr("All")}
}
}
None of them gets updated.
I've done some research and found this on bug reports
https://bugreports.qt.io/browse/QTBUG-68350
This seems to get fixed on 5.12, but for various reason we need to keep the same version, is there a way i can fix it for this version? (5.11.2)
EDIT: I dont find a way to translate comboBox. Is there another way of doing translations? even if it means to open a new instance of the app? Can someone point me to a link? Can't find a way to do this.
EDIT2: Is there a way to force the model of the combobox to be updated by javascript? when the changeLanguage() method is called?
note: As a complaint i'm finding the support / community to get answers for Qt problems terrible, really bad, but maybe its my problem.
One option is to add a QAbstracstListModel which does the translation. I made myself a base class, which can be inherited. This also gives you a lot of flexibility for converting a selected item to a value (in this example I am using int, but you can make it anything), which is connected to a C++ backend (I used backend.selectedPanel for your example)
<< Edit: See answer of Felix for nice addition of dynamic translation >>
base header:
class baseEnum : public QAbstractListModel
{
Q_OBJECT
public:
virtual int rowCount(const QModelIndex &parent) const = 0;
virtual QVariant data(const QModelIndex &index, int role) const = 0;
QHash<int, QByteArray> roleNames() const;
Q_INVOKABLE int getIndex(int value);
Q_INVOKABLE int getValue(int index);
}
base cpp:
QHash<int, QByteArray> baseEnum::roleNames() const
{
QHash<int, QByteArray> result;
result.insert(Qt::DisplayRole, "text");
result.insert(Qt::UserRole + 1, "value");
return result;
}
int baseEnum::getIndex(int value)
{
for(int i=0;i<rowCount(QModelIndex());++i)
if(data(createIndex(i, 0, nullptr), Qt::UserRole + 1).toInt() == value)
return i;
return -1;
}
int baseEnum::getValue(int index)
{
return data(createIndex(index, 0, nullptr), Qt::UserRole + 1).toInt();
}
derived header:
class FancyEnum : public baseEnum
{
Q_OBJECT
public:
int rowCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
};
derived cpp:
int FancyEnum::rowCount(const QModelIndex &parent) const
{
if(!parent.isValid())
return 5;
return 0;
}
QVariant FancyEnum::data(const QModelIndex &index, int role) const
{
switch(index.row())
{
case 0: return role == Qt::DisplayRole ? QVariant(tr("None")) : QVariant(0);
case 1: return role == Qt::DisplayRole ? QVariant(tr("Subpanel")) : QVariant(1);
case 2: return role == Qt::DisplayRole ? QVariant(tr("All")) : QVariant(2);
}
return role == Qt::DisplayRole ? QVariant(QString("<%1>").arg(index.row())) : QVariant(0);
}
Register it somewhere:
qmlRegisterType<FancyEnum>("your.path.here", 1, 0, "FancyEnum");
usage in QML:
ComboBox {
model: FancyEnum { id: myEnum }
textRole: "text"
currentIndex: myEnum.getIndex(backend.selectedPanel) : 0
onActivated: backend.selectedPanel = myEnum.getValue(index) }
}
This is an addition to #Amfasis answer. It extends the very useful "baseEnum" model by adding the capability to detect and react to restranslation events
For the GUI to actually detect that the text was changed after a restranslation, the model must "notify" the gui that data has changed. However, to do that, the model must know when data changed. Thankfully, Qt has the LanguageChange event to do so. The following code catches that event and uses it to notify the gui of the data change.
// in the header file:
class baseEnum : public QAbstractListModel
{
Q_OBJECT
public:
// ... all the stuff from before
bool event(QEvent *event);
};
// and in the cpp file:
bool baseEnum::event(QEvent *ev)
{
if(ev) {
switch(ev->type()) {
case QEvent::LanguageChange:
// notifiy models that the display data has changed for all rows
emit dataChanged(index(0),
index(rowCount(QModelIndex{}) - 1),
{Qt::DisplayRole});
break;
}
}
return QAbstractListModel::event(ev);
}
Unfortunately, I can't test all the cases at the moment (and I don't have an access to the Qt 5.11.2), but I think this should work:
ComboBox {
textRole: "text"
Layout.fillWidth: true
displayText: qsTr(currentText)
model: ListModel {
ListElement{text: "None"}
ListElement{text: "Subpanel"}
ListElement{text: "All"}
}
delegate: Button {
width: ListView.view.width
text: qsTr(model.text)
}
}
Or, another way is to re-create a model when language is changed:
ComboBox {
id: combobox
textRole: "text"
Layout.fillWidth: true
model: ListModel {
dynamicRoles: true
}
Component.onCompleted: {
reload()
}
Connections {
target: trans // this is a translator from a git project you are referring to
onLanguageChanged: {
combobox.reload()
}
}
function reload() {
var i = combobox.currentIndex
combobox.model = [
{text: qsTr("None")},
{text: qsTr("Subpanel")},
{text: qsTr("All")}
]
combobox.currentIndex = i
}
}

Extending QFileSystemModel by a bool variable

I would like to extend the QFileSystemModel. I would like to have a custom role IsSelectedRole where I can store whether a bool value which stores whether a file got selected in a QML TreeView. More precisely, I don't know how to set the setData and data function of my QFileSystemModel derived class, like where to store the data. I guess making my own tree of bool variables should work, but I hope that there is an easier way.
Qt already has a model that stores item selections: QItemSelectionModel. This model is used by the widget views as well as the QML views. All that's left for you to do is to overlay the data from that model on top of the data from QFileSystemModel, in a viewmodel.
You definitely shouldn't be deriving from QFileSystemModel. The viewmodel maintains the state for a single view only, and should be a proxy that overlays your role on top of the underlying model.
The proxy doesn't have to assume anything about the underlying model, and can work on any model, not just the QFileSystemModel.
For example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/filesystem-model-select-50132273
#include <QtWidgets>
#include <algorithm>
class SelectionProxy : public QIdentityProxyModel {
Q_OBJECT
Q_PROPERTY(QItemSelectionModel* selectionModel
READ selectionModel WRITE setSelectionModel NOTIFY selectionModelChanged)
Q_PROPERTY(QVector<int> roles READ roles WRITE setRoles)
using self_t = SelectionProxy;
using base_t = QIdentityProxyModel;
using model_t = QItemSelectionModel;
QPointer<QItemSelectionModel> m_model;
QVector<QMetaObject::Connection> m_modelConnections;
QVector<int> m_roles{IsSelectedRole};
QModelIndex topLeft() const {
return sourceModel()->index(0, 0);
}
QModelIndex bottomRight() const {
return sourceModel()->index(sourceModel()->rowCount()-1, sourceModel()->columnCount()-1);
}
void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) {
auto sel = selected;
sel.merge(deselected, QItemSelectionModel::Select);
for (auto &range : qAsConst(sel)) {
auto topLeft = mapFromSource(range.topLeft());
auto bottomRight = mapFromSource(range.bottomRight());
emit dataChanged(topLeft, bottomRight, m_roles);
}
}
void onModelChanged(QAbstractItemModel *model) {
setSourceModel(model);
}
bool check(const QModelIndex &index, int role) const {
return index.isValid() && m_model && m_roles.contains(role);
}
public:
static constexpr int IsSelectedRole = Qt::UserRole + 44;
SelectionProxy(QObject *parent = {}) : QIdentityProxyModel(parent) {}
QItemSelectionModel *selectionModel() const { return m_model; }
virtual void setSelectionModel(QItemSelectionModel *model) {
if (model == m_model) return;
for (auto &conn : m_modelConnections)
disconnect(conn);
m_model = model;
m_modelConnections = {
connect(m_model, &model_t::selectionChanged, this, &self_t::onSelectionChanged),
connect(m_model, &model_t::modelChanged, this, &self_t::onModelChanged) };
setSourceModel(model->model());
emit selectionModelChanged(m_model);
}
Q_SIGNAL void selectionModelChanged(QItemSelectionModel *);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!check(index, role))
return base_t::data(index, role);
return m_model->isSelected(mapToSource(index));
}
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!check(index, role))
return base_t::setData(index, value, role);
using Sel = QItemSelectionModel;
m_model->select(mapToSource(index), value.toBool() ? Sel::SelectCurrent : (Sel::Deselect | Sel::Current));
return true;
}
QVector<int> roles() const { return m_roles; }
void setRoles(QVector<int> roles) {
std::sort(roles.begin(), roles.end());
if (roles == m_roles)
return;
std::swap(roles, m_roles);
if (!m_model)
return;
QVector<int> allRoles;
std::merge(roles.begin(), roles.end(), m_roles.begin(), m_roles.end(), std::back_inserter(allRoles));
emit dataChanged(topLeft(), bottomRight(), allRoles);
}
void setRole(int role) {
setRoles({role});
}
};
#include "main.moc"
A simple demonstrator sets up two views of the filesystem model; the bottom view shows true/false indicating the selection status of the first view.
int main(int argc, char **argv) {
QApplication app{argc, argv};
QWidget win;
QVBoxLayout layout{&win};
QTreeView left, right;
layout.addWidget(&left);
layout.addWidget(&right);
QFileSystemModel model;
SelectionProxy selProxy;
model.setRootPath(QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
auto rootPathIndex = model.index(model.rootPath());
left.setModel(&model);
left.setSelectionMode(QAbstractItemView::MultiSelection);
left.setRootIndex(rootPathIndex);
selProxy.setRole(Qt::DisplayRole);
selProxy.setSelectionModel(left.selectionModel());
right.setModel(&selProxy);
right.setRootIndex(selProxy.mapFromSource(rootPathIndex));
for (int col : {1,2,3})
right.hideColumn(col);
win.show();
return app.exec();
}

Cannot refresh QT's view while changing model

I am learning how to integrate qml with c++.
I've implemented a custom model class StringListModel, which inherits QAbstratListModel.
And, I have a main.qml to use StringListModel.
QML view can show initial values correctly.
I have another thread to change model periodically.
I do use beginResetModel() and endResetModel() to indicate model changed.
However, while the model keeps been changed, the view didn't update.
Here is my source code.
Please teach me what went wrong.
THANKS!
=== main.qml ===
Rectangle {
width: 360
height: 360
Grid {
id: gridview
columns: 2
spacing: 20
Repeater {
id: repeater
model: StringListModel {
id:myclass
}
delegate: Text {
text: model.title
}
}
}
=== custom class.h ===
class StringListModel : public QAbstractListModel{
Q_OBJECT
public:
StringListModel(QObject *parent = 0);
int rowCount(const QModelIndex &parent = QModelIndex()) const;
QVariant data(const QModelIndex &index, int role) const;
QHash<int, QByteArray> roleNames() const;
void newItem();
private:
QStringList stringList;
};
class UpdateThread : public QThread {
Q_OBJECT
StringListModel *mMyClass;
public:
UpdateThread(StringListModel * myClass);
protected:
void run();
};
=== custom class.cpp ===
StringListModel::StringListModel(QObject *parent) : QAbstractListModel(parent)
{
stringList << "one" << "two" << "three";
QThread *thread = new UpdateThread(this);
thread->start();
}
int StringListModel::rowCount(const QModelIndex &parent) const
{
return stringList.count();
}
QVariant StringListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() >= stringList.size())
return QVariant();
if (role == Qt::UserRole + 1)
return stringList.at(index.row());
else
return QVariant();
}
QHash<int, QByteArray> StringListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[Qt::UserRole + 1] = "title";
return roles;
}
void StringListModel::newItem()
{
qDebug() << "size: " << stringList.size();
beginResetModel();
stringList << "new";
endResetModel();
}
UpdateThread::UpdateThread(StringListModel * myClass)
{
mMyClass = myClass;
}
void UpdateThread::run()
{
while (true) {
mMyClass->newItem();
msleep(1000);
}
}
You're dutifully ignoring the matters of synchronizing access to the model. When you have any object that's accessed from more than one thread (even something as "simple" as a raw pointer), you must deal with the ramifications of such access.
I suggest you don't mess with threads unless you have measurements showing that you'll benefit.

Resources