Qt Model within a model? - c++

I have a Qt model which could very well be a QAbstractListModel. Each "row" represents an object I have stored in a QList. I'm displaying this in QML in a ListView. However, each object has one property that happens to be an array of strings. I would like to display this as a ListView within the delegate that displays that row. But I don't know how to expose that model (for the string array property of the object) to QML. I can't expose it through the data function since Models are QObjects, which cannot be QVariants. I thought of using QAbstractItemModel instead, but I still don't know how to get a model for my ListView. In case it matters, I'm using Qt 5.0.0 release.

You can return QVariantList from your main QAbstractListModel and this can then be assigned as a model to your internal ListView that you have in the delegate. I have added a small example that has a very simple one row model with internal model as an example.
The c++ model class:
class TestModel : public QAbstractListModel
{
public:
enum EventRoles {
StringRole = Qt::UserRole + 1
};
TestModel()
{
m_roles[ StringRole] = "stringList";
setRoleNames(m_roles);
}
int rowCount(const QModelIndex & = QModelIndex()) const
{
return 1;
}
QVariant data(const QModelIndex &index, int role) const
{
if(role == StringRole)
{
QVariantList list;
list.append("string1");
list.append("string2");
return list;
}
}
QHash<int, QByteArray> m_roles;
};
Now you can set this model to QML and use it like this:
ListView {
anchors.fill: parent
model: theModel //this is your main model
delegate:
Rectangle {
height: 100
width: 100
color: "red"
ListView {
anchors.fill: parent
model: stringList //the internal QVariantList
delegate: Rectangle {
width: 50
height: 50
color: "green"
border.color: "black"
Text {
text: modelData //role to get data from internal model
}
}
}
}
}

Related

QML QT backend and GUI design - doesn't update the object

I have simple object with collection which is created and managed under C++ code and I want to let user view it and modify it from GUI (QML - presentation of the collection and add/remove commands), but lifetime and business logic should be managed by backend (C++)
class QtModel : public QAbstractListModel {
Q_OBJECT
public:
explicit QtModel(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;
void test();
private:
std::vector<std::shared_ptr<Data>> collection_;
};
where test method push a new element:
void QtModel::test(){
collection_.push_back(std::make_shared<Data>("test"));
}
I tried follow this way:
https://doc.qt.io/qt-5/qtqml-cppintegration-contextproperties.html
And qml code just takes the current state of the object. The further modifications are ignored:
application = std::make_unique<QGuiApplication>((int &)argc, argv);
engine = std::make_shared<QQmlApplicationEngine>();
qt_model_.test(); // 1 element, GUI shows 1 element
engine->rootContext()->setContextProperty("myGlobalObject", &qt_model_);
// those are ignored
qt_model_.test(); // 2 elements, GUI shows 1 element
qt_model_.test(); // 3 elements, GUI shows 1 element
qt_model_.test(); // 4 elements, GUI shows 1 element
application->exec();
For presentation I am using GridLayout like this:
GridLayout {
anchors.fill: parent
flow: width > height ? GridLayout.LeftToRight : GridLayout.TopToBottom
Repeater {
model: myGlobalObject
delegate : Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Style.appLightBackgroundColor
Label {
anchors.centerIn: parent
text: model.name
color: Style.fontDarkColor
}
}
}
}
So the object in QML is not updated, while the modifications are taken place after its registration in C++
You are not sending RowsInserted signals from the test function, so QML cannot know when to update. Please adjust like so:
void QtModel::test(){
beginInsertRows({}, collection_.size(), collection_.size() + 1);
collection_.push_back(std::make_shared<Data>("test"));
endInsertRows();
}

QAbstractListModel returning unexpcted result on Qt::DisplayRole

I have created a very minimal QAbstractListModel with two roles, display and test
InvoiceTabModel::InvoiceTabModel(QObject *parent): QAbstractListModel(parent)
{
}
QVariant InvoiceTabModel::data(const QModelIndex &index, int role) const
{
Q_UNUSED(index)
if(role == 123)
return QVariant("testRole");
return QVariant("displayRole");
}
int InvoiceTabModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 3;
}
QHash<int, QByteArray> InvoiceTabModel::roleNames() const
{
return { {Qt::DisplayRole, "display"}, {123, "test"} };
}
I attached this model to a repeater
Repeater{
id: invoiceTab
anchors.fill: parent
model: invoice.tabmodel
Button{
width: 100
height: parent.height
text: test
//text: display
}
}
The problem is that when I use display role the text is displayed as 2, but when I use test in qml, the string is displaying correctly
Using test Role
When using the display role
Where does this 2 come from?
display is a property of Button.
When using data coming from a model, always use the model. prefix to disambiguate (model.display).

