I follow the Using C++ Models with Qt Quick Views and AbstractItemModel example project in Qt 5.5. Screen class is the model data; ScreenManager is derived from QAbstractListModel acting as the model source in QML GridView. In the GridView, I also add a MouseArea and some animation in delegate to implement items drag and drop.
My expected result is when dragging a screen around, whenever it's on top of another screen, the target screen will be moved to the last position of dragged screen until button release to drop the dragged screen. All the movement should use the animation declared in QML.
Now it could show screens correctly and recognize the selected screen. But it fails to swap the dragged and dropped screens. The underlying list has swapped the element but it doesn't reflect to the view.
A similar question is this, I try to use beginMoveRows and endMoveRows. But my program crashes on calling endMoveRows. layoutChanged would rearrange the whole model. Because I have animation on grid item movement. layoutChanged would cause non affected screens shifting from top left to their original position.
Edit: endMoveRows crash is caused by invalid operation described here.
Edit: the GridView has a 3 * 5 items. Since it's in a QList, I assume I only need to move on rows.
Screen.h
class Screen
{
public:
Screen(QString name, int gridId, bool active = false);
QString name() const;
int gridId() const;
bool active() const;
void setActive(bool a);
private:
QString m_name;
int m_gridId;
bool m_active;
};
ScreenManager.h
#include "Screen.h"
#include <QAbstractListModel>
class ScreenManager : public QAbstractListModel
{
Q_OBJECT
public:
enum ScreenRoles {
NameRole = Qt::UserRole + 1,
GridIDRole,
ActiveRole
};
ScreenManager();
void addScreen(const Screen& screen);
int rowCount(const QModelIndex& parent = QModelIndex()) const;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
Q_INVOKABLE int getScreenGridId(int index);
Q_INVOKABLE bool getScreenActive(int index);
Q_INVOKABLE void swapScreens(int index1, int index2);
protected:
QHash<int, QByteArray> roleNames() const;
private:
QList<Screen> m_screens;
};
ScreenManager.cpp
#include "ScreenManager.h"
#include "Screen.h"
ScreenManager::ScreenManager()
{
int index = 0;
for (;index < 15; index++) {
addScreen(Screen(QString ("Screen%1").arg(index), index, true));
}
}
void ScreenManager::addScreen(const Screen& screen)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_screens << screen;
endInsertRows();
}
int ScreenManager::rowCount(const QModelIndex& parent) const {
Q_UNUSED(parent);
return m_screens.count();
}
QVariant ScreenManager::data(const QModelIndex& index, int role) const
{
if (index.row() < 0 || index.row() >= m_screens.count())
return QVariant();
const Screen& screen = m_screens[index.row()];
if (role == NameRole)
return screen.name();
else if (role == GridIDRole)
return screen.gridId();
else if (role == ActiveRole)
return screen.active();
return QVariant();
}
QHash<int, QByteArray> ScreenManager::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[GridIDRole] = "gridId";
roles[ActiveRole] = "active";
return roles;
}
int ScreenManager::getScreenGridId(int index)
{
return m_screens.at(index).gridId();
}
bool ScreenManager::getScreenActive(int index)
{
return m_screens.at(index).active();
}
void ScreenManager::swapScreens(int index1, int index2)
{
int min = index1 < index2 ? index1 : index2;
int max = index1 > index2 ? index1 : index2;
bool r = beginMoveRows(QModelIndex(), min, min, QModelIndex(), max);
r = beginMoveRows(QModelIndex(), max-1, max-1, QModelIndex(), min);
m_screens.swap(index1, index2);
endMoveRows();
}
QML GridView related code
ScreenManager {
id : screenManager
}
GridView {
id: gridView
x: 82
y: 113
width: cellWidth * 5
height: cellHeight * 3
clip: true
anchors.bottom: parent.bottom
anchors.bottomMargin: 70
anchors.topMargin: 100
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
flickableDirection: Flickable.HorizontalAndVerticalFlick
cellWidth: 90; cellHeight: 90;
property bool ignoreMovementAnimation: true
MouseArea {
id: gridViewMouseArea
hoverEnabled: true
preventStealing : true
property int currentGridId: -1
property int preIndex
property int index: gridView.indexAt(mouseX, mouseY)
anchors.fill: parent
onPressAndHold: {
currentGridId = screenManager.getScreenGridId(index)
preIndex = index
preIndexBackup = preIndex
gridView.ignoreMovementAnimation = false
}
onReleased: currentGridId = -1
onPositionChanged: {
if (currentGridId != -1 && index != -1 && index != preIndex) {
if (screenManager.getScreenActive(index)) {
screenManager.swapScreens(preIndex, index)
preIndex = index
}
}
}
}
model: screenManager
delegate: Component {
Item {
id: gridViewDelegate
width: gridView.cellWidth; height: gridView.cellHeight
Image {
id: itemImage
parent: gridView
x: gridViewDelegate.x + 5
y: gridViewDelegate.y + 5
width: gridViewDelegate.width - 10
height: gridViewDelegate.height - 10;
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/res/image/screen_icon.png"
visible: active
Text {
text: name
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
anchors.fill: parent;
border.color: "grey"
border.width: 6
color: "transparent"; radius: 5
visible: itemImage.state === "active"
}
// specify the movement's animation for non-active screen icons
Behavior on x {
enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
}
Behavior on y {
enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
}
// specify the shaking animation for non-active screen icons when hold one icon
SequentialAnimation on rotation {
NumberAnimation { to: 2; duration: 60 }
NumberAnimation { to: -2; duration: 120 }
NumberAnimation { to: 0; duration: 60 }
running: gridViewMouseArea.currentGridId != -1 && itemImage.state !== "active"
loops: Animation.Infinite
alwaysRunToEnd: true
}
// specify the active screen's new position and size
states: State {
name: "active"
when: gridViewMouseArea.currentGridId == gridId
PropertyChanges {
target: itemImage
x: gridViewMouseArea.mouseX - width/2
y: gridViewMouseArea.mouseY - height/2
scale: 0.5
z: 10
}
}
// specify the scale speed for the active screen icon
transitions: Transition {
NumberAnimation { property: "scale"; duration: 200}
}
}
}
}
}
main.cpp
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
qmlRegisterType<ScreenManager>("com.gui", 1, 0, "ScreenManager");
QQmlApplicationEngine engine(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
It turns out I need to deal some corner cases to avoid no-op or invalid move operation.
void ScreenManager::swapScreens(int index1, int index2)
{
int min = index1 < index2 ? index1 : index2;
int max = index1 > index2 ? index1 : index2;
m_screens.swap(index1, index2);
beginMoveRows(QModelIndex(), max, max, QModelIndex(), min);
endMoveRows();
if (max - min > 1) {
beginMoveRows(QModelIndex(), min + 1, min + 1, QModelIndex(), max + 1);
endMoveRows();
}
}
Related
I have a simple sample project here which demonstrate the problem.
I've included below what I believe is the relevant source, but the remainder is available in the project link above or I can edit and include more if useful.
Based on some research, it appears that I need to use the Qt::DecorationRole in my data function and return an image when the column is 1. However, that part of the code is never executed. I am missing some important and obvious about how the role concept works with Qt QML TableView's.
What do I need to change so I can draw a circle in Column 1 (average age)? I'd like this circle to be red if the age < 13, yellow if < 35, and green otherwise.
main.qml
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import Qt.labs.qmlmodels 1.0
import Backend 1.0
ApplicationWindow
{
id: root
visible: true
width: 768
height: 450
minimumWidth: 768
minimumHeight: 450
property string backendReference: Backend.objectName
TableView
{
id: tableView
columnWidthProvider: function( column )
{
return 100;
}
rowHeightProvider: function( column )
{
return 23;
}
anchors.fill: parent
topMargin: columnsHeader.implicitHeight
model: Backend.modelResults.list
ScrollBar.horizontal: ScrollBar {}
ScrollBar.vertical: ScrollBar {}
clip: true
delegate: DelegateChooser {
// role: "type"
DelegateChoice {
roleValue: "decoration"
Rectangle
{
color: 'red'
anchors.fill: parent
}
}
DelegateChoice {
// roleValue: "display"
Rectangle
{
Text
{
text: display
anchors.fill: parent
anchors.margins: 10
color: 'black'
font.pixelSize: 15
verticalAlignment: Text.AlignVCenter
}
}
}
}
// Rectangle
// {
// Text
// {
// text: display
// anchors.fill: parent
// anchors.margins: 10
// color: 'black'
// font.pixelSize: 15
// verticalAlignment: Text.AlignVCenter
// }
// }
Rectangle // mask the headers
{
z: 3
color: "#222222"
y: tableView.contentY
x: tableView.contentX
width: tableView.leftMargin
height: tableView.topMargin
}
Row
{
id: columnsHeader
y: tableView.contentY
z: 2
Repeater
{
model: tableView.columns > 0 ? tableView.columns : 1
Label
{
width: tableView.columnWidthProvider(modelData)
height: 35
text: Backend.modelResults.list.headerData( modelData, Qt.Horizontal )
font.pixelSize: 15
padding: 10
verticalAlignment: Text.AlignVCenter
background: Rectangle
{
color: "#eeeeee"
}
}
}
}
ScrollIndicator.horizontal: ScrollIndicator { }
ScrollIndicator.vertical: ScrollIndicator { }
}
}
modeldata.cpp
#include "modeldata.h"
//
// ModelList
//
ModelList::
ModelList( QObject* parent )
: QAbstractTableModel (parent )
{
// mRoleNames = QAbstractTableModel::roleNames();
// mRoleNames.insert( 1, QByteArray( "type" ) );
}
int
ModelList::
rowCount(const QModelIndex &) const
{
int size = mList.size();
return size;
}
int
ModelList::
columnCount( const QModelIndex & ) const
{
return 2;
}
QVariant
ModelList::
data( const QModelIndex& index, int role ) const
{
const ModelItem modelItem = mList.at( index.row() );
QVariant result = QVariant();
if ( role == Qt::DisplayRole )
{
if ( index.column() == 0 )
{
result = QVariant( QString( modelItem.population ) );
}
else
{
result = QVariant( QString::number( modelItem.averageAge ) );
}
}
if ( role == Qt::DecorationRole )
{
qDebug() << "decorate 1";
}
return result;
}
QVariant
ModelList::
headerData( int section, Qt::Orientation orientation, int role ) const
{
if ( section == 0 )
return QVariant( QString( "Population" ) );
else
return QVariant( QString( "Average Age" ) );
}
int
ModelList::
size() const
{
return mList.size();
}
const QList<ModelItem>&
ModelList::
list() const
{
return mList;
}
void
ModelList::
removeAt( int index )
{
if ( index < 0 || index >= mList.size() )
return;
beginRemoveRows( QModelIndex(), index, index );
mList.removeAt( index );
endRemoveRows();
emit sizeChanged();
}
void
ModelList::
add( const QString& population, const int averageAge )
{
ModelItem item;
item.population = population;
item.averageAge = averageAge;
add( item );
}
QHash<int, QByteArray>
ModelList::
roleNames() const
{
return {
{ Qt::DisplayRole, "display" },
{ Qt::DecorationRole, "decorations" }
};
// return this->mRoleNames;
}
void
ModelList::
add(const ModelItem& item)
{
const int index = mList.size();
beginInsertRows( QModelIndex(), index, index );
mList.append( item );
endInsertRows();
emit sizeChanged();
}
void
ModelList::
reset()
{
if ( mList.isEmpty() )
return;
beginRemoveRows( QModelIndex(), 0, mList.size() - 1 );
mList.clear();
endRemoveRows();
emit sizeChanged();
}
//
// ModelResults
//
ModelResults::ModelResults(QObject* parent)
: QObject(parent)
{
mList = new ModelList( this );
qRegisterMetaType<ModelItem>("ModelItem");
}
ModelList* ModelResults::list() const
{
return mList;
}
void ModelResults::reset()
{
mList->reset();
}
I have been able to get the correct circle drawn in the averageAge field.
My ModelItem looks like:
struct ModelItem
{
Q_GADGET
Q_PROPERTY( QString population MEMBER population )
Q_PROPERTY( int averageAge MEMBER averageAge )
Q_PROPERTY( bool selected MEMBER selected )
public:
enum class Role {
Selection = Qt::UserRole,
ColumnType,
ColorValue
};
Q_ENUM(Role)
QString population;
int averageAge;
bool selected { false };
bool operator!=( const ModelItem& other )
{
return other.population != this->population
|| other.averageAge != this->averageAge;
}
};
The key point here is the definition of the ColumnType and ColorValue Role.
I needed a roleNames function for my custom role
QHash<int, QByteArray>
ModelList::
roleNames() const
{
return {
{ Qt::DisplayRole, "display" },
{ int( ModelItem::Role::Selection ), "selected" },
{ int( ModelItem::Role::ColumnType ), "type" },
{ int( ModelItem::Role::ColorValue ), "colorValue" }
};
}
The custom roles needed to be supplied by roleNames and have the strings "type" and "colorValue" specified.
My data function looks like:
QVariant
ModelList::
data( const QModelIndex& index, int role ) const
{
const ModelItem modelItem = mList.at( index.row() );
QVariant result = QVariant();
if ( role == Qt::DisplayRole )
{
if ( index.column() == 0 )
{
result = QVariant( QString( modelItem.population ) );
}
else
{
result = QVariant( QString::number( modelItem.averageAge ) );
}
}
if ( role == int( ModelItem::Role::Selection ) )
{
result = QVariant( QString( modelItem.selected ? "#eeeeee" : "white" ) );
}
if ( role == int( ModelItem::Role::ColumnType ) )
{
if ( index.column() == 0 )
result = QVariant( QString( "stringValue" ) );
else
result = QVariant( QString( "colorValue" ) );
}
if ( role == int( ModelItem::Role::ColorValue ) )
{
QString color;
if ( modelItem.averageAge < 13 )
color = "red";
else if ( modelItem.averageAge < 35 )
color = "yellow";
else
color = "green";
result = QVariant( color );
}
qDebug() << role << " " << result;
return result;
}
A key point here is that when the role ColumnType is used, I return whether or not the column is a stringValue or a colorValue.
Additionally, when the role ColorValue is used, I look at the averageAge of the modelItem and return a string containing the color to be used.
The final piece is to have the QML use the custom roles.
delegate: DelegateChooser
{
role: "type"
DelegateChoice
{
roleValue: "colorValue"
delegate: Rectangle
{
color: selected
Rectangle
{
color: colorValue
width: parent.height
height: parent.height
radius: width * 0.5;
anchors.horizontalCenter: parent.horizontalCenter;
}
MouseArea
{
anchors.fill: parent
onClicked:
{
var idx = Backend.modelResults.list.index( row, column )
console.log( "Clicked cell: ", idx.row, " ", Backend.modelResults.list.data( idx ) )
Backend.modelResults.list.select( idx.row );
}
}
}
}
DelegateChoice
{
delegate: Rectangle
{
color: selected
Text
{
text: display
anchors.fill: parent
anchors.margins: 10
color: 'black'
font.pixelSize: 15
verticalAlignment: Text.AlignVCenter
}
MouseArea
{
anchors.fill: parent
onClicked:
{
var idx = Backend.modelResults.list.index( row, column )
console.log( "Clicked cell: ", idx.row, " ", Backend.modelResults.list.data( idx ) )
Backend.modelResults.list.select( idx.row );
}
}
}
}
}
First, for the DelegateChooser, the role is specified by our custom "type" role. The system knows to call our data function with this role. When the data function returns "colorValue", the first DelegateChoice is selected based because the roleValue is "colorValue". The second DelegateChoice does not have a roleValue because it appears there needs to be a default DelegateChoice and "stringValue" is the default.
Second, the "colorValue" delegate choice has defined color: colorValue. This causes the system to again call the data function with the ColorValue role and it then returns the correct color for the cell.
The example project has been updated.
Suggested improvement to this solution are welcome.
I know there are a lot of different questions on stackoverflow and other forums about dynamically filling a combobox from c++, but out of all those questions i cant find an answer that i need. Currently i am fetching a list from my database in C++ and store that in my CompanyList class. My CompanyModel class uses that class and communicates to my qml ui. In my QML editor i set the model to CompanyModel.list and the textRole to the value i want from the struct.
The problem that im facing is that i am not getting any errors, but my combobox is still empty. I cant find the problem so i hope someone can look over the mistake i might have made.
My Company Struct
struct CompanyStruct {
int id;
QString name;
};
My Company List
CompanyList::CompanyList(QObject *parent) : QObject(parent)
{
appendItem({-1, "test company"});
appendItem({-2, "test company 2"});
}
QVector<CompanyStruct> CompanyList::items() const
{
return mItems;
}
bool CompanyList::setItemAt(int index, const CompanyStruct &item)
{
if (index < 0 || index >= mItems.size())
return false;
const CompanyStruct &oldItem = mItems.at(index);
if (item.id == oldItem.id && item.name == oldItem.name)
return false;
mItems[index] = item;
return true;
}
void CompanyList::appendItem()
{
emit preItemAppended();
CompanyStruct company;
company.id = -1;
company.name = "This is a test company!";
mItems.append(company);
emit postItemAppended();
}
void CompanyList::appendItem(CompanyStruct item)
{
emit preItemAppended();
mItems.append(item);
emit postItemAppended();
}
My Company Model
#include "companymodel.h"
#include "companylist.h"
CompanyModel::CompanyModel(QObject *parent)
: QAbstractListModel(parent)
, mList(nullptr)
{
}
int CompanyModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid() || !mList)
return 0;
return mList->items().size();
}
QVariant CompanyModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !mList)
return QVariant();
const CompanyStruct item = mList->items().at(index.row());
switch (role) {
case IdRole:
return QVariant(item.id);
case NameRole:
return QVariant(item.name);
}
return QVariant();
}
bool CompanyModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!mList)
return false;
CompanyStruct item = mList->items().at(index.row());
switch (role) {
case IdRole:
item.id = value.toInt();
break;
case NameRole:
item.name = value.toString();
break;
}
if (mList->setItemAt(index.row(), item)) {
emit dataChanged(index, index, QVector<int>() << role);
return true;
}
return false;
}
Qt::ItemFlags CompanyModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsSelectable;
}
QHash<int, QByteArray> CompanyModel::roleNames() const
{
QHash<int, QByteArray> names;
names[IdRole] = "id";
names[NameRole] = "name";
return names;
}
CompanyList *CompanyModel::list() const
{
return mList;
}
void CompanyModel::setList(CompanyList *list)
{
beginResetModel();
if (mList)
mList->disconnect(this);
mList = list;
if (mList) {
connect(mList, &CompanyList::preItemAppended, this, [=]() {
const int index = mList->items().size();
beginInsertRows(QModelIndex(), index, index);
});
connect(mList, &CompanyList::postItemAppended, this, [=]() {
endInsertRows();
});
}
endResetModel();
}
My Main.cpp
qmlRegisterType<CompanyModel>("Company", 1,0, "CompanyModel");
qmlRegisterUncreatableType<CompanyList>("Company", 1,0, "CompanyList", "CompanyList should not be created in QML");
CompanyList companyList;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("companyList", &companyList);
My combobox in QML
import Company 1.0
ComboBox {
Layout.column: 1
Layout.columnSpan: 3
Layout.row: 4
id: cbSelectKlant
implicitWidth: parent.width*0.6
implicitHeight: parent.height*0.05
background: Rectangle {
color: "white"
border.color: "#6abc93"
border.width: 3
width: parent.width
height: parent.height
}
textRole: "name"
model: CompanyModel.list
}
Normally you will create your models in C++ and expose them to QML.
You don't even have to use qmlRegisterType if you derive your presentation data from QObject.
For the sake of simplicity I made header only code except main.cpp
//company.h file:
#ifndef COMPANY_H
#define COMPANY_H
#include <QObject>
class Company : public QObject
{
Q_OBJECT
Q_PROPERTY(int id READ id WRITE setId CONSTANT)
Q_PROPERTY(QString name READ name WRITE setName CONSTANT)
Q_PROPERTY(QString displayName READ displayName CONSTANT)
public:
Company(QObject *parent=nullptr) : QObject(parent)
{}
int id() const { return mID; }
void setId(const int id) { mID = id; }
QString name() const { return mName; }
void setName(const QString& name) { mName = name; }
QString displayName() { return "Company: " + mName + ", ID: " + QString::number(mID); }
private:
int mID;
QString mName;
};
#endif // COMPANY_H
//companylistmodel.h file:
#ifndef COMPANYLISTMODEL_H
#define COMPANYLISTMODEL_H
#include "company.h"
#include <QAbstractListModel>
#include <vector>
class CompanyListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles
{
CompanyRole = Qt::UserRole
};
CompanyListModel(QObject* parent = nullptr) : QAbstractListModel(parent)
{
mCompanies.push_back(new Company(this));
mCompanies.back()->setName("Google"); mCompanies.back()->setId(1);
mCompanies.push_back(new Company(this));
mCompanies.back()->setName("Microsoft"); mCompanies.back()->setId(2);
mCompanies.push_back(new Company(this));
mCompanies.back()->setName("CraftUnique"); mCompanies.back()->setId(3);
}
int rowCount(const QModelIndex& parent = QModelIndex()) const override { return mCompanies.size(); }
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override { return QVariant::fromValue(mCompanies.at(index.row())); }
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override { return true; } // use property instead
virtual QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> names;
names[CompanyRole] = "companyRole";
return names;
}
private:
std::vector<Company*> mCompanies;
};
#endif // COMPANYLISTMODEL_H
// main.cpp file:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickView>
#include <QQmlContext>
#include "companylistmodel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQuickView view;
CompanyListModel clm;
view.rootContext()->setContextProperty("clm", &clm);
view.setSource(QStringLiteral("qrc:/main.qml"));
view.show();
return app.exec();
}
//main.qml file:
import QtQuick 2.12
import QtQuick.Controls 2.12
Rectangle {
anchors.fill: parent
color: "white"
Component.onCompleted: { console.log(clm) }
ComboBox {
id: cbSelectKlant
implicitWidth: parent.width
implicitHeight: 30
model: clm
textRole: "companyRole.displayName"
contentItem: Label {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
text: cbSelectKlant.currentText
}
background: Rectangle {
color: "gray"
}
delegate: ItemDelegate {
implicitWidth: parent.width
implicitHeight: 30
contentItem: Label {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
text: companyRole.name + " (" + companyRole.id + ")"
}
}
}
}
You can even assign values from QML like: companyRole.name = "SomeCompany"
And this won't call the setData method of your model, but directly update the property itself.
The first part of the problem is that you are not initializing mList in the code given (which I think is a bit short of a MRE ).
Change the constructor to the following:
CompanyModel::CompanyModel(QObject *parent)
: QAbstractListModel(parent)
, mList(new CompanyList(this))
{
}
And you are also allowing the qml-developer in you to instantiate the CompanyModel in QML (first line of main.cpp), which you are kinda doing, but not actually (last line of the ComboBox QML code). This way the CompanyModel is not instantiated, but you are still trying to get the list property from the class-definition, which will be undefined (IIC), leading to an empty ComboBox without error.
This can be solved by instantiating it in QML:
import Company 1.0
ComboBox {
Layout.column: 1
Layout.columnSpan: 3
Layout.row: 4
id: cbSelectKlant
implicitWidth: parent.width*0.6
implicitHeight: parent.height*0.05
background: Rectangle {
color: "white"
border.color: "#6abc93"
border.width: 3
width: parent.width
height: parent.height
}
textRole: "name"
model: theModel.list
CompanyModel {
id: theModel
}
}
However, I doubt this is what you want, since you will not be able to control theModel from C++. Which is probably also why you are setting the rootContext property, but you are not actually using it (last line of main.cpp).
So, change the QML to this:
import Company 1.0
ComboBox {
Layout.column: 1
Layout.columnSpan: 3
Layout.row: 4
id: cbSelectKlant
implicitWidth: parent.width*0.6
implicitHeight: parent.height*0.05
background: Rectangle {
color: "white"
border.color: "#6abc93"
border.width: 3
width: parent.width
height: parent.height
}
textRole: "name"
model: companyList.list
}
As a last advise, also change the first line of main.cpp to:
qmlRegisterUncreatableType<CompanyModel>("Company", 1,0, "CompanyModel", "CompanyModel should not be created in QML");
I'm trying to connect several GeoCoordinates with a polyline in my map. The Coordinates are stored in an model (QAbstractListModel) class, where I can modify (remove and add) the Coords in C++.
Simply displaying markers for every coordinate works fine, but when I connect them with a Polyline and delete some existing coordinates from my model, the Polyline does not get updated.
map.qml
import QtQuick 2.0
import QtLocation 5.6
import QtPositioning 5.6
Item {
width: 512
height: 512
visible: true
id: mainWindow
Map { //our map
anchors.fill: parent
id: map
plugin: mapPlugin
zoomLevel: 14
focus: true
Plugin {
id: mapPlugin
name: "osm"
}
MapItemView {
model: markerModel
delegate: mapcomponent
}
MapItemView {
model: markerModel
delegate: MapPolyline {
antialiasing: true
line.color: "darkBlue"
line.width: 3
path: pathRole
}
}
Component {
id: mapcomponent
MapQuickItem{
id: marker
coordinate: positionRole
sourceItem: Image{
id: markerimage
width: 20
height: 30
source: "qrc:/marker.png"
}
anchorPoint.x: markerimage.width / 2
anchorPoint.y: markerimage.height
}
}
MouseArea {
anchors.fill: parent
onClicked: {
var coord = parent.toCoordinate(Qt.point(mouse.x,mouse.y))
markerModel.addMarker(coord)
}
}
}
}
markermodel.h which stores the coordinates
class MarkerModel : public QAbstractListModel {
Q_OBJECT
public:
using QAbstractListModel::QAbstractListModel;
enum MarkerRoles{positionRole = Qt::UserRole, pathRole};
Q_INVOKABLE void addMarker(const QGeoCoordinate &coordinate) {
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_coordinates.append(coordinate);
endInsertRows();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
Q_UNUSED(parent)
return m_coordinates.count();
}
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if(row + count > m_coordinates.count() || row < 0)
return false;
beginRemoveRows(parent, row, row+count-1);
for(int i = 0; i < count; ++i)
m_coordinates.removeAt(row + i);
endRemoveRows();
return true;
}
bool removeRow(int row, const QModelIndex &parent = QModelIndex()) {
return removeRows(row, 1, parent);
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (index.row() < 0 || index.row() >= m_coordinates.count())
return QVariant();
if(role == Qt::DisplayRole)
return QVariant::fromValue(index.row());
if(role == MarkerModel::positionRole)
return QVariant::fromValue(m_coordinates[index.row()]);
if(role == MarkerModel::pathRole) {
QVariantList coordlist;
for(auto &geocoord : m_coordinates)
coordlist << QVariant::fromValue(geocoord);
return coordlist;
}
return QVariant();
}
QHash<int, QByteArray> roleNames() const {
QHash<int, QByteArray> roles;
roles[positionRole] = "positionRole";
roles[pathRole] = "pathRole";
return roles;
}
private:
QList<QGeoCoordinate> m_coordinates;
};
A gif showing my problem:
Did I miss something when deleting rows in my model? Is there a way to force qml to render the polyline with completly new data from the model?
Having a pathRole doesn't make much sense here. This role is the same for all the rows of your model.
You display n MapPolyline where n is the number of coordinates, you only need one for your entire model.
I would advise you to remove the pathRole and just expose a path property in your model, and emit its notify signal in addMarker and removeRows.
As they indicate your code has a logic error, in my answer I will show the implementation since #GrecKo already explained the correct logic:
markermodel.h
#ifndef MARKERMODEL_H
#define MARKERMODEL_H
#include <QAbstractListModel>
#include <QGeoCoordinate>
class MarkerModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(QVariantList path READ path NOTIFY pathChanged)
public:
enum MarkerRoles{positionRole = Qt::UserRole, pathRole};
MarkerModel(QObject *parent=nullptr): QAbstractListModel(parent)
{
connect(this, &QAbstractListModel::rowsInserted, this, &MarkerModel::pathChanged);
connect(this, &QAbstractListModel::rowsRemoved, this, &MarkerModel::pathChanged);
connect(this, &QAbstractListModel::dataChanged, this, &MarkerModel::pathChanged);
connect(this, &QAbstractListModel::modelReset, this, &MarkerModel::pathChanged);
connect(this, &QAbstractListModel::rowsMoved, this, &MarkerModel::pathChanged);
}
Q_INVOKABLE void addMarker(const QGeoCoordinate &coordinate) {
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_coordinates.append(coordinate);
endInsertRows();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
if(parent.isValid()) return 0;
return m_coordinates.count();
}
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if(row + count > m_coordinates.count() || row < 0)
return false;
beginRemoveRows(parent, row, row+count-1);
for(int i = 0; i < count; ++i)
m_coordinates.removeAt(row + i);
endRemoveRows();
return true;
}
bool removeRow(int row, const QModelIndex &parent = QModelIndex()) {
return removeRows(row, 1, parent);
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (index.row() < 0 || index.row() >= m_coordinates.count())
return QVariant();
if(role == Qt::DisplayRole)
return QVariant::fromValue(index.row());
else if(role == MarkerModel::positionRole)
return QVariant::fromValue(m_coordinates[index.row()]);
return QVariant();
}
QHash<int, QByteArray> roleNames() const override{
QHash<int, QByteArray> roles;
roles[positionRole] = "positionRole";
return roles;
}
QVariantList path() const{
QVariantList path;
for(const QGeoCoordinate & coord: m_coordinates){
path << QVariant::fromValue(coord);
}
return path;
}
signals:
void pathChanged();
private:
QList<QGeoCoordinate> m_coordinates;
};
#endif // MARKERMODEL_H
*.qml
import QtQuick 2.0
import QtLocation 5.6
import QtPositioning 5.6
Item {
width: 512
height: 512
visible: true
id: mainWindow
Map { //our map
anchors.fill: parent
id: map
plugin: mapPlugin
zoomLevel: 14
focus: true
Plugin {
id: mapPlugin
name: "osm"
}
MapItemView {
model: markerModel
delegate: mapcomponent
}
MapPolyline{
antialiasing: true
line.color: "darkBlue"
line.width: 3
path: markerModel.path
}
Component {
id: mapcomponent
MapQuickItem{
id: marker
coordinate: positionRole
sourceItem: Image{
id: markerimage
width: 20
height: 30
source: "qrc:/marker.png"
}
anchorPoint.x: markerimage.width / 2
anchorPoint.y: markerimage.height
}
}
MouseArea {
anchors.fill: parent
onClicked: {
var coord = parent.toCoordinate(Qt.point(mouse.x,mouse.y))
markerModel.addMarker(coord)
}
}
}
}
Maybe my question isn't clear but i hope that with the code my problem become more clear.
I have a created these two structs and i use them in FormModel class :
struct Tile
{
QString name;
QString color;
}; Q_DECLARE_METATYPE(Tile)
struct Form
{
QString nameForm;
Tile grid[9] ; // i want for each form 4 tile that in qml are 4 rectangles
}; Q_DECLARE_METATYPE(Form)
class FormModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit FormModel(QObject *parent = 0);
~FormModel();
enum dashBoardRoles {
NameForm=Qt::UserRole+1,
Grid,
};
int rowCount(const QModelIndex &parent=QModelIndex()) const;
QVariant data(const QModelIndex &index, int role) const;
Q_INVOKABLE bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) Q_DECL_OVERRIDE;
Q_INVOKABLE bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) Q_DECL_OVERRIDE;
QHash<int, QByteArray> roleNames() const;
signals:
public slots:
private:
QList<Form> dashboard;
};
This is data method of QAbstractListModel:
QVariant FormModel::data(const QModelIndex &index, int role) const
{
if(index.row()>0 && index.row() >= dashboard.count())
return QVariant();
Form dashTemp = dashboard[index.row()];
if(role== NameForm)
return dashTemp.nameForm;
if(role== Grid)
{
QVariant gridVect = QVariant::fromValue(dashTemp.grid);
return gridVect;
}
return QVariant();
}
QHash<int, QByteArray> FormModel::roleNames() const
{
QHash <int,QByteArray> roles;
roles [NameForm]="nameForm";
roles [Grid]="grid";
return roles;
}
this is the qml code:
Window {
id:app
visible: true
width: 480
height: 800
title: qsTr("Hello World")
ListView {
model: myForms // i have defined the name in main.cpp
anchors.fill: parent
delegate: ColumnLayout{
spacing: 30
// nome Form
Text{
text: nameForm
font.pointSize:20
color: "red"
font.capitalization: Font.Capitalize
}
// grigla mattonelle
GridView {
id:grid
width: 300; height: 300
cellWidth: 100 ; cellHeight: 100
model: 9
delegate: Item{
width : grid.cellWidth
height: grid.cellHeight
Rectangle{
anchors.centerIn: parent
width:parent.width-10
height: parent.height-10
color: grid[index].color
}
}
}
}
}
}
How i can get the color from the data of role Grid? that in qml i call with grid[index].color....
QML does not directly recognize the struct, for this you have to use Q_GADGET with Q_PROPERTY, it is also advisable to use QVariantList instead of an array since QML can cast it directly. All the above I have implemented it in the following code:
formmodel.h
#ifndef FORMMODEL_H
#define FORMMODEL_H
#include <QAbstractListModel>
struct Tile
{
Q_GADGET
Q_PROPERTY(QString name MEMBER name)
Q_PROPERTY(QString color MEMBER color)
public:
QString name;
QString color;
Tile(const QString& name="", const QString& color=""){
this->name = name;
this->color = color;
}
};
Q_DECLARE_METATYPE(Tile)
struct Form
{
Q_GADGET
Q_PROPERTY(QString nameForm MEMBER nameForm)
Q_PROPERTY(QVariantList grid MEMBER grid)
public:
Form(){
}
QString nameForm;
QVariantList grid;
};
Q_DECLARE_METATYPE(Form)
class FormModel : public QAbstractListModel
{
enum dashBoardRoles {
NameForm=Qt::UserRole+1,
Grid
};
public:
FormModel(QObject *parent = Q_NULLPTR);
QHash<int, QByteArray> roleNames() const;
int rowCount(const QModelIndex &parent=QModelIndex()) const;
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const;
private:
QList<Form> dashboard;
};
#endif // FORMMODEL_H
formmodel.cpp
#include "formmodel.h"
#include <QColor>
FormModel::FormModel(QObject *parent):QAbstractListModel(parent)
{
for(int i=0; i<10; i++){
Form form;
form.nameForm = QString("name %1").arg(i);
for(int j=0; j<9; j++){
QColor color(qrand() % 256, qrand() % 256, qrand() % 256);
Tile tile{QString("name %1 %2").arg(i).arg(j), color.name()};
form.grid<< QVariant::fromValue(tile);
}
dashboard<< form;
}
}
QHash<int, QByteArray> FormModel::roleNames() const
{
QHash <int,QByteArray> roles;
roles [NameForm]="nameForm";
roles [Grid]="grid";
return roles;
}
int FormModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return dashboard.count();
}
QVariant FormModel::data(const QModelIndex &index, int role) const
{
if(index.row()<0 && index.row() >= dashboard.count())
return QVariant();
Form dashTemp = dashboard[index.row()];
if(role== NameForm)
return dashTemp.nameForm;
else if(role== Grid)
return dashTemp.grid;
return QVariant();
}
main.qml
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Layouts 1.3
Window {
id:app
visible: true
width: 480
height: 800
title: qsTr("Hello World")
ListView {
model: myForms
anchors.fill: parent
delegate: ColumnLayout{
spacing: 30
Text{
text: nameForm
font.pointSize:20
color: "red"
font.capitalization: Font.Capitalize
}
GridView {
id:gr
width: 300; height: 300
cellWidth: 100 ; cellHeight: 100
model: grid // grid.length
delegate: Item{
width : gr.cellWidth
height: gr.cellHeight
Rectangle{
anchors.centerIn: parent
width:parent.width-10
height: parent.height-10
color: grid[index].color
Text {
id: name
anchors.fill: parent
text: grid[index].name
}
}
}
}
}
}
}
The complete example can be found in the following link.
I use qt quick 2.0. A gridview is bound to a c++ model as described in here. In the gridview's delegate, I use an image to show an icon. I try to use state property to change image source and bind state to the model.
The expected result would be on release the selected screen image should be changed to running icon.
The actual result it image doesn't change. If I use setName instead of setState in ScreenManager::setScreenState, it shows changed screen name correctly.
Is there any better solution?
Screen.h
class Screen
{
public:
Screen(QString name, int gridId, bool active = false);
QString name() const;
int gridId() const;
bool active() const;
QString state() const;
void setName(QString n);
void setActive(bool a);
void setState(QString s);
private:
QString m_name;
int m_gridId;
bool m_active;
QString m_state;
};
ScreenManager.h
class ScreenManager : public QAbstractListModel
{
Q_OBJECT
public:
enum ScreenRoles {
NameRole = Qt::UserRole + 1,
GridIDRole,
ActiveRole,
StateRole
};
ScreenManager();
void addScreen(const Screen& screen);
int rowCount(const QModelIndex& parent = QModelIndex()) const;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
QModelIndex getIndex(int row, int column = 0,
const QModelIndex &parent = QModelIndex()) const;
Q_INVOKABLE int getScreenGridId(int index);
Q_INVOKABLE bool getScreenActive(int index);
Q_INVOKABLE void swapScreens(int index1, int index2);
Q_INVOKABLE void setScreenState(int index, QString s);
Q_INVOKABLE QString getScreenState(int index);
protected:
QHash<int, QByteArray> roleNames() const;
private:
QList<Screen> m_screens;
};
ScreenManager.cpp
#include "ScreenManager.h"
#include "Screen.h"
ScreenManager::ScreenManager()
{
int index = 0;
for (;index < 15; index++) {
addScreen(Screen(QString ("Screen%1").arg(index), index, false));
}
m_screens[2].setActive(true);
m_screens[6].setActive(true);
m_screens[7].setActive(true);
m_screens[8].setActive(true);
m_screens[12].setActive(true);
}
void ScreenManager::addScreen(const Screen& screen)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_screens.append(screen);
endInsertRows();
}
int ScreenManager::rowCount(const QModelIndex& parent) const {
Q_UNUSED(parent);
return m_screens.count();
}
QVariant ScreenManager::data(const QModelIndex& index, int role) const
{
if (index.row() < 0 || index.row() >= m_screens.count())
return QVariant();
const Screen& screen = m_screens[index.row()];
if (role == NameRole)
return screen.name();
else if (role == GridIDRole)
return screen.gridId();
else if (role == ActiveRole)
return screen.active();
else if (role == StateRole)
return screen.state();
return QVariant();
}
QModelIndex ScreenManager::getIndex(int row, int column,
const QModelIndex &parent) const
{
return hasIndex(row, column, parent) ?
createIndex(row, column, (void*)&m_screens[row])
: QModelIndex();
}
QHash<int, QByteArray> ScreenManager::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[GridIDRole] = "gridId";
roles[ActiveRole] = "active";
roles[StateRole] = "state";
return roles;
}
int ScreenManager::getScreenGridId(int index)
{
return m_screens.at(index).gridId();
}
bool ScreenManager::getScreenActive(int index)
{
return m_screens.at(index).active();
}
void ScreenManager::swapScreens(int index1, int index2)
{
int min = index1 < index2 ? index1 : index2;
int max = index1 > index2 ? index1 : index2;
m_screens.swap(index1, index2);
beginMoveRows(QModelIndex(), max, max, QModelIndex(), min);
endMoveRows();
if (max - min > 1) {
beginMoveRows(QModelIndex(), min + 1, min + 1, QModelIndex(), max + 1);
endMoveRows();
}
}
void ScreenManager::setScreenState(int index, QString s)
{
// if use setName, the grid view can show the changed screen name
m_screens[index].setState(s);
dataChanged(getIndex(0), getIndex(rowCount() - 1));
}
QString ScreenManager::getScreenState(int index)
{
return m_screens[index].state();
}
QML
GridView {
id: gridView
x: 82
y: 113
width: cellWidth * 5
height: cellHeight * 3
clip: true
anchors.bottom: parent.bottom
anchors.bottomMargin: 70
anchors.topMargin: 100
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
flickableDirection: Flickable.HorizontalAndVerticalFlick
cellWidth: 90; cellHeight: 90;
property bool ignoreMovementAnimation: true
MouseArea {
id: gridViewMouseArea
hoverEnabled: true
preventStealing : true
property int currentGridId: -1
property int preIndex
property int index: gridView.indexAt(mouseX, mouseY)
anchors.fill: parent
onPressAndHold: {
currentGridId = screenManager.getScreenGridId(index)
preIndex = index
gridView.ignoreMovementAnimation = false
}
onReleased: {
currentGridId = -1
screenManager.setScreenState(index, "running");
}
onPositionChanged: {
if (currentGridId != -1 && index != -1 && index != preIndex) {
if (screenManager.getScreenActive(index)) {
screenManager.swapScreens(preIndex, index)
preIndex = index
}
}
}
}
model: screenManager
delegate: Component {
Item {
id: gridViewDelegate
width: gridView.cellWidth; height: gridView.cellHeight
state: state
states: [
State {
name: "running"
PropertyChanges {
target: itemImage
source: "qrc:/res/image/screen_icon_running.png"
}
},
State {
name: "idle"
PropertyChanges {
target: itemImage
source: "qrc:/res/image/screen_icon_idle.png"
}
}
]
Image {
id: itemImage
parent: gridView
x: gridViewDelegate.x + 5
y: gridViewDelegate.y + 5
width: gridViewDelegate.width - 10
height: gridViewDelegate.height - 10;
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/res/image/screen_icon.png"
visible: active
Text {
text: name
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
anchors.fill: parent;
border.color: "grey"
border.width: 6
color: "transparent"; radius: 5
visible: itemImage.state === "active"
}
// specify the movement's animation for non-active screen icons
Behavior on x {
enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
}
Behavior on y {
enabled: !gridView.ignoreMovementAnimation && itemImage.state !== "active"
NumberAnimation { duration: 400; easing.type: Easing.OutBack }
}
// specify the shaking animation for non-active screen icons when hold one icon
SequentialAnimation on rotation {
NumberAnimation { to: 2; duration: 60 }
NumberAnimation { to: -2; duration: 120 }
NumberAnimation { to: 0; duration: 60 }
running: gridViewMouseArea.currentGridId != -1 && itemImage.state !== "active"
loops: Animation.Infinite
alwaysRunToEnd: true
}
// specify the active screen's new position and size
states: State {
name: "active"
when: gridViewMouseArea.currentGridId == gridId
PropertyChanges {
target: itemImage
x: gridViewMouseArea.mouseX - width/2
y: gridViewMouseArea.mouseY - height/2
scale: 0.5
z: 10
}
}
// specify the scale speed for the active screen icon
transitions: Transition {
NumberAnimation { property: "scale"; duration: 200}
}
}
}
}
}
You have a naming problem, because state is also know for the state property in your item delegate.
After i changed:
roles[StateRole] = "statetest";
in you c++
and:
state: statetest
in your qml,
it works.
Or simply:
state: model.state
in your qml
I solved the problem by binding image source to another property in Screen, stateIamge, rather than binding it to state directly.
But I'm still interested to see why binding to state doesn't work.