├── .clang-format ├── .gitignore ├── CMakeLists.txt ├── README.md ├── cpp ├── data_item.cpp ├── data_item.h ├── data_item_2.cpp ├── data_item_2.h ├── provider.cpp ├── provider.h └── qobject_list_model.h ├── main.cpp ├── qml ├── DataItemDelegate.qml └── Main.qml └── resources.qrc /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: WebKit 2 | BreakBeforeBraces: Custom 3 | SpacesInContainerLiterals: true 4 | SpacesInParentheses: true 5 | SpacesInSquareBrackets: false 6 | SpaceBeforeParens: true 7 | AlignConsecutiveDeclarations: true 8 | AlignConsecutiveAssignments: false 9 | AlwaysBreakTemplateDeclarations: true 10 | SpaceAfterTemplateKeyword: true 11 | AllowShortCaseLabelsOnASingleLine: true 12 | IndentCaseLabels: true 13 | IndentPPDirectives: AfterHash 14 | AlignTrailingComments: true 15 | CompactNamespaces: false 16 | NamespaceIndentation: All 17 | MaxEmptyLinesToKeep: 2 18 | BraceWrapping: 19 | AfterEnum: false 20 | AfterStruct: false 21 | BeforeElse: true 22 | BeforeCatch: true 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Qt Creator and other User files 2 | *.user 3 | *.txt.user 4 | *.autosave 5 | .qmake.stash 6 | 7 | # MacOS X folder preferences files 8 | .DS_Store 9 | 10 | # Generated make files 11 | Makefile 12 | Makefile.* 13 | 14 | # Generated objects files 15 | *.o 16 | *.a 17 | *.so 18 | *.so.* 19 | 20 | # Compliled files 21 | *.exe 22 | *.dll 23 | 24 | # MacOS X generated dynamic libraries 25 | *.dylib 26 | 27 | # QML cache files 28 | *.qmlc 29 | 30 | # IDE settings 31 | .idea/ 32 | cmake-build-debug 33 | cmake-build-release 34 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.12) 2 | 3 | project(listview-generic-cpp-model LANGUAGES CXX) 4 | 5 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 6 | set(CMAKE_AUTOMOC ON) 7 | set(CMAKE_AUTORCC ON) 8 | 9 | find_package(Qt5 COMPONENTS Core Quick REQUIRED) 10 | 11 | set(SOURCE_FILES 12 | main.cpp 13 | resources.qrc 14 | cpp/data_item.h 15 | cpp/data_item.cpp 16 | cpp/provider.cpp 17 | cpp/provider.h 18 | cpp/qobject_list_model.h cpp/data_item_2.cpp cpp/data_item_2.h) 19 | 20 | add_executable(${PROJECT_NAME} ${SOURCE_FILES}) 21 | target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Quick) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generic C++ model for QML ListView 2 | 3 | Here is an example of implementation for generic c++ model to use with `ListView` QML component. 4 | 5 | Standard way of implementing such a model is to extend `QAbstractItemModel` or `QAbstractListModel` classes for every purpose. Such models are fast and can contain any type of underlying items and any of items' container is possible to use so they can be really cheap in memory consumption but requires lots of boilerplate work to be done. Another disadvantage is that any change in the items must be made with participation of model itself or through it to be able for the `ListView` to know about them. 6 | 7 | Lets say we've got collection of `QObject` instances, what then? Such objects support signals and meta-property system, so they can inform the view about changes themselves and we don't need to pass these changes through the list model. 8 | 9 | In this case we would like to have some generic type implementing both interfaces: 10 | - for ListView to inform about items adding/removing 11 | - kind of QList to add/remove those items easily 12 | 13 | Unfortunately, QObject can not be made generic with standard c++ template system but we still able to use macros. 14 | 15 | `list_model.h` and `list_model.cpp` are all you need. Header file contains just one simple macro to define your own model for any ListView model based on QObject items: `DECLARE_LIST_MODEL( NAME, ITEM_TYPE )` which you can basically understand as `class NAME`. You can find additional info the header file itself. 16 | 17 | Please, follow this project as an example of usage of such a model. Cheers! 18 | -------------------------------------------------------------------------------- /cpp/data_item.cpp: -------------------------------------------------------------------------------- 1 | #include "data_item.h" 2 | #include 3 | 4 | namespace app { 5 | DataItem::DataItem( int id, QString value ) 6 | : QObject( NULL ) 7 | , m_id( id ) 8 | , m_value( value ) { 9 | } 10 | 11 | 12 | int DataItem::id() { 13 | return m_id; 14 | } 15 | 16 | 17 | void DataItem::setId( const int& v ) { 18 | if ( v != m_id ) { 19 | m_id = v; 20 | emit changed(); 21 | } 22 | } 23 | 24 | 25 | QString DataItem::value() { 26 | return m_value; 27 | } 28 | 29 | 30 | void DataItem::setValue( const QString& v ) { 31 | if ( v != m_value ) { 32 | m_value = v; 33 | emit changed(); 34 | qDebug() << "Value for" << id() << "is changed to:" << v; 35 | } 36 | } 37 | 38 | 39 | void DataItem::doubleId() { 40 | m_id *= 2; 41 | emit changed(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cpp/data_item.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qobject_list_model.h" 4 | #include 5 | #include 6 | #include 7 | 8 | namespace app { 9 | // Some sort of data structure as a collection element 10 | class DataItem : public QObject { 11 | Q_OBJECT 12 | Q_PROPERTY( int id READ id WRITE setId NOTIFY changed ) 13 | Q_PROPERTY( QString value READ value WRITE setValue NOTIFY changed ) 14 | 15 | public: 16 | explicit DataItem( int id = 42, QString value = "The main question" ); 17 | 18 | int id(); 19 | QString value(); 20 | 21 | void setId( const int& value ); 22 | void setValue( const QString& value ); 23 | 24 | // Example of some business logic that changes internal model state. 25 | Q_INVOKABLE void doubleId(); 26 | 27 | signals: 28 | void changed(); 29 | 30 | private: 31 | int m_id; 32 | QString m_value; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /cpp/data_item_2.cpp: -------------------------------------------------------------------------------- 1 | #include "data_item_2.h" 2 | 3 | namespace app { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /cpp/data_item_2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace app { 6 | class DataItem2 : public QObject { 7 | Q_OBJECT 8 | Q_PROPERTY( int dat MEMBER dat CONSTANT ) 9 | 10 | public: 11 | int dat; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /cpp/provider.cpp: -------------------------------------------------------------------------------- 1 | #include "provider.h" 2 | #include 3 | 4 | namespace app { 5 | 6 | Provider::Provider( QObject* parent ) 7 | : QObject( parent ) { 8 | } 9 | 10 | 11 | void Provider::addItem() { 12 | auto item = QSharedPointer( new DataItem( m_items.count() ) ); 13 | m_items << item; 14 | } 15 | 16 | 17 | void Provider::addItems3() { 18 | QList> source; 19 | source << QSharedPointer( new DataItem( m_items.count() ) ) 20 | << QSharedPointer( new DataItem( m_items.count() + 1 ) ) 21 | << QSharedPointer( new DataItem( m_items.count() + 2 ) ); 22 | m_items << source; 23 | } 24 | 25 | void Provider::changeItem() { 26 | if ( m_items.count() == 0 ) 27 | return; 28 | 29 | int index = m_items.count() / 2; 30 | m_items.replace( index, QSharedPointer( new DataItem( 111, "Changed Item" ) ) ); 31 | } 32 | 33 | 34 | void Provider::removeItem() { 35 | if ( m_items.count() == 0 ) 36 | return; 37 | 38 | int index = m_items.count() / 2; 39 | m_items.removeAt( index ); 40 | } 41 | 42 | 43 | QObjectListModel_DataItem* Provider::items() { 44 | return &m_items; 45 | } 46 | 47 | 48 | QObjectListModel_DataItem2* Provider::items2() { 49 | return &m_items2; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cpp/provider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "data_item.h" 4 | #include "data_item_2.h" 5 | #include "qobject_list_model.h" 6 | #include 7 | #include 8 | #include 9 | 10 | // "app" namespace is defined on purpose to show the case of using 11 | // list model within namespaces 12 | namespace app { 13 | 14 | DECLARE_Q_OBJECT_LIST_MODEL( DataItem ) 15 | DECLARE_Q_OBJECT_LIST_MODEL( DataItem2 ) 16 | 17 | // Provider is an example of class with some business logic that 18 | // needs to expose some collection of items of class DataItem 19 | class Provider : public QObject { 20 | Q_OBJECT 21 | Q_PROPERTY( QObjectListModel_DataItem* items READ items CONSTANT ) 22 | Q_PROPERTY( QObjectListModel_DataItem2* items2 READ items2 CONSTANT ) 23 | 24 | public: 25 | explicit Provider( QObject* parent = Q_NULLPTR ); 26 | 27 | Q_INVOKABLE void addItem(); 28 | Q_INVOKABLE void addItems3(); 29 | Q_INVOKABLE void changeItem(); 30 | Q_INVOKABLE void removeItem(); 31 | 32 | private: 33 | // Since this getter is not safe (ownership remains to c++) 34 | // and it is used for QML only it'd better to make it private. 35 | QObjectListModel_DataItem* items(); 36 | QObjectListModel_DataItem m_items; 37 | 38 | QObjectListModel_DataItem2* items2(); 39 | QObjectListModel_DataItem2 m_items2; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /cpp/qobject_list_model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace __qobjectsqmllist { 8 | // role name to be used in delegates for list items 9 | static const char ITEM_ROLE_NAME[] = "modelData"; 10 | 11 | // instantiating optimization. 12 | // since we don't need actual model index when we work with 13 | // flat list, we can instantiate it just once and use everywhere 14 | static const QModelIndex ROOT_MODEL_INDEX; 15 | 16 | template 17 | class QObjectListModelBase : public QAbstractListModel, public QList> { 18 | using ITEM = QSharedPointer; 19 | using LIST = QList; 20 | 21 | public: 22 | enum CollectionRole { 23 | ItemDataRole = Qt::UserRole 24 | }; 25 | 26 | int rowCount( const QModelIndex& parent ) const override { 27 | Q_UNUSED( parent ) 28 | return LIST::count(); 29 | } 30 | 31 | QVariant data( const QModelIndex& index, int role ) const override { 32 | auto row = index.row(); 33 | 34 | if ( row < 0 || row >= LIST::count() ) 35 | return QVariant(); 36 | 37 | if ( role == ItemDataRole ) 38 | return QVariant::fromValue( LIST::at( row ).data() ); 39 | 40 | return QVariant(); 41 | } 42 | 43 | QHash roleNames() const override { 44 | auto roles = QAbstractListModel::roleNames(); 45 | roles[ItemDataRole] = __qobjectsqmllist::ITEM_ROLE_NAME; 46 | return roles; 47 | } 48 | 49 | 50 | void insert( int i, const ITEM& item ) { 51 | if ( i < 0 || i > LIST::count() ) 52 | return; 53 | 54 | beginInsertRows( __qobjectsqmllist::ROOT_MODEL_INDEX, i, i ); 55 | LIST::insert( i, item ); 56 | endInsertRows(); 57 | emitChanged(); 58 | } 59 | 60 | void append( const ITEM& item ) { 61 | auto i = LIST::count(); 62 | beginInsertRows( __qobjectsqmllist::ROOT_MODEL_INDEX, i, i ); 63 | LIST::append( item ); 64 | endInsertRows(); 65 | emitChanged(); 66 | } 67 | 68 | void append( const LIST& list ) { 69 | if ( list.count() == 0 ) 70 | return; 71 | 72 | auto start = LIST::count(); 73 | auto end = LIST::count() + list.count() - 1; 74 | beginInsertRows( __qobjectsqmllist::ROOT_MODEL_INDEX, start, end ); 75 | LIST::append( list ); 76 | endInsertRows(); 77 | emitChanged(); 78 | } 79 | 80 | void removeAt( int i ) { 81 | if ( i < 0 || i > LIST::count() - 1 ) 82 | return; 83 | 84 | beginRemoveRows( __qobjectsqmllist::ROOT_MODEL_INDEX, i, i ); 85 | LIST::removeAt( i ); 86 | endRemoveRows(); 87 | emitChanged(); 88 | } 89 | 90 | bool removeOne( const ITEM& item ) { 91 | int index = LIST::indexOf( item ); 92 | if ( index != -1 ) { 93 | removeAt( index ); 94 | return true; 95 | } 96 | return false; 97 | } 98 | 99 | void removeLast() { 100 | if ( LIST::count() == 0 ) 101 | return; 102 | 103 | auto i = LIST::count() - 1; 104 | beginRemoveRows( __qobjectsqmllist::ROOT_MODEL_INDEX, i, i ); 105 | LIST::removeAt( i ); 106 | endRemoveRows(); 107 | emitChanged(); 108 | } 109 | 110 | void removeFirst() { 111 | if ( LIST::count() == 0 ) 112 | return; 113 | 114 | beginRemoveRows( __qobjectsqmllist::ROOT_MODEL_INDEX, 0, 0 ); 115 | LIST::removeAt( 0 ); 116 | endRemoveRows(); 117 | emitChanged(); 118 | } 119 | 120 | void replace( int i, const ITEM& item ) { 121 | removeAt( i ); 122 | insert( i, item ); 123 | emitChanged(); 124 | } 125 | 126 | void clear() { 127 | beginResetModel(); 128 | LIST::clear(); 129 | endResetModel(); 130 | emitChanged(); 131 | } 132 | 133 | // --- QList-style comfort ;) --- 134 | 135 | QObjectListModelBase& operator+=( const ITEM& t ) { 136 | append( t ); 137 | return *this; 138 | } 139 | 140 | QObjectListModelBase& operator<<( const ITEM& t ) { 141 | append( t ); 142 | return *this; 143 | } 144 | 145 | QObjectListModelBase& operator+=( const LIST& list ) { 146 | append( list ); 147 | return *this; 148 | } 149 | 150 | QObjectListModelBase& operator<<( const LIST& list ) { 151 | append( list ); 152 | return *this; 153 | } 154 | 155 | protected: 156 | virtual void emitChanged() = 0; 157 | }; 158 | } 159 | 160 | /* 161 | * QML ListView generic c++ model for collection of QObject-s. 162 | * Usage: 163 | * 1) Declare new List Model: 164 | * DECLARE_Q_OBJECT_LIST_MODEL( app::DataItem ) 165 | * 2) Register types in Qt Meta-system: 166 | * qmlRegisterUncreatableType( "App", 1, 0, "DataItem", "interface" ); 167 | * qmlRegisterUncreatableType( "App", 1, 0, "DataItemList", "interface" ); 168 | * 3) Define and implement Q_PROPERTY: 169 | * Q_PROPERTY( QObjectList* items READ items CONSTANT ) 170 | * 4) Use this property as simple QList> 171 | * 5) Use "modelData" conventional injected property to get access to list item data in ListView Delegates. 172 | * 173 | * In QML ListView delegate the ROLE "modelData" will be available to get access to list item. 174 | * Since this role is conventional when using ListView with JS arrays, it is very handy 175 | * to use JS arrays as mock data providers during view prototyping. 176 | * 177 | * NOTE: Not all QList methods are usable to get this thing work. Please refer to this class source 178 | * and feel free to implement the rest methods. 179 | * Also, since QList doesn't have that methods marked as "virtual" 180 | * it is impossible to use this class with pointer to QList<>*. 181 | * 182 | * @author Vadim Usoltsev 183 | */ 184 | #define DECLARE_Q_OBJECT_LIST_MODEL( TYPE ) \ 185 | class QObjectListModel_##TYPE : public __qobjectsqmllist::QObjectListModelBase { \ 186 | Q_OBJECT \ 187 | Q_PROPERTY( int length READ count NOTIFY changed ) \ 188 | Q_PROPERTY( int isEmpty READ isEmpty NOTIFY changed ) \ 189 | \ 190 | Q_SIGNALS: \ 191 | void changed(); \ 192 | \ 193 | protected: \ 194 | virtual void emitChanged() { \ 195 | emit changed(); \ 196 | } \ 197 | \ 198 | Q_INVOKABLE QVariant item( int i ) { \ 199 | if ( i < 0 || i >= count() ) \ 200 | return QVariant(); \ 201 | \ 202 | auto obj = at( i ).data(); \ 203 | QQmlEngine::setObjectOwnership( obj, QQmlEngine::CppOwnership ); \ 204 | return QVariant::fromValue( obj ); \ 205 | } \ 206 | }; 207 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | int main( int argc, char* argv[] ) { 8 | QCoreApplication::setAttribute( Qt::AA_EnableHighDpiScaling ); 9 | QGuiApplication app( argc, argv ); 10 | 11 | // These types are NOT supposed to be created in QML 12 | // for example, as ListDataModel { } or DataItem { } 13 | 14 | qmlRegisterUncreatableType( "App", 1, 0, "DataItem", "interface" ); 15 | qmlRegisterUncreatableType( "App", 1, 0, "DataItem2", "interface" ); 16 | qmlRegisterUncreatableType( "App", 1, 0, "ListModel_DataItem", "interface" ); 17 | qmlRegisterUncreatableType( "App", 1, 0, "ListModel_DataItem2", "interface" ); 18 | 19 | qmlRegisterType( "App", 1, 0, "Provider" ); 20 | 21 | QQmlApplicationEngine engine; 22 | engine.load( QUrl( QStringLiteral( "qrc:/qml/Main.qml" ) ) ); 23 | 24 | if ( engine.rootObjects().isEmpty() ) 25 | return -1; 26 | 27 | return app.exec(); 28 | } 29 | -------------------------------------------------------------------------------- /qml/DataItemDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.9 2 | import QtQuick.Controls 2.2 3 | import App 1.0 // IMPORTANT! DataItem is declared there. 4 | 5 | Row { 6 | // Typed "modelData" property. 7 | // It is possible to use bi-directional binding with this property 8 | // since it is declared as a pointer to instance. 9 | // property var modelData: { 10 | // "id": 2, 11 | // "value": 42, 12 | // "doubleId": function() { console.log("id doubled") } 13 | // } 14 | 15 | property var d 16 | 17 | height: implicitHeight 18 | spacing: 5 19 | padding: 5 20 | 21 | Text { 22 | text: modelData.id 23 | width: 50 24 | padding: 10 25 | } 26 | 27 | Rectangle { 28 | width: 200 29 | height: input.implicitHeight 30 | color: "#D8D8D8" 31 | border.color: "#B6B5B5" 32 | 33 | TextInput { 34 | id: input 35 | anchors.fill: parent 36 | padding: 10 37 | text: modelData.value // Using structure properties 38 | onTextChanged: modelData.value = text // Changing structure properties 39 | } 40 | } 41 | 42 | Button { 43 | anchors.verticalCenter: parent.verticalCenter 44 | height: input.implicitHeight 45 | text: "double id" 46 | onClicked: modelData.doubleId() // Executing business logic 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /qml/Main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.9 2 | import QtQuick.Window 2.3 3 | import QtQuick.Controls 2.2 4 | import QtQuick.Layouts 1.3 5 | import App 1.0 6 | 7 | Window { 8 | visible: true 9 | width: 600 10 | height: 500 11 | title: qsTr("c++ generic list model example") 12 | 13 | Provider { 14 | id: provider 15 | 16 | // Provider - is our basic implementation of c++ view controller 17 | // that exposes some collection-like data of struct items. 18 | // + itemsModel is used to utilize ListView.model 19 | // + itemsModel.item(index) is used to get particular item by its index for delegate 20 | } 21 | 22 | 23 | Row { 24 | id: buttons 25 | spacing: 20 26 | padding: 10 27 | 28 | Button { 29 | text: "add item" 30 | onClicked: provider.addItem() 31 | } 32 | 33 | Button { 34 | text: "add 3" 35 | onClicked: provider.addItems3() 36 | } 37 | 38 | Button { 39 | text: "change center" 40 | onClicked: provider.changeItem() 41 | } 42 | 43 | Button { 44 | text: "remove center" 45 | onClicked: provider.removeItem() 46 | } 47 | 48 | Text { 49 | anchors.verticalCenter: parent.verticalCenter 50 | text: "Count: " + provider.items.length 51 | } 52 | } 53 | 54 | ListView { 55 | id: listView 56 | anchors.fill: parent 57 | anchors.topMargin: buttons.implicitHeight + 10 58 | anchors.bottomMargin: 10 59 | boundsBehavior: Flickable.DragOverBounds 60 | clip: true 61 | cacheBuffer: 2 62 | 63 | // model of "itemsIndex" provides indices only. 64 | // also it dispatches all necessary signals 65 | // that allow view to react on indices changes 66 | model: provider.items 67 | 68 | delegate: DataItemDelegate { 69 | d: provider.items.item(index) 70 | anchors.left: parent.left 71 | anchors.right: parent.right 72 | } 73 | 74 | removeDisplaced: Transition { 75 | NumberAnimation { properties: "x,y"; duration: 100; easing.type: Easing.InOutQuad } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | qml/Main.qml 4 | qml/DataItemDelegate.qml 5 | 6 | 7 | --------------------------------------------------------------------------------