QML TableView refresh in realtime only updates UI every 1 second - qt

I have a task of showing big amount of data in realtime (as close to that as possible) with UI update every 10 to 100ms (best to worst values), I've managed to create a test model generating random numbers and populating the table view with new set of random values with a Timer. I tried to set different time intervals from 1 to 100 ms, and I can see the timer fires and the new set of data is created, but the UI updates strictly in 1 second every time despite the timer interval value.
Can you guide me on how to handle UI updates in less than 1 second.
I tried different amount of data from tables of 50x1000 to just 50x50. Every time I get UI update rate of 1 second.
It is based on the "Game Of Life" Qt example, so some of the elements are just not used and are obsolete, but as soon as they are "disabled" I think they don't make any influence on the rest of the code and the problem itself.
You can see that in nextStep() method I invoke timestamp logging to console and I can see in the output that the method is invoked according to the timer, but the UI only updates visually every second.
main.qml
ApplicationWindow {
id: root
visible: true
width: 760
height: 810
minimumWidth: 475
minimumHeight: 300
color: "#09102B"
title: qsTr("Conway’s Game of Life")
//! [tableview]
TableView {
id: tableView
anchors.fill: parent
rowSpacing: 1
columnSpacing: 1
ScrollBar.horizontal: ScrollBar {}
ScrollBar.vertical: ScrollBar {}
delegate: Rectangle {
id: cell
implicitWidth: 45
implicitHeight: 15
color: model.value > 100 ? "#f3f3f4" : "#b5b7bf"
Label {
width: parent.width
height: parent.height
text: model.value
}
}
//! [tableview]
//! [model]
model: GameOfLifeModel {
id: gameOfLifeModel
}
//! [model]
//! [scroll]
contentX: 0;
contentY: 0;
//! [scroll]
}
footer: Rectangle {
signal nextStep
id: footer
height: 50
color: "#F3F3F4"
RowLayout {
anchors.centerIn: parent
//! [next]
Button {
text: qsTr("Next")
onClicked: gameOfLifeModel.nextStep()
}
//! [next]
Item {
width: 50
}
Button {
text: timer.running ? "❙❙" : "▶️"
onClicked: timer.running = !timer.running
}
}
FpsItem {
id: fpsItem
anchors.left: parent
color: "black"
}
Timer {
id: timer
interval: 10
running: true
repeat: true
onTriggered: gameOfLifeModel.nextStep()
}
}
}
gameoflifemodel.cpp
GameOfLifeModel::GameOfLifeModel(QObject *parent)
: QAbstractTableModel(parent) {}
//! [modelsize]
int GameOfLifeModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return height;
}
int GameOfLifeModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return width;
}
//! [modelsize]
//! [read]
QVariant GameOfLifeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || role != CellRole)
return QVariant();
return m_state[index.column()][index.row()];
}
//! [read]
//! [write / not used]
bool GameOfLifeModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (role != CellRole || data(index, role) == value)
return false;
m_state[index.column()][index.row()] = value.toBool();
emit dataChanged(index, index, {role});
return true;
}
//! [write]
Qt::ItemFlags GameOfLifeModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEditable;
}
//! [update]
void GameOfLifeModel::nextStep()
{
srand(time(NULL));
qDebug() << QTime::currentTime().toString("yyyy/MM/dd hh:mm:ss,zzz");
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
m_state[j][i] = (rand() % 1000) + 1;
}
}
emit dataChanged(index(0, 0), index(height - 1, width - 1), {CellRole});
}
//! [update]
gameoflifemodel.h
//! [modelclass]
class GameOfLifeModel : public QAbstractTableModel
{
Q_OBJECT
Q_ENUMS(Roles)
public:
enum Roles {
CellRole
};
QHash<int, QByteArray> roleNames() const override {
return {
{ CellRole, "value" }
};
}
explicit GameOfLifeModel(QObject *parent = nullptr);
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;
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
Q_INVOKABLE void nextStep();
private:
static constexpr int width = 50;
static constexpr int height = 50;
static constexpr int size = width * height;
template <class T, size_t ROW, size_t COL>
using NativeMatrix = T[ROW][COL];
NativeMatrix<int, height, width> m_state;
};
//! [modelclass]

Okay, the problem is solved. The issue appeared to be in the random number generation process, not in the table view.
In the code I used
srand(time(NULL))
The time(NULL) returns seconds and not milliseconds, thus I was getting the same numbers and they were updated only after 1 second and visually it was similar to updating the UI only every second. In order to solve this I switched to using this to generate random numbers and everything worked well:
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
/* using nano-seconds instead of seconds */
srand((time_t)ts.tv_nsec);

Related

qml TableView itemdelegate not firing (using a QAbstractTableModel)

