「QMLのコントロール - ListModelのカスタム」の版間の差分

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動
 
(同じ利用者による、間の4版が非表示)
172行目: 172行目:
<br>
<br>
===== roleNamesメソッド =====
===== roleNamesメソッド =====
roleNamesメソッドは必ずオーバーライドする必要がある。<br>
このメソッドは、QML側でアクセスするプロパティ名を定義するものである。<br>
<br>
  <syntaxhighlight lang="c++">
  <syntaxhighlight lang="c++">
  // QAbstractListModelクラスのオーバーライドが必須のメソッド
  // QAbstractListModelクラスのオーバーライドが必須のメソッド
193行目: 196行目:
  </syntaxhighlight>
  </syntaxhighlight>
<br>
<br>
==== データ操作メソッド ====
==== データ操作メソッド ====
===== appendItemメソッド =====
===== appendItemメソッド =====
296行目: 300行目:
  }
  }
  </syntaxhighlight>
  </syntaxhighlight>
<br><br>
== ListViewとの連携 ==
==== エントリポイント ====
<syntaxhighlight lang="c++">
#include "CustomListModel.h"  // カスタムリストモデルクラスの定義
// ...略
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    // カスタムリストモデルクラスのインスタンスを生成
    CustomListModel *model = new CustomListModel();
    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("customModel", model);  // カスタムリストモデルクラスの登録
    engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));
    return app.exec();
}
  </syntaxhighlight>
