Qt Quick TableView の例 - Conway's Game of Life

Conway's Game of Life の例では、QMLTableView タイプを使用して、C++ のモデルを表示し、ユーザがパンする方法を示しています。

例の実行

Qt Creator からサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳細については、Building and Running an Example を参照してください。

QML ユーザーインターフェース

TableView {
    id: tableView
    anchors.fill: parent

    rowSpacing: 1
    columnSpacing: 1

    ScrollBar.horizontal: ScrollBar {}
    ScrollBar.vertical: ScrollBar {}

    delegate: Rectangle {
        id: cell
        implicitWidth: 15
        implicitHeight: 15

        required property var model
        required property bool value

        color: value ? "#f3f3f4" : "#b5b7bf"

        MouseArea {
            anchors.fill: parent
            onClicked: parent.model.value = !parent.value
        }
    }

この例では、TableView コンポーネントを使用して、セルのグリッドを表示します。各セルはTableViewのデリゲート(Rectangle QML コンポーネント)によって画面に描画されます。ユーザーがセルをクリックすると、model.value

contentX: (contentWidth - width) / 2;
contentY: (contentHeight - height) / 2;

アプリケーションが起動すると、contentXcontentY プロパティを使ってスクロール位置を更新し、contentWidthcontentHeight を使ってビューのスクロール位置を計算することで、TableView が中央にスクロールされます。

model: GameOfLifeModel {
    id: gameOfLifeModel
}

C++モデル

class GameOfLifeModel : public QAbstractTableModel
{
    Q_OBJECT
    QML_ELEMENT

    Q_ENUMS(Roles)
public:
    enum Roles {
        CellRole
    };

    QHash<int, QByteArray> roleNames() const override {
        return {
            { CellRole, "value" }
        };
    }

    explicit GameOfLifeModel(QObject *parent = nullptr);

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role = Qt::EditRole) override;

    Qt::ItemFlags flags(const QModelIndex &index) const override;

    Q_INVOKABLE void nextStep();
    Q_INVOKABLE bool loadFile(const QString &fileName);
    Q_INVOKABLE void loadPattern(const QString &plainText);
    Q_INVOKABLE void clear();

private:
    static constexpr int width = 256;
    static constexpr int height = 256;
    static constexpr int size = width * height;

    using StateContainer = std::array<bool, size>;
    StateContainer m_currentState;

    int cellNeighborsCount(const QPoint &cellCoordinates) const;
    static bool areCellCoordinatesValid(const QPoint &coordinates);
    static QPoint cellCoordinatesFromIndex(int cellIndex);
    static std::size_t cellIndex(const QPoint &coordinates);
};

GameOfLifeModel クラスはQAbstractTableModel を継承しているので、TableView コンポーネントのモデルとして使用できます。そのため、TableView コンポーネントがモデルとやりとりできるように、いくつかの関数を実装する必要があります。クラスのprivate の部分を見ればわかるように、モデルは固定サイズの配列を使ってすべてのセルの現在の状態を保存します。また、クラスをQMLに公開するために、QML_ELEMENT マクロを使っています。

int GameOfLifeModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;

    return height;
}

int GameOfLifeModel::columnCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;

    return width;
}

ここでは、TableView コンポーネントがテーブルのサイズを知ることができるように、rowCountcolumnCount メソッドが実装されています。このメソッドは単にwidthheight 定数の値を返すだけです。

QVariant GameOfLifeModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || role != CellRole)
        return QVariant();

    return QVariant(m_currentState[cellIndex({index.column(), index.row()})]);
}

このメソッドは、TableView コンポーネントがモデルからデータを要求するときに呼び出されます。この例では、セルが生きているかどうかという1つのデータしか持っていません。この情報はC++のコードではRoles enumのCellRole 値で表現されます。これはQMLのコードではvalue プロパティに対応します(この2つの間のリンクはC++クラスのroleNames() 関数で行われます)。

GameOfLifeModel クラスは、index パラメータによって、要求されたデータがどのセルからのものかを特定することができます。 パラメータは、行と列を含むQModelIndex です。

データの更新

bool GameOfLifeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role != CellRole || data(index, role) == value)
        return false;

    m_currentState[cellIndex({index.column(), index.row()})] = value.toBool();
    emit dataChanged(index, index, {role});

    return true;
}

setData メソッドは、QML インターフェースからプロパティの値が設定されたときに呼び出されます。この例では、セルがクリックされたときにセルの状態をトグルします。data() 関数と同じように、このメソッドはindexrole のパラメータを受け取ります。さらに、新しい値はQVariant として渡され、toBool 関数を使ってブール値に変換します。

モデル・オブジェクトの内部状態を更新するときには、dataChanged シグナルを発信して、TableView コンポーネントに表示データを更新する必要があることを伝える必要があります。この場合、クリックされたセルだけが影響を受けるので、更新される表の範囲はセルのインデックスで始まり、セルで終わります。

void GameOfLifeModel::nextStep()
{
    StateContainer newValues;

    for (std::size_t i = 0; i < size; ++i) {
        bool currentState = m_currentState[i];

        int cellNeighborsCount = this->cellNeighborsCount(cellCoordinatesFromIndex(static_cast<int>(i)));

        newValues[i] = currentState == true
                ? cellNeighborsCount == 2 || cellNeighborsCount == 3
                : cellNeighborsCount == 3;
    }

    m_currentState = std::move(newValues);

    emit dataChanged(index(0, 0), index(height - 1, width - 1), {CellRole});
}

この関数は、Q_INVOKABLE マクロを定義に含んでいるので、QML コードから直接呼び出すことができます。この関数は、ユーザーが「次へ」ボタンをクリックするか、タイマーがtriggered() シグナルを発したときに、ゲームを繰り返し実行します。

コンウェイのゲーム・オブ・ライフのルールに従って、各セルは隣のセルの現在の状態に応じて新しい状態を計算する。グリッド全体について新しい状態が計算されると、それは現在の状態を置き換え、テーブル全体についてdataChangedシグナルが発せられます。

bool GameOfLifeModel::loadFile(const QString &fileName)
{
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly))
        return false;

    QTextStream in(&file);
    loadPattern(in.readAll());

    return true;
}

void GameOfLifeModel::loadPattern(const QString &plainText)
{
    clear();

    QStringList rows = plainText.split("\n");
    QSize patternSize(0, rows.count());
    for (QString row : rows) {
        if (row.size() > patternSize.width())
            patternSize.setWidth(row.size());
    }

    QPoint patternLocation((width - patternSize.width()) / 2, (height - patternSize.height()) / 2);

    for (int y = 0; y < patternSize.height(); ++y) {
        const QString line = rows[y];

        for (int x = 0; x < line.length(); ++x) {
            QPoint cellPosition(x + patternLocation.x(), y + patternLocation.y());
            m_currentState[cellIndex(cellPosition)] = line[x] == 'O';
        }
    }

    emit dataChanged(index(0, 0), index(height - 1, width - 1), {CellRole});
}

アプリケーションを開くと、Conway's Game of Lifeがどのように動作するかを示すパターンがロードされます。これらの2つの関数は、パターンが保存されているファイルをロードし、それを解析します。nextStep 関数と同様に、パターンが完全にロードされると、テーブル全体に対してdataChanged シグナルが発せられます。

プロジェクト例 @ code.qt.io

本書に含まれる文書の著作権は、それぞれの所有者に帰属します 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。