├── screenshot.png
├── qml.qrc
├── qtquickcontrols2.conf
├── README.md
├── main.cpp
├── JsonUtils.hpp
├── CMakeLists.txt
├── sample.json
└── main.qml
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielchristo/qml-tableview/HEAD/screenshot.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------