Im trying to get my first QML TableView to work in Qt 5.2 (since we are stuck on that
version right now at work) using a QAbstractTableModel on the backend.
My main issue is that for some reason the itemDelegate is never firing so
I never see anything in the View except the outline of the TableView.
I have also verified that theData_ is filled with 2 dimensional numbers
in every row/column in the constructor and I do an emit layoutChanged()
as well as an emit dataChanged() in the constructor.
I realize I have no error checking for an invalid QModelIndex in the data() call
at this time.
I also did not implement index() at all.
Also is there any need to use a ROLE here?
The data Im displaying is a single integer (as a QString) per cell, nothing more at this time.
Thanks for your help
qml:
TableView {
width: 600
height: 600
model: myModel
visible: true
itemDelegate: Rectangle {
color: "lightgray"
width: 100
height: 20
Text {
text: styleData.value
color: "black"
}
}
}
relevant code from subclassed QAbstractTableModel:
int MyModel::rowCount(const QModelIndex&) const
{
return 10;
}
int MyModel::columnCount(const QModelIndex&) const
{
return 3;
}
QVariant MyModel::data(const QModelIndex& index, int role) const
{
const int row = index.row();
const int col = index.column();
return QString("%1").arg(this->theData_[col][row]);
}
Before Qt 5.12 there was only a TableView component that belongs to Qt QuickControl 1 that only supports a list type model where each column reflects the information of a role so this is probably your problem since you have not created any TableViewColumn. On the other hand, as of >= Qt5.12, another TableView already exists, which it supports as a table type model.
mymodel.h
#ifndef MYMODEL_H
#define MYMODEL_H
#include <QAbstractListModel>
struct Data{
int number;
QString text;
};
class MyModel : public QAbstractListModel
{
Q_OBJECT
public:
enum CustomRoles{
NumberRole = Qt::UserRole,
TextRole
};
explicit MyModel(QObject *parent = nullptr);
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;
private:
QList<Data> m_data;
};
#endif // MYMODEL_H
mymodel.cpp
#include "mymodel.h"
MyModel::MyModel(QObject *parent)
: QAbstractListModel(parent)
{
// for test
for(int i=0; i< 10; i++){
Data d{i, QString::number(i)};
m_data.push_back(d);
}
}
int MyModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return m_data.count();
}
QVariant MyModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if(role == NumberRole)
return m_data.at(index.row()).number;
if(role == TextRole)
return m_data.at(index.row()).text;
return QVariant();
}
QHash<int, QByteArray> MyModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NumberRole] = "number";
roles[TextRole] = "text";
return roles;
}
main.qml
// ...
TableView {
width: 600
height: 600
model: myModel
visible: true
TableViewColumn {
role: "number"
title: "Number"
width: 100
}
TableViewColumn {
role: "text"
title: "Text"
width: 200
}
itemDelegate: Rectangle {
color: "lightgray"
width: 100
height: 20
Text {
text: styleData.value
color: "black"
}
}
}
// ...

Is this the Minimum Viable TreeView Model in QML?

