Get model data by role name - qt

There's a model (currently it's derived from QAbstractItemModel, but it can also be any model that provides named data roles) from which I need to get data, knowing a row/column and a role name. I can get index by row and column:
const index = mYModel.index(row, column);
I can also get data by index and role:
const data = myModel.data(index, role);
I haven't found any way to somehow find out the role by its name. Am I missing something or is this impossible at all?
Here's the pseudocode:
// MyModel.h
class MyModel: public QAbstractItemModel
{
public:
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> roleNames;
roleNames[Qt::UserRole + 1] = "someRole";
return roleNames;
}
};
// Item.qml
Item {
ListView {
model: myModel
delegate: myDelegate
}
Button {
onClicked: {
const rowIndex = Math.random(myModel.count);
// here I need to get data for item at
// row rowIndex, column 0 and role name "someRole"
}
}
}

Since your model is used in a ListView, I would use the itemAtIndex function of that ListView:
ListView {
id: theList
model: myModel
delegate: UnknownItem {
property var theModel: model //have to add this property
}
}
Button {
onClicked: {
let item = theList.itemAtIndex(theList.currentIndex)
if(item) {
console.log("clicked", item.theModel.title)
}
}
}
Note that this works with integer index, not on the QModelIndex you have in your question.

I don't see any built-in features to do that. So the easiest solution for me is to create a helper class that will retrieve the necessary data from the given model.
Helper class:
class ModelHelper : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariant model READ model WRITE setModel)
public:
explicit ModelHelper(QObject *parent = nullptr)
: QObject(parent)
{}
QVariant model() const
{
return m_model;
}
void setModel(const QVariant &model)
{
m_model = model;
}
Q_INVOKABLE QVariant data(int row, const QString &roleName) const
{
if (const QAbstractItemModel *model = m_model.value<QAbstractItemModel*>()) {
const QHash<int, QByteArray> roleNames = model->roleNames();
for (auto i = roleNames.cbegin(); i != roleNames.cend(); ++i) {
if (i.value() == roleName) {
return model->data(model->index(row, 0), i.key());
}
}
}
return QVariant();
}
private:
QVariant m_model;
};
//...
qmlRegisterType<ModelHelper>("Test.ModelUtils", 1, 0, "ModelHelper");
How to use:qml
import Test.ModelUtils 1.0
//...
ListModel {
id: listModel
ListElement { name: "John" }
ListElement { name: "Mike" }
}
ModelHelper {
id: modelHelper
model: listModel
}
Button {
text: "Print Data"
onClicked: {
const data = [];
for (let i = 0; i < listModel.count; ++i) {
data.push(modelHelper.data(i, "name"));
}
console.log(data);
}
}

Related

TableView does't update, but list into model is full

