The most complex and ellegant way to export QList of custom objects to QML is to subclass one of the Qt abstract models ( QAbstractItemModel, or easier QAbstractListModel, QAbstractTableModel ) and build up a whole model with user specified roles. Subclassing QAbstractItemModel allows you to define all the properties and behaviour of your model, however in this case it is enough to subclass QAbstractListModel. In that case we have to provide implementations of only a few pure virtual methods plus some methods to manipulate models content.
Following implementation will show how to subclass QAbstractListModel with a class providing custom roles which will be used for text, its color and image source path. The good point is that they are defined as QVariants so it doesn’t matter if you pass a color, number or text. Expected result is similar to the one in a previous example.
Lets start with defining a list model item. Methods necessary to define fine read/write item are Getter, Setter and a method returning QHash binding role with member name description. Following example should explain this more clearly.
Base class definition:
listitemapi.h
#ifndef LISTITEMAPI_H #define LISTITEMAPI_H #include <QObject> class ListItemAPI : public QObject { Q_OBJECT public: ListItemAPI( QObject* a_pParent = 0 ) : QObject( a_pParent ) {} virtual ~ListItemAPI() {} virtual QVariant GetData( int a_iRole ) const = 0; virtual void SetData( int a_iRole, QVariant a_variantData ) = 0; virtual QHash<int, QByteArray> RoleNames() const = 0; }; #endif // LISTITEMAPI_H
Real item class implementation:
mylistitem.h
#ifndef MYLISTITEM_H #define MYLISTITEM_H #include <QVariant> #include <QColor> #include <QHash> #include "listitemapi.h" class MyListItem : public ListItemAPI { Q_OBJECT public: enum ERoles { ROLE_1 = Qt::UserRole + 1 , ROLE_2 = Qt::UserRole + 2 , ROLE_3 = Qt::UserRole + 3 , ROLE_4 = Qt::UserRole + 4 , ROLE_5 = Qt::UserRole + 5 }; explicit MyListItem( QObject* a_pParent = 0 ); MyListItem( const MyListItem& a_rOther ); virtual ~MyListItem(); virtual QVariant GetData( int a_iRole ) const; virtual void SetData( int a_iRole, QVariant a_variantData ); virtual QHash<int, QByteArray> RoleNames() const; private: void SetInitialValues(); QHash<ERoles, QVariant> m_aDataHash; }; #endif // MYLISTITEM_H
mylistitem.cpp
#include "mylistitem.h" MyListItem::MyListItem( QObject* a_pParent ) : ListItemAPI( a_pParent ) { SetInitialValues(); } MyListItem::MyListItem( const MyListItem& a_rOther ) : ListItemAPI( a_rOther.parent() ) { m_aDataHash = a_rOther.m_aDataHash; } MyListItem::~MyListItem() { } QVariant MyListItem::GetData( int a_iRole ) const { return m_aDataHash[static_cast<ERoles>( a_iRole )]; } void MyListItem::SetData( int a_iRole, QVariant a_variantData ) { m_aDataHash[static_cast<ERoles>( a_iRole )] = a_variantData; } QHash<int, QByteArray> MyListItem::RoleNames() const { QHash<int, QByteArray> aRoleNames; aRoleNames[ROLE_1] = "m_role1"; aRoleNames[ROLE_2] = "m_role2"; aRoleNames[ROLE_3] = "m_role3"; aRoleNames[ROLE_4] = "m_role4"; aRoleNames[ROLE_5] = "m_role5"; return aRoleNames; } void MyListItem::SetInitialValues() { m_aDataHash[ROLE_1] = QVariant( "string" ); m_aDataHash[ROLE_2] = QVariant( QColor( "#FFFFFF" ) ); m_aDataHash[ROLE_3] = QVariant( 123 ); m_aDataHash[ROLE_4] = QVariant( "otherString" ); m_aDataHash[ROLE_5] = QVariant( 11.34 ); }
In header i define few general purpose roles. Depending on use it is up to single list instance to interpret which role relates to which property. Using QVariant allows to set many various property types to one variable (of QVariant type). (NOTE: remember that if you bind a single item role from c++ string to qml color then it will result in property binding error).
In this class i also define copy constructor so i can set some common default properties for all list objects in one object and then just copy it to other items.
Having an item defined we can move further to QAbstractListModel reimplementation:
mylistmodel.h
#ifndef MYLISTMODEL_H #define MYLISTMODEL_H #include <QAbstractListModel> #include "listitemapi.h" class MyListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY( int count READ rowCount NOTIFY signalCountChanged ) public: explicit MyListModel( QObject* a_pParent = 0 ); explicit MyListModel( ListItemAPI* a_pPrototype, QObject* a_pParent = 0 ); virtual ~MyListModel(); // QAbstractListModel reimplemented methods int rowCount( const QModelIndex& a_rParent = QModelIndex() ) const; QVariant data( const QModelIndex& a_rIndex, int a_iRole = Qt::DisplayRole ) const; QHash<int, QByteArray> roleNames() const; bool removeRows( int a_iRow, int a_iCount, const QModelIndex& a_rParent = QModelIndex() ); // Methods created to match QML Model style of data manipulation Q_INVOKABLE void append( ListItemAPI* a_pItem ); Q_INVOKABLE QVariant get( int a_iIndex ); Q_INVOKABLE void clear(); inline ListItemAPI* GetItem( int a_iIndex ); signals: void signalCountChanged( int a_iNewCount ); protected: ListItemAPI* m_pPrototype; QList<ListItemAPI*> m_aItems; }; ///////////////////////////////////////////////////////// ListItemAPI* MyListModel::GetItem( int a_iIndex ) { if ( m_aItems.size() > a_iIndex ) { return m_aItems[a_iIndex]; } return 0; } #endif // MYLISTMODEL_H
and its following implementation:
mylistmodel.cpp
#include "mylistmodel.h" MyListModel::MyListModel( QObject* a_pParent ) : QAbstractListModel( a_pParent ) , m_pPrototype( 0 ) , m_aItems ( QList<ListItemAPI*>() ) { } MyListModel::MyListModel( ListItemAPI* a_pPrototype, QObject* a_pParent ) : QAbstractListModel( a_pParent ) , m_pPrototype( a_pPrototype ) , m_aItems ( QList<ListItemAPI*>() ) { } MyListModel::~MyListModel() { clear(); } int MyListModel::rowCount( const QModelIndex& a_rParent ) const { Q_UNUSED( a_rParent ); return m_aItems.size(); } QVariant MyListModel::data( const QModelIndex& a_rIndex, int a_iRole ) const { if ( a_rIndex.row() < 0 ) { return QVariant(); } if ( a_rIndex.row() > m_aItems.size() ) { return QVariant(); } ListItemAPI* pValidate = m_aItems.at( a_rIndex.row() ); if ( pValidate ) { return pValidate->GetData( a_iRole ); } return QVariant(); } QHash<int, QByteArray> MyListModel::roleNames() const { if ( m_pPrototype ) { return m_pPrototype->RoleNames(); } return ( QHash<int, QByteArray>() ); } bool MyListModel::removeRows( int a_iRow, int a_iCount, const QModelIndex& a_rParent ) { if ( a_iRow < 0 ) { return false; } if ( a_iCount <= 0 ) { return false; } if ( ( a_iRow + a_iCount ) > m_aItems.size() ) { return false; } beginRemoveRows( a_rParent, a_iRow, a_iRow + a_iCount - 1 ); for ( int i = 0; i < a_iCount; i++ ) { ListItemAPI* pItem = m_aItems.takeAt( a_iRow ); pItem->deleteLater(); pItem = 0; } endRemoveRows(); emit signalCountChanged( rowCount() ); return true; } void MyListModel::append( ListItemAPI* a_pItem ) { if ( a_pItem != 0 ) { beginInsertRows( QModelIndex(), rowCount(), rowCount() ); m_aItems.append( a_pItem ); endInsertRows(); emit signalCountChanged( rowCount() ); } } QVariant MyListModel::get( int a_iIndex ) { if ( a_iIndex < 0 ) { return QVariant(); } if ( a_iIndex >= m_aItems.size() ) { return QVariant(); } QMap<QString, QVariant> aItemData; ListItemAPI* pItem = m_aItems.at( a_iIndex ); if ( pItem ) { QHashIterator<int, QByteArray> aRolesItr( pItem->RoleNames() ); while ( aRolesItr.hasNext() ) { aRolesItr.next(); aItemData.insert( aRolesItr.value(), QVariant( pItem->GetData( aRolesItr.key() ) ) ); } return QVariant( aItemData ); } return QVariant(); } void MyListModel::clear() { if ( m_aItems.size() > 0 ) { removeRows( 0, m_aItems.size() ); } }
In general reimplementation is done according to Qt docs with one exception. This is a ‘prototype item’. When creating an instance of listmodel class we have to pass prototype item to tell the class what types of items will be filled with. Here roleNames() class implementation is redirected to prototypes RoleNames() definition. Doing it this way i can have one common MyListModel class for all lists exported to QML and only to create new MyListModelItems (well, in fact i don’t even subclass MyListModelItem, i just add more item Roles with numbering and manage their meanings separately for every use).
The last class presented in this tutorial is actual QML listView reflection in C++. It provides a few methods to manipulate data in both ways (QmlC++).
customlistview.h
#ifndef CUSTOMLISTVIEW_H #define CUSTOMLISTVIEW_H #include <QQuickItem> #include "mylistmodel.h" #include "mylistitem.h" class CustomListView : public QQuickItem { Q_OBJECT Q_PROPERTY( int m_iCurrentIndex READ GetCurrentIndex WRITE SetCurrentIndex NOTIFY signalCurrentIndexChanged ) Q_PROPERTY( MyListModel* m_pListModel READ GetListModel NOTIFY signalListModelChanged ) public: explicit CustomListView( QQuickItem* a_pParent = 0 ); explicit CustomListView( MyListModel* a_pListModel, QQuickItem* a_pParent = 0 ); virtual ~CustomListView(); inline int GetCurrentIndex() const { return m_iCurrentIndex; } Q_INVOKABLE void SetCurrentIndex( int a_iNewCurrentIndex ); void ClearElements(); void AppendElement( MyListItem* a_pNewElement ); inline MyListModel* GetListModel() const { return m_pListModel; } signals: void signalCurrentIndexChanged( int a_iNewCurrentIndex ); void signalListModelChanged(); protected: MyListModel* m_pListModel; int m_iCurrentIndex; }; #endif // CUSTOMLISTVIEW_H
customlistview.cpp
#include "customlistview.h" CustomListView::CustomListView( MyListModel* a_pListModel, QQuickItem* a_pParent ) : QQuickItem( a_pParent ) , m_pListModel( a_pListModel ) , m_iCurrentIndex( -1 ) { } CustomListView::CustomListView( QQuickItem* a_pParent ) : QQuickItem( a_pParent ) , m_pListModel( new MyListModel( new MyListItem() ) ) , m_iCurrentIndex( -1 ) { } CustomListView::~CustomListView() { if ( m_pListModel ) { delete m_pListModel; m_pListModel = 0; } } void CustomListView::SetCurrentIndex( int a_iNewCurrentIndex ) { if ( a_iNewCurrentIndex != m_iCurrentIndex ) { m_iCurrentIndex = a_iNewCurrentIndex; emit signalCurrentIndexChanged( m_iCurrentIndex ); } } void CustomListView::ClearElements() { if ( m_pListModel ) { m_pListModel->clear(); m_iCurrentIndex = -1; emit signalListModelChanged(); } } void CustomListView::AppendElement( MyListItem* a_pNewElement ) { if ( a_pNewElement && m_pListModel ) { m_pListModel->append( a_pNewElement ); emit signalListModelChanged(); } }
Here i have defined MyListModel as Q_PROPERTY (this is possible as QAbstractListModel is a QObject derived class, unlike the QList class) to be able to refer to its items through it. There is also currentIndex property to notify Qml/C++ about listview index changes. You can also define here other properties used to specify PathView behaviour and bind them in qml such as:
pathItemCount: id_listView.m_iPathItemCount
highlightMoveDuration: id_listView.m_iAnimationDuration
..and a few others.
QmlListView.qml
import MyCustoms 1.0 import QtQuick 2.0 CustomListView { id: id_listView onSignalCurrentIndexChanged: { id_listViewPath.currentIndex = m_iCurrentIndex } Component { id: id_delegate Item { id: id_delegate_item scale: PathView.iconScale opacity: PathView.iconOpacity Image { id: id_img anchors.horizontalCenter: id_delegate_item.horizontalCenter width: 60 height: 60 source: m_role1 } } } PathView { id: id_listViewPath anchors.fill: id_listView model: id_listView.m_pListModel delegate: id_delegate pathItemCount: 3 snapMode: PathView.NoSnap highlightMoveDuration: 300 interactive: true preferredHighlightBegin: 0.5 preferredHighlightEnd: 0.5 highlightRangeMode: PathView.StrictlyEnforceRange dragMargin: 20 path: Path { id: id_path startX: id_listViewPath.width / 2 startY: id_listViewPath.y PathAttribute { name: "iconScale"; value: 0.5 } PathAttribute { name: "iconOpacity"; value: 0.5 } PathLine { x: id_listViewPath.width / 2; relativeY: id_listViewPath.height / 2 } PathAttribute { name: "iconScale"; value: 1 } PathAttribute { name: "iconOpacity"; value: 1 } PathLine { x: id_listViewPath.width / 2; relativeY: id_listViewPath.height / 2 } } onCurrentIndexChanged: { id_listView.m_iCurrentIndex = currentIndex } } Text { id: id_text anchors.left: id_listView.left anchors.verticalCenter: id_listView.verticalCenter text: id_listView.m_pListModel.get(id_listView.m_iCurrentIndex) ? id_listView.m_pListModel.get(id_listView.m_iCurrentIndex).m_role2 : "" color: id_listView.m_pListModel.get(id_listView.m_iCurrentIndex) ? id_listView.m_pListModel.get(id_listView.m_iCurrentIndex).m_role3 : "red" } }
main.qml
import QtQuick 2.0 import QtQuick.Window 2.0 import MyCustoms 1.0 import "res/qml" Window { id: id_root visible: true width: 400 height: width Rectangle { id: id_rect width: id_root.width height: id_root.height QmlListView { id: id_listView anchors.fill: id_rect objectName: "ListView_Instance" } } }
My main.qml file looks like this, and to get a pointer to my id_listView item after creation of main.qml with Window object i use ‘findChild’ method. A good idea is to set listView’s objectName so we never get mislead when more instances of objects of the same type have the same parent().
The whole setup is done in main and is self explaining and consists of a few main parts:
Create application window
Retrieve pointer to my listview class
Set listview model items
main.cpp
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QDebug> #include "mylistitem.h" #include "customlistview.h" void registerCustomQMLTypes(); void fillModel(CustomListView* a_pView); int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); registerCustomQMLTypes(); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); CustomListView* pQmlListView = 0; QList<QObject*> aRoots = engine.rootObjects(); for( int i=0; i<aRoots.size(); i++ ) { pQmlListView = aRoots[i]->findChild<CustomListView*>("ListView_Instance"); if (pQmlListView != 0) break; } fillModel(pQmlListView); return app.exec(); } void registerCustomQMLTypes() { qmlRegisterType<MyListItem>("MyCustoms", 1, 0, "MyListItem"); qmlRegisterType<MyListModel>("MyCustoms", 1, 0, "MyListModel"); qmlRegisterType<CustomListView>("MyCustoms", 1, 0, "CustomListView"); } void fillModel(CustomListView* a_pView) { if (a_pView) { MyListModel* pModel = a_pView->GetListModel(); if (pModel) { MyListItem* pItem = new MyListItem(); if (pItem) { pItem->SetData(MyListItem::ROLE_1, QVariant("qrc:/res/img/cat0.png")); pItem->SetData(MyListItem::ROLE_2, QVariant("Text_0")); pItem->SetData(MyListItem::ROLE_3, QVariant( QColor(0x00, 0xFF, 0xFF))); pModel->append(pItem); } pItem = new MyListItem(); if (pItem) { pItem->SetData(MyListItem::ROLE_1, QVariant("qrc:/res/img/cat1.png")); pItem->SetData(MyListItem::ROLE_2, QVariant("Second cat")); pItem->SetData(MyListItem::ROLE_3, QVariant( QColor(0x00, 0x00, 0x00))); pModel->append(pItem); } . . . } a_pView->SetCurrentIndex(0); } }
When creating new items for a model i use 3 roles. I decided that ROLE_1 will describe list delegate Image.source property (and it is a string), ROLE_2 describes an item text (also string) and item text color (QColor). This properties are binded to QML names from MyListItem file:
QHash<int, QByteArray> MyListItem::RoleNames() const { QHash<int, QByteArray> aRoleNames; aRoleNames[ROLE_1] = "m_role1"; aRoleNames[ROLE_2] = "m_role2"; aRoleNames[ROLE_3] = "m_role3"; aRoleNames[ROLE_4] = "m_role4"; aRoleNames[ROLE_5] = "m_role5"; return aRoleNames; }
so in QML names are recognised as m_roleX:
Image { id: id_img anchors.horizontalCenter: id_delegate_item.horizontalCenter width: 60 height: 60 source: m_role1 }
If you have any questions feel free to write to me and i will try to explain things in more clear way.
Whole project compiled under Ubuntu 14.04 using Qt 5.2 is available here. (Should work with previous Qt versions as well)
In the next post I will write about creating truly flexible ListViews with proper-user defined scalling and with automated ListView delegates creation.