<br>
==== カスタムリストモデルの呼び出し ====
<syntaxhighlight lang="qml">
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
Window {
    id:      root
    visible: true
    width:  800
    height:  600
    title:  "Custom List Example"
    color:  "#ffffff"
    // カスタムリストモデルを使用するメインコンポーネント
    ColumnLayout {
      anchors.fill: parent
      ListView {
          Layout.fillWidth: true
          Layout.fillHeight: true
          model: customModel  // C++側で登録したカスタムリストモデルのインスタンス
          clip: true
          // 各アイテムの表示定義
          delegate: Rectangle {
            width: ListView.view.width
            height: 60
            radius: 5
            color: "#f0f0f0"
            RowLayout {
                anchors.fill: parent
                spacing: 10
                // NameとValueの表示
                Column {
                  Layout.fillWidth: true
                  spacing: 5
                  Text {
                      text: name
                  }
                  Text {
                      text: "Value: " + value
                  }
                }
                // Tagの表示 (カンマ区切り)
                Rectangle {
                  width:  Math.min(tagText.implicitWidth + 20, 150)
                  height:  24
                  radius:  12
                  visible: tags.length > 0
                  Text {
                      id:              tagText
                      text:            tags.join(", ")
                      width:            parent.width - 20
                      elide:            Text.ElideRight  // テキストが指定された幅を超えた場合は省略記号 (...) を表示
                      maximumLineCount: 1
                      anchors.centerIn: parent
                  }
                }
                // 削除ボタン
                Button {
                  text: "x"
                  onClicked: customModel.removeItem(index)
                }
            }
          }
      }
      // 新規アイテムの追加フォーム
      RowLayout {
          Layout.fillWidth: true
          TextField {
            id:              nameInput
            Layout.fillWidth: true
            placeholderText:  "Name"
          }
          TextField {
            id:                    valueInput
            Layout.preferredWidth: 80
            placeholderText:      "Value"
            validator:            IntValidator {}
          }
          TextField {
            id:              tagsInput
            Layout.fillWidth: true
            placeholderText:  "tags (カンマ区切り)"
          }
          Button {
            text: "Add"
            onClicked: {
                if (nameInput.text && valueInput.text) {
                  customModel.appendItem(
                      nameInput.text,
                      parseInt(valueInput.text),
                      tagsInput.text.split(",").map(tag => tag.trim())
                  )
                  nameInput.text  = ""
                  valueInput.text = ""
                  tagsInput.text  = ""
                }
            }
          }
      }
    }
}
</syntaxhighlight>
<br>
==== カスタムリストモデルクラスの定義 ====
<syntaxhighlight lang="c++">
// CustomListModel.h
#include <QAbstractListModel>
#include <QVector>
// カスタムデータ構造の定義
struct CustomData {
    QString    name;
    int        value;
    QStringList tags;  // 配列データ
};
class CustomListModel : public QAbstractListModel
{
    Q_OBJECT
private:
    // モデルのデータを保持するコンテナ
    QVector<CustomData> m_items;
public:
    // モデルで使用するロールの定義
    // QML側からアクセスする時に使用する識別子
    enum CustomRoles {
        NameRole = Qt::UserRole + 1,
        ValueRole,
        TagsRole
    };
    explicit CustomListModel(QObject *parent = nullptr) : QAbstractListModel(parent)
    {}
    // QAbstractListModelの必須オーバーライド関数
    // データの行数を取得する
    int rowCount(const QModelIndex &parent = QModelIndex()) const override
    {
      // 親インデックスが有効な場合は0を返す
      // リストモデルでは子アイテムを持たないため
      if (parent.isValid()) return 0;
      return m_items.size();
    }
    // 指定されたインデックスとロールに対応するデータを取得する
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
    {
      // インデックスの有効性確認
      if (!index.isValid() || index.row() >= m_items.size()) return QVariant();
      // 指定された行のデータを取得
      const CustomData &item = m_items[index.row()];
      // ロールに応じて適切なデータを返す
      switch (role) {
          case NameRole:  return item.name;
          case ValueRole: return item.value;
          case TagsRole:  return QVariant::fromValue(item.tags);
          default:        return QVariant();
      }
    }
    // ロール名をQML側に公開する
    QHash<int, QByteArray> roleNames() const override
    {
      // ロール名とロールIDのマッピングを定義
      // これにより、QML側でロール名を使用してデータにアクセス可能となる
      QHash<int, QByteArray> roles;
      roles[NameRole] = "name";
      roles[ValueRole] = "value";
      roles[TagsRole] = "tags";
      return roles;
    }
    // データ操作用メソッド
    // QML側から呼び出し可能なメソッド
    Q_INVOKABLE void appendItem(const QString &name, int value, const QStringList &tags)
    {
      // データ追加開始を通知
      beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
      // 新しいアイテムを追加
      CustomData item{name, value, tags};
      m_items.append(item);
      // データ追加完了を通知
      endInsertRows();
    }
    Q_INVOKABLE void removeItem(int index)
    {
      // インデックスの有効性確認
      if (index < 0 || index >= m_items.size()) return;
      // データ削除開始を通知
      beginRemoveRows(QModelIndex(), index, index);
      // アイテムを削除
      m_items.removeAt(index);
      // データ削除完了を通知
      endRemoveRows();
    }
    Q_INVOKABLE void clearItems()
    {
      // データが存在しない場合
      if (m_items.isEmpty()) return;
      // データ削除開始を通知
      beginRemoveRows(QModelIndex(), 0, m_items.size() - 1);
      // 全てのアイテムを削除
      m_items.clear();
      // データ削除完了を通知
      endRemoveRows();
    }
    Q_INVOKABLE void updateItem(int index, const QString &name, int value, const QStringList &tags)
    {
      // インデックスの有効性確認
      if (index < 0 || index >= m_items.size()) return;
      // アイテムを更新
      CustomData &item = m_items[index];
      item.name  = name;
      item.value = value;
      item.tags  = tags;
      // データ更新を通知
      // この通知により、QMLビューが自動的に更新される
      emit dataChanged(createIndex(index, 0), createIndex(index, 0));
    }
};
</syntaxhighlight>
<br>
<br><br>
<br><br>



2024年11月27日 (水) 23:13時点における最新版

概要

カスタムリストモデルの目的は、QMLのビューコンポーネントに対して柔軟なデータ構造を提供することである。
これは、ListView、GridView、TableView等のビューコンポーネントと組み合わせて使用される。