I am trying to update a table with data coming from an external process (into QML I lunch a CLI tool that returns a lot of strings and with that, I update the table). I am using TableView to show these data, and I wrote a Model class and a List class: the Model class extend QAbstractTableModel, indeed the list class extend QObject.
I am sure that the list is full (I print the contents with qDebug()), but the table is always empty! Can you help me?
I added some debug prints and I can see that the rowCount function of the model is called a lot of time and return the correct length of the list, but the data function is called only when index.row() is equal to zero! I don't understand why!
#ifndef PAYEELIST_H
#define PAYEELIST_H
#include <QObject>
#include <QList>
#include "payee.h"
class PayeeList : public QObject
{
Q_OBJECT
public:
explicit PayeeList(QObject *parent = nullptr);
QList<Payee> items() const;
// bool setItemAt (int index, const Payee &item);
void clear (void);
void append (const Payee& item);
signals:
void preItemAppended ();
void postItemAppended ();
void preItemRemoved (int index);
void postItemRemoved ();
//public slots:
// void appendItem ();
private:
QList<Payee> mItems;
};
#endif // PAYEELIST_H
#include "payeelist.h"
PayeeList::PayeeList(QObject *parent) : QObject(parent)
{
}
QList<Payee> PayeeList::items() const
{
return mItems;
}
void PayeeList::clear()
{
mItems.clear();
}
void PayeeList::append(const Payee &item)
{
emit preItemAppended();
mItems.append(item);
emit postItemAppended();
}
#ifndef PAYEEMODEL_H
#define PAYEEMODEL_H
#include <QAbstractTableModel>
#include "payeelist.h"
//class PayeeList;
class PayeeModel : public QAbstractTableModel
{
Q_OBJECT
Q_PROPERTY(PayeeList *list READ list WRITE setList)
public:
explicit PayeeModel(QObject *parent = nullptr);
enum
{
HeadingRole = Qt::UserRole + 1,
DataRole
};
enum
{
IdColumn = 0,
NameColumn,
TypeColumn
};
// Basic functionality:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
virtual QHash<int, QByteArray> roleNames() const override;
PayeeList *list (void) const;
void setList (PayeeList *list);
private:
PayeeList *mList;
};
#endif // PAYEEMODEL_H
#include "payeemodel.h"
PayeeModel::PayeeModel(QObject *parent)
: QAbstractTableModel(parent),
mList(nullptr)
{
}
int PayeeModel::rowCount(const QModelIndex &parent) const
{
// For list models only the root node (an invalid parent) should return the list's size. For all
// other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
if (parent.isValid() || !mList)
return 0;
qDebug() << "LIST SIZE:" << mList->items().size();
return mList->items().size() + 1;
}
int PayeeModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid() || !mList)
return 0;
return 3;
}
QVariant PayeeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !mList)
return QVariant();
switch (role)
{
case DataRole:
{
qDebug() << "INDEX ROW:" << index.row();
if (index.row() > 0)
{
const Payee item = mList->items().at(index.row() - 1);
switch (index.column())
{
case IdColumn:
return QString::number(item.id());
break;
case NameColumn:
return QString(item.name());
break;
case TypeColumn:
return QString(item.type().name());
break;
}
}
else
{
switch (index.column())
{
case IdColumn:
return QString("Id");
break;
case NameColumn:
return QString("Name");
break;
case TypeColumn:
return QString("Type");
break;
}
}
}
break;
case HeadingRole:
if (index.row() == 0)
{
return true;
}
else
{
return false;
}
break;
default:
break;
}
return QVariant();
}
QHash<int, QByteArray> PayeeModel::roleNames() const
{
QHash<int, QByteArray> names;
names[HeadingRole] = "tableheading";
names[DataRole] = "tabledata";
return names;
}
PayeeList *PayeeModel::list (void) const
{
return mList;
}
void PayeeModel::setList (PayeeList *list)
{
beginResetModel();
if (mList)
{
mList->disconnect(this);
}
mList = list;
if (mList)
{
connect(mList, &PayeeList::preItemAppended, this, [=]()
{
const int index = mList->items().size();
beginInsertRows(QModelIndex(), index, index);
});
connect(mList, &PayeeList::postItemAppended, this, [=]()
{
endInsertRows();
});
}
endResetModel();
}
The QML file is:
import QtQuick 2.12
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import PlutoTool 1.0
Rectangle {
id: payeePage
Layout.fillWidth: true
property var title: "TITLE"
TableView {
columnSpacing: 1
rowSpacing: 1
anchors.fill: parent
clip: false
property var columnWidths: [50, (parent.width - 220), 150]
columnWidthProvider: function (column) { return columnWidths[column] }
model: PayeeModel {
list: lPayeeList
}
delegate: Rectangle {
implicitWidth: 200
implicitHeight: 30
border.color: "black"
border.width: 0
color: (tableheading == true) ? "#990033":"#EEEEEE"
Text {
text: model.tabledata
color: (tableheading == true) ? "#FFFFFF":"#000000"
font.bold: (tableheading == true) ? true : false
anchors.centerIn: parent
}
Component.onCompleted: {
console.log(model.tabledata);
}
}
}
}

QSqlQueryModel TableView custom delegate

