Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmltablemodel.cpp
Go to the documentation of this file.
1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#include <QtCore/qloggingcategory.h>
8
9#include <QtQml/qqmlinfo.h>
10#include <QtQml/qqmlengine.h>
11
13
14using namespace Qt::StringLiterals;
15
16Q_STATIC_LOGGING_CATEGORY(lcTableModel, "qt.qml.tablemodel")
17
18/*!
19 \qmltype TableModel
20//! \nativetype QQmlTableModel
21 \inqmlmodule Qt.labs.qmlmodels
22 \brief Encapsulates a simple table model.
23 \since 5.14
24
25 The TableModel type stores JavaScript/JSON objects as data for a table
26 model that can be used with \l TableView. It is intended to support
27 very simple models without requiring the creation of a custom
28 QAbstractTableModel subclass in C++.
29
30 \snippet qml/tablemodel/fruit-example-simpledelegate.qml file
31
32 The model's initial row data is set with either the \l rows property or by
33 calling \l appendRow(). Each column in the model is specified by declaring
34 a \l TableModelColumn instance, where the order of each instance determines
35 its column index. Once the model's \l Component::completed() signal has been
36 emitted, the columns and roles will have been established and are then
37 fixed for the lifetime of the model.
38
39 To access a specific row, the \l getRow() function can be used.
40 It's also possible to access the model's JavaScript data
41 directly via the \l rows property, but it is not possible to
42 modify the model data this way.
43
44 To add new rows, use \l appendRow() and \l insertRow(). To modify
45 existing rows, use \l setRow(), \l moveRow(), \l removeRow(), and
46 \l clear().
47
48 It is also possible to modify the model's data via the delegate,
49 as shown in the example above:
50
51 \snippet qml/tablemodel/fruit-example-simpledelegate.qml delegate
52
53 If the type of the data at the modified role does not match the type of the
54 data that is set, it will be automatically converted via
55 \l {QVariant::canConvert()}{QVariant}.
56
57 \section1 Supported Row Data Structures
58
59 TableModel is designed to work with JavaScript/JSON data, where each row
60 is a list of simple key-value pairs:
61
62 \code
63 {
64 // Each property is one cell/column.
65 checked: false,
66 amount: 1,
67 fruitType: "Apple",
68 fruitName: "Granny Smith",
69 fruitPrice: 1.50
70 },
71 // ...
72 \endcode
73
74 As model manipulation in Qt is done via row and column indices,
75 and because object keys are unordered, each column must be specified via
76 TableModelColumn. This allows mapping Qt's built-in roles to any property
77 in each row object.
78
79 Complex row structures are supported, but with limited functionality.
80 As TableModel has no way of knowing how each row is structured,
81 it cannot manipulate it. As a consequence of this, the copy of the
82 model data that TableModel has stored in \l rows is not kept in sync
83 with the source data that was set in QML. For these reasons, manipulation
84 of the data is not supported.
85
86 For example, suppose you wanted to use a data source where each row is an
87 array and each cell is an object. To use this data source with TableModel,
88 define a getter:
89
90 \code
91 TableModel {
92 TableModelColumn {
93 display: function(modelIndex) { return rows[modelIndex.row][0].checked }
94 }
95 // ...
96
97 rows: [
98 [
99 { checked: false, checkable: true },
100 { amount: 1 },
101 { fruitType: "Apple" },
102 { fruitName: "Granny Smith" },
103 { fruitPrice: 1.50 }
104 ]
105 // ...
106 ]
107 }
108 \endcode
109
110 The row above is one example of a complex row.
111
112 \note Row manipulation functions such as \l appendRow(), \l removeRow(),
113 etc. are not supported when using complex rows.
114
115 \section1 Using DelegateChooser with TableModel
116
117 For most real world use cases, it is recommended to use DelegateChooser
118 as the delegate of a TableView that uses TableModel. This allows you to
119 use specific roles in the relevant delegates. For example, the snippet
120 above can be rewritten to use DelegateChooser like so:
121
122 \snippet qml/tablemodel/fruit-example-delegatechooser.qml file
123
124 The most specific delegates are declared first: the columns at index \c 0
125 and \c 1 have \c bool and \c integer data types, so they use a
126 \l [QtQuickControls2]{CheckBox} and \l [QtQuickControls2]{SpinBox},
127 respectively. The remaining columns can simply use a
128 \l [QtQuickControls2]{TextField}, and so that delegate is declared
129 last as a fallback.
130
131 \section1 Using the TableModel with External JSON Sources
132
133 If the structure of the data in a JSON object matches the structure defined
134 by the TableModel, it can be assigned directly to the model using the rows
135 property. All rows in the data must conform to the columns defined by the model.
136
137 For example, if the model is defined as:
138
139 \snippet main.qml model
140
141 The source has the required data but not necessarily in the
142 required structure, you can download and parse it into a form that
143 conforms to the structure that the model defines and assign the result directly.
144
145 \snippet main.qml parse
146
147 and the result can be assigned directly
148
149 \snippet main.qml assign
150
151 You can also provide the JSON object by importing a JavaScript file as a
152 module. See the documentation for \l TreeModel for an example on how to do this.
153
154 \warning Ensure that the JSON data comes from a trusted source. Since the
155 model dinamically populates its rows based on the input, malformed or
156 untrusted JSON can lead to unexpected behavior or performance issues.
157
158 \sa TableModelColumn, TableView, QAbstractTableModel
159*/
160
161QQmlTableModel::QQmlTableModel(QObject *parent)
162 : QQmlAbstractColumnModel(parent)
163{
164}
165
166QQmlTableModel::~QQmlTableModel()
167 = default;
168
169/*!
170 \qmlproperty var TableModel::rows
171
172 This property holds the model data in the form of an array of rows:
173
174 \snippet qml/tablemodel/fruit-example-simpledelegate.qml rows
175
176 \sa getRow(), setRow(), moveRow(), appendRow(), insertRow(), clear(), rowCount, columnCount
177*/
178QVariant QQmlTableModel::rows() const
179{
180 return mRows;
181}
182
183void QQmlTableModel::setRows(const QVariant &rows)
184{
185 const std::optional<QVariantList> validated = validateRowsArgument(rows);
186 if (!validated)
187 return;
188
189 const QVariantList rowsAsVariantList = *validated;
190 if (rowsAsVariantList == mRows) {
191 // No change.
192 return;
193 }
194
195 if (!mComponentCompleted) {
196 // Store the rows until we can call setRowsPrivate() after component completion.
197 mRows = rowsAsVariantList;
198 return;
199 }
200
201 setRowsPrivate(rowsAsVariantList);
202}
203
204void QQmlTableModel::setRowsPrivate(const QVariantList &rowsAsVariantList)
205{
206 Q_ASSERT(mComponentCompleted);
207
208 // By now, all TableModelColumns should have been set.
209 if (mColumns.isEmpty()) {
210 qmlWarning(this) << "No TableModelColumns were set; model will be empty";
211 return;
212 }
213
214 const bool firstTimeValidRowsHaveBeenSet = mColumnMetadata.isEmpty();
215 if (!firstTimeValidRowsHaveBeenSet) {
216 // This is not the first time rows have been set; validate each one.
217 for (int rowIndex = 0; rowIndex < rowsAsVariantList.size(); ++rowIndex) {
218 // validateNewRow() expects a QVariant wrapping a QJSValue, so to
219 // simplify the code, just create one here.
220 const QVariant row = QVariant::fromValue(rowsAsVariantList.at(rowIndex));
221 if (!validateNewRow("setRows()"_L1, row, SetRowsOperation))
222 return;
223 }
224 }
225
226 const int oldRowCount = mRowCount;
227
228 beginResetModel();
229
230 // We don't clear the column or role data, because a TableModel should not be reused in that way.
231 // Once it has valid data, its columns and roles are fixed.
232 mRows = rowsAsVariantList;
233 mRowCount = mRows.size();
234
235 // Gather metadata the first time rows is set.
236 if (firstTimeValidRowsHaveBeenSet && !mRows.isEmpty())
237 fetchColumnMetadata();
238
239 endResetModel();
240 emit rowsChanged();
241
242 if (mRowCount != oldRowCount)
243 emit rowCountChanged();
244}
245
246QVariant QQmlTableModel::dataPrivate(const QModelIndex &index, const QString &roleName) const
247{
248 const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column());
249 const QString propertyName = columnMetadata.roles.value(roleName).name;
250 const QVariantMap rowData = mRows.at(index.row()).toMap();
251 return rowData.value(propertyName);
252}
253
254void QQmlTableModel::setDataPrivate(const QModelIndex &index, const QString &roleName, QVariant value)
255{
256 int row = index.row();
257 QVariantMap modifiedRow = mRows.at(row).toMap();
258 modifiedRow[roleName] = value;
259 mRows[row] = modifiedRow;
260}
261
262// TODO: Turn this into a snippet that compiles in CI
263/*!
264 \qmlmethod void TableModel::appendRow(var row)
265
266 Adds a new row to the end of the model, with the
267 values (cells) in \a row.
268
269 \code
270 model.appendRow({
271 checkable: true,
272 amount: 1,
273 fruitType: "Pear",
274 fruitName: "Williams",
275 fruitPrice: 1.50,
276 })
277 \endcode
278
279 \sa insertRow(), setRow(), removeRow()
280*/
281void QQmlTableModel::appendRow(const QVariant &row)
282{
283 if (!validateNewRow("appendRow()"_L1, row, AppendOperation))
284 return;
285
286 doInsert(mRowCount, row);
287}
288
289/*!
290 \qmlmethod void TableModel::clear()
291
292 Removes all rows from the model.
293
294 \sa removeRow()
295*/
296void QQmlTableModel::clear()
297{
298 QQmlEngine *engine = qmlEngine(this);
299 Q_ASSERT(engine);
300 setRows(QVariant::fromValue(engine->newArray()));
301}
302
303/*!
304 \qmlmethod var TableModel::getRow(int rowIndex)
305
306 Returns the row at \a rowIndex in the model.
307
308 Note that this equivalent to accessing the row directly
309 through the \l rows property:
310
311 \code
312 Component.onCompleted: {
313 // These two lines are equivalent.
314 console.log(model.getRow(0).display);
315 console.log(model.rows[0].fruitName);
316 }
317 \endcode
318
319 \note the returned object cannot be used to modify the contents of the
320 model; use setRow() instead.
321
322 \sa setRow(), appendRow(), insertRow(), removeRow(), moveRow()
323*/
324QVariant QQmlTableModel::getRow(int rowIndex)
325{
326 if (!validateRowIndex("getRow()"_L1, "rowIndex"_L1, rowIndex, NeedsExisting))
327 return QVariant();
328 return mRows.at(rowIndex);
329}
330
331/*!
332 \qmlmethod void TableModel::insertRow(int rowIndex, var row)
333
334 Adds a new row to the model at position \a rowIndex, with the
335 values (cells) in \a row.
336
337 \code
338 model.insertRow(2, {
339 checkable: true, checked: false,
340 amount: 1,
341 fruitType: "Pear",
342 fruitName: "Williams",
343 fruitPrice: 1.50,
344 })
345 \endcode
346
347 The \a rowIndex must point to an existing item in the table, or one past
348 the end of the table (equivalent to \l appendRow()).
349
350 \sa appendRow(), setRow(), removeRow(), rowCount
351*/
352void QQmlTableModel::insertRow(int rowIndex, const QVariant &row)
353{
354 if (!validateNewRow("insertRow()"_L1, row) ||
355 !validateRowIndex("insertRow()"_L1, "rowIndex"_L1, rowIndex, CanAppend))
356 return;
357
358 doInsert(rowIndex, row);
359}
360
361void QQmlTableModel::doInsert(int rowIndex, const QVariant &row)
362{
363 beginInsertRows(QModelIndex(), rowIndex, rowIndex);
364
365 // Adding rowAsVariant.toList() will add each invidual variant in the list,
366 // which is definitely not what we want.
367 const QVariant rowAsVariant = row.userType() == QMetaType::QVariantMap ? row : row.value<QJSValue>().toVariant();
368
369 mRows.insert(rowIndex, rowAsVariant);
370 ++mRowCount;
371
372 qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index "
373 << rowIndex << ":\n" << rowAsVariant.toMap();
374
375 // Gather metadata the first time a row is added.
376 if (mColumnMetadata.isEmpty())
377 fetchColumnMetadata();
378
379 endInsertRows();
380 emit rowCountChanged();
381 emit rowsChanged();
382}
383
384/*!
385 \qmlmethod void TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
386
387 Moves \a rows from the index at \a fromRowIndex to the index at
388 \a toRowIndex.
389
390 The from and to ranges must exist; for example, to move the first 3 items
391 to the end of the list:
392
393 \code
394 model.moveRow(0, model.rowCount - 3, 3)
395 \endcode
396
397 \sa appendRow(), insertRow(), removeRow(), rowCount
398*/
399void QQmlTableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
400{
401 if (fromRowIndex == toRowIndex) {
402 qmlWarning(this) << "moveRow(): \"fromRowIndex\" cannot be equal to \"toRowIndex\"";
403 return;
404 }
405
406 if (rows <= 0) {
407 qmlWarning(this) << "moveRow(): \"rows\" is less than or equal to 0";
408 return;
409 }
410
411 if (!validateRowIndex("moveRow()"_L1, "fromRowIndex"_L1, fromRowIndex, NeedsExisting))
412 return;
413
414 if (!validateRowIndex("moveRow()"_L1, "toRowIndex"_L1, toRowIndex, NeedsExisting))
415 return;
416
417 if (fromRowIndex + rows > mRowCount) {
418 qmlWarning(this) << "moveRow(): \"fromRowIndex\" (" << fromRowIndex
419 << ") + \"rows\" (" << rows << ") = " << (fromRowIndex + rows)
420 << ", which is greater than rowCount() of " << mRowCount;
421 return;
422 }
423
424 if (toRowIndex + rows > mRowCount) {
425 qmlWarning(this) << "moveRow(): \"toRowIndex\" (" << toRowIndex
426 << ") + \"rows\" (" << rows << ") = " << (toRowIndex + rows)
427 << ", which is greater than rowCount() of " << mRowCount;
428 return;
429 }
430
431 qCDebug(lcTableModel).nospace() << "moving " << rows
432 << " row(s) from index " << fromRowIndex
433 << " to index " << toRowIndex;
434
435 // Based on the same call in QQmlListModel::moveRow().
436 beginMoveRows(QModelIndex(), fromRowIndex, fromRowIndex + rows - 1, QModelIndex(),
437 toRowIndex > fromRowIndex ? toRowIndex + rows : toRowIndex);
438
439 // Based on ListModel::moveRow().
440 if (fromRowIndex > toRowIndex) {
441 // Only move forwards - flip if moving backwards.
442 const int from = fromRowIndex;
443 const int to = toRowIndex;
444 fromRowIndex = to;
445 toRowIndex = to + rows;
446 rows = from - to;
447 }
448
449 QList<QVariant> store;
450 store.reserve(rows);
451 for (int i = 0; i < (toRowIndex - fromRowIndex); ++i)
452 store.append(mRows.at(fromRowIndex + rows + i));
453 for (int i = 0; i < rows; ++i)
454 store.append(mRows.at(fromRowIndex + i));
455 for (int i = 0; i < store.size(); ++i)
456 mRows[fromRowIndex + i] = store[i];
457
458 qCDebug(lcTableModel).nospace() << "after moving, rows are:\n" << mRows;
459
460 endMoveRows();
461 emit rowsChanged();
462}
463
464/*!
465 \qmlmethod void TableModel::removeRow(int rowIndex, int rows = 1)
466
467 Removes a number of \a rows at \a rowIndex from the model.
468
469 \sa clear(), rowCount
470*/
471void QQmlTableModel::removeRow(int rowIndex, int rows)
472{
473 if (!validateRowIndex("removeRow()"_L1, "rowIndex"_L1, rowIndex, NeedsExisting))
474 return;
475
476 if (rows <= 0) {
477 qmlWarning(this) << "removeRow(): \"rows\" is less than or equal to zero";
478 return;
479 }
480
481 if (rowIndex + rows - 1 >= mRowCount) {
482 qmlWarning(this) << "removeRow(): \"rows\" " << rows
483 << " exceeds available rowCount() of " << mRowCount
484 << " when removing from \"rowIndex\" " << rowIndex;
485 return;
486 }
487
488 beginRemoveRows(QModelIndex(), rowIndex, rowIndex + rows - 1);
489
490 auto firstIterator = mRows.begin() + rowIndex;
491 // The "last" argument to erase() is exclusive, so we go one past the last item.
492 auto lastIterator = firstIterator + rows;
493 mRows.erase(firstIterator, lastIterator);
494 mRowCount -= rows;
495
496 endRemoveRows();
497 emit rowCountChanged();
498 emit rowsChanged();
499
500 qCDebug(lcTableModel).nospace() << "removed " << rows
501 << " items from the model, starting at index " << rowIndex;
502}
503
504/*!
505 \qmlmethod void TableModel::setRow(int rowIndex, var row)
506
507 Changes the row at \a rowIndex in the model with \a row.
508
509 All columns/cells must be present in \c row, and in the correct order.
510
511 \code
512 model.setRow(0, {
513 checkable: true,
514 amount: 1,
515 fruitType: "Pear",
516 fruitName: "Williams",
517 fruitPrice: 1.50,
518 })
519 \endcode
520
521 If \a rowIndex is equal to \c rowCount(), then a new row is appended to the
522 model. Otherwise, \a rowIndex must point to an existing row in the model.
523
524 \sa appendRow(), insertRow(), rowCount
525*/
526void QQmlTableModel::setRow(int rowIndex, const QVariant &row)
527{
528 if (!validateNewRow("setRow()"_L1, row) ||
529 !validateRowIndex("setRow()"_L1, "rowIndex"_L1, rowIndex, CanAppend))
530 return;
531
532 if (rowIndex != mRowCount) {
533 // Setting an existing row.
534 mRows[rowIndex] = row;
535
536 // For now we just assume the whole row changed, as it's simpler.
537 const QModelIndex topLeftModelIndex(createIndex(rowIndex, 0));
538 const QModelIndex bottomRightModelIndex(createIndex(rowIndex, mColumnCount - 1));
539 emit dataChanged(topLeftModelIndex, bottomRightModelIndex);
540 emit rowsChanged();
541 } else {
542 // Appending a row.
543 doInsert(rowIndex, row);
544 }
545}
546
547
548QVariant QQmlTableModel::firstRow() const
549{
550 return mRows.first();
551}
552
553void QQmlTableModel::setInitialRows()
554{
555 setRowsPrivate(mRows);
556}
557
558/*!
559 \qmlmethod QModelIndex TableModel::index(int row, int column)
560
561 Returns a \l QModelIndex object referencing the given \a row and \a column,
562 which can be passed to the data() function to get the data from that cell,
563 or to setData() to edit the contents of that cell.
564
565 \code
566 import QtQml 2.14
567 import Qt.labs.qmlmodels 1.0
568
569 TableModel {
570 id: model
571
572 TableModelColumn { display: "fruitType" }
573 TableModelColumn { display: "fruitPrice" }
574
575 rows: [
576 { fruitType: "Apple", fruitPrice: 1.50 },
577 { fruitType: "Orange", fruitPrice: 2.50 }
578 ]
579
580 Component.onCompleted: {
581 for (var r = 0; r < model.rowCount; ++r) {
582 console.log("An " + model.data(model.index(r, 0)).display +
583 " costs " + model.data(model.index(r, 1)).display.toFixed(2))
584 }
585 }
586 }
587 \endcode
588
589 \sa {QModelIndex and related Classes in QML}, data()
590*/
591// Note: we don't document the parent argument, because you never need it, because
592// cells in a TableModel don't have parents. But it is there because this function is an override.
593QModelIndex QQmlTableModel::index(int row, int column, const QModelIndex &parent) const
594{
595 return row >= 0 && row < rowCount() && column >= 0 && column < columnCount() && !parent.isValid()
596 ? createIndex(row, column)
597 : QModelIndex();
598}
599
600QModelIndex QQmlTableModel::parent(const QModelIndex &index) const
601{
602 Q_UNUSED(index);
603 return {};
604}
605
606/*!
607 \qmlproperty int TableModel::rowCount
608 \readonly
609
610 This read-only property holds the number of rows in the model.
611
612 This value changes whenever rows are added or removed from the model.
613*/
614int QQmlTableModel::rowCount(const QModelIndex &parent) const
615{
616 if (parent.isValid())
617 return 0;
618
619 return mRowCount;
620}
621
622/*!
623 \qmlproperty int TableModel::columnCount
624 \readonly
625
626 This read-only property holds the number of columns in the model.
627
628 The number of columns is fixed for the lifetime of the model
629 after the \l rows property is set or \l appendRow() is called for the first
630 time.
631*/
632int QQmlTableModel::columnCount(const QModelIndex &parent) const
633{
634 Q_UNUSED(parent);
635
636 return mColumnCount;
637}
638
639/*!
640 \qmlmethod variant TableModel::data(QModelIndex index, string role)
641
642 Returns the data from the table cell at the given \a index belonging to the
643 given \a role.
644
645 \sa index(), setData()
646*/
647
648/*!
649 \qmlmethod bool TableModel::setData(QModelIndex index, variant value, string role)
650
651 Inserts or updates the data field named by \a role in the table cell at the
652 given \a index with \a value. Returns true if successful, false if not.
653
654 \sa data(), index()
655*/
656
657bool QQmlTableModel::validateRowIndex(QLatin1StringView functionName, QLatin1StringView argumentName,
658 int rowIndex, RowOption operation) const
659{
660 if (rowIndex < 0) {
661 qmlWarning(this).noquote() << functionName << ": \"" << argumentName << "\" cannot be negative";
662 return false;
663 }
664
665 if (operation == NeedsExisting) {
666 if (rowIndex >= mRowCount) {
667 qmlWarning(this).noquote() << functionName << ": \"" << argumentName
668 << "\" " << rowIndex << " is greater than or equal to rowCount() of " << mRowCount;
669 return false;
670 }
671 } else {
672 if (rowIndex > mRowCount) {
673 qmlWarning(this).noquote() << functionName << ": \"" << argumentName
674 << "\" " << rowIndex << " is greater than rowCount() of " << mRowCount;
675 return false;
676 }
677 }
678
679 return true;
680}
681
682QT_END_NAMESPACE
683
684#include "moc_qqmltablemodel_p.cpp"
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)