通常のListModelは、単純なKey-Value形式のプロパティのみをサポートしており、配列やネストされたオブジェクトを直接持つことができない。
これはListModelの大きな制限の1つとなっており、例えば、アイテムごとにタグのリストやサブアイテムを持ちたい場合、ListModelでは実現が困難である。

基本的な仕組みとして、カスタムリストモデルはC++側で定義してQAbstractListModelクラスを継承する。
このモデルはデータの保持と管理を担当しており、QMLインターフェースからアクセス可能なプロパティやメソッドを提供する。
特に、配列やネストされたオブジェクト等の複雑なデータ構造もサポートすることが可能となる。

モデルの重要な要素としてロールという概念があり、これは、各アイテムが持つプロパティを定義するものである。
例えば、連絡先リストのモデルの場合、名前、電話番号、メールアドレス、関連する連絡先の配列等をロールとして定義できる。
これらのロールは、QML側からアイテムデリゲートからアクセスすることができる。

データの更新と同期において、モデルのデータが変更された場合、適切なシグナルを発行してQMLビューに通知する必要がある。
これにより、ビューは自動的に更新されて最新のデータが表示できる。

パフォーマンスにおいては、大量のデータや複雑なデータ構造を扱う場合に効率的な実装が必要となる。
必要なデータのみをロードする遅延ローディング、表示される項目のみを処理するビューポートベースの最適化等を実装する。

さらに、モデルはソート、フィルタリング、データの追加・削除等の操作もサポートできる。 これらの操作は、QSortFilterProxyModelクラスを使用して定義することも可能である。
QSortFilterProxyModelクラスを使用することにより、複雑なデータ構造に対して、カスタムな並べ替えやフィルタリングのロジックが実装できる。

モデルとビューの分離という設計パターンにより、データの管理とその表示を明確に分けることができるため、コードの保守性と再利用性が向上する。
これは、Qt Quick / QMLアプリケーションの重要な設計原則の1つである。


カスタムリストモデルの定義

QAbstractListModelクラスの継承

カスタムリストモデルは、QAbstractListModelクラスを継承する必要がある。
このクラスは、データの格納、アクセス、変更のための機能を提供する。

 #include <QAbstractListModel>
 
 class <クラス名> : public QAbstractListModel
 {
    Q_OBJECT
 
 public:
    explicit CustomListModel(QObject *parent = nullptr) : QAbstractListModel(parent)
    {}
 
    // ...略
 };
 
 // 例:
 class CustomListModel : public QAbstractListModel
 {
    Q_OBJECT
 
 public:
    explicit CustomListModel(QObject *parent = nullptr) : QAbstractListModel(parent)
    {}
 
    // ...略
 };


モデルのデータ構造

データ構造の定義は、構造体を使用して各アイテムが持つデータを定義する。

以下の例では、名前、値、タグリストを定義している。

 // カスタムデータ構造の定義
 
 struct <構造体名> {
    // ...略
 };
 
 // 例: 配列を含むデータ構造
 // デフォルトのListModelではデータに配列を持つことができないため
 struct CustomData {
    QString name;
    int value;
    QStringList tags;  // 配列データ
 };


モデルのロール

列挙体を使用して、QML側からアクセスするためのロールを定義する。
これは、Qt::UserRoleから開始する一意の値を持つ。

以下の例では、CustomRoles列挙体を定義している。

 // モデルで使用するロールの定義
 // QML側からアクセスする場合に使用する識別子となる
 
 enum CustomRoles {
    // ...略
 };
 
 // 例: 3つのデータを持つカスタムリストモデルのロール
 enum CustomRoles {
    NameRole = Qt::UserRole + 1,
    ValueRole,
    TagsRole
 };


モデルのデータ保持

 // モデルのデータを保持するコンテナ
 // 変数名は任意
 
 #include <QVector>
 
 QVector<カスタムデータの構造体> 変数名;
 
 // 例: 上記の例のCustomData構造体の場合
 class CustomListModel : public QAbstractListModel
 {
    Q_OBJECT
 
 private:
    QVector<CustomData> m_items;
 
    // ...略
 };


