Qt Quick TableView 示例 - 康威的生命游戏
Conway's Game of Life示例展示了如何使用 QMLTableView 类型来显示用户可随意移动的 C++ 模型。
运行示例
要从 Qt Creator,打开Welcome 模式,并从Examples 中选择示例。更多信息,请参阅Qt Creator: Tutorial:构建并运行。
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的委托(即矩形 QML 组件)绘制在屏幕上。我们读取单元格的值,并在用户点击时使用model.value
更改它。
contentX: (contentWidth - width) / 2; contentY: (contentHeight - height) / 2;
应用程序启动时,TableView 会滚动到中心位置,使用contentX
和contentY
属性更新滚动位置,使用contentWidth
和contentHeight
计算视图应滚动到的位置。
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; }
在这里,我们实现了rowCount
和columnCount
方法,这样TableView 组件就能知道表格的大小。它只需返回width
和height
常量的值。
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}); }
应用程序打开时,会加载一个模式来演示Conway's Game of Life 的工作原理。这两个函数会加载存储模式的文件并对其进行解析。与nextStep
函数一样,一旦图案完全加载完毕,整个表格就会发出dataChanged
信号。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.