Qt 6.10でQRangeModelを紹介した際、今後のリリースでいくつかの制限事項に取り組むと述べました。Qt 6.11では、QRangeModelがstd::views::filterのようなキャッシュ範囲をサポートし、ガジェット・オブジェクト・連想コンテナ以外のアイテムに対するロールデータの読み書きのカスタマイズポイントも提供されます。最大の追加点として、QAbstractItemModel APIを使用せずに、基礎となるモデルのデータと構造を安全に操作できるようになります。
Qt 6.11のQRangeModelに追加された機能の詳細とコードスニペットは、Qt Forumの投稿をご覧ください。
すべてのアイテムまたは行が同一のQObject型のインスタンスにより裏付けられているQRangeModelでは、それらのオブジェクトのプロパティが変更された際に、モデルが自動的にdataChanged()シグナルを発行できるようになりました。これにより、QAbstractItemModel APIを介することなく、データの同期を保つ便利な仕組みが実現されます。たとえば、次のようなアイテム型によってデータが実装されているとします。
class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(QString firstName READ firstName WRITE setFirstName NOTIFY firstNameChanged)
Q_PROPERTY(QString lastName READ lastName WRITE setLastName NOTIFY lastNameChanged)
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
public:
explicit Person( /* ~~~ */ );
// ~~~
Q_SIGNALS:
// ~~~
void ageChanged(int age);
private:
int m_age = 0;
};
QRangeModelは、このようなオブジェクトのリストを、各プロパティに対応する3列のテーブルとして表します。
// backend code
QList<Person *> data = {
new Person("Max", "Mustermann", 37),
new Person("Eva", "Nordmann", 39),
new Person("Bob", "Mountainman", 42),
};
model = new QRangeModel(&data);
model->setAutoConnectPolicy(QRangeModel::AutoConnectPolicy::Full);
// frontend code
tableView = new QTableView(this);
tableView->setModel(model);
QRangeModelのautoConnectPolicyをFullまたはOnReadに設定すると、以降のプロパティ変更時に、モデルが対応するインデックスとロールに対してQAbstractItemModel::dataChanged()を発行し、すべてのビューが更新されます。
// backend code
data[0]->setAge(data[0]->age() + 1);
唯一の要件は、値が変化した際にセッターが発行する通知シグナルをプロパティが持つことです。 モデルのデータ型としてQObjectインスタンスを使用するのはコスト面で不利ですが、この機能追加により、QModelIndexやQVariantを扱うことなく、小規模なモデル内のデータを直接操作しやすくなります。
単純な操作でQAbstractItemModel固有の作法を不要にするという考え方は、Qt 6.11のアイテムモデルにおける最大の追加機能にも通じています。QRangeModelAdapterは新しいテンプレートクラスで、QRangeModelが操作するデータ構造を安全かつ便利に扱えるようにします。基礎となるレンジのアイテムデータや行・列の構造は、QModelIndexやQVariantを扱うことなくアダプター経由で変更・アクセスが可能で、モデルのシグナル発行、永続インデックスの無効化・更新、およびビューへの変更通知はアダプターが自動的に処理します。
struct Backend
{
QList<int> data { 1, 2, 3, 4 };
QRangeModelAdapter adapter(std::ref(data));
void updateData();
};
class Frontend : public QMainWindow
{
public:
Frontend()
{
// ~~~
listView->setModel(backend.adapter.model()); // the adapter creates and owns the model
}
};
// ~~~
void Backend::updateData()
{
adapter[0] = 23; // emits dataChanged()
adapter.insertRow(0, 78); // prepends 78, calls begin/endInsertRows
adapter.removeRows(2, 2); // removes two rows, calls begin/endRemoveRows
}
基礎となるデータ構造がテーブルの場合、個々のアイテムは次のように変更できます。
void Backend::updateData()
{
adapter.at(1, 0) = 44; // works with C++17
adapter[0, 1] = 55; // with C++23's multidimensional subscript operator
}
ツリー構造のデータ構造では、行インデックスのパスを使って行にアクセスできます。
void Backend::updateData()
{
adapter.at({0, 1}, 1) = "zero/one:one"; // second column in the second child of the first toplevel row
}
インデックスベースの行・列アクセスに加えて、QRangeModelAdapterはイテレーターAPIも提供します。
void Backend::storeData() const
{
for (const auto &item : adapter)
storage << item;
}
テーブルの場合、テーブルの次元を維持しながらすべてのセルをクリアするには、次のようにします。
void Backend::clearData()
{
for (auto row : adapter) {
for (auto item : row) {
item = {};
}
}
}
QRangeModelAdapterはQt 6.11ではテクノロジープレビューです。APIはやや独特で、基礎となるデータ構造によってセマンティクスが異なります。またC++17のメタプログラミング技法を多用しているため、ドキュメントがやや複雑になっており、テンプレートAPIのレンダリング改善に向けたqdoc開発者への良い課題ともなっています。ぜひフィードバックをお寄せください。
カスタムアイテムアクセスを実装するための新しいカスタマイズポイントを追加しました。型に対してQRangeModel::ItemAccessを特殊化し、ロール固有の値の読み書きを行う静的なreadRole・writeRoleアクセサーを実装します。
template <>
struct QRangeModel::ItemAccess<Person>
{
static QVariant readRole(const Person &item, int role)
{
switch (role) {
case Qt::DisplayRole:
return item.firstName() + " " + item.lastName();
case Qt::UserRole:
return item.firstName();
case Qt::UserRole + 1:
return item.lastName();
case Qt::UserRole + 2:
return item.age();
}
return {};
}
static bool writeRole(Person &item, const QVariant &data, int role)
{
bool ok = true;
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole: {
const QStringList names = data.toString().split(u' ');
if (names.size() > 0)
item.setFirstName(names.at(0));
if (names.size() > 1)
item.setLastName(names.at(1));
break;
}
case Qt::UserRole:
item.setFirstName(data.toString());
break;
case Qt::UserRole + 1:
item.setLastName(data.toString());
break;
case Qt::UserRole + 2:
item.setAge(data.toInt(&ok));
break;
default:
ok = false;
break;
}
return ok;
}
};
ItemAccessの特殊化は組み込みのアクセス機構より優先されます。また、多列行として扱える型であっても、QRangeModelはその型をマルチロールアイテムとして解釈します。そのため、タプルライクな型・ガジェット・完全にカスタムな構造体に対するアクセスのカスタマイズと最適化に活用できます。
最後に、std::rangesユーザー向けの小さな改善点です。QRangeModelは定数レンジに対してstd::begin/endが不要になりました。これにより、std::views::filterなどをモデルへの入力として使用できます。
const QDate today = QDate::currentDate();
model = new QRangeModel(std::views::iota(today.addYears(-100), today.addYears(100))
| std::views::filter([](QDate date) { return date.dayOfWeek() < 6; })
);
もしstd::rangesがもうお馴染みの存在で、C++の最先端技術についてもっと知りたいという方は、C++26のリフレクションを使ってカスタム構造体の定型コードをさらに削減した、ハッカソンプロジェクトに関するブログ記事をご覧ください。
Qt 6.11では、C++データ構造をUIデータのソースとして活用しやすくなりました。QRangeModelAdapterを用いたデータ変更はC++開発者に馴染み深いAPI概念を採用しつつ、QAbstractItemModelクライアントへの変更通知も確実に行われます。QObjectプロパティへのdataChanged()の自動バインディング、より多くのC++20レンジのサポート、そして新しいカスタマイズポイントにより、C++開発者向けのQtモダンアイテムモデルの機能がさらに充実しました。