オーバーライドが必須のメソッド

rowCountメソッド
 // QAbstractListModelクラスのオーバーライドが必須のメソッド
 // データの行数を取得する
 
 int rowCount(const QModelIndex &parent = QModelIndex()) const override
 
 // 例: 上記の例のCustomData構造体の場合
 int rowCount(const QModelIndex &parent = QModelIndex()) const override
 
 int CustomListModel::rowCount(const QModelIndex &parent = QModelIndex()) const
 {
    // 親インデックスが有効な場合は0を返す
    // リストモデルでは子アイテムを持たないため
    if (parent.isValid()) return 0;
    return m_items.size();
 }


dataメソッド
 // QAbstractListModelクラスのオーバーライドが必須のメソッド
 // 指定されたインデックスとロールに対応するデータを取得する
 
 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
 
 // 例: 上記の例のCustomData構造体の場合
 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
 
 QVariant CustomListModel::data(const QModelIndex &index, int role = Qt::DisplayRole) const
 {
    // インデックスの有効性確認
    if (!index.isValid() || index.row() >= m_items.size()) return QVariant();
 
    // 指定された行のデータを取得
    const CustomData &item = m_items[index.row()];
 
    // ロールに応じて適切なデータを返す
    switch (role) {
       case NameRole:  return item.name;
       case ValueRole: return item.value;
       case TagsRole:  return QVariant::fromValue(item.tags);
       default:        return QVariant();
    }
 }


roleNamesメソッド

roleNamesメソッドは必ずオーバーライドする必要がある。
このメソッドは、QML側でアクセスするプロパティ名を定義するものである。

 // QAbstractListModelクラスのオーバーライドが必須のメソッド
 // QML側で使用するロール名を定義する
 
 QHash<int, QByteArray> roleNames() const override
 
 // 例: 上記の例のCustomData構造体の場合
 QHash<int, QByteArray> roleNames() const override
 
 QHash<int, QByteArray> CustomListModel::roleNames() const
 {
    // ロール名とロールIDのマッピングを定義
    // これにより、QML側でロール名を使用してデータにアクセス可能となる
    QHash<int, QByteArray> roles;
    roles[NameRole]  = "name";
    roles[ValueRole] = "value";
    roles[TagsRole]  = "tags";
    return roles;
 }


データ操作メソッド

appendItemメソッド

新しいアイテムを追加する。

 // QML側から呼び出し可能なメソッドとして使用する
 
 Q_INVOKABLE void appendItem(<カスタムデータ構造の変数 1>, <カスタムデータ構造の変数 2>, <カスタムデータ構造の変数 3>, ...);
 
 // 例: 上記の例のCustomData構造体の場合
 Q_INVOKABLE void appendItem(const QString &name, int value, const QStringList &tags);
 
 void CustomListModel::appendItem(const QString &name, int value, const QStringList &tags)
 {
    // データ追加開始を通知
    beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
 
    // 新しいアイテムを追加
    CustomData item{name, value, tags};
    m_items.append(item);
 
    // データ追加完了を通知
    endInsertRows();
 }


removeItemメソッド

指定したインデックスのアイテムを削除する。

 // QML側から呼び出し可能なメソッドとして使用する
 
 Q_INVOKABLE void removeItem(int index);
 
 // 例: 上記の例のCustomData構造体の場合
 void CustomListModel::removeItem(int index)
 {
    // インデックスの有効性確認
    if (index < 0 || index >= m_items.size()) return;
 
    // データ削除開始を通知
    beginRemoveRows(QModelIndex(), index, index);
 
    // アイテムを削除
    m_items.removeAt(index);
 
    // データ削除完了を通知
    endRemoveRows();
 }


clearItemsメソッド

全てのアイテムを削除する。

 // QML側から呼び出し可能なメソッドとして使用する
 
 Q_INVOKABLE void clearItems();
 
 // 例: 上記の例のCustomData構造体の場合
 void CustomListModel::clearItems()
 {
    // データが存在しない場合
    if (m_items.isEmpty()) return;
 
    // データ削除開始を通知
    beginRemoveRows(QModelIndex(), 0, m_items.size() - 1);
 
    // 全てのアイテムを削除
    m_items.clear();
 
    // データ削除完了を通知
    endRemoveRows();
 }