I have a QSqlQueryModel and a TableView to display the data from the model. The code and output data results are fine. But, I just want to display an image in front of each row in the TableView. However, with my current QML code, the image is repeated along with the elements in my table columns. I have added example screenshots for reference
Current Output (Screenshot)
What I want
My Code is as below
Test.qml
import QtQuick 2.12
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.3
Page {
id : somepageid
TableView{
id: testTable
model: QueryModel
height: 500
width: 400
delegate:
Row{
Image {
id: statusImg
height: 18
width: 18
source: "../../../Images/icons/tick.png"
}
Text {
text: display
}
}
}
}
QueryModel.cpp
#include "querymodel.h"
QueryModel::QueryModel(QObject *parent): QSqlQueryModel(parent)
{
}
void QueryModel::setQuery(const QString &query, const QSqlDatabase &db)
{
QSqlQueryModel::setQuery(query, db);
generateRoleNames();
}
void QueryModel::setQuery(const QSqlQuery &query)
{
QSqlQueryModel::setQuery(query);
generateRoleNames();
}
QVariant QueryModel::data(const QModelIndex &index, int role) const
{
QVariant value;
if(role < Qt::UserRole) {
value = QSqlQueryModel::data(index, role);
}
else {
int columnIdx = role - Qt::UserRole - 1;
QModelIndex modelIndex = this->index(index.row(), columnIdx);
value = QSqlQueryModel::data(modelIndex, Qt::DisplayRole);
}
return value;
}
QHash<int, QByteArray> QueryModel::roleNames() const
{
return {{Qt::DisplayRole, "display"}};
}
void QueryModel::callSql()
{
QSqlDatabase dbMysql = QSqlDatabase::database();
this->setQuery(this->tmpSql(), dbMysql);
}
QString QueryModel::tmpSql() const
{
return m_tmpSql;
}
void QueryModel::setTmpSql(QString tmpSql)
{
if (m_tmpSql == tmpSql)
return;
m_tmpSql = tmpSql;
emit tmpSqlChanged(m_tmpSql);
}
void QueryModel::generateRoleNames()
{
m_roleNames.clear();
for( int i = 0; i < record().count(); i ++) {
m_roleNames.insert(Qt::UserRole + i + 1, record().fieldName(i).toUtf8());
}
}
A possible solution is to use a Loader:
// ...
delegate: Row{
Loader{
active: model.column === 0
sourceComponent: Image {
id: statusImg
height: 18
width: 18
source: "../../../Images/icons/tick.png"
}
}
Text {
text: model.display
}
}
// ...

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

returning a custom QObject subclass from QAbstractListModel and using it in a ListView

