QTableModel data not being called - qt

I have a custom table model that extends QAbstractTableModel. I've implemented data(), rowCount() and columnCount().
This is my model:
point_data_view_model::point_data_view_model(QObject *parent) :
QAbstractTableModel(parent)
{
}
int point_data_view_model::rowCount(const QModelIndex & /*parent*/) const
{
return 2;
}
int point_data_view_model::columnCount(const QModelIndex & /*parent*/) const
{
return 3;
}
QVariant point_data_view_model::data(const QModelIndex &index, int role) const
{
if (role == Qt::DisplayRole)
{
return QString("Row%1, Column%2")
.arg(index.row() + 1)
.arg(index.column() +1);
}
return QVariant();
}
But when I want to show the table view in my form, nothing is displayed.
This is the form's constructor:
SelectWindow::SelectWindow(QWidget *parent) :
QMainWindow(parent)
,ui(new Ui::SelectWindow)
{
ui->setupUi(this);
point_data_view_model myModel;
ui->tableView->setModel(&myModel);
myModel.timerHit();
ui->tableView->show();
}
In other words, my data function in the model is not called.
What am I missing?

point_data_view_model myModel;
That's a big problem. myModel is local to that constructor and will get destroyed when the function exits. You need to store that model somewhere that persists as long as you need to use it - a member variable of your SelectWindow should be appropriate.

You're creating myModel on stack. It gets deleted immediately after calling ui->tableView->show();. So the view loses the model and doesn't have a chance to call data. You should create myModel using new:
point_data_view_model* myModel = new point_data_view_model();

Related

How to add an extra item to a QML ComboBox which is not in the model?