I'm making a folding list of three items: "Hey", "Whats", and "Up?". I want to put it into a tree view. I know this list will only ever contain these three items. Therefore, I would like to know how to "nest" these items together.
I know there are implementation for agile systems that support adding and removing parent/child objects, finding indexes... powerful models. However, I literally only need to display these items in an expandable/collapsable view. Here is what I've read through relating to C++ and QAbstractItemModels:
QML Treeview
QML QAbstractItemModel
This Question by My_Cat_Jessica
This question by kavaliero which was based on:
This 'working' example by Qt themselves (Doesn't actually work for TreeView. Works for QTreeView though!)
Here is the simplest viable code to implement a treeview with model:
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
Window {
id: mywindow
visible: true
width: 640
height: 480
TreeView {
id: treeview
anchors.fill: parent
TableViewColumn {
title: "Phrase"
role: "phrase"
}
model: phraseModel
}
ListModel {
id: phraseModel
ListElement { phrase: "Hey"; }
ListElement { phrase: "What's"; }
ListElement { phrase: "Up?"; }
}
}
I would like for the output to result in a nested stack like this:
Hey
What's
Up?
But I am getting everything in a single column all aligned with each other:
Hey
What's
Up?
I know I haven't assigned parents, and I'm not entirely sure how to do that - But I'm not even sure if that's what needs done to this code. So my question is this: What's the final step missing to stack these three elements into an expandable/collapsible view?
There is no native QML model that can use the TreeView, so I have implemented a model that tries to be generic:
TreeElement
// treeelement.h
#ifndef TreeElement_H
#define TreeElement_H
#include <QObject>
#include <QQmlListProperty>
class TreeElement : public QObject
{
Q_OBJECT
public:
Q_PROPERTY(QQmlListProperty<TreeElement> items READ items)
Q_CLASSINFO("DefaultProperty", "items")
TreeElement(QObject *parent = Q_NULLPTR);
Q_INVOKABLE TreeElement *parentItem() const;
bool insertItem(TreeElement *item, int pos = -1);
QQmlListProperty<TreeElement> items();
TreeElement *child(int index) const;
void clear();
Q_INVOKABLE int pos() const;
Q_INVOKABLE int count() const;
private:
static void appendElement(QQmlListProperty<TreeElement> *property, TreeElement *value);
static int countElement(QQmlListProperty<TreeElement> *property);
static void clearElement(QQmlListProperty<TreeElement> *property);
static TreeElement *atElement(QQmlListProperty<TreeElement> *property, int index);
QList<TreeElement *> m_childs;
TreeElement *m_parent;
};
#endif // TreeElement_H
// treeelement.cpp
#include "treeelement.h"
TreeElement::TreeElement(QObject *parent) :
QObject(parent),
m_parent(nullptr) {}
TreeElement *TreeElement::parentItem() const{
return m_parent;
}
QQmlListProperty<TreeElement> TreeElement::items(){
return QQmlListProperty<TreeElement> (this,
this,
&TreeElement::appendElement,
&TreeElement::countElement,
&TreeElement::atElement,
&TreeElement::clearElement);
}
TreeElement *TreeElement::child(int index) const{
if(index < 0 || index >= m_childs.length())
return nullptr;
return m_childs.at(index);
}
void TreeElement::clear(){
qDeleteAll(m_childs);
m_childs.clear();
}
bool TreeElement::insertItem(TreeElement *item, int pos){
if(pos > m_childs.count())
return false;
if(pos < 0)
pos = m_childs.count();
item->m_parent = this;
item->setParent(this);
m_childs.insert(pos, item);
return true;
}
int TreeElement::pos() const{
TreeElement *parent = parentItem();
if(parent)
return parent->m_childs.indexOf(const_cast<TreeElement *>(this));
return 0;
}
int TreeElement::count() const{
return m_childs.size();
}
void TreeElement::appendElement(QQmlListProperty<TreeElement> *property, TreeElement *value){
TreeElement *parent = qobject_cast<TreeElement *>(property->object);
parent->insertItem(value);
}
int TreeElement::countElement(QQmlListProperty<TreeElement> *property){
TreeElement *parent = qobject_cast<TreeElement *>(property->object);
return parent->count();
}
void TreeElement::clearElement(QQmlListProperty<TreeElement> *property){
TreeElement *parent = qobject_cast<TreeElement *>(property->object);
parent->clear();
}
TreeElement *TreeElement::atElement(QQmlListProperty<TreeElement> *property, int index){
TreeElement *parent = qobject_cast<TreeElement *>(property->object);
if(index < 0 || index >= parent->count())
return nullptr;
return parent->child(index);
}
TreeModel
// treemodel.h
#ifndef TreeModel_H
#define TreeModel_H
#include <QAbstractItemModel>
#include <QQmlListProperty>
class TreeElement;
class TreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
Q_PROPERTY(QQmlListProperty<TreeElement> items READ items)
Q_PROPERTY(QVariantList roles READ roles WRITE setRoles NOTIFY rolesChanged)
Q_CLASSINFO("DefaultProperty", "items")
TreeModel(QObject *parent = Q_NULLPTR);
~TreeModel() override;
QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE;
int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
QQmlListProperty<TreeElement> items();
QVariantList roles() const;
void setRoles(const QVariantList &roles);
Q_INVOKABLE QModelIndex indexFromElement(TreeElement *item);
Q_INVOKABLE bool insertElement(TreeElement *item, const QModelIndex &parent = QModelIndex(), int pos = -1);
TreeElement *elementFromIndex(const QModelIndex &index) const;
private:
TreeElement *m_root;
QHash<int, QByteArray> m_roles;
signals:
void rolesChanged();
};
#endif // TreeModel_H
// treemodel.cpp
#include "treemodel.h"
#include "treeelement.h"
TreeModel::TreeModel(QObject *parent) :
QAbstractItemModel(parent){
m_root = new TreeElement;
}
TreeModel::~TreeModel(){
delete m_root;
}
QHash<int, QByteArray> TreeModel::roleNames() const{
return m_roles;
}
QVariant TreeModel::data(const QModelIndex &index, int role) const{
if (!index.isValid())
return QVariant();
TreeElement *item = static_cast<TreeElement*>(index.internalPointer());
QByteArray roleName = m_roles[role];
QVariant name = item->property(roleName.data());
return name;
}
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const{
if (!index.isValid())
return nullptr;
return QAbstractItemModel::flags(index);
}
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const{
if (!hasIndex(row, column, parent))
return QModelIndex();
TreeElement *parentItem = elementFromIndex(parent);
TreeElement *childItem = parentItem->child(row);
if (childItem)
return createIndex(row, column, childItem);
else
return QModelIndex();
}
QModelIndex TreeModel::parent(const QModelIndex &index) const{
if (!index.isValid())
return QModelIndex();
TreeElement *childItem = static_cast<TreeElement*>(index.internalPointer());
TreeElement *parentItem = static_cast<TreeElement *>(childItem->parentItem());
if (parentItem == m_root)
return QModelIndex();
return createIndex(parentItem->pos(), 0, parentItem);
}
int TreeModel::rowCount(const QModelIndex &parent) const{
if (parent.column() > 0)
return 0;
TreeElement *parentItem = elementFromIndex(parent);
return parentItem->count();
}
int TreeModel::columnCount(const QModelIndex &parent) const{
Q_UNUSED(parent)
return 1;
}
QQmlListProperty<TreeElement> TreeModel::items(){
return m_root->items();
}
QVariantList TreeModel::roles() const{
QVariantList list;
QHashIterator<int, QByteArray> i(m_roles);
while (i.hasNext()) {
i.next();
list.append(i.value());
}
return list;
}
void TreeModel::setRoles(const QVariantList &roles){
static int nextRole = Qt::UserRole + 1;
for(QVariant role : roles) {
m_roles.insert(nextRole, role.toByteArray());
nextRole ++;
}
emit rolesChanged();
}
QModelIndex TreeModel::indexFromElement(TreeElement *item){
QVector<int> positions;
QModelIndex result;
if(item) {
do{
int pos = item->pos();
positions.append(pos);
item = item->parentItem();
} while(item != nullptr);
for (int i = positions.size() - 2; i >= 0 ; i--)
result = index(positions[i], 0, result);
}
return result;
}
bool TreeModel::insertElement(TreeElement *item, const QModelIndex &parent, int pos){
TreeElement *parentElement = elementFromIndex(parent);
if(pos >= parentElement->count())
return false;
if(pos < 0)
pos = parentElement->count();
beginInsertRows(parent, pos, pos);
bool retValue = parentElement->insertItem(item, pos);
endInsertRows();
return retValue;
}
TreeElement *TreeModel::elementFromIndex(const QModelIndex &index) const{
if(index.isValid())
return static_cast<TreeElement *>(index.internalPointer());
return m_root;
}
main.cpp
#include "treemodel.h"
#include "treeelement.h"
#include <QGuiApplication>
#include <QQmlApplicationEngine>
static void registertypes(){
qmlRegisterType<TreeElement>("foo", 1, 0, "TreeElement");
qmlRegisterType<TreeModel>("foo", 1, 0, "TreeModel");
}
Q_COREAPP_STARTUP_FUNCTION(registertypes)
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
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();
}
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 1.4
import foo 1.0
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
TreeModel {
id: treemodel
roles: ["phrase"]
TreeElement{
property string phrase: "Hey"
TreeElement{
property string phrase: "What's"
TreeElement{
property string phrase: "Up?"
}
}
}
}
TreeView {
anchors.fill: parent
model: treemodel
TableViewColumn {
title: "Name"
role: "phrase"
width: 200
}
}
}
Output:
The complete example you find here
I created a collapsible frame in QML (a group box with a title and a content). If you are sure that you will never change the structure, you can use for your purpose:
I simplified the code by removing the useless parts (animations, decorations, etc.). So the code below could be improved. But, I kept the idea:
// CollapsibleGroupBox.qml
Item {
property alias contentItem: content.contentItem;
property string title: ""
Item {
id: titleBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 30
Row {
anchors.fill: parent
CheckBox {
Layout.alignment: Qt.AlignLeft
id: expand
checked: true;
}
Text {
Layout.alignment: Qt.AlignLeft
text: title
}
}
}
Pane {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: titleBar.bottom
anchors.bottom: parent.bottom
topPadding: 0
visible: expand.checked
id: content
}
}
// Main.qml
Item {
height: 500
width: 500
CollapsibleGroupBox {
anchors.fill: parent
title: "Hey!"
contentItem: CollapsibleGroupBox {
title: "What's"
contentItem: CollapsibleGroupBox {
title: "up?"
}
}
}
}
You will get:
You can replace the checkbox by a MouseArea, also.
I've also created a model that only uses QML components:
import QtQuick 2.9
import QtQuick.Window 2.2
import UISettings 1.0
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls 1.4 as SV
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Flickable {
id: flick
anchors.fill: parent
clip: true
contentHeight: col.implicitHeight
property var mymodel: {
"animals": {
"big": {
"land": "elephants",
"water": "whales"
},
"small": {
"land": "mice",
"water": "fish"
}
},
"plants": {
"trees": "evergreens"
}
}
Column {
id: col
Component.onCompleted: componentListView.createObject(this, {"objmodel":flick.mymodel});
}
Component {
id: componentListView
Repeater {
id: repeater
property var objmodel: ({})
model: Object.keys(objmodel)
ColumnLayout {
Layout.leftMargin: 50
Button {
property var sprite: null
text: modelData
onClicked: {
if(sprite === null) {
if(typeof objmodel[modelData] === 'object')
sprite = componentListView.createObject(parent, {"objmodel":objmodel[modelData]});
}
else
sprite.destroy()
}
}
}
}
}
}
}

