QMLとC++によるデータ分離アーキテクチャの実践 - QAbstractListModelを用いた動的リスト実装

データ分離アーキテクチャの概要

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側ではdisplayNamecategoryTypeプロパティを通じてモデルデータにアクセス可能になります。

タグ: QML Qt abstractlistmodel data-binding Cplusplus

6月20日 19:25 投稿