I have a QML ComboBox which has a QAbstractListModel attached to it. Something like this:
ComboBox {
model: customListModel
}
And I would like it to display an extra item in the drop down list which is not in the model.
For example, let's say there are two items in the customListModel: Apple and Orange.
And in the drop down list it should display the following options:
Select all
Apple
Orange
I can't add it to the model because it contains custom objects and I use this model a couple of other places in the program and it would screw up everything.
How can I add this "Select all" option to the ComboBox???
One way to do it is to create a proxy model of some sort. Here's a couple ideas:
You could derive your own QAbstractProxyModel that adds the "Select All" item to the data. This is probably the more complex option, but also the more efficient. An example of creating a proxy this way can be found here.
You could also make your proxy in QML. It would look something like this:
Combobox {
model: ListModel {
id: proxyModel
ListElement { modelData: "Select All" }
Component.onCompleted: {
for (var i = 0; i < customListModel.count; i++) {
proxyModel.append(customModel.get(i);
}
}
}
}
A solution is to customize the popup to add a header.
You could implement the entire popup component, or exploit the fact that its contentItem is a ListView and use the header property:
ListModel {
id: fruitModel
ListElement {
name: "Apple"
}
ListElement {
name: "Orange"
}
}
ComboBox {
id: comboBox
model: fruitModel
textRole: "name"
Binding {
target: comboBox.popup.contentItem
property: "header"
value: Component {
ItemDelegate {
text: "SELECT ALL"
width: ListView.view.width
onClicked: doSomething()
}
}
}
}
I found myself wanting to do something similar recently and was surprised that there's no simple way to do it; there are ways to do it but not really a dedicated API for it, not even for widgets.
I've tried both of the answers mentioned here and would like to summarise them, as well provide complete examples for each approach. My requirement was to have a "None" entry, so my answer is in that context, but you can easily replace that with "Select All".
Using QSortFilterProxyModel
The C++ code for this is based on this answer by #SvenA (thank you for sharing working code!).
Pros:
To avoid repeating myself too much: the pros for this are the absence of the cons in the other approach. For example: key navigation works, no need to touch any styling stuff, etc. These two alone are pretty big reasons why you would want to choose this approach, even if it does mean extra work writing (or copy-pasting :)) the model code (which is something you will only have to do once).
Cons:
Since you are using the 0 index for the "None" entry, you have to treat it as a special entry, unlike the -1 index, which is already established as meaning no item is selected. This means a little extra JavaScript code to handle that index being selected, but the header approach also requires this when it's clicked.
It is a lot of code for one extra entry, but again; you should only have to do it once, and then you can reuse it.
It is an extra level of indirection in terms of model operations. Assuming most ComboBox models are relatively small, this is not a problem. In practice I doubt this would be a bottleneck.
Conceptually a "None" entry could be considered a kind of metadata; i.e. it doesn't belong in the model itself, so this solution could be seen as less conceptually correct.
main.qml:
import QtQuick 2.15
import QtQuick.Controls 2.15
import App 1.0
ApplicationWindow {
width: 640
height: 480
visible: true
title: "\"None\" entry (proxy) currentIndex=" + comboBox.currentIndex + " highlightedIndex=" + comboBox.highlightedIndex
ComboBox {
id: comboBox
textRole: "display"
model: ProxyModelNoneEntry {
sourceModel: MyModel {}
}
}
}
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QSortFilterProxyModel>
#include <QDebug>
class MyModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit MyModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
private:
QVector<QString> mData;
};
MyModel::MyModel(QObject *parent) :
QAbstractListModel(parent)
{
for (int i = 0; i < 10; ++i)
mData.append(QString::fromLatin1("Item %1").arg(i + 1));
}
int MyModel::rowCount(const QModelIndex &) const
{
return mData.size();
}
QVariant MyModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return QVariant();
switch (role) {
case Qt::DisplayRole:
return mData.at(index.row());
}
return QVariant();
}
class ProxyModelNoneEntry : public QSortFilterProxyModel
{
Q_OBJECT
public:
ProxyModelNoneEntry(QString entryText = tr("(None)"), QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override;
QModelIndex mapToSource(const QModelIndex &proxyIndex) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
private:
QString mEntryText;
};
ProxyModelNoneEntry::ProxyModelNoneEntry(QString entryText, QObject *parent) :
QSortFilterProxyModel(parent)
{
mEntryText = entryText;
}
int ProxyModelNoneEntry::rowCount(const QModelIndex &/*parent*/) const
{
return QSortFilterProxyModel::rowCount() + 1;
}
QModelIndex ProxyModelNoneEntry::mapFromSource(const QModelIndex &sourceIndex) const
{
if (!sourceIndex.isValid())
return QModelIndex();
else if (sourceIndex.parent().isValid())
return QModelIndex();
return createIndex(sourceIndex.row()+1, sourceIndex.column());
}
QModelIndex ProxyModelNoneEntry::mapToSource(const QModelIndex &proxyIndex) const
{
if (!proxyIndex.isValid())
return QModelIndex();
else if (proxyIndex.row() == 0)
return QModelIndex();
return sourceModel()->index(proxyIndex.row() - 1, proxyIndex.column());
}
QVariant ProxyModelNoneEntry::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return QVariant();
if (index.row() == 0) {
if (role == Qt::DisplayRole)
return mEntryText;
else
return QVariant();
}
return QSortFilterProxyModel::data(createIndex(index.row(),index.column()), role);
}
Qt::ItemFlags ProxyModelNoneEntry::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
if (index.row() == 0)
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
return QSortFilterProxyModel::flags(createIndex(index.row(),index.column()));
}
QModelIndex ProxyModelNoneEntry::index(int row, int column, const QModelIndex &/*parent*/) const
{
if (row > rowCount())
return QModelIndex();
return createIndex(row, column);
}
QModelIndex ProxyModelNoneEntry::parent(const QModelIndex &/*child*/) const
{
return QModelIndex();
}
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<ProxyModelNoneEntry>("App", 1, 0, "ProxyModelNoneEntry");
qmlRegisterType<MyModel>("App", 1, 0, "MyModel");
qmlRegisterAnonymousType<QAbstractItemModel>("App", 1);
QQmlApplicationEngine engine;
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
#include "main.moc"
Using ListView's header
Pros:
The -1 index -- which is already established as meaning no item is selected -- can be used to refer to the "None" entry.
No need to set up a QSortFilterProxyModel-subclass in C++ and expose it to QML.
Conceptually a "None" entry could be considered a kind of metadata; i.e. it doesn't belong in the model itself, so this solution could be seen as more conceptually correct.
Cons:
Not possible to select the "None" entry with arrow key navigation. I briefly tried working around this (see commented-out code), but had no success.
Have to mimic the "current item" styling that the delegate component has. What that entails depends on the style; if you wrote the style yourself, then you could move the delegate component into its own file and reuse it for the header. However, if you you're using someone else's style, you can't do that, and will have to write it from scratch (though, you will usually only need to do this once). For example, for the Default ("Basic", in Qt 6) style it means:
Setting an appropriate font.weight.
Setting highlighted.
Setting hoverEnabled.
Have to set displayText yourself.
Since the header item isn't considered a ComboBox item, the highlightedIndex property (which is read-only) will not account for it. Can be worked around by setting highlighted to hovered in the delegate.
Have to do the following when the header is clicked:
Set currentIndex (i.e. to -1 on click).
Close the ComboBox's popup.
Emit activated() manually.
main.qml:
import QtQuick 2.0
import QtQuick.Controls 2.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: "\"None\" entry (header) currentIndex=" + comboBox.currentIndex + " highlightedIndex=" + comboBox.highlightedIndex
Binding {
target: comboBox.popup.contentItem
property: "header"
value: Component {
ItemDelegate {
text: qsTr("None")
font.weight: comboBox.currentIndex === -1 ? Font.DemiBold : Font.Normal
palette.text: comboBox.palette.text
palette.highlightedText: comboBox.palette.highlightedText
highlighted: hovered
hoverEnabled: comboBox.hoverEnabled
width: ListView.view.width
onClicked: {
comboBox.currentIndex = -1
comboBox.popup.close()
comboBox.activated(-1)
}
}
}
}
ComboBox {
id: comboBox
model: 10
displayText: currentIndex === -1 ? qsTr("None") : currentText
onActivated: print("activated", index)
// Connections {
// target: comboBox.popup.contentItem.Keys
// function onUpPressed(event) { comboBox.currentIndex = comboBox.currentIndex === 0 ? -1 : comboBox.currentIndex - 1 }
// }
}
}
Conclusion
I agree with the idea that "None" and "Select All" are more metadata than model data. In that sense I prefer the header approach. In my particular use case that made me look into this, I don't allow key navigation and I have already overridden the delegate property of ComboBox, so I can reuse that code for the header.
However, if you need key navigation, or you don't want to have to reimplement the delegate for the header, the QSortFilterProxyModel approach would be more practical.

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();
}

