QGIM: support gadgets and objects as rows and multi-role values

We can interpret the properties of a gadget or object in two ways:
either as different columns in a fixed-width table, or as different
role-values in a single item, matching the named properties against
the role names.

If used in a table structure, then the properties can only be
interpreted as different role values, as we already have a row type
with multiple columns.

In a list it can be either. To disambiguate in this case, check
whether there is a "display" property. If so, then we assume that
the gadget is supposed to be a multi-role item.

Change-Id: I99400304986f643506e590c5287f4d800654ec77
Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-02-26 11:07:20 +01:00
parent ec5f4fe580
commit 8da1ec6a09
5 changed files with 407 additions and 23 deletions

View File

@ -107,6 +107,41 @@ namespace std {
//! [tuple_protocol]
#endif // __cpp_concepts && forward_like
namespace gadget {
//! [gadget]
class Book
{
Q_GADGET
Q_PROPERTY(QString title READ title)
Q_PROPERTY(QString author READ author)
Q_PROPERTY(QString summary MEMBER m_summary)
Q_PROPERTY(int rating READ rating WRITE setRating)
public:
Book(const QString &title, const QString &author);
// C++ rule of 0: destructor, as well as copy/move operations
// provided by the compiler.
// read-only properties
QString title() const { return m_title; }
QString author() const { return m_author; }
// read/writable property with input validation
int rating() const { return m_rating; }
void setRating(int rating)
{
m_rating = qBound(0, rating, 5);
}
private:
QString m_title;
QString m_author;
QString m_summary;
int m_rating = 0;
};
//! [gadget]
} // namespace gadget
void color_map()
{
//! [color_map]
@ -126,3 +161,45 @@ QListView list;
list.setModel(&colorModel);
//! [color_map]
}
namespace multirole_gadget {
//! [color_gadget_0]
class ColorEntry
{
Q_GADGET
Q_PROPERTY(QString display MEMBER m_colorName)
Q_PROPERTY(QColor decoration READ decoration)
Q_PROPERTY(QString toolTip READ toolTip)
public:
ColorEntry(const QString &color = {})
: m_colorName(color)
{}
QColor decoration() const
{
return QColor::fromString(m_colorName);
}
QString toolTip() const
{
return QColor::fromString(m_colorName).name();
}
private:
QString m_colorName;
};
//! [color_gadget_0]
void color_list() {
//! [color_gadget_1]
const QStringList colorNames = QColor::colorNames();
QList<QGenericItemModel::SingleColumn<ColorEntry>> colors;
colors.reserve(colorNames.size());
for (const QString &name : colorNames)
colors << ColorEntry{name};
QGenericItemModel colorModel(colors);
QListView list;
list.setModel(&colorModel);
//! [color_gadget_1]
}
} // namespace multirole_gadget

View File

@ -91,6 +91,24 @@ QT_BEGIN_NAMESPACE
\snippet qgenericitemmodel/main.cpp pair_int_QString
An easier and more flexible alternative to implementing the tuple protocol
for a C++ type is to use Qt's \l{Meta-Object System}{meta-object system} to
declare a type with \l{Qt's Property System}{properties}. This can be a
value type that is declared as a \l{Q_GADGET}{gadget}, or a QObject subclass.
\snippet qgenericitemmodel/main.cpp gadget
Using QObject subclasses allows properties to be \l{Qt Bindable Properties}
{bindable}, or to have change notification signals. However, using QObject
instances for items has significant memory overhead.
Using Qt gadgets or objects is more convenient and can be more flexible
than implementing the tuple protocol. Those types are also directly
accessible from within QML. However, the access through the property system
comes with some runtime overhead. For performance critical models, consider
implementing the tuple protocol for compile-time generation of the access
code.
\section2 Multi-role items
The type of the items that the implementations of data(), setData(),
@ -118,6 +136,19 @@ QT_BEGIN_NAMESPACE
\c{int}. When using \c{int}, itemData() returns the container as is, and
doesn't have to create a copy of the data.
Gadgets and QObject types are also represented at multi-role items if they
are the item type in a table. The names of the properties have to match the
names of the roles.
\snippet qgenericitemmodel/main.cpp color_gadget_0
When used in a list, these types are ambiguous: they can be represented as
multi-column rows, with each property represented as a separate column. Or
they can be single items with each property being a role. To disambiguate,
use the QGenericItemModel::SingleColumn wrapper.
\snippet qgenericitemmodel/main.cpp color_gadget_1
\section2 The C++ tuple protocol
As seen in the \c{numberNames} example above, the row type can be a tuple,

View File

@ -13,6 +13,9 @@ class Q_CORE_EXPORT QGenericItemModel : public QAbstractItemModel
{
Q_OBJECT
public:
template <typename T>
using SingleColumn = std::tuple<T>;
template <typename Range,
QGenericItemModelDetails::if_is_range<Range> = true>
explicit QGenericItemModel(Range &&range, QObject *parent = nullptr);
@ -143,6 +146,11 @@ protected:
using row_ptr = std::conditional_t<rows_are_pointers, row_type, row_type *>;
using const_row_ptr = const std::remove_pointer_t<row_type> *;
template <typename T>
static constexpr bool has_metaobject =
(QtPrivate::QMetaTypeForType<std::remove_pointer_t<T>>::flags() & QMetaType::IsGadget)
|| (QtPrivate::QMetaTypeForType<T>::flags() & QMetaType::PointerToQObject);
using ModelData = QGenericItemModelDetails::ModelData<std::conditional_t<
std::is_pointer_v<Range>,
Range, std::remove_reference_t<Range>>
@ -217,7 +225,14 @@ public:
Qt::ItemFlags f = Structure::defaultFlags();
if constexpr (static_column_count <= 0) {
if constexpr (has_metaobject<row_type>) {
if (index.column() < row_traits::fixed_size()) {
const QMetaObject mo = std::remove_pointer_t<row_type>::staticMetaObject;
const QMetaProperty prop = mo.property(index.column() + mo.propertyOffset());
if (prop.isWritable())
f |= Qt::ItemIsEditable;
}
} else if constexpr (static_column_count <= 0) {
if constexpr (isMutable())
f |= Qt::ItemIsEditable;
} else if constexpr (std::is_reference_v<row_reference> && !std::is_const_v<row_reference>) {
@ -244,7 +259,17 @@ public:
return m_itemModel->QAbstractItemModel::headerData(section, orientation, role);
}
if constexpr (static_column_count >= 1) {
if constexpr (has_metaobject<row_type>) {
using meta_type = std::remove_pointer_t<row_type>;
if (row_traits::fixed_size() == 1) {
const QMetaType metaType = QMetaType::fromType<meta_type>();
result = QString::fromUtf8(metaType.name());
} else if (section <= row_traits::fixed_size()) {
const QMetaProperty prop = meta_type::staticMetaObject.property(
section + meta_type::staticMetaObject.propertyOffset());
result = QString::fromUtf8(prop.name());
}
} else if constexpr (static_column_count >= 1) {
const QMetaType metaType = meta_type_at<row_type>(section);
if (metaType.isValid())
result = QString::fromUtf8(metaType.name());
@ -257,11 +282,18 @@ public:
QVariant data(const QModelIndex &index, int role) const
{
QVariant result;
const auto readData = [this, &result, role](const auto &value) {
const auto readData = [this, column = index.column(), &result, role](const auto &value) {
Q_UNUSED(this);
using value_type = q20::remove_cvref_t<decltype(value)>;
using multi_role = QGenericItemModelDetails::is_multi_role<value_type>;
if constexpr (multi_role::value) {
if constexpr (has_metaobject<value_type>) {
if (row_traits::fixed_size() <= 1) {
result = readRole(role, value);
} else if (column <= row_traits::fixed_size()
&& (role == Qt::DisplayRole || role == Qt::EditRole)) {
result = readProperty(column, value);
}
} else if constexpr (multi_role::value) {
const auto it = [this, &value, role]{
Q_UNUSED(this);
if constexpr (multi_role::int_key)
@ -347,10 +379,20 @@ public:
}
});
const auto writeData = [this, &data, role](auto &&target) -> bool {
const auto writeData = [this, column = index.column(), &data, role](auto &&target) -> bool {
using value_type = q20::remove_cvref_t<decltype(target)>;
using multi_role = QGenericItemModelDetails::is_multi_role<value_type>;
if constexpr (multi_role::value) {
if constexpr (has_metaobject<value_type>) {
if (QMetaType::fromType<value_type>() == data.metaType()) {
target = data.value<value_type>();
return true;
} else if (row_traits::fixed_size() <= 1) {
return writeRole(role, target, data);
} else if (column <= row_traits::fixed_size()
&& (role == Qt::DisplayRole || role == Qt::EditRole)) {
return writeProperty(column, target, data);
}
} else if constexpr (multi_role::value) {
Qt::ItemDataRole roleToSet = Qt::ItemDataRole(role);
// If there is an entry for EditRole, overwrite that; otherwise,
// set the entry for DisplayRole.
@ -476,20 +518,32 @@ public:
Q_EMIT dataChanged(index, index, {});
});
auto clearData = [column = index.column()](auto &&target) {
if constexpr (has_metaobject<row_type>) {
if (row_traits::fixed_size() <= 1) {
// multi-role object/gadget: reset all properties
return resetProperty(-1, target);
} else if (column <= row_traits::fixed_size()) {
return resetProperty(column, target);
}
} else { // normal structs, values, associative containers
target = {};
return true;
}
return false;
};
row_reference row = rowData(index);
if constexpr (dynamicColumns()) {
*std::next(std::begin(row), index.column()) = {};
success = true;
success = clearData(*std::next(std::begin(row), index.column()));
} else if constexpr (static_column_count == 0) {
row = row_type{};
success = true;
success = clearData(row);
} else if (QGenericItemModelDetails::isValid(row)) {
for_element_at(row, index.column(), [&success](auto &&target){
for_element_at(row, index.column(), [&clearData, &success](auto &&target){
using target_type = decltype(target);
if constexpr (std::is_lvalue_reference_v<target_type>
&& !std::is_const_v<std::remove_reference_t<target_type>>) {
target = {};
success = true;
success = clearData(target);
}
});
}
@ -649,6 +703,124 @@ protected:
return false;
}
template <typename ItemType>
QMetaProperty roleProperty(int role) const
{
const QMetaObject *mo = &ItemType::staticMetaObject;
const QByteArray roleName = roleNames().value(role);
if (const int index = mo->indexOfProperty(roleName.data()); index >= 0)
return mo->property(index);
return {};
}
template <typename ItemType>
QVariant readRole(int role, ItemType *gadget) const
{
using item_type = std::remove_pointer_t<ItemType>;
QVariant result;
QMetaProperty prop = roleProperty<item_type>(role);
if (!prop.isValid() && role == Qt::EditRole)
prop = roleProperty<item_type>(Qt::DisplayRole);
if (prop.isValid())
result = readProperty(prop, gadget);
return result;
}
template <typename ItemType>
QVariant readRole(int role, const ItemType &gadget) const
{
return readRole(role, &gadget);
}
template <typename ItemType>
static QVariant readProperty(const QMetaProperty &prop, ItemType *gadget)
{
if constexpr (std::is_base_of_v<QObject, ItemType>)
return prop.read(gadget);
else
return prop.readOnGadget(gadget);
}
template <typename ItemType>
static QVariant readProperty(int property, ItemType *gadget)
{
using item_type = std::remove_pointer_t<ItemType>;
const QMetaObject &mo = item_type::staticMetaObject;
const QMetaProperty prop = mo.property(property + mo.propertyOffset());
return readProperty(prop, gadget);
}
template <typename ItemType>
static QVariant readProperty(int property, const ItemType &gadget)
{
return readProperty(property, &gadget);
}
template <typename ItemType>
bool writeRole(int role, ItemType *gadget, const QVariant &data)
{
using item_type = std::remove_pointer_t<ItemType>;
auto prop = roleProperty<item_type>(role);
if (!prop.isValid() && role == Qt::EditRole)
prop = roleProperty<item_type>(Qt::DisplayRole);
return writeProperty(prop, gadget, data);
}
template <typename ItemType>
bool writeRole(int role, ItemType &&gadget, const QVariant &data)
{
return writeRole(role, &gadget, data);
}
template <typename ItemType>
static bool writeProperty(const QMetaProperty &prop, ItemType *gadget, const QVariant &data)
{
if constexpr (std::is_base_of_v<QObject, ItemType>)
return prop.write(gadget, data);
else
return prop.writeOnGadget(gadget, data);
}
template <typename ItemType>
static bool writeProperty(int property, ItemType *gadget, const QVariant &data)
{
using item_type = std::remove_pointer_t<ItemType>;
const QMetaObject &mo = item_type::staticMetaObject;
return writeProperty(mo.property(property + mo.propertyOffset()), gadget, data);
}
template <typename ItemType>
static bool writeProperty(int property, ItemType &&gadget, const QVariant &data)
{
return writeProperty(property, &gadget, data);
}
template <typename ItemType>
static bool resetProperty(int property, ItemType *object)
{
using item_type = std::remove_pointer_t<ItemType>;
const QMetaObject &mo = item_type::staticMetaObject;
bool success = true;
if (property == -1) {
// reset all properties
if constexpr (std::is_base_of_v<QObject, item_type>) {
for (int p = mo.propertyOffset(); p < mo.propertyCount(); ++p)
success = writeProperty(mo.property(p), object, {}) && success;
} else { // reset a gadget by assigning a default-constructed
*object = {};
}
} else {
success = writeProperty(mo.property(property + mo.propertyOffset()), object, {});
}
return success;
}
template <typename ItemType>
static bool resetProperty(int property, ItemType &&object)
{
return resetProperty(property, &object);
}
// helpers
const_row_reference rowData(const QModelIndex &index) const
{

View File

@ -170,6 +170,25 @@ namespace QGenericItemModelDetails
static constexpr int fixed_size() { return 0; }
};
// Specialization for gadgets
// clang doesn't accept multiple specializations using std::void_t directly
template <class... Ts> struct make_void { using type = void; };
template <typename T>
struct row_traits<T, typename make_void<decltype(T::staticMetaObject)>::type>
{
static constexpr int static_size = 0;
static int fixed_size(){
// Interpret a gadget in a list as a multi-column row item. To
// disambiguate, stick it into a SingleColumn wrapper.
static const int columnCount = []{
const QMetaObject &mo = T::staticMetaObject;
return mo.propertyCount() - mo.propertyOffset();
}();
return columnCount;
}
};
template <typename T>
[[maybe_unused]] static constexpr int static_size_v =
row_traits<q20::remove_cvref_t<std::remove_pointer_t<T>>>::static_size;

View File

@ -12,15 +12,62 @@
#include <QtTest/qabstractitemmodeltester.h>
#endif
#include <list>
#include <vector>
#if defined(__cpp_lib_ranges)
#include <ranges>
#endif
class Item
{
Q_GADGET
Q_PROPERTY(QString display READ display WRITE setDisplay)
Q_PROPERTY(QColor decoration READ decoration WRITE setDecoration)
Q_PROPERTY(QString toolTip READ toolTip WRITE setToolTip)
public:
Item() = default;
Item(const QString &display, QColor decoration, const QString &toolTip)
: m_display(display), m_decoration(decoration), m_toolTip(toolTip)
{
}
QString display() const { return m_display; }
void setDisplay(const QString &display) { m_display = display; }
QColor decoration() const { return m_decoration; }
void setDecoration(QColor decoration) { m_decoration = decoration; }
QString toolTip() const { return m_toolTip.isEmpty() ? display() : m_toolTip; }
void setToolTip(const QString &toolTip) { m_toolTip = toolTip; }
private:
QString m_display;
QColor m_decoration;
QString m_toolTip;
};
class Object : public QObject
{
Q_OBJECT
Q_PROPERTY(QString string READ string WRITE setString)
Q_PROPERTY(int number READ number WRITE setNumber)
public:
using QObject::QObject;
QString string() const { return m_string; }
void setString(const QString &string) { m_string = string; }
int number() const { return m_number; }
void setNumber(int number) { m_number = number; }
private:
// note: default values need to be convertible to each other
QString m_string = "1234";
int m_number = 42;
};
struct Row
{
QString m_item;
Item m_item;
int m_number;
QString m_description;
@ -41,7 +88,7 @@ struct Row
namespace std {
template <> struct tuple_size<Row> : std::integral_constant<size_t, 3> {};
template <> struct tuple_element<0, Row> { using type = QString; };
template <> struct tuple_element<0, Row> { using type = Item; };
template <> struct tuple_element<1, Row> { using type = int; };
template <> struct tuple_element<2, Row> { using type = QString; };
}
@ -110,9 +157,9 @@ private:
std::array<int, 5> fixedArrayOfNumbers = {1, 2, 3, 4, 5};
int cArrayOfNumbers[5] = {1, 2, 3, 4, 5};
Row cArrayFixedColumns[3] = {
{"red", 0xff0000, "The color red"},
{"green", 0x00ff00, "The color green"},
{"blue", 0x0000ff, "The color blue"}
{{"red", Qt::red, "0xff0000"}, 0xff0000, "The color red"},
{{"green", Qt::green, "0x00ff00"}, 0x00ff00, "The color green"},
{{"blue", Qt::blue, "0x0000ff"}, 0x0000ff, "The color blue"}
};
// dynamic number of rows, fixed number of columns
@ -130,10 +177,27 @@ private:
{31, 32, 33, 34, 35, 36, 37, 38, 39, 40},
{41, 42, 43, 44, 45, 46, 47, 48, 49, 50},
};
std::vector<Item> vectorOfGadgets = {
{"red", Qt::red, "0xff0000"},
{"green", Qt::green, "0x00ff00"},
{"blue", Qt::blue, "0x0000ff"},
};
std::vector<QGenericItemModel::SingleColumn<Item>> listOfGadgets = {
{{"red", Qt::red, "0xff0000"}},
{{"green", Qt::green, "0x00ff00"}},
{{"blue", Qt::blue, "0x0000ff"}},
};
std::vector<Row> vectorOfStructs = {
{"red", 1, "one"},
{"green", 2, "two"},
{"blue", 3, "three"},
{{"red", Qt::red, "0xff0000"}, 1, "one"},
{{"green", Qt::green, "0x00ff00"}, 2, "two"},
{{"blue", Qt::blue, "0x0000ff"}, 3, "three"},
};
Object row1;
Object row2;
Object row3;
std::list<Object *> listOfObjects = {
&row1, &row2, &row3
};
// bad (but legal) get() overload that never returns a mutable reference
@ -152,8 +216,16 @@ private:
{21.0, 22.0, 23.0, 24.0, 25.0},
};
// item is pointer
Item itemAsPointer = {"red", Qt::red, "0xff0000"};
std::vector<std::vector<Item *>> tableOfPointers = {
{&itemAsPointer, &itemAsPointer},
{&itemAsPointer, &itemAsPointer},
{&itemAsPointer, &itemAsPointer},
};
// rows are pointers
Row rowAsPointer = {"blue", 0x0000ff, "Blau"};
Row rowAsPointer = {{"blue", Qt::blue, "0x0000ff"}, 0x0000ff, "Blau"};
std::vector<Row *> tableOfRowPointers = {
&rowAsPointer,
&rowAsPointer,
@ -275,17 +347,29 @@ void tst_QGenericItemModel::createTestData()
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_POINTER(vectorOfStructs)
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(vectorOfConstStructs)
<< int(std::tuple_size_v<ConstRow>) << ChangeActions(ChangeAction::ChangeRows);
ADD_POINTER(vectorOfConstStructs)
<< int(std::tuple_size_v<ConstRow>) << ChangeActions(ChangeAction::ChangeRows);
ADD_COPY(vectorOfGadgets)
<< 3 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_POINTER(vectorOfGadgets)
<< 3 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(listOfGadgets)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_POINTER(listOfGadgets)
<< 1 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(listOfObjects)
<< 2 << (ChangeAction::ChangeRows | ChangeAction::SetData);
ADD_COPY(tableOfNumbers)
<< 5 << ChangeActions(ChangeAction::All);
ADD_POINTER(tableOfNumbers)
<< 5 << ChangeActions(ChangeAction::All);
// only adding as pointer, copy would operate on the same data
ADD_POINTER(tableOfPointers)
<< 2 << ChangeActions(ChangeAction::All);
ADD_POINTER(tableOfRowPointers)
<< int(std::tuple_size_v<Row>) << (ChangeAction::ChangeRows | ChangeAction::SetData);
@ -598,6 +682,7 @@ void tst_QGenericItemModel::insertRows()
const QVariant lastValue = lastItem.data();
QEXPECT_FAIL("tableOfPointersPointer", "No item created", Continue);
QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue);
QEXPECT_FAIL("listOfObjectsCopy", "No object created", Continue);
// associative containers are default constructed with no valid data
ignoreFailureFromAssociativeContainers();