updateItemメソッド

既存のアイテムを更新する。

 // QML側から呼び出し可能なメソッドとして使用する
 
 Q_INVOKABLE void updateItem(int index, <カスタムデータ構造の変数 1>, <カスタムデータ構造の変数 2>, <カスタムデータ構造の変数 3>, ...);
 
 // 例: 上記の例のCustomData構造体の場合
 Q_INVOKABLE void updateItem(int index, const QString &name, int value, const QStringList &tags);
 
 void CustomListModel::updateItem(int index, const QString &name, int value, const QStringList &tags)
 {
    // インデックスの有効性確認
    if (index < 0 || index >= m_items.size()) return;
 
    // アイテムを更新
    CustomData &item = m_items[index];
    item.name = name;
    item.value = value;
    item.tags = tags;
 
    // データ更新を通知
    // このシグナルにより、QMLのビューが自動的に更新される
    emit dataChanged(createIndex(index, 0), createIndex(index, 0));
 }



ListViewとの連携

エントリポイント

 #include "CustomListModel.h"  // カスタムリストモデルクラスの定義
 
 // ...略
 
 int main(int argc, char *argv[])
 {
    QGuiApplication app(argc, argv);
 
    // カスタムリストモデルクラスのインスタンスを生成
    CustomListModel *model = new CustomListModel();
 
    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("customModel", model);  // カスタムリストモデルクラスの登録
    engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));
 
    return app.exec();
 }


カスタムリストモデルの呼び出し

 import QtQuick
 import QtQuick.Controls
 import QtQuick.Layouts
 import QtQuick.Window
 
 Window {
    id:      root
    visible: true
    width:   800
    height:  600
    title:   "Custom List Example"
    color:   "#ffffff"
 
    // カスタムリストモデルを使用するメインコンポーネント
    ColumnLayout {
       anchors.fill: parent
 
       ListView {
          Layout.fillWidth: true
          Layout.fillHeight: true
          model: customModel  // C++側で登録したカスタムリストモデルのインスタンス
          clip: true
 
          // 各アイテムの表示定義
          delegate: Rectangle {
             width: ListView.view.width
             height: 60
             radius: 5
             color: "#f0f0f0"
 
             RowLayout {
                anchors.fill: parent
                spacing: 10
 
                // NameとValueの表示
                Column {
                   Layout.fillWidth: true
                   spacing: 5
 
                   Text {
                      text: name
                   }
 
                   Text {
                      text: "Value: " + value
                   }
                }
 
                // Tagの表示 (カンマ区切り)
                Rectangle {
                   width:   Math.min(tagText.implicitWidth + 20, 150)
                   height:  24
                   radius:  12
                   visible: tags.length > 0
 
                   Text {
                      id:               tagText
                      text:             tags.join(", ")
                      width:            parent.width - 20
                      elide:            Text.ElideRight  // テキストが指定された幅を超えた場合は省略記号 (...) を表示
                      maximumLineCount: 1
                      anchors.centerIn: parent
                   }
                }
 
                // 削除ボタン
                Button {
                   text: "x"
                   onClicked: customModel.removeItem(index)
                }
             }
          }
       }
 
       // 新規アイテムの追加フォーム
       RowLayout {
          Layout.fillWidth: true
 
          TextField {
             id:               nameInput
             Layout.fillWidth: true
             placeholderText:  "Name"
          }
 
          TextField {
             id:                    valueInput
             Layout.preferredWidth: 80
             placeholderText:       "Value"
             validator:             IntValidator {}
          }
 
          TextField {
             id:               tagsInput
             Layout.fillWidth: true
             placeholderText:  "tags (カンマ区切り)"
          }
 
          Button {
             text: "Add"
             onClicked: {
                if (nameInput.text && valueInput.text) {
                   customModel.appendItem(
                      nameInput.text,
                      parseInt(valueInput.text),
                      tagsInput.text.split(",").map(tag => tag.trim())
                   )
                   nameInput.text  = ""
                   valueInput.text = ""
                   tagsInput.text  = ""
                }
             }
          }
       }
    }
 }