How to display values from lists in a UI QML (QT) inside a Repeater

I've been facing this issue for days without coming to a conclusion, I hope someone could give me some useful hints to solve it.
I'll try to simplify the issue with an example:
In my C++ code I defined the class MyObjectModel that will act
later as a model in the Repeater block in my main ui.qml file.
MyObjectModel is visible to the QQmlApplicationEngine.
MyObjectModel
has 2 attributes (lists) : xCoordinatesList and yCoordinatesList.
They represent the x and y pixel coordinates of a list of points.
I.e. xCoordinatesList = [100, 200], yCoordinatesList = [10, 20] mean logically that I have 2 points with the following pixel coordinates that I want to display on the screen: (100,10), (10,20).
The xCoordinatesList and yCoordinatesList are Roles of my model
for the QML engine. This means for instance that in a common
.qml file i can clearly print the content of xCoordinatesList by
typing:
Component.onCompleted
{
console.log("x coordinates: ",xCoordinatesList);
}
The question is: how can I display at the same time the list of points on the screen?
If I want to display only one dot (so one couple of coordinates) my code works. I really don't know how to extend it to make it print all of them.
In my MainForm.ui.qml I defined a Repeater inside a Rectangle :
Rectangle
{
....
Repeater
{
model: dotModel
delegate:
DotItem
{
id: dotItem;
objectName: "dotItem";
DotPositionOnMap
{
id: dotPositionId;
objectWidth: dotItem.width;
objectHeight: dotItem.height;
}
x: dotPositionId.xPositionOnMap;
y: dotPositionId.yPositionOnMap;
}
}
....
}
I need a Repeater because MyObjectModel which holds the two lists of x and y coordinates can dynamically change over time.
dotModel is just a fake model I use for an other purpose.
DotItem is my qml Item that identifies the red dot circle image I want to depict on the screen for each couple of elements in xCoordinatesList, yCoordinatesList.
DotItem.ui.qml
import QtQuick 2.4
import QtQuick.Layouts 1.1
Item
{
width: 10
height: 10
opacity: 1
Image
{
id: dotItemImage
anchors.fill: parent
source: "red_dot.png"
}
}
red_dot.png image should be displayed for each point depicted on the screen.
DotPositionOnMap.qml is responsible for computing the right x and y pixel position on the screen.
import QtQuick 2.5
import "calcCurrentPos_script.js" as CurrentPos
Item
{
// Values filled from MainForm.ui.qml
property int objectWidth
property int objectHeight
// Getting current coordinates
// Fetching element 0 from both lists
property real currentx: CurrentPos.getCurrentxPoint(0);
property real currenty: CurrentPos.getCurrentyPoint(0);
// Generating the x and y pixel position on map.
// Toy example
property int xPositionOnMap : currentx-(objectWidth/2);
property int yPositionOnMap : currenty-(objectHeight/2);
}
Where calcCurrentPos_script.js
function getCurrentxPoint(val)
{
return xCoordinatesList[val];
}
function getCurrentyPoint(val)
{
return yCoordinatesList[val];
}
In this way I can only display one dot on the screen since I specify in DotPositionOnMap.qml which point to fetch:
// Fetching element 0 in this case
property real currentx: CurrentPos.getCurrentxPoint(0);
property real currenty: CurrentPos.getCurrentyPoint(0);
I used javascript for this attempt because I thought I could use a for loop to scan all the elements to be displayed, but it didn't work.
Extract of my model
QVariant MyModelObject::data(const QModelIndex& index, int role) const
{
const MyModelObject& object = objects.values().value(index.row());
....
if(role == XRole)
{
QList<TrackPoint> tkrList = object.getList();
QList<QVariant> tkrVariantList;
for(auto track: trackpointList)
{
tkrVariantList.append(track.getPosition().getX());
}
return QVariant(tkrVariantList);
}
else if(role == YRole)
{
QList<TrackPoint> tkrList = object.getList();
QList<QVariant> tkrVariantList;
for(auto track: trackpointList)
{
tkrVariantList.append(track.getPosition().getY());
}
return QVariant(tkrVariantList);
}
}
....
....
QHash<int, QByteArray> MyModelObject::roleNames() const
{
QHash<int, QByteArray> roles;
roles[XRole] = "xCoordinatesList";
roles[YRole] = "yCoordinatesList";
return roles;
}
I truly appreciate any ideas concerning the generalisation of this implementation.
Thank you
The Qt documentation is very clear. The first you have to read is that article. I think that in your case the simpliest way is list-based model. Or, of cource, you can subclass QAbstractListModel.
Your question is not so clear and you didn't provide the code of your model but maybe this small example will help you:
Declaration
class MyModel : public QAbstractListModel
{
Q_OBJECT
public:
enum PointRoles {
XRole = Qt::UserRole + 1,
YRole
};
MyModel(QObject *parent = Q_NULLPTR);
int rowCount(const QModelIndex &parent = QModelIndex()) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
QHash<int, QByteArray> roleNames() const;
private:
QList<QPoint> m_list;
};
Implementation
MyModel::MyModel(QObject *parent) :
QAbstractListModel(parent)
{
}
QHash<int, QByteArray> MyModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[XRole] = "xcoord";
roles[YRole] = "ycoord";
return roles;
}
int MyModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_list.count();
}
QVariant MyModel::data(const QModelIndex &index, int role) const
{
if(role == XRole)
return m_list[index.row()].x();
else if(role == YRole)
return m_list[index.row()].y();
return QVariant();
}
Registation
qmlRegisterType<MyModel>("qt.test", 1, 0, "PointModel");
Usage
Window {
visible: true
width: 800
height: 800
PointModel {id: mymodel}
Repeater {
model: mymodel
delegate: Rectangle {
x: 100 + xcoord
y: 100 + ycoord
width: 20
height: 20
color: "red"
radius: 10
}
}
}
* SOLVED *
In your main.qml you can have something like this, which handles the global drawing routine.
Repeater
{
model: dotModel
delegate:
DotPositionOnMap{}
}
DotPositionOnMap.qml will read the list of x and y coordinates and for each element of both will create a dot Item object which will be displayed on the screen.
import QtQuick 2.5
Item
{
id: dotPositionOnMap
objectName: "dotoPositionOnMap"
Component.onCompleted:
{
// yCoordinatesList would have given the same result
var dotListLength = xCoordinatesList.length;
// Dot properties can by handled dynamically
var dotWidth = 5;
var dotHeight = 5;
var dotRadius = 10;
var dotColor = "red"
// For each entry of xCoordinatesList and yCoordinatesList
// I generate a DotShape qml object that will display a dot on the screen
for(var i = 0; i < dotListLength; i++)
{
var currentXValue = xCoordinatesList[i];
var currentYValue = yCoordinatesList[i];
// x and y pixrl values for a DotShape.qml instance
var xPositionOnMap = currentXValue-(dotWidth/2);
var yPositionOnMap = currentYValue-(dotHeight/2);
// Creating DotShape.qml component
var comp = Qt.createComponent("DotShape.qml");
var dotComponent = comp.createObject(dotPositionOnMap,
{
"x": xPositionOnMap,
"y": yPositionOnMap,
"width": dotWidth,
"height": dotHeight,
"color": dotColor,
"radius": dotRadius
});
} // end for
} // end script
} // end Item
Finally DotShape.qml , which's just a small red dot plotted at x,y coordinate
import QtQuick 2.5
Rectangle
{
// Other properties are generated at runtime
id: dotShapeId;
objectName: "dotShapeId";
}

