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 \sa TableModelColumn, TableView, QAbstractTableModel
132*/
133
134QQmlTableModel::QQmlTableModel(QObject *parent)
135 : QQmlAbstractColumnModel(parent)
136{
137}
138
139QQmlTableModel::~QQmlTableModel()
140 = default;
141
142/*!
143 \qmlproperty var TableModel::rows
144
145 This property holds the model data in the form of an array of rows:
146
147 \snippet qml/tablemodel/fruit-example-simpledelegate.qml rows
148
149 \sa getRow(), setRow(), moveRow(), appendRow(), insertRow(), clear(), rowCount, columnCount
150*/
151QVariant QQmlTableModel::rows() const
152{
153 return mRows;
154}
155
156void QQmlTableModel::setRows(const QVariant &rows)
157{
158 const std::optional<QVariantList> validated = validateRowsArgument(rows);
159 if (!validated)
160 return;
161
162 const QVariantList rowsAsVariantList = *validated;
163 if (rowsAsVariantList == mRows) {
164 // No change.
165 return;
166 }
167
168 if (!mComponentCompleted) {
169 // Store the rows until we can call setRowsPrivate() after component completion.
170 mRows = rowsAsVariantList;
171 return;
172 }
173
174 setRowsPrivate(rowsAsVariantList);
175}
176
177void QQmlTableModel::setRowsPrivate(const QVariantList &rowsAsVariantList)
178{
179 Q_ASSERT(mComponentCompleted);
180
181 // By now, all TableModelColumns should have been set.
182 if (mColumns.isEmpty()) {
183 qmlWarning(this) << "No TableModelColumns were set; model will be empty";
184 return;
185 }
186
187 const bool firstTimeValidRowsHaveBeenSet = mColumnMetadata.isEmpty();
188 if (!firstTimeValidRowsHaveBeenSet) {
189 // This is not the first time rows have been set; validate each one.
190 for (int rowIndex = 0; rowIndex < rowsAsVariantList.size(); ++rowIndex) {
191 // validateNewRow() expects a QVariant wrapping a QJSValue, so to
192 // simplify the code, just create one here.
193 const QVariant row = QVariant::fromValue(rowsAsVariantList.at(rowIndex));
194 if (!validateNewRow("setRows()"_L1, row, SetRowsOperation))
195 return;
196 }
197 }
198
199 const int oldRowCount = mRowCount;
200
201 beginResetModel();
202
203 // We don't clear the column or role data, because a TableModel should not be reused in that way.
204 // Once it has valid data, its columns and roles are fixed.
205 mRows = rowsAsVariantList;
206 mRowCount = mRows.size();
207
208 // Gather metadata the first time rows is set.
209 if (firstTimeValidRowsHaveBeenSet && !mRows.isEmpty())
210 fetchColumnMetadata();
211
212 endResetModel();
213 emit rowsChanged();
214
215 if (mRowCount != oldRowCount)
216 emit rowCountChanged();
217}
218
219QVariant QQmlTableModel::dataPrivate(const QModelIndex &index, const QString &roleName) const
220{
221 const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column());
222 const QString propertyName = columnMetadata.roles.value(roleName).name;
223 const QVariantMap rowData = mRows.at(index.row()).toMap();
224 return rowData.value(propertyName);
225}
226
227void QQmlTableModel::setDataPrivate(const QModelIndex &index, const QString &roleName, QVariant value)
228{
229 int row = index.row();
230 QVariantMap modifiedRow = mRows.at(row).toMap();
231 modifiedRow[roleName] = value;
232 mRows[row] = modifiedRow;
233}
234
235// TODO: Turn this into a snippet that compiles in CI
236/*!
237 \qmlmethod void TableModel::appendRow(var row)
238
239 Adds a new row to the end of the model, with the
240 values (cells) in \a row.
241
242 \code
243 model.appendRow({
244 checkable: true,
245 amount: 1,
246 fruitType: "Pear",
247 fruitName: "Williams",
248 fruitPrice: 1.50,
249 })
250 \endcode
251
252 \sa insertRow(), setRow(), removeRow()
253*/
254void QQmlTableModel::appendRow(const QVariant &row)
255{
256 if (!validateNewRow("appendRow()"_L1, row, AppendOperation))
257 return;
258
259 doInsert(mRowCount, row);
260}
261
262/*!
263 \qmlmethod void TableModel::clear()
264
265 Removes all rows from the model.
266
267 \sa removeRow()
268*/
269void QQmlTableModel::clear()
270{
271 QQmlEngine *engine = qmlEngine(this);
272 Q_ASSERT(engine);
273 setRows(QVariant::fromValue(engine->newArray()));
274}
275
276/*!
277 \qmlmethod var TableModel::getRow(int rowIndex)
278
279 Returns the row at \a rowIndex in the model.
280
281 Note that this equivalent to accessing the row directly
282 through the \l rows property:
283
284 \code
285 Component.onCompleted: {
286 // These two lines are equivalent.
287 console.log(model.getRow(0).display);
288 console.log(model.rows[0].fruitName);
289 }
290 \endcode
291
292 \note the returned object cannot be used to modify the contents of the
293 model; use setRow() instead.
294
295 \sa setRow(), appendRow(), insertRow(), removeRow(), moveRow()
296*/
297QVariant QQmlTableModel::getRow(int rowIndex)
298{
299 if (!validateRowIndex("getRow()"_L1, "rowIndex"_L1, rowIndex, NeedsExisting))
300 return QVariant();
301 return mRows.at(rowIndex);
302}
303
304/*!
305 \qmlmethod void TableModel::insertRow(int rowIndex, var row)
306
307 Adds a new row to the model at position \a rowIndex, with the
308 values (cells) in \a row.
309
310 \code
311 model.insertRow(2, {
312 checkable: true, checked: false,
313 amount: 1,
314 fruitType: "Pear",
315 fruitName: "Williams",
316 fruitPrice: 1.50,
317 })
318 \endcode
319
320 The \a rowIndex must point to an existing item in the table, or one past
321 the end of the table (equivalent to \l appendRow()).
322
323 \sa appendRow(), setRow(), removeRow(), rowCount
324*/
325void QQmlTableModel::insertRow(int rowIndex, const QVariant &row)
326{
327 if (!validateNewRow("insertRow()"_L1, row) ||
328 !validateRowIndex("insertRow()"_L1, "rowIndex"_L1, rowIndex, CanAppend))
329 return;
330
331 doInsert(rowIndex, row);
332}
333
334void QQmlTableModel::doInsert(int rowIndex, const QVariant &row)
335{
336 beginInsertRows(QModelIndex(), rowIndex, rowIndex);
337
338 // Adding rowAsVariant.toList() will add each invidual variant in the list,
339 // which is definitely not what we want.
340 const QVariant rowAsVariant = row.userType() == QMetaType::QVariantMap ? row : row.value<QJSValue>().toVariant();
341
342 mRows.insert(rowIndex, rowAsVariant);
343 ++mRowCount;
344
345 qCDebug(lcTableModel).nospace() << "inserted the following row to the model at index "
346 << rowIndex << ":\n" << rowAsVariant.toMap();
347
348 // Gather metadata the first time a row is added.
349 if (mColumnMetadata.isEmpty())
350 fetchColumnMetadata();
351
352 endInsertRows();
353 emit rowCountChanged();
354 emit rowsChanged();
355}
356
357/*!
358 \qmlmethod void TableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
359
360 Moves \a rows from the index at \a fromRowIndex to the index at
361 \a toRowIndex.
362
363 The from and to ranges must exist; for example, to move the first 3 items
364 to the end of the list:
365
366 \code
367 model.moveRow(0, model.rowCount - 3, 3)
368 \endcode
369
370 \sa appendRow(), insertRow(), removeRow(), rowCount
371*/
372void QQmlTableModel::moveRow(int fromRowIndex, int toRowIndex, int rows)
373{
374 if (fromRowIndex == toRowIndex) {
375 qmlWarning(this) << "moveRow(): \"fromRowIndex\" cannot be equal to \"toRowIndex\"";
376 return;
377 }
378
379 if (rows <= 0) {
380 qmlWarning(this) << "moveRow(): \"rows\" is less than or equal to 0";
381 return;
382 }
383
384 if (!validateRowIndex("moveRow()"_L1, "fromRowIndex"_L1, fromRowIndex, NeedsExisting))
385 return;
386
387 if (!validateRowIndex("moveRow()"_L1, "toRowIndex"_L1, toRowIndex, NeedsExisting))
388 return;
389
390 if (fromRowIndex + rows > mRowCount) {
391 qmlWarning(this) << "moveRow(): \"fromRowIndex\" (" << fromRowIndex
392 << ") + \"rows\" (" << rows << ") = " << (fromRowIndex + rows)
393 << ", which is greater than rowCount() of " << mRowCount;
394 return;
395 }
396
397 if (toRowIndex + rows > mRowCount) {
398 qmlWarning(this) << "moveRow(): \"toRowIndex\" (" << toRowIndex
399 << ") + \"rows\" (" << rows << ") = " << (toRowIndex + rows)
400 << ", which is greater than rowCount() of " << mRowCount;
401 return;
402 }
403
404 qCDebug(lcTableModel).nospace() << "moving " << rows
405 << " row(s) from index " << fromRowIndex
406 << " to index " << toRowIndex;
407
408 // Based on the same call in QQmlListModel::moveRow().
409 beginMoveRows(QModelIndex(), fromRowIndex, fromRowIndex + rows - 1, QModelIndex(),
410 toRowIndex > fromRowIndex ? toRowIndex + rows : toRowIndex);
411
412 // Based on ListModel::moveRow().
413 if (fromRowIndex > toRowIndex) {
414 // Only move forwards - flip if moving backwards.
415 const int from = fromRowIndex;
416 const int to = toRowIndex;
417 fromRowIndex = to;
418 toRowIndex = to + rows;
419 rows = from - to;
420 }
421
422 QList<QVariant> store;
423 store.reserve(rows);
424 for (int i = 0; i < (toRowIndex - fromRowIndex); ++i)
425 store.append(mRows.at(fromRowIndex + rows + i));
426 for (int i = 0; i < rows; ++i)
427 store.append(mRows.at(fromRowIndex + i));
428 for (int i = 0; i < store.size(); ++i)
429 mRows[fromRowIndex + i] = store[i];
430
431 qCDebug(lcTableModel).nospace() << "after moving, rows are:\n" << mRows;
432
433 endMoveRows();
434 emit rowsChanged();
435}
436
437/*!
438 \qmlmethod void TableModel::removeRow(int rowIndex, int rows = 1)
439
440 Removes a number of \a rows at \a rowIndex from the model.
441
442 \sa clear(), rowCount
443*/
444void QQmlTableModel::removeRow(int rowIndex, int rows)
445{
446 if (!validateRowIndex("removeRow()"_L1, "rowIndex"_L1, rowIndex, NeedsExisting))
447 return;
448
449 if (rows <= 0) {
450 qmlWarning(this) << "removeRow(): \"rows\" is less than or equal to zero";
451 return;
452 }
453
454 if (rowIndex + rows - 1 >= mRowCount) {
455 qmlWarning(this) << "removeRow(): \"rows\" " << rows
456 << " exceeds available rowCount() of " << mRowCount
457 << " when removing from \"rowIndex\" " << rowIndex;
458 return;
459 }
460
461 beginRemoveRows(QModelIndex(), rowIndex, rowIndex + rows - 1);
462
463 auto firstIterator = mRows.begin() + rowIndex;
464 // The "last" argument to erase() is exclusive, so we go one past the last item.
465 auto lastIterator = firstIterator + rows;
466 mRows.erase(firstIterator, lastIterator);
467 mRowCount -= rows;
468
469 endRemoveRows();
470 emit rowCountChanged();
471 emit rowsChanged();
472
473 qCDebug(lcTableModel).nospace() << "removed " << rows
474 << " items from the model, starting at index " << rowIndex;
475}
476
477/*!
478 \qmlmethod void TableModel::setRow(int rowIndex, var row)
479
480 Changes the row at \a rowIndex in the model with \a row.
481
482 All columns/cells must be present in \c row, and in the correct order.
483
484 \code
485 model.setRow(0, {
486 checkable: true,
487 amount: 1,
488 fruitType: "Pear",
489 fruitName: "Williams",
490 fruitPrice: 1.50,
491 })
492 \endcode
493
494 If \a rowIndex is equal to \c rowCount(), then a new row is appended to the
495 model. Otherwise, \a rowIndex must point to an existing row in the model.
496
497 \sa appendRow(), insertRow(), rowCount
498*/
499void QQmlTableModel::setRow(int rowIndex, const QVariant &row)
500{
501 if (!validateNewRow("setRow()"_L1, row) ||
502 !validateRowIndex("setRow()"_L1, "rowIndex"_L1, rowIndex, CanAppend))
503 return;
504
505 if (rowIndex != mRowCount) {
506 // Setting an existing row.
507 mRows[rowIndex] = row;
508
509 // For now we just assume the whole row changed, as it's simpler.
510 const QModelIndex topLeftModelIndex(createIndex(rowIndex, 0));
511 const QModelIndex bottomRightModelIndex(createIndex(rowIndex, mColumnCount - 1));
512 emit dataChanged(topLeftModelIndex, bottomRightModelIndex);
513 emit rowsChanged();
514 } else {
515 // Appending a row.
516 doInsert(rowIndex, row);
517 }
518}
519
520
521QVariant QQmlTableModel::firstRow() const
522{
523 return mRows.first();
524}
525
526void QQmlTableModel::setInitialRows()
527{
528 setRowsPrivate(mRows);
529}
530
531/*!
532 \qmlmethod QModelIndex TableModel::index(int row, int column)
533
534 Returns a \l QModelIndex object referencing the given \a row and \a column,
535 which can be passed to the data() function to get the data from that cell,
536 or to setData() to edit the contents of that cell.
537
538 \code
539 import QtQml 2.14
540 import Qt.labs.qmlmodels 1.0
541
542 TableModel {
543 id: model
544
545 TableModelColumn { display: "fruitType" }
546 TableModelColumn { display: "fruitPrice" }
547
548 rows: [
549 { fruitType: "Apple", fruitPrice: 1.50 },
550 { fruitType: "Orange", fruitPrice: 2.50 }
551 ]
552
553 Component.onCompleted: {
554 for (var r = 0; r < model.rowCount; ++r) {
555 console.log("An " + model.data(model.index(r, 0)).display +
556 " costs " + model.data(model.index(r, 1)).display.toFixed(2))
557 }
558 }
559 }
560 \endcode
561
562 \sa {QModelIndex and related Classes in QML}, data()
563*/
564// Note: we don't document the parent argument, because you never need it, because
565// cells in a TableModel don't have parents. But it is there because this function is an override.
566QModelIndex QQmlTableModel::index(int row, int column, const QModelIndex &parent) const
567{
568 return row >= 0 && row < rowCount() && column >= 0 && column < columnCount() && !parent.isValid()
569 ? createIndex(row, column)
570 : QModelIndex();
571}
572
573QModelIndex QQmlTableModel::parent(const QModelIndex &index) const
574{
575 Q_UNUSED(index);
576 return {};
577}
578
579/*!
580 \qmlproperty int TableModel::rowCount
581 \readonly
582
583 This read-only property holds the number of rows in the model.
584
585 This value changes whenever rows are added or removed from the model.
586*/
587int QQmlTableModel::rowCount(const QModelIndex &parent) const
588{
589 if (parent.isValid())
590 return 0;
591
592 return mRowCount;
593}
594
595/*!
596 \qmlproperty int TableModel::columnCount
597 \readonly
598
599 This read-only property holds the number of columns in the model.
600
601 The number of columns is fixed for the lifetime of the model
602 after the \l rows property is set or \l appendRow() is called for the first
603 time.
604*/
605int QQmlTableModel::columnCount(const QModelIndex &parent) const
606{
607 Q_UNUSED(parent);
608
609 return mColumnCount;
610}
611
612/*!
613 \qmlmethod variant TableModel::data(QModelIndex index, string role)
614
615 Returns the data from the table cell at the given \a index belonging to the
616 given \a role.
617
618 \sa index(), setData()
619*/
620
621/*!
622 \qmlmethod bool TableModel::setData(QModelIndex index, variant value, string role)
623
624 Inserts or updates the data field named by \a role in the table cell at the
625 given \a index with \a value. Returns true if successful, false if not.
626
627 \sa data(), index()
628*/
629
630bool QQmlTableModel::validateRowIndex(QLatin1StringView functionName, QLatin1StringView argumentName,
631 int rowIndex, RowOption operation) const
632{
633 if (rowIndex < 0) {
634 qmlWarning(this).noquote() << functionName << ": \"" << argumentName << "\" cannot be negative";
635 return false;
636 }
637
638 if (operation == NeedsExisting) {
639 if (rowIndex >= mRowCount) {
640 qmlWarning(this).noquote() << functionName << ": \"" << argumentName
641 << "\" " << rowIndex << " is greater than or equal to rowCount() of " << mRowCount;
642 return false;
643 }
644 } else {
645 if (rowIndex > mRowCount) {
646 qmlWarning(this).noquote() << functionName << ": \"" << argumentName
647 << "\" " << rowIndex << " is greater than rowCount() of " << mRowCount;
648 return false;
649 }
650 }
651
652 return true;
653}
654
655QT_END_NAMESPACE
656
657#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)