Using TableView as ListView-delegate

I have a model that contains several QStandardItemModels. Now I want to create a view that displays a TableView for each QStandardItemModel.
I had the idea to have a ListView that has a TableView as delegate, something like this:
ListView {
id: myListView
anchors {
fill: parent
margins: 5
}
model: testModel
delegate: CompareDelegate { }
}
And in CompareDelegate.qml:
Item {
id: base
width: 500
height: 300
TableView {
anchors.fill: parent
}
}
How do I get the TableView inside the delegate to use the appropriate QStandardItemModel within the model of the ListView?
I hope the question is somewhat clear.
Thanks in advance.
Interesting use case... I am still not sure if it's a good idea and am wondering if there's a better way to do it, but I couldn't think of reasons why it's bad, so I tried it out of curiosity:
main.cpp
#include <QApplication>
#include <QtQml>
#include <QtWidgets>
class IndividualModel : public QStandardItemModel
{
public:
IndividualModel(QObject* parent = 0) :
QStandardItemModel(4, 2, parent)
{
for (int row = 0; row < 4; ++row) {
QStandardItem *item0 = new QStandardItem;
item0->setData(QString("row %1, column 0").arg(row), TitleRole);
setItem(row, 0, item0);
QStandardItem *item1 = new QStandardItem;
item1->setData(QString("row %1, column 1").arg(row), AuthorRole);
setItem(row, 1, item1);
}
}
enum {
TitleRole = Qt::UserRole,
AuthorRole
};
QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE
{
QHash<int, QByteArray> names;
names[TitleRole] = "title";
names[AuthorRole] = "author";
return names;
}
};
class CompositeModel : public QAbstractItemModel
{
public:
CompositeModel()
{
for (int i = 0; i < 3; ++i) {
QStandardItemModel *model = new IndividualModel(this);
mModels.append(model);
}
}
enum {
ModelRole = Qt::UserRole
};
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE
{
if (!index.isValid())
return QVariant();
if (role != ModelRole)
return QVariant();
return QVariant::fromValue(mModels.at(index.row()));
}
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE
{
if (!hasIndex(row, column, parent))
return QModelIndex();
return createIndex(row, column);
}
QModelIndex parent(const QModelIndex &) const Q_DECL_OVERRIDE
{
return QModelIndex();
}
int rowCount(const QModelIndex & = QModelIndex()) const Q_DECL_OVERRIDE
{
return mModels.size();
}
int columnCount(const QModelIndex & = QModelIndex()) const Q_DECL_OVERRIDE
{
return 1;
}
QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE
{
QHash<int, QByteArray> names;
names[ModelRole] = "individualModel";
return names;
}
private:
Q_DISABLE_COPY(CompositeModel)
QVector<QStandardItemModel*> mModels;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QQmlApplicationEngine engine;
CompositeModel compositeModel;
engine.rootContext()->setContextProperty("compositeModel", QVariant::fromValue(&compositeModel));
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
main.qml
import QtQuick 2.0
import QtQuick.Window 2.0
import QtQuick.Controls 1.0
Window {
visible: true
width: 300
height: 600
ListView {
anchors.fill: parent
model: compositeModel
delegate: TableView {
model: individualModel
TableViewColumn {
role: "title"
title: "Title"
width: 100
}
TableViewColumn {
role: "author"
title: "Author"
width: 200
}
}
}
}
This works, but... it crashes on exit. :)
I've been unable to reproduce it while debugging with Creator, so I've only been able to get a stack trace manually on the command line with gdb, which I'm not too familiar with (have always used debuggers in IDEs :)). The crash is in QML's guts somewhere, while accessing some property. However, I've spent too much time on this to not post the answer, and I think it is still useful. Maybe some kind soul will find the problem and edit my post. :)

