Qt Quick 表格视图示例 - 康威生命游戏

《康威生命游戏》示例显示了如何使用 QML TableView 类型来显示用户可以浏览的 C++ 模型。

运行示例

要从 Qt Creator 运行示例,请打开 欢迎 模式并从 示例 中选择示例。如需更多信息,请访问 构建和运行示例

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;

应用程序启动时,使用 TableViewcontentXcontentY 属性将 TableView 居中,以更新滚动位置,并使用 contentWidthcontentHeight 计算视图应滚动到何处。

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_ELEMENT 宏以便将类公开给 QML。

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;
}

在此,我们实现了 rowCountcolumnCount 方法,以便 TableView 组件可以知道表的大小。它简单地返回 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 组件从模型请求数据时,将调用此方法。在我们的示例中,每个单元格只有一条数据:是否活着。此信息由我们 C++ 代码中 Roles 枚举的 CellRole 值表示;这与 QML 代码中的 value 属性相对应(这两者之间的联系是通过我们 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;
}

当从 QML 接口设置属性值时调用 setData 方法:在我们的示例中,当我们点击单元格时,它会切换单元格的状态。与 data() 函数类似,此方法接收一个 index 和一个 role 参数。此外,新值作为 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});
}

此函数可以直接从 QML 代码中调用,因为它在其定义中包含 Q_INVOKABLE 宏。它会在用户点击 下一步 按钮或计时器发出 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});
}

当应用程序打开时,会加载一个模式以演示 康威生命游戏 的工作方式。这两个函数加载存储模式的文件并解析它。和 nextStep 函数一样,一旦模式完全加载,就会对整个表格发出一个 dataChanged 信号。

示例项目 @ code.qt.io

© 2024 Qt 公司有限公司。本文档中的文档贡献的版权属于其各自的所有者。提供的文档是根据由自由软件基金会发布并于 GNU 自由文档许可版本 1.3 的条款授予的。Qt 和相应的徽标是芬兰和/或世界上其他国家的 Qt 公司有限公司的商标。所有其他商标都是其各自所有者的财产。