How can I return a custom QObject sub-class from QAbstractListModel and use it in a QML ListView.
I tried to return the objects as display role and I use in my qml display.property to access properties, it works fine but I saw on some posts people using model as the qobject from qml and accessing properties as model.property. Am I missing something?.
Another question: If I want to expose the object at the ListView level and using it to set some other panel like a master-view detail is exposing the role (in my case display) as a variant property in the delegate and setting it at the listview level with the onCurrentItemChanged signal is the correct way to do it ??
this is what I am trying but it does not work:
#ifndef NOTE_H
#define NOTE_H
#include <QObject>
class Note : public QObject
{
Q_OBJECT
Q_PROPERTY(QString note READ note WRITE setNote NOTIFY noteChanged)
Q_PROPERTY(int id READ id WRITE setId NOTIFY idChanged)
QString m_note;
int m_id;
public:
explicit Note(QObject *parent = 0);
Note(QString note, int id, QObject *parent = 0);
QString note() const
{
return m_note;
}
int id() const
{
return m_id;
}
signals:
void noteChanged(QString note);
void idChanged(int id);
public slots:
void setNote(QString note)
{
if (m_note == note)
return;
m_note = note;
emit noteChanged(note);
}
void setId(int id)
{
if (m_id == id)
return;
m_id = id;
emit idChanged(id);
}
};
#endif // NOTE_H
the view model:
#ifndef NOTESVIEWMODEL_H
#define NOTESVIEWMODEL_H
#include <QAbstractListModel>
#include <QVector>
#include "note.h"
class NotesViewModel : public QAbstractListModel
{
Q_OBJECT
QVector<Note*> notes;
public:
NotesViewModel();
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override;
};
#endif // NOTESVIEWMODEL_H
implementation of the view model:
NotesViewModel::NotesViewModel()
{
notes.append(new Note("note 1", 1));
notes.append(new Note("note 2", 2));
notes.append(new Note("note 3", 3));
notes.append(new Note("note 4", 4));
notes.append(new Note("note 5", 5));
}
QVariant NotesViewModel::data(const QModelIndex &index, int role) const
{
qDebug() << "fetching data : " << index.row();
if(!index.isValid()) return QVariant();
if(index.row() >= 5) return QVariant();
if(role == Qt::DisplayRole)
return QVariant::fromValue(notes[index.row()]);
return QVariant();
}
int NotesViewModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return notes.count();
}
Since you've already answered your second question, let me take on the first one, i.e. display.propertyName vs model.propertyName
Basically the first one is just a short hand for model.display.propertyName, i.e. the "display" property of the model's data at the given index is being accessed. In your case that returns an object, which has properties on its on.
The model.propertyName could also be written as just propertyName, meaning the model's data() method is called with "role" being the numerical equivalent for "propertyName".
This mapping from "propertyName" to its numerical equivalent is done with the QAbstractItemModel::roleNames() method.
Its default implementation has some base mappings such as mapping "display" to Qt::DisplayRole.
I played a little with the qml ListView trying to figure out how to expose the currentItem data to the outside world. This is how I managed to do it, maybe it will be of use for newbies like me.
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
ListModel {
id: modell
ListElement {
fname: "houssem"
age: 26
}
ListElement {
fname: "Anna"
age: 26
}
ListElement {
fname: "Nicole"
age: 26
}
ListElement {
fname: "Adam"
age: 27
}
}
ListView {
id: lv
height: 100
width: 200
clip: true
model: modell
property string selectedName: currentItem.name
delegate: Component {
Item {
id: mainItem
width: ListView.view.width
height: 80
property string name: fname
Text {
text: "name " + fname + " age " + age
}
MouseArea {
anchors.fill: parent
onClicked: mainItem.ListView.view.currentIndex = index
}
}
}
}
Text {
anchors.right: parent.right
text: lv.selectedName
}
}
As you can see in the code, to expose the data of currentItem I had just to declare properties in the delegate Item (name property of type string in the present example) and then bind a property on ListView (selectedName in this example) to the currentItem.property, this way the property on the ListView will be updated automatically when I select other items from the list and I will have access to this items form other Items of the UI.

Exposing c++ model in qml

I have created this c++ class and exposed to a Qml ListView however, it has some problems. I can view that there are items in the list, however I am not able to see any data (I see 25 empty buttons). In the console, the following message is displayed: "type is undefined". I have checked and type should correctly a full string;
ListView{
id: listView
anchors.fill: parent
model: postsModel
delegate: Component{
Button{
text: type
}
}
}
#include "listmodel.h"
ListModel::ListModel(QObject *parent)
: QAbstractListModel(parent)
{
}
QString ListItem::type() const
{
return m_type;
}
QVariantMap ListItem::dataMap() const
{
return m_dataMap;
}
void ListModel::addElement(const ListItem& element)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_elementsList << element;
endInsertRows();
}
QVariant ListModel::data(const QModelIndex & index, int role) const
{
if (index.row() < 0 || index.row() >= m_elementsList.count())
return QVariant();
const ListItem &item= m_elementsList[index.row()];
if (role == TypeRole)
return item.type();
else if (role == DataRole)
return item.dataMap();
return QVariant();
}
int ListModel::rowCount(const QModelIndex & parent) const {
return m_elementsList.count();
}
QHash<int, QByteArray> ListModel::roleNames()
{
QHash<int, QByteArray> roles;
roles[TypeRole] = "type";
roles[DataRole] = "dataMap";
return roles;
}
Alright, I found the solution to my problem. The problem was that the ListView was not being updated by the model that I implemented. This was due to my mistake consisting in not calling beginInsertRows() and endInsertRows() functions.

Resources