MapItemView is not updated after a dataChanged signal

I am using the QML MapItemView component with a C++ QAbstractListModel-based model. The MapItemView is working fine when the model is reset, or whenever a new item is added or an existing item is removed. However, the MapItemView is not reflecting changes to already added items.
I have first experienced this issue with Qt 5.4 but I still face it after updating to Qt 5.5
The following example shows the issue with 2 different models : a C++ model based on QAbstractListModel and a QML ListModel.
It is possible to switch from one model to another, pressing the top-right button:
When the QML model is used, clicking in the map will add a new element and modify the first element.
The C++ model used a QTimer to modify its content every seconds.
The MapItemView is not showing the model changes whatever the model type is. When switching from one model to another, one can see that the MapView gets updated.
I am probably missing something very obvious but I don't see what it is. Thank you in advance for your help.
The main.cpp code :
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "playermodel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
PlayerModel playerModel;
engine.rootContext()->setContextProperty("playerModel", &playerModel);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
The C++ model header (playermodel.h) :
#ifndef PLAYERMODEL_H
#define PLAYERMODEL_H
#include <QObject>
#include <QAbstractListModel>
#include <QGeoPositionInfoSource>
#include <QTimer>
#include <QDebug>
struct PlayerData
{
PlayerData(){ }
PlayerData(int _Azimuth, double lat, double lng){
Azimuth = _Azimuth;
Latitude = lat;
Longitude = lng;
}
int Azimuth = -1;
double Latitude = 0.;
double Longitude = 0.;
QVariant getRole(int role) const;
enum Roles{
RoleAzimuth = Qt::UserRole + 1,
RoleLatitude,
RoleLongitude
};
};
class PlayerModel : public QAbstractListModel
{
Q_OBJECT
public:
PlayerModel();
~PlayerModel();
int rowCount(const QModelIndex & parent = QModelIndex()) const;
QVariant data( const QModelIndex & index, int role = Qt::DisplayRole ) const;
Q_INVOKABLE Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE;
virtual bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole);
bool removeRows(int row, int count, const QModelIndex & parent = QModelIndex());
void resetModel();
void updateModel();
public slots:
void testUpdateModel();
protected:
QHash<int, QByteArray> roleNames() const;
private:
QTimer m_timer;
QVector< PlayerData> m_lstValues ;
};
#endif // PLAYERMODEL_H
The C++ model (playermodel.cpp)
#include "playermodel.h"
QVariant PlayerData::getRole(int role) const
{
switch (role)
{
case Roles::RoleAzimuth:
return Azimuth;
case Roles::RoleLatitude:
return Latitude;
case Roles::RoleLongitude:
return Longitude;
default:
return QVariant();
}
}
PlayerModel::PlayerModel()
{
resetModel();
connect(&m_timer, SIGNAL(timeout()), this, SLOT(testUpdateModel()));
m_timer.start(1000);
}
PlayerModel::~PlayerModel()
{
}
void PlayerModel::testUpdateModel()
{
updateModel();
}
int PlayerModel::rowCount(const QModelIndex & parent) const
{
Q_UNUSED(parent);
return m_lstValues.size();
}
QVariant PlayerModel::data( const QModelIndex & index, int role ) const
{
if ( (index.row() < 0) || (index.row() >= rowCount()) )
return QVariant();
return m_lstValues[ index.row()].getRole( role);
}
void PlayerModel::resetModel()
{
qDebug() << "Reset players model";
beginResetModel();
m_lstValues.clear();
//populate with dummy value
m_lstValues.push_back( PlayerData( 10, 47.1, -1.6 ));
m_lstValues.push_back( PlayerData( 20, 47.2, -1.6 ));
m_lstValues.push_back( PlayerData( 30, 47.1, -1.5 ));
m_lstValues.push_back( PlayerData( 40, 47.2, -1.5 ));
endResetModel();
}
void PlayerModel::updateModel()
{
qDebug() << "update players model upon timeout";
//change the Azimuth of every model items
int row = 0;
for (PlayerData player : m_lstValues)
{
setData( index(row), (player.Azimuth + 1) % 360, PlayerData::RoleAzimuth);
row++;
}
//qDebug() << "First element azimuth is now : " << data( index(0),PlayerData::RoleAzimuth).toInt() << "°";
}
bool PlayerModel::setData(const QModelIndex & index, const QVariant & value, int role)
{
if ( (index.row() < 0) || (index.row() >= rowCount()) ) return false;
PlayerData& player = m_lstValues[ index.row() ];
switch (role)
{
case PlayerData::RoleAzimuth:
player.Azimuth = value.toInt();
break;
case PlayerData::RoleLatitude:
player.Latitude = value.toDouble();
break;
case PlayerData::RoleLongitude:
player.Longitude = value.toDouble();
break;
}
emit dataChanged(index, index );//, QVector<int>( role));
return true;
}
bool PlayerModel::removeRows(int row, int count, const QModelIndex & parent)
{
Q_UNUSED(count);
Q_UNUSED(parent);
beginRemoveRows(QModelIndex(), row, row);
m_lstValues.remove( row);
endRemoveRows();
return true;
}
QHash<int, QByteArray> PlayerModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[PlayerData::Roles::RoleAzimuth] = "Azimuth";
roles[PlayerData::Roles::RoleLatitude] = "Latitude";
roles[PlayerData::Roles::RoleLongitude] = "Longitude";
return roles;
}
Qt::ItemFlags PlayerModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return 0;
return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}
and finally the QML file :
import QtQuick 2.4
import QtQuick.Window 2.2
import QtLocation 5.3
import QtPositioning 5.0
Window {
id:mainWnd
visible: true
width : 1024
height:768
property bool useQMLModel: true
Map {
id: map
anchors.fill: parent
anchors.margins: 50
plugin: Plugin{ name:"osm";}
center: QtPositioning.coordinate(47.1, -1.6)
zoomLevel: map.maximumZoomLevel
MapItemView{
id:mapItemView
model: mainWnd.useQMLModel ? qmlModel : playerModel
delegate: MapQuickItem {
//anchorPoint:
id:delegateMQI
rotation: model.Azimuth
sourceItem: Rectangle{
id:defaultDelegate
width:32
height:32
radius:16
opacity: 0.6
rotation:Azimuth
color:"blue"
Text{
text: Azimuth
anchors.centerIn : parent
}
}
coordinate: QtPositioning.coordinate(Latitude,Longitude)
}
}
MouseArea{
anchors.fill: parent
enabled : useQMLModel
//preventStealing: true
propagateComposedEvents: true
onClicked:
{
//Modify an item
var newAzim = Math.random()*360;
qmlModel.setProperty(0, "Azimuth", newAzim);
//Check modification
console.log("Azim:" + qmlModel.get(0).Azimuth );
qmlModel.setProperty(0, "Color", "blue");
//add a new item
qmlModel.append({"Latitude": 47.05 + Math.random() *0.2, "Longitude":-1.75 + Math.random() *0.3, "Azimuth":0, "Color":"red"})
console.log("Nb item:" + qmlModel.count );
map.update();
map.fitViewportToMapItems();
mouse.accepted = false
}
}
}
Connections{
target:mapItemView.model
onDataChanged:{
if (useQMLModel)
console.log("dataChanged signal Azim:" + qmlModel.get(0).Azimuth );
else
console.log("dataChanged signal Azim:" + playerModel.data( topLeft, 0x0101) );
}
}
ListModel{
id:qmlModel
ListElement {
Latitude: 47.1
Longitude: -1.6
Azimuth: 10.0
}
}
Rectangle{
anchors.top : parent.top
anchors.left : parent.left
width : 400
height : 300
radius: 10
color:"grey"
ListView{
id:lstView
model:mapItemView.model
anchors.fill:parent
delegate: Text{
width:parent.width
height:50
verticalAlignment: TextInput.AlignVCenter
fontSizeMode : Text.Fit
font.pixelSize: 42
minimumPixelSize: 5
text: "Latitude : " + Latitude + " - Longitude :" + Longitude + " - Azimuth : " + Azimuth
}
}
}
Rectangle{
anchors.right : parent.right
anchors.top : parent.top
radius : 10
color : "red"
width : 200
height : 50
Text{
anchors.centerIn: parent
text:"switch model"
}
MouseArea{
anchors.fill: parent
onClicked:{
mainWnd.useQMLModel = !mainWnd.useQMLModel;
}
}
}
}
Just in case that someone faces the same issue reported by the post's author, the problem was solved in Qt 5.6.0
Note that this is fixed by changeset Ib92252d18c2229bc6d43e11362b8f13cdb48f315 (https://codereview.qt-project.org/#/c/123660/ ) already merged in the 5.6 branch

QtQuick TableView not working with C++-QAbstractTableModel

I am trying to get the Qt model/view-architecture working with a QML-View, but for whatever reason it is only partially working.
What works:
rowCount
data
roleNames
Not working:
columnCount (the method is called, but seems to have no effect, as long is it is > 0)
headerData (is this actually supposed to set the column headers? All examples set the headers in QML)
flags
setData
What I am trying to do (for some weeks already), is to create a simple ApplicationView with a TableView and a C++-model, which is editable by the view.
Right now only a whole row is selectable, not a single cell. The table data doesn't seem to be editable at all. Can anyone give me a hint?
main.qml
import QtQuick 2.3
import QtQuick.Controls 1.2
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
TableView {
model: theModel
TableViewColumn {
role: "nameRole"
width: 75
}
TableViewColumn {
role: "ageRole"
width: 50
}
}
}
ModelItem.hpp
#ifndef MODELITEM
#define MODELITEM
#include <QString>
struct ModelItem {
ModelItem(QString name_, int age_)
: name(name_), age(age_) {}
QString name;
int age;
};
#endif // MODELITEM
TableModel.hpp
#ifndef TABLEMODEL_HPP
#define TABLEMODEL_HPP
#include <QAbstractTableModel>
#include "ModelItem.hpp"
class TableModel : public QAbstractTableModel
{
Q_OBJECT
public:
TableModel(QObject *parent = 0);
//works
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
int rowCount(const QModelIndex &parent) const;
int columnCount(const QModelIndex &parent) const;
//does not work
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
QHash<int, QByteArray> roleNames() const;
enum Roles {
NameRole = Qt::UserRole + 1,
AgeRole
};
Qt::ItemFlags flags(const QModelIndex &index) const;
bool setData(const QModelIndex &index, const QVariant &value, int role);
private:
QList<ModelItem*> items;
};
#endif // TABLEMODEL_HPP
TableModel.cpp
#include "TableModel.hpp"
TableModel::TableModel(QObject *parent)
: QAbstractTableModel(parent) {
items.append(new ModelItem("Hugo",33));
items.append(new ModelItem("Egon",34));
items.append(new ModelItem("Balder",66));
qDebug("TableModel initialisiert");
}
int TableModel::columnCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
qDebug("columnCount");
return 2;
}
int TableModel::rowCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
qDebug("rowCount");
return items.count();
}
QVariant TableModel::data(const QModelIndex &index, int role) const {
qDebug("data");
switch (role) {
case NameRole: return items[index.row()]->name;
case AgeRole: return items[index.row()]->age;
}
}
QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const {
qDebug("headerData");
switch (role) {
case NameRole: return "1";
case AgeRole: return "2";
};
return QVariant();
}
QHash<int, QByteArray> TableModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[NameRole] = "nameRole";
roles[AgeRole] = "ageRole";
qDebug("roleNames initialised");
return roles;
}
Qt::ItemFlags TableModel::flags(const QModelIndex &index) const {
qDebug("--flags called--");
return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}
bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role) {
qDebug("setData called");
switch (role) {
case NameRole: items[index.row()]->name = value.toString();
case AgeRole: items[index.row()]->age = value.toInt();
}
emit dataChanged(index, index);
return true;
}
main.cpp
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "TableModel.hpp"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QQmlApplicationEngine engine;
TableModel model;
engine.rootContext()->setContextProperty("theModel", &model);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
columnCount and rowCount called in QAbstractItemModel::index method, which is called by TableView before data method.
QModelIndex QAbstractTableModel::index(int row, int column, const QModelIndex &parent) const
{
return hasIndex(row, column, parent) ? createIndex(row, column, 0) : QModelIndex();
}
bool QAbstractItemModel::hasIndex(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column < 0)
return false;
return row < rowCount(parent) && column < columnCount(parent);
}
columnCount has no effect, as long is it is > 0, because TableView called the index method with column always equal to 0.
headerData and flags did not affect the QML TableView. You can only set the headers on the QML side. To create an editable TableView, you should implement your custom itemDelegate
main.qml
ApplicationWindow {
visible: true
id: root
Component {
id: editableDelegate
Item {
Text {
width: parent.width
anchors.margins: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
elide: styleData.elideMode
text: styleData.value !== undefined ? styleData.value : ""
color: styleData.textColor
visible: !styleData.selected
}
Loader {
id: loaderEditor
anchors.fill: parent
anchors.margins: 4
Connections {
target: loaderEditor.item
onEditingFinished: {
theModel.setData(styleData.row, styleData.column, loaderEditor.item.text)
}
}
sourceComponent: styleData.selected ? editor : null
Component {
id: editor
TextInput {
id: textinput
color: styleData.textColor
text: styleData.value
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: textinput.forceActiveFocus()
}
}
}
}
}
}
TableView {
id: table
anchors.fill: parent
model: theModel
itemDelegate: editableDelegate;
TableViewColumn {
role: "nameRole"
width: 75
title: "name"
}
TableViewColumn {
role: "ageRole"
width: 50
title: "age"
}
}
}
To apply the changes to your model, you should implement the setData method, like this:
TableModel.h
bool setData(const QModelIndex &index, const QVariant &value, int role) {
switch (role) {
case NameRole: items[index.row()]->name = value.toString(); break;
case AgeRole: items[index.row()]->age = value.toInt(); break;
}
emit dataChanged(index, index);
return true;
}
Q_INVOKABLE bool setData(int row, int column, const QVariant value)
{
int role = Qt::UserRole + 1 + column;
return setData(index(row,0), value, role);
}
The above answer covers things nicely. I just want to add a couple of notes:
in QML you can simply do model.role = value within an item delegate instead of having to call setData() by hand
several QAbstractItemModel methods, including setData(), have been made Q_INVOKABLE in Qt 5.5: https://codereview.qt-project.org/#/c/107171/

Resources