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 dynamically 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 if (mRows.empty())
299 return;
300 beginResetModel();
301 mRows.clear();
302 mRowCount = 0;
303 endResetModel();
304 emit rowCountChanged();
305 emit rowsChanged();
306}
307
308/*!
309 \qmlmethod var TableModel::getRow(int rowIndex)
310
311 Returns the row at \a rowIndex in the model.
312
313 Note that this equivalent to accessing the row directly
314 through the \l rows property:
315
316 \code
317 Component.onCompleted: {
318 // These two lines are equivalent.
319 console.log(model.getRow(0).display);
320 console.log(model.rows[0].fruitName);
321 }
322 \endcode
323
324 \note the returned object cannot be used to modify the contents of the
325 model; use setRow() instead.
326
327 \sa setRow(), appendRow(), insertRow(), removeRow(), moveRow()
328*/
329QVariant QQmlTableModel::getRow(int rowIndex)
330{
331 if (!validateRowIndex("getRow()"_L1, "rowIndex"_L1, rowIndex, NeedsExisting))
332 return QVariant();
333 return mRows.at(rowIndex);
334}
335
336/*!
337 \qmlmethod void TableModel::insertRow(int rowIndex, var row)
338
339 Adds a new row to the model at position \a rowIndex, with the
340 values (cells) in \a row.
341
342 \code
343 model.insertRow(2, {
344 checkable: true, checked: false,
345 amount: 1,
346 fruitType: "Pear",
347 fruitName: "Williams",
348 fruitPrice: 1.50,
349 })
350 \endcode
351
352 The \a rowIndex must point to an existing item in the table, or one past
353 the end of the table (equivalent to \l appendRow()).
354
355 \sa appendRow(), setRow(), removeRow(), rowCount
356*/
357void QQmlTableModel::insertRow(int rowIndex, const QVariant &row)
358{
359 if (!validateNewRow("insertRow()"_L1, row) ||
360 !validateRowIndex("insertRow()"_L1, "rowIndex"_L1, rowIndex, CanAppend))
361 return;
362
363 doInsert(rowIndex, row);
364}
365
366void QQmlTableModel::doInsert(int rowIndex, const QVariant &row)
367{
368 beginInsertRows(QModelIndex(), rowIndex, rowIndex);
369
370 // Adding rowAsVariant.toList() will add each invidual variant in the list,
371 // which is definitely not what we want.
372 const QVariant rowAsVariant = row.userType() == QMetaType::QVariantMap ? row : row.value<QJSValue>().toVariant();
373
374 mRows.insert(rowIndex, rowAsVariant);
375 ++mRowCount;
376
377 qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index "
378 << rowIndex << ":\n" << rowAsVariant.toMap();
379
380 // Gather metadata the first time a row is added.
381 if (mColumnMetadata.isEmpty())
382 fetchColumnMetadata();
383
384 endInsertRows();
385 emit rowCountChanged();
386 emit rowsChanged();
387}
388
389/*!
390 \qmlmethod void TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
391
392 Moves \a rows from the index at \a fromRowIndex to the index at
393 \a toRowIndex.
394
395 The from and to ranges must exist; for example, to move the first 3 items
396 to the end of the list:
397
398 \code
399 model.moveRow(0, model.rowCount - 3, 3)
400 \endcode
401
402 \sa appendRow(), insertRow(), removeRow(), rowCount
403*/
404void QQmlTableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
405{
406 if (fromRowIndex == toRowIndex) {
407 qmlWarning(this) << "moveRow(): \"fromRowIndex\" cannot be equal to \"toRowIndex\"";
408 return;
409 }
410
411 if (rows <= 0) {
412 qmlWarning(this) << "moveRow(): \"rows\" is less than or equal to 0";
413 return;
414 }
415
416 if (!validateRowIndex("moveRow()"_L1, "fromRowIndex"_L1, fromRowIndex, NeedsExisting))
417 return;
418
419 if (!validateRowIndex("moveRow()"_L1, "toRowIndex"_L1, toRowIndex, NeedsExisting))
420 return;
421
422 if (fromRowIndex + rows > mRowCount) {
423 qmlWarning(this) << "moveRow(): \"fromRowIndex\" (" << fromRowIndex
424 << ") + \"rows\" (" << rows << ") = " << (fromRowIndex + rows)
425 << ", which is greater than rowCount() of " << mRowCount;
426 return;
427 }
428
429 if (toRowIndex + rows > mRowCount) {
430 qmlWarning(this) << "moveRow(): \"toRowIndex\" (" << toRowIndex
431 << ") + \"rows\" (" << rows << ") = " << (toRowIndex + rows)
432 << ", which is greater than rowCount() of " << mRowCount;
433 return;
434 }
435
436 qCDebug(lcTableModel).nospace() << "moving " << rows
437 << " row(s) from index " << fromRowIndex
438 << " to index " << toRowIndex;
439
440 // Based on the same call in QQmlListModel::moveRow().
441 beginMoveRows(QModelIndex(), fromRowIndex, fromRowIndex + rows - 1, QModelIndex(),
442 toRowIndex > fromRowIndex ? toRowIndex + rows : toRowIndex);
443
444 // Based on ListModel::moveRow().
445 if (fromRowIndex > toRowIndex) {
446 // Only move forwards - flip if moving backwards.
447 const int from = fromRowIndex;
448 const int to = toRowIndex;
449 fromRowIndex = to;
450 toRowIndex = to + rows;
451 rows = from - to;
452 }
453
454 QList<QVariant> store;
455 store.reserve(rows);
456 for (int i = 0; i < (toRowIndex - fromRowIndex); ++i)
457 store.append(mRows.at(fromRowIndex + rows + i));
458 for (int i = 0; i < rows; ++i)
459 store.append(mRows.at(fromRowIndex + i));
460 for (int i = 0; i < store.size(); ++i)
461 mRows[fromRowIndex + i] = store[i];
462
463 qCDebug(lcTableModel).nospace() << "after moving, rows are:\n" << mRows;
464
465 endMoveRows();
466 emit rowsChanged();
467}
468
469/*!
470 \qmlmethod void TableModel::removeRow(int rowIndex, int rows = 1)
471
472 Removes a number of \a rows at \a rowIndex from the model.
473
474 \sa clear(), rowCount
475*/
476void QQmlTableModel::removeRow(int rowIndex, int rows)
477{
478 if (!validateRowIndex("removeRow()"_L1, "rowIndex"_L1, rowIndex, NeedsExisting))
479 return;
480
481 if (rows <= 0) {
482 qmlWarning(this) << "removeRow(): \"rows\" is less than or equal to zero";
483 return;
484 }
485
486 if (rowIndex + rows - 1 >= mRowCount) {
487 qmlWarning(this) << "removeRow(): \"rows\" " << rows
488 << " exceeds available rowCount() of " << mRowCount
489 << " when removing from \"rowIndex\" " << rowIndex;
490 return;
491 }
492
493 beginRemoveRows(QModelIndex(), rowIndex, rowIndex + rows - 1);
494
495 auto firstIterator = mRows.begin() + rowIndex;
496 // The "last" argument to erase() is exclusive, so we go one past the last item.
497 auto lastIterator = firstIterator + rows;
498 mRows.erase(firstIterator, lastIterator);
499 mRowCount -= rows;
500
501 endRemoveRows();
502 emit rowCountChanged();
503 emit rowsChanged();
504
505 qCDebug(lcTableModel).nospace() << "removed " << rows
506 << " items from the model, starting at index " << rowIndex;
507}
508
509/*!
510 \qmlmethod void TableModel::setRow(int rowIndex, var row)
511
512 Changes the row at \a rowIndex in the model with \a row.
513
514 All columns/cells must be present in \c row, and in the correct order.
515
516 \code
517 model.setRow(0, {
518 checkable: true,
519 amount: 1,
520 fruitType: "Pear",
521 fruitName: "Williams",
522 fruitPrice: 1.50,
523 })
524 \endcode
525
526 If \a rowIndex is equal to \c rowCount(), then a new row is appended to the
527 model. Otherwise, \a rowIndex must point to an existing row in the model.
528
529 \sa appendRow(), insertRow(), rowCount
530*/
531void QQmlTableModel::setRow(int rowIndex, const QVariant &row)
532{
533 if (!validateNewRow("setRow()"_L1, row) ||
534 !validateRowIndex("setRow()"_L1, "rowIndex"_L1, rowIndex, CanAppend))
535 return;
536
537 if (rowIndex != mRowCount) {
538 // Setting an existing row.
539 mRows[rowIndex] = row;
540
541 // For now we just assume the whole row changed, as it's simpler.
542 const QModelIndex topLeftModelIndex(createIndex(rowIndex, 0));
543 const QModelIndex bottomRightModelIndex(createIndex(rowIndex, mColumnCount - 1));
544 emit dataChanged(topLeftModelIndex, bottomRightModelIndex);
545 emit rowsChanged();
546 } else {
547 // Appending a row.
548 doInsert(rowIndex, row);
549 }
550}
551
552
553QVariant QQmlTableModel::firstRow() const
554{
555 return mRows.first();
556}
557
558void QQmlTableModel::setInitialRows()
559{
560 setRowsPrivate(mRows);
561}
562
563/*!
564 \qmlmethod QModelIndex TableModel::index(int row, int column)
565
566 Returns a \l QModelIndex object referencing the given \a row and \a column,
567 which can be passed to the data() function to get the data from that cell,
568 or to setData() to edit the contents of that cell.
569
570 \code
571 import QtQml 2.14
572 import Qt.labs.qmlmodels 1.0
573
574 TableModel {
575 id: model
576
577 TableModelColumn { display: "fruitType" }
578 TableModelColumn { display: "fruitPrice" }
579
580 rows: [
581 { fruitType: "Apple", fruitPrice: 1.50 },
582 { fruitType: "Orange", fruitPrice: 2.50 }
583 ]
584
585 Component.onCompleted: {
586 for (var r = 0; r < model.rowCount; ++r) {
587 console.log("An " + model.data(model.index(r, 0)).display +
588 " costs " + model.data(model.index(r, 1)).display.toFixed(2))
589 }
590 }
591 }
592 \endcode
593
594 \sa {QModelIndex and related Classes in QML}, data()
595*/
596// Note: we don't document the parent argument, because you never need it, because
597// cells in a TableModel don't have parents. But it is there because this function is an override.
598QModelIndex QQmlTableModel::index(int row, int column, const QModelIndex &parent) const
599{
600 return row >= 0 && row < rowCount() && column >= 0 && column < columnCount() && !parent.isValid()
601 ? createIndex(row, column)
602 : QModelIndex();
603}
604
605QModelIndex QQmlTableModel::parent(const QModelIndex &index) const
606{
607 Q_UNUSED(index);
608 return {};
609}
610
611/*!
612 \qmlproperty int TableModel::rowCount
613 \readonly
614
615 This read-only property holds the number of rows in the model.
616
617 This value changes whenever rows are added or removed from the model.
618*/
619int QQmlTableModel::rowCount(const QModelIndex &parent) const
620{
621 if (parent.isValid())
622 return 0;
623
624 return mRowCount;
625}
626
627/*!
628 \qmlproperty int TableModel::columnCount
629 \readonly
630
631 This read-only property holds the number of columns in the model.
632
633 The number of columns is fixed for the lifetime of the model
634 after the \l rows property is set or \l appendRow() is called for the first
635 time.
636*/
637int QQmlTableModel::columnCount(const QModelIndex &parent) const
638{
639 Q_UNUSED(parent);
640
641 return mColumnCount;
642}
643
644/*!
645 \qmlmethod variant TableModel::data(QModelIndex index, string role)
646
647 Returns the data from the table cell at the given \a index belonging to the
648 given \a role.
649
650 \sa index(), setData()
651*/
652
653/*!
654 \qmlmethod bool TableModel::setData(QModelIndex index, variant value, string role)
655
656 Inserts or updates the data field named by \a role in the table cell at the
657 given \a index with \a value. Returns true if successful, false if not.
658
659 \sa data(), index()
660*/
661
662bool QQmlTableModel::validateRowIndex(QLatin1StringView functionName, QLatin1StringView argumentName,
663 int rowIndex, RowOption operation) const
664{
665 if (rowIndex < 0) {
666 qmlWarning(this).noquote() << functionName << ": \"" << argumentName << "\" cannot be negative";
667 return false;
668 }
669
670 if (operation == NeedsExisting) {
671 if (rowIndex >= mRowCount) {
672 qmlWarning(this).noquote() << functionName << ": \"" << argumentName
673 << "\" " << rowIndex << " is greater than or equal to rowCount() of " << mRowCount;
674 return false;
675 }
676 } else {
677 if (rowIndex > mRowCount) {
678 qmlWarning(this).noquote() << functionName << ": \"" << argumentName
679 << "\" " << rowIndex << " is greater than rowCount() of " << mRowCount;
680 return false;
681 }
682 }
683
684 return true;
685}
686
687QT_END_NAMESPACE
688
689#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)