QML TableView with dynamic number of columns

I have been trying to use a QML TableView to display a QAbstractTableModel. The missing part of the equation seems to be that it is not possible to have a variable number of columns in the TableView, despite overriding QAbstractItemModel::roleNames which should tell Qt the number and name of my columns. I tried testing this out using only QML:
import QtQuick 2.0
import QtQuick.Controls 1.1
Rectangle {
anchors.fill: parent
property real showImage: 1.0
width: 500
TableView {
id: myTable
model: myModel
// TableViewColumn {
// role: "title"; title: "Name"; width: 200
// }
}
ListModel {
id: myModel
ListElement {
title: "item one"
}
ListElement {
title: "item two"
}
}
}
When run this doesn't show anything despite the TableView's mode containing ListElements with roles defined in them.
However if the above code is uncommented and a TableViewColumn is defined then the column will display data for that role as expected but the table will still not display any other roles. Obviously that will only work for a statically defined number of columns and not my case where the number of columns is not known until run time.
The example given is basically the same as my real life example except that my model is defined in C++.
It seems as if this may have already been asked here but it did not gain any response.
EDIT: I had tried calling a javascript function:
function addColumnToTable(roleName) {
var columnString = 'import QtQuick 2.3; import QtQuick.Controls 1.2; TableViewColumn {role: "'
+ roleName + '"; title: "' + roleName + '"; width: 40}';
var column = Qt.createQmlObject(
columnString
, myTable
, "dynamicSnippet1")
myTable.addColumn(column);
}
From C++:
QVariant roleName = "name";
QObject *root = view->rootObject();
QMetaObject::invokeMethod(root, "addColumnToTable", Q_ARG(QVariant, roleName));
This at least allowed me to dynamically add columns from C++ although not from within the model/view architecture. Yoann's solution is far and away better than this though.
You could create dynamically as many TableViewColumn as you need, using the resources property of your TableView.
You will have to add a method in your custom model class which will give you the roleNames you want to display.
QML:
Component
{
id: columnComponent
TableViewColumn{width: 100 }
}
TableView {
id: view
anchors.fill: parent
resources:
{
var roleList = myModel.customRoleNames
var temp = []
for(var i=0; i<roleList.length; i++)
{
var role = roleList[i]
temp.push(columnComponent.createObject(view, { "role": role, "title": role}))
}
return temp
}
model: myModel
MyModel.h:
class MyModel: public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(QStringList userRoleNames READ userRoleNames CONSTANT)
public:
explicit MyModel(QObject *parent = 0);
enum MyModelRoles {
UserRole1 = Qt::UserRole + 1,
UserRole2,
...
};
QStringList userRoleNames();
int rowCount(const QModelIndex & parent = QModelIndex()) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
...
private:
QHash<int, QByteArray> roleNames() const;
...
};
MyModel.cpp:
...
...
QHash<int, QByteArray> MyModel::roleNames() const {
QHash<int, QByteArray> roles = QAbstractListModel::roleNames ();
roles[UserRole1] = "whatever";
roles[UserRole2] = "youwant";
return roles;
}
QStringList MyModel::userRoleNames() // Return ordered List of user-defined roles
{
QMap<int, QString> res;
QHashIterator<int, QByteArray> i(roleNames());
while (i.hasNext()) {
i.next();
if(i.key() > Qt::UserRole)
res[i.key()] = i.value();
}
return res.values();
}
...
...
The solution with resources does not work for me (Qt 5.6). The TableView is not updated.
This works for me:
Component{
id: columnComponent
TableViewColumn{width: 30 }
}
TableView {
id: tableView
model: listModel
property var titles: somethingDynamic
property var curTitles: {
var t=[]
for(var i=0;i<columnCount;i++){
t.push(getColumn(i).title)
}
return t
}
onTitlesChanged:{
for(var i=0;i<titles.length;i++){
if(curTitles.indexOf(titles[i])==-1){
var column = addColumn(columnComponent)
column.title=titles[i]
column.role=titles[i]
}
}
for(var i=curTitles.length-1;i>=0;i--){
if(titles.indexOf(curTitles[i])==-1){
removeColumn(i)
}
}
}
}
}
It also correctly updates drag-drop-reordered columns.
Another way to do it with an Instantiator:
TableView{
id: view
model: tableViewModel
Instantiator{
model: someStringList
onObjectAdded: view.addColumn(object)
onObjectRemoved: view.removeColumn(object)
delegate: TableViewColumn{
width: 100
title: modelData
role: modelData
}
}
}
This code (and the one from #palfi) cause some warning in the console:
For each column created there is:
qml: TableView::insertColumn(): you cannot add a column to multiple views
And then when column are removed it produce this warning:
file:///usr/lib/qt/qml/QtQuick/Controls/Private/BasicTableView.qml:297: Error: Invalid attempt to destroy() an indestructible object
If someone has a better solution, please post it!

Data is not displayed in TableView

I try to build simple TableView with model from C++. My table has as many rows and columns, as return rowCount and columnCount method. This means, model is 'connected' with view, but in each cell does not display the message: 'Some data'
here is my code:
class PlaylistModel : public QAbstractTableModel
{
Q_OBJECT
public:
PlaylistModel(QObject *parent=0): QAbstractTableModel(parent), rows(0){}
int rowCount(const QModelIndex & /*parent*/) const
{
return 5;
}
int columnCount(const QModelIndex & /*parent*/) const
{
return 3;
}
QModelIndex index(int row, int column, const QModelIndex &parent) const {
return createIndex(row, column);
}
QModelIndex parent(const QModelIndex &child) const {
return child.parent();
}
QVariant data(const QModelIndex &index, int role) const{
if (role == Qt::DisplayRole)
{
return QString("Some data");
}
return QVariant();
}
(...)
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
PlaylistModel plModel(0);
engine.rootContext()->setContextProperty("myModel", &plModel);
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
and qml
TableView {
id: trackList
width: 100; height: 100
model: myModel
TableViewColumn { role: "id"; title: "Id"; width: 30 }
TableViewColumn { role: "name"; title: "Name"; width: 100}
TableViewColumn { role: "duration"; title: "Duration"; width: 20 }
}
Where I make a mistake?
Short answer
Because your QML is not asking for Qt::DisplayRole. Change your TableVievColumn to
TableViewColumn { role: "display"; title: "xxx"; width: 20 }
The QML is now asking for Qt::DisplayRole, and "Some data" is shown in this column.
Long answer
QML is asking for three user-defined roles: "id", "name", and "duration". However, the three roles are not bulit-in roles. Therefore you need to implement the three roles in your model class.
First, you should provide a set of roles to the model. The model returns data to views using QAbstractItemModel::data function. The type of role is int, we can write an enum in the model class:
class PlaylistModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum MyTableRoles
{
IdRole = Qt::UserRole + 1,
NameRole,
DurationRole
}
//...
};
Now, in the data function, returns the corresponding value any time the view is asking:
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()){return QVariant();}
switch(role)
{
case IdRole:
return GetIdFromMyTable(index);
case NameRole:
return GetNameFromMyTable(index);
case DurationRole:
return GetDurationFromMyTable(index);
}
//...
return QVariant();
}
Next, provide a string-to-int mapping for each role. The role in the model is in type of int, however in the QML the role is type of string. (see the role property in TableViewColumn.) Therefore we should provide a string-to-int mapping for each role so the QML can correctly asking for the required data. The mapping should be provided in QAbstractItemModel::roleNames():
QHash<int, QByteArray> PlaylistModel::roleNames() const
{
QHash<int, QByteArray> roleNameMap;
roleNameMap[IdRole] = "id";
roleNameMap[NameRole] = "name";
roleNameMap[DurationRole] = "duration";
return roleNameMap;
}
Finally your table in QML now can display the things you want.
Some references
When subclassing QAbstractTableModel,
you must implement rowCount(), columnCount(), and data(). Default implementations of the index() and parent() functions are provided by QAbstractTableModel.
When using C++ models in QML,
The roles of a QAbstractItemModel subclass can be exposed to QML by reimplementing QAbstractItemModel::roleNames().
And if you do not reimplement the roleNames function, you can use only the default roles declared in QAbstractItemModel::roleNames. And this is the reason why the short answer works.
You did not implemented index method.
According to documentation,
When subclassing QAbstractItemModel, at the very least you must
implement index(), parent(), rowCount(), columnCount(), and data().
These functions are used in all read-only models, and form the basis
of editable models.
in other words, You have to implement your own low-level item management, AbstractItemModel will not do it for you. You should create indexes with createIndex and destroy them when nesessary etc.
If you don't want to play these games and just want to implement your own quick&dirty model, consider subclassing QStandardItemModel.