├── CMakeLists.txt ├── JsonUtils.hpp ├── README.md ├── main.cpp ├── main.qml ├── qml.qrc ├── qtquickcontrols2.conf ├── sample.json └── screenshot.png /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(QML_TableView LANGUAGES CXX) 4 | 5 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 6 | 7 | set(CMAKE_AUTOUIC ON) 8 | set(CMAKE_AUTOMOC ON) 9 | set(CMAKE_AUTORCC ON) 10 | 11 | set(CMAKE_CXX_STANDARD 11) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | 14 | # QtCreator supports the following variables for Android, which are identical to qmake Android variables. 15 | # Check http://doc.qt.io/qt-5/deployment-android.html for more information. 16 | # They need to be set before the find_package(Qt5 ...) call. 17 | 18 | #if(ANDROID) 19 | # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") 20 | # if (ANDROID_ABI STREQUAL "armeabi-v7a") 21 | # set(ANDROID_EXTRA_LIBS 22 | # ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so 23 | # ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so) 24 | # endif() 25 | #endif() 26 | 27 | find_package(Qt5 COMPONENTS Core Quick Widgets REQUIRED) 28 | 29 | if(ANDROID) 30 | add_library(QML_TableView SHARED 31 | main.cpp 32 | JsonUtils.hpp 33 | qml.qrc 34 | ) 35 | else() 36 | add_executable(QML_TableView 37 | main.cpp 38 | JsonUtils.hpp 39 | qml.qrc 40 | ) 41 | endif() 42 | 43 | target_compile_definitions(QML_TableView 44 | PRIVATE $<$,$>:QT_QML_DEBUG>) 45 | target_link_libraries(QML_TableView 46 | PRIVATE Qt5::Core Qt5::Quick Qt5::Widgets) 47 | -------------------------------------------------------------------------------- /JsonUtils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef JSONUTILS_HPP 2 | #define JSONUTILS_HPP 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class JsonUtils : public QObject { 12 | Q_OBJECT 13 | 14 | public: 15 | 16 | explicit JsonUtils(QObject* parent = nullptr) { Q_UNUSED(parent) }; 17 | 18 | Q_INVOKABLE void saveJson(QString json){ 19 | QString fileName = QFileDialog::getSaveFileName(nullptr, tr("Save File"), QDir::homePath(), tr("Json (*.json)")); 20 | if(fileName.isEmpty() || fileName.isNull()) return; 21 | 22 | QFile file(fileName); 23 | if(file.open(QFile::WriteOnly | QFile::Text)){ 24 | // converting to json document only to get the indented text 25 | auto jsonDoc = QJsonDocument::fromJson(json.toUtf8()); 26 | file.write(jsonDoc.toJson()); 27 | } 28 | file.close(); 29 | }; 30 | 31 | Q_INVOKABLE QString getFileContent(QUrl url){ 32 | QFile file(url.toLocalFile()); 33 | if(!file.open(QFile::ReadOnly | QFile::Text)) 34 | qDebug() << "Open file url error"; 35 | QTextStream in(&file); 36 | return in.readAll(); 37 | } 38 | 39 | }; 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QML Custom Tableview # 2 | 3 | A very simple qml dynamic tableview example, with: 4 | 5 | - qt quick control delegates 6 | - vertical and horizontal headers 7 | - json serialization 8 | 9 | 10 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "JsonUtils.hpp" 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 10 | 11 | QApplication app(argc, argv); 12 | 13 | QQmlApplicationEngine engine; 14 | 15 | JsonUtils jsonUtils; 16 | engine.rootContext()->setContextProperty("JsonUtils", &jsonUtils); 17 | 18 | const QUrl url(QStringLiteral("qrc:/main.qml")); 19 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 20 | &app, [url](QObject *obj, const QUrl &objUrl) { 21 | if (!obj && url == objUrl) 22 | QCoreApplication::exit(-1); 23 | }, Qt::QueuedConnection); 24 | engine.load(url); 25 | 26 | return app.exec(); 27 | } 28 | -------------------------------------------------------------------------------- /main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Controls 2.5 3 | import QtQuick.Layouts 1.3 4 | import QtQuick.Dialogs 1.2 5 | import Qt.labs.qmlmodels 1.0 6 | 7 | ApplicationWindow { 8 | 9 | id: root 10 | visible: true 11 | width: 640; height: 480 12 | title: qsTr("QML TableView Example - by Gabriel Christo") 13 | 14 | property var combobox_model: ["Value 1", "Value 2", "Value 3"] // combobox stringlist 15 | property var horizontal_header_data: ["Combobox", "Checkbox", "TextFields", "Spinbox", "Slider"] // table header 16 | 17 | // updates model's row number 18 | function update_table_model(new_rows_number){ 19 | // row data model (must be paired with table model) 20 | let row_data = { 21 | combobox_index: 0, 22 | checkbox_state: 0, 23 | textfield_1_string: "", 24 | textfield_2_string: "", 25 | spinbox_value: 0, 26 | slider_value: 0 27 | } 28 | if(new_rows_number === 0 || isNaN(new_rows_number)){ 29 | tablemodel.clear() 30 | } 31 | else if(new_rows_number > tablemodel.rowCount){ 32 | for(let i = tablemodel.rowCount; i < new_rows_number; i++) tablemodel.appendRow(row_data) 33 | } 34 | else if(new_rows_number < tablemodel.rowCount){ 35 | tablemodel.removeRow(new_rows_number, tablemodel.rowCount - new_rows_number) 36 | } 37 | tableview.forceLayout() // forces tableview update 38 | } 39 | 40 | // file dialog to pick json file 41 | FileDialog { 42 | id: filedialog 43 | title: "Select a json file" 44 | folder: shortcuts.documents 45 | nameFilters: ["Json (*.json)"] 46 | onAccepted: { 47 | tablemodel.rows = JSON.parse(JsonUtils.getFileContent(filedialog.fileUrl)) // updates model with json data 48 | rowsnumber.text = tablemodel.rowCount // updating textfield value 49 | } 50 | } 51 | 52 | Column { 53 | 54 | height: parent.height; width: parent.width 55 | 56 | RowLayout { 57 | spacing: 10 58 | height: 0.1 * parent.height 59 | anchors.horizontalCenter: parent.horizontalCenter 60 | Label { text: "Rows:" } 61 | TextField { 62 | id: rowsnumber; text: "0"; selectByMouse: true 63 | validator: IntValidator{} 64 | onTextEdited: update_table_model(parseInt(this.text)) 65 | } 66 | Button { 67 | text: "Create Json" 68 | // passing table model data as string to backend 69 | onClicked:{ 70 | JsonUtils.saveJson(JSON.stringify(tablemodel.rows)) 71 | } 72 | } 73 | Button { 74 | text: "Load Json" 75 | onClicked: filedialog.open() // dialog to select json file 76 | } 77 | } 78 | 79 | TableView { 80 | id: tableview 81 | width: 0.85 * parent.width; height: 0.8 * parent.height 82 | anchors.horizontalCenter: parent.horizontalCenter 83 | clip: true // clip content to table dimensions 84 | boundsBehavior: Flickable.StopAtBounds 85 | reuseItems: false // forces table to destroy delegates 86 | columnSpacing: 1 // in case of big/row spacing, you need to take care of width/height providers (to get along with it) 87 | 88 | // margins to vertical/horizontal headers 89 | leftMargin: verticalHeader.width 90 | topMargin: horizontalHeader.height 91 | 92 | // scrollbar config 93 | ScrollBar.horizontal: ScrollBar{ 94 | //policy: "AlwaysOn" 95 | } 96 | ScrollBar.vertical: ScrollBar{ 97 | //policy: "AlwaysOn" 98 | } 99 | ScrollIndicator.horizontal: ScrollIndicator { } 100 | ScrollIndicator.vertical: ScrollIndicator { } 101 | 102 | // width and height providers 103 | property var columnWidths: [100, 80, 120, 100, 100] 104 | columnWidthProvider: function(column){ return columnWidths[column] } 105 | rowHeightProvider: function (column) { return 25 } 106 | 107 | // table horizontal header 108 | Row { 109 | id: horizontalHeader 110 | y: tableview.contentY 111 | z: 2 112 | Repeater { 113 | model: tableview.columns 114 | Label { 115 | width: tableview.columnWidthProvider(modelData); height: 30 116 | text: horizontal_header_data[index] 117 | padding: 10 118 | verticalAlignment: Text.AlignVCenter; horizontalAlignment: Text.AlignHCenter 119 | color: "white" 120 | background: Rectangle { color: "#b5b5b5" } 121 | } 122 | } 123 | } 124 | 125 | // table vertical header 126 | Column { 127 | id: verticalHeader 128 | x: tableview.contentX 129 | z: 2 130 | Repeater { 131 | model: tableview.rows 132 | Label { 133 | width: 30; height: tableview.rowHeightProvider(modelData) 134 | text: index 135 | padding: 10 136 | verticalAlignment: Text.AlignVCenter; horizontalAlignment: Text.AlignHCenter 137 | color: "white" 138 | background: Rectangle { color: "#b5b5b5" } 139 | } 140 | } 141 | } 142 | 143 | // defining model columns' roles 144 | model: TableModel { 145 | id: tablemodel 146 | TableModelColumn{ display: "combobox_index" } 147 | TableModelColumn{ display: "checkbox_state" } 148 | TableModelColumn{ display: "textfield_1_string"; edit: "textfield_2_string" } // using two roles 149 | TableModelColumn{ display: "spinbox_value" } 150 | TableModelColumn{ display: "slider_value" } 151 | } 152 | 153 | // defining custom delegates and model connection 154 | delegate: DelegateChooser { 155 | DelegateChoice { 156 | column: 0 157 | delegate: ComboBox { // here we need to be careful: conflict between table model and combobox model 158 | currentIndex: display 159 | model: combobox_model 160 | onActivated: display = this.currentIndex // we've only used 'display' keyword, because combobox model doesnt have this as role name 161 | } 162 | } 163 | DelegateChoice { 164 | column: 1 165 | delegate: CheckBox { 166 | checkState: model.display 167 | onToggled: model.display = this.checkState 168 | } 169 | } 170 | DelegateChoice { 171 | column: 2 172 | delegate: RowLayout { // two textfields in same column model 173 | spacing: 0 174 | TextField { 175 | implicitWidth: parent.width / 2 176 | text: model.display 177 | placeholderText: "x" 178 | selectByMouse: true 179 | onTextEdited: model.display = this.text 180 | } 181 | TextField { 182 | implicitWidth: parent.width / 2 183 | text: model.edit 184 | placeholderText: "y" 185 | selectByMouse: true 186 | onTextEdited: model.edit = this.text 187 | } 188 | } 189 | } 190 | DelegateChoice { 191 | column: 3 192 | delegate: SpinBox { 193 | value: model.display 194 | from: 0; to: 99 195 | stepSize: 1 196 | onValueModified: model.display = this.value 197 | } 198 | } 199 | DelegateChoice { 200 | column: 4 201 | delegate: Slider { 202 | value: model.display 203 | from: 0; to: 32 204 | stepSize: 1 205 | onMoved: model.display = this.value 206 | } 207 | } 208 | } 209 | } 210 | 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | qtquickcontrols2.conf 5 | 6 | 7 | -------------------------------------------------------------------------------- /qtquickcontrols2.conf: -------------------------------------------------------------------------------- 1 | ; This file can be edited to change the style of the application 2 | ; Read "Qt Quick Controls 2 Configuration File" for details: 3 | ; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html 4 | 5 | [Controls] 6 | Style=Fusion 7 | -------------------------------------------------------------------------------- /sample.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "checkbox_state": 2, 4 | "combobox_index": 0, 5 | "slider_value": 3, 6 | "spinbox_value": 1, 7 | "textfield_1_string": "1", 8 | "textfield_2_string": "" 9 | }, 10 | { 11 | "checkbox_state": 0, 12 | "combobox_index": 1, 13 | "slider_value": 5, 14 | "spinbox_value": 2, 15 | "textfield_1_string": "2", 16 | "textfield_2_string": "2" 17 | }, 18 | { 19 | "checkbox_state": 2, 20 | "combobox_index": 2, 21 | "slider_value": 10, 22 | "spinbox_value": 3, 23 | "textfield_1_string": "3", 24 | "textfield_2_string": "" 25 | }, 26 | { 27 | "checkbox_state": 0, 28 | "combobox_index": 0, 29 | "slider_value": 15, 30 | "spinbox_value": 4, 31 | "textfield_1_string": "4", 32 | "textfield_2_string": "4" 33 | }, 34 | { 35 | "checkbox_state": 2, 36 | "combobox_index": 1, 37 | "slider_value": 18, 38 | "spinbox_value": 5, 39 | "textfield_1_string": "5", 40 | "textfield_2_string": "" 41 | }, 42 | { 43 | "checkbox_state": 0, 44 | "combobox_index": 2, 45 | "slider_value": 24, 46 | "spinbox_value": 6, 47 | "textfield_1_string": "6", 48 | "textfield_2_string": "6" 49 | }, 50 | { 51 | "checkbox_state": 2, 52 | "combobox_index": 0, 53 | "slider_value": 26, 54 | "spinbox_value": 0, 55 | "textfield_1_string": "7", 56 | "textfield_2_string": "" 57 | }, 58 | { 59 | "checkbox_state": 0, 60 | "combobox_index": 1, 61 | "slider_value": 30, 62 | "spinbox_value": 0, 63 | "textfield_1_string": "", 64 | "textfield_2_string": "" 65 | }, 66 | { 67 | "checkbox_state": 2, 68 | "combobox_index": 2, 69 | "slider_value": 32, 70 | "spinbox_value": 0, 71 | "textfield_1_string": "", 72 | "textfield_2_string": "" 73 | }, 74 | { 75 | "checkbox_state": 2, 76 | "combobox_index": 0, 77 | "slider_value": 26, 78 | "spinbox_value": 0, 79 | "textfield_1_string": "", 80 | "textfield_2_string": "" 81 | }, 82 | { 83 | "checkbox_state": 0, 84 | "combobox_index": 0, 85 | "slider_value": 22, 86 | "spinbox_value": 0, 87 | "textfield_1_string": "", 88 | "textfield_2_string": "" 89 | }, 90 | { 91 | "checkbox_state": 2, 92 | "combobox_index": 0, 93 | "slider_value": 17, 94 | "spinbox_value": 0, 95 | "textfield_1_string": "", 96 | "textfield_2_string": "" 97 | }, 98 | { 99 | "checkbox_state": 0, 100 | "combobox_index": 0, 101 | "slider_value": 12, 102 | "spinbox_value": 0, 103 | "textfield_1_string": "", 104 | "textfield_2_string": "33" 105 | }, 106 | { 107 | "checkbox_state": 2, 108 | "combobox_index": 0, 109 | "slider_value": 9, 110 | "spinbox_value": 0, 111 | "textfield_1_string": "", 112 | "textfield_2_string": "44" 113 | }, 114 | { 115 | "checkbox_state": 0, 116 | "combobox_index": 0, 117 | "slider_value": 5, 118 | "spinbox_value": 0, 119 | "textfield_1_string": "", 120 | "textfield_2_string": "55" 121 | }, 122 | { 123 | "checkbox_state": 2, 124 | "combobox_index": 2, 125 | "slider_value": 0, 126 | "spinbox_value": 0, 127 | "textfield_1_string": "", 128 | "textfield_2_string": "" 129 | } 130 | ] 131 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielchristo/qml-tableview/6ec9f15f14b4c8b12721a33c02c49e2aa4372599/screenshot.png --------------------------------------------------------------------------------