データ分離アーキテクチャの概要
Qt5以降、QMLはデスクトップアプリケーション開発における第一級のUI構築技術として定着しました。本稿では、QMLが担当するプレゼンテーション層とC++が担当するデータ層を明確に分離する実装パターンを解説します。このアプローチにより、QMLは高速なレンダリングに専念し、データ管理やビジネスロジックはC++側で堅牢に実装できます。両者の関係性は、Web開発におけるHTMLとJavaScriptの協調に似ています。
動作概要
実装例では、QMLのListViewコンポーネントがC++で定義されたカスタムモデルからデータを動的に取得して表示します。アプリケーション起動後、タイマー駆動で新規アイテムが定期的にモデルに追加され、UIにリアルタイムに反映されます。
実装詳解
メインプログラム
アプリケーションエントリポイントでは、モデルインスタンスを生成し、QMLエンジンにコンテキストプロパティとして登録します。QTimerを利用した非同期更新機構により、実行時のデータ追加動作を検証できます。
int main(int argc, char** argv)
{
QGuiApplication app(argc, argv);
ProductInventory inventoryModel;
inventoryModel.appendProduct(ProductItem("ノートパソコン", "電子機器"));
inventoryModel.appendProduct(ProductItem("オフィスチェア", "家具"));
inventoryModel.appendProduct(ProductItem("メモ帳", "文房具"));
QQuickView mainView;
mainView.setResizeMode(QQuickView::SizeRootObjectToView);
QQmlContext* qmlContext = mainView.rootContext();
qmlContext->setContextProperty("inventoryModel", &inventoryModel);
mainView.setSource(QUrl("qrc:/views/main.qml"));
mainView.show();
QTimer dynamicUpdater;
int additionCounter = 0;
QObject::connect(&dynamicUpdater, &QTimer::timeout,
[&inventoryModel, &additionCounter]() {
const QStringList productNames = {"ワイヤレスマウス", "4Kモニター", "メカニカルキーボード"};
const QStringList productTypes = {"電子機器", "電子機器", "電子機器"};
if (additionCounter < productNames.size()) {
inventoryModel.appendProduct(ProductItem(productNames[additionCounter],
productTypes[additionCounter]));
additionCounter++;
}
});
dynamicUpdater.start(1800);
return app.exec();
}
モデル定義ヘッダ
QAbstractListModelを継承したカスタムモデルクラスと、データ構造体を定義します。Roleベースのアクセス機構を提供するため、enumでロール識別子を宣言します。
// 製品情報を格納するデータ構造体
struct ProductItem
{
ProductItem(const QString& name, const QString& category);
QString getName() const;
QString getCategory() const;
private:
QString m_productName;
QString m_productCategory;
};
// リストモデルの実装クラス
class ProductInventory : public QAbstractListModel
{
Q_OBJECT
public:
enum InventoryRoles {
NameRole = Qt::UserRole + 10,
CategoryRole = Qt::UserRole + 11
};
explicit ProductInventory(QObject* parent = nullptr);
void appendProduct(const ProductItem& item);
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
protected:
QHash<int, QByteArray> roleNames() const override;
private:
QList<ProductItem> m_productList;
};
モデル実装
データ変更時には必ずbeginInsertRows()とendInsertRows()を呼び出して、ビューへの変更通知を行います。roleNames()では、QML側で利用可能なプロパティ名を定義します。
ProductItem::ProductItem(const QString& name, const QString& category)
: m_productName(name), m_productCategory(category) {}
QString ProductItem::getName() const { return m_productName; }
QString ProductItem::getCategory() const { return m_productCategory; }
ProductInventory::ProductInventory(QObject* parent)
: QAbstractListModel(parent) {}
void ProductInventory::appendProduct(const ProductItem& item)
{
const int currentRow = rowCount();
beginInsertRows(QModelIndex(), currentRow, currentRow);
m_productList.append(item);
endInsertRows();
}
int ProductInventory::rowCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return m_productList.count();
}
QVariant ProductInventory::data(const QModelIndex& index, int role) const
{
if (!index.isValid() || index.row() >= m_productList.count())
return QVariant();
const ProductItem& item = m_productList.at(index.row());
switch (role) {
case NameRole: return item.getName();
case CategoryRole: return item.getCategory();
default: return QVariant();
}
}
QHash<int, QByteArray> ProductInventory::roleNames() const
{
QHash<int, QByteArray> roleMapping;
roleMapping[NameRole] = "displayName";
roleMapping[CategoryRole] = "categoryType";
return roleMapping;
}
この実装により、QML側ではdisplayNameとcategoryTypeプロパティを通じてモデルデータにアクセス可能になります。