カスタムリストモデルクラスの定義

 // CustomListModel.h
 
 #include <QAbstractListModel>
 #include <QVector>
 
 // カスタムデータ構造の定義
 struct CustomData {
    QString     name;
    int         value;
    QStringList tags;  // 配列データ
 };
 
 class CustomListModel : public QAbstractListModel
 {
    Q_OBJECT
 
 private:
    // モデルのデータを保持するコンテナ
    QVector<CustomData> m_items;
 
 public:
    // モデルで使用するロールの定義
    // QML側からアクセスする時に使用する識別子
    enum CustomRoles {
        NameRole = Qt::UserRole + 1,
        ValueRole,
        TagsRole
    };
 
    explicit CustomListModel(QObject *parent = nullptr) : QAbstractListModel(parent)
    {}
 
    // QAbstractListModelの必須オーバーライド関数
    // データの行数を取得する
    int rowCount(const QModelIndex &parent = QModelIndex()) const override
    {
       // 親インデックスが有効な場合は0を返す
       // リストモデルでは子アイテムを持たないため
       if (parent.isValid()) return 0;
       return m_items.size();
    }
 
    // 指定されたインデックスとロールに対応するデータを取得する
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
    {
       // インデックスの有効性確認
       if (!index.isValid() || index.row() >= m_items.size()) return QVariant();
 
       // 指定された行のデータを取得
       const CustomData &item = m_items[index.row()];
 
       // ロールに応じて適切なデータを返す
       switch (role) {
          case NameRole:  return item.name;
          case ValueRole: return item.value;
          case TagsRole:  return QVariant::fromValue(item.tags);
          default:        return QVariant();
       }
    }
 
    // ロール名をQML側に公開する
    QHash<int, QByteArray> roleNames() const override
    {
       // ロール名とロールIDのマッピングを定義
       // これにより、QML側でロール名を使用してデータにアクセス可能となる
       QHash<int, QByteArray> roles;
       roles[NameRole] = "name";
       roles[ValueRole] = "value";
       roles[TagsRole] = "tags";
       return roles;
    }
 
    // データ操作用メソッド
    // QML側から呼び出し可能なメソッド
    Q_INVOKABLE void appendItem(const QString &name, int value, const QStringList &tags)
    {
       // データ追加開始を通知
       beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
 
       // 新しいアイテムを追加
       CustomData item{name, value, tags};
       m_items.append(item);
 
       // データ追加完了を通知
       endInsertRows();
    }
 
    Q_INVOKABLE void removeItem(int index)
    {
       // インデックスの有効性確認
       if (index < 0 || index >= m_items.size()) return;
 
       // データ削除開始を通知
       beginRemoveRows(QModelIndex(), index, index);
 
       // アイテムを削除
       m_items.removeAt(index);
 
       // データ削除完了を通知
       endRemoveRows();
    }
 
    Q_INVOKABLE void clearItems()
    {
       // データが存在しない場合
       if (m_items.isEmpty()) return;
 
       // データ削除開始を通知
       beginRemoveRows(QModelIndex(), 0, m_items.size() - 1);
 
       // 全てのアイテムを削除
       m_items.clear();
 
       // データ削除完了を通知
       endRemoveRows();
    }
 
    Q_INVOKABLE void updateItem(int index, const QString &name, int value, const QStringList &tags)
    {
       // インデックスの有効性確認
       if (index < 0 || index >= m_items.size()) return;
 
       // アイテムを更新
       CustomData &item = m_items[index];
       item.name  = name;
       item.value = value;
       item.tags  = tags;
 
       // データ更新を通知
       // この通知により、QMLビューが自動的に更新される
       emit dataChanged(createIndex(index, 0), createIndex(index, 0));
    }
 };