QML ListView is not updated on model reset

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 !

Sort a QAbstractListModel derived model by role in QML ListView

I've created a QAbstractListModel derived model based on an underlying QHash. Since I need to use the model in QML, I cannot make use of the sorting functionality Qt widgets and views have integrated.
I tried using a QSortFilterProxyModel but it doesn't seem to work with my model. Getting the model to properly work in QML wasn't tedious enough, and now I am stuck on sorting.
Any suggestions are appreciated.
Here is the model source:
typedef QHash<QString, uint> Data;
class NewModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
NewModel(QObject * parent = 0) : QAbstractListModel(parent) {}
enum Roles {WordRole = Qt::UserRole, CountRole};
QHash<int, QByteArray> roleNames() const {
QHash<int, QByteArray> roles;
roles[WordRole] = "word";
roles[CountRole] = "count";
return roles;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const {
if (index.row() < 0 || index.row() >= m_data.size()) return QVariant();
Data::const_iterator iter = m_data.constBegin() + index.row();
switch (role) {
case WordRole:
return iter.key();
case CountRole:
return iter.value();
} return QVariant();
}
int rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent)
return m_data.size();
}
int count() const { return m_data.size(); }
public slots:
void append(const QString &word) {
bool alreadyThere = m_data.contains(word);
if (alreadyThere) m_data[word]++;
else m_data.insert(word, 1);
Data::const_iterator iter = m_data.find(word);
uint position = delta(iter);
if (alreadyThere) {
QModelIndex index = createIndex(position, 0);
emit dataChanged(index, index);
} else {
beginInsertRows(QModelIndex(), position, position);
endInsertRows();
emit countChanged();
}
}
void prepend(const QString &word) {
if (m_data.contains(word)) m_data[word]++;
else m_data.insert(word, 1);
}
signals:
void countChanged();
private:
uint delta(Data::const_iterator i) {
uint d = 0;
while (i != m_data.constBegin()) { ++d; --i; }
return d;
}
Data m_data;
};
Here is "trying" to sort it:
NewModel model;
QAbstractItemModel * pm = qobject_cast<QAbstractItemModel *>(&model);
QSortFilterProxyModel proxy;
proxy.setSourceModel(pm);
proxy.setSortRole(NewModel::WordRole);
proxy.setDynamicSortFilter(true);
Alas, the proxy works as a model, but it doesn't sort the entries.
If you enable QSortFilterProxyModel::setDynamicSortFilter(true), you need to call QSortFilterProxyModel::sort(...) function once to let the proxy know which way to sort.
With that, any time the model is updated the proxy will sort everything again just automatically.
proxy.setDynamicSortFilter(true);
proxy.sort(0);
First of all, There's no need for qobject_cast<QAbstractItemModel *> downcasting -- the NewModel is a derived class of the QAbstractItemModel and the polymorphism principle says that you can use a subclass everywhere where a parent class is applicable.
Second, your prepend method does not use beginInsertRows and endInsertRows. That's a violation of the MVC API. You'll get data corruption in the attached views and proxy models if you use it this way.
Third, you haven't mentioned whether you're actually using your proxy model as the model for the attached view :).
Finally, you are using QHash as a backing store of your data with QHash::iterator for insertion. That's an itneresting solution, but something which just cannot work -- an insertion or removal can cause the hash table to grow/shrink, which means changing all data you publish via your model indexes. This is just not going to work. Don't use QHash when you need a stable order. The O(n) complexity of your delta method should be interpreted as a warning; this is a wrong approach.
Have a Look at https://github.com/oKcerG/SortFilterProxyModel. This project exposes the functionality of QSortFilterProxyModel nicely to QML. I used it in different projects and it junst worked. However if you want to implement your own solution it's something to get your ideas.

Resources