警告

本节包含从 C++ 自动翻译到 Python 的代码片段,可能包含错误。

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

“康威生命游戏”示例展示了如何使用 QML TableView 类型来显示用户可以滚动的 C++ 模型。

../_images/gameoflife.png

运行示例#

要从 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 属性来更新滚动位置,并且使用 contentWidthcontentHeight 来计算视图应该滚动到的地方。

model: GameOfLifeModel {
    id: gameOfLifeModel
}

C++ 模型#

class GameOfLifeModel(QAbstractTableModel):

    Q_OBJECT
    QML_ELEMENT
    Q_ENUMS(Roles)
# public
    Roles = {
        CellRole

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


    GameOfLifeModel = explicit(QObject parent = None)
    int rowCount(QModelIndex parent = QModelIndex()) override
    int columnCount(QModelIndex parent = QModelIndex()) override
    QVariant data(QModelIndex index, int role = Qt.DisplayRole) override
    bool setData(QModelIndex index, QVariant value,
                 role = Qt.EditRole) override()
    Qt.ItemFlags flags(QModelIndex index) override
    Q_INVOKABLE void nextStep()
    Q_INVOKABLE bool loadFile(QString fileName)
    Q_INVOKABLE void loadPattern(QString plainText)
    Q_INVOKABLE void clear()
# private
    staticexpr int width = 256
    staticexpr int height = 256
    staticexpr int size = width * height
    StateContainer = std::array<bool, size>
    m_currentState = StateContainer()
    cellNeighborsCount = int(QPoint cellCoordinates)
    areCellCoordinatesValid = bool(QPoint coordinates)
    cellCoordinatesFromIndex = QPoint(int cellIndex)
    std.size_t cellIndex(QPoint coordinates)

GameOfLifeModel 类扩展了 QAbstractTableModel,因此它可以作为我们的 TableView 组件的模型使用。因此,它需要实现一些函数,使 TableView 组件能够与模型交互。如您在类的 private 部分所见,模型使用一个固定大小的数组来存储所有单元格的当前状态。我们还将 QML_ELEMENT 宏用于将类公开给 QML。

def rowCount(self, QModelIndex parent):

    if parent.isValid():
        return 0
    return height

def columnCount(self, QModelIndex parent):

    if parent.isValid():
        return 0
    return width

在这里,实现了 rowCountcolumnCount 方法,这样 TableView 组件就可以知道表格的大小。它只是返回 widthheight 常量的值。

def data(self, QModelIndex index, int role):

    if not index.isValid() or role not = CellRole:
        return QVariant()
    return QVariant(m_currentState[cellIndex({index.column(), index.row()})])

该方法在TableView组件从模型请求一些数据时被调用。在我们的示例中,每个单元格只有一条数据:是否存活。这个信息由我们在C++代码中Role枚举的CellRole值来表示;这对应于QML代码中的value属性(这两者之间的链接是由我们的C++类中的roleNames()函数创建的)。

GameOfLifeModel类可以通过index参数识别请求数据的单元格是哪一个,index是一个QModelIndex类型,它包含行和列。

数据更新

def setData(self, QModelIndex index, QVariant value, int role):

    if role != CellRole or data(index, role) == value:
        return False
    m_currentState[cellIndex({index.column(), index.row()})] = value.toBool()
    dataChanged.emit(index, index, {role})
    return True

当通过QML界面设置属性值时,会调用setData方法:在我们的示例中,当点击单元格时,它会切换单元格的状态。与data()函数一样,这个方法接收一个index和一个role参数。此外,新值作为QVariant传递,我们使用toBool函数将其转换为布尔值。

当我们更新模型对象的内部状态时,我们需要发出dataChanged信号来告知TableView组件需要更新显示的数据。在这种情况下,只有被点击的单元格受到影响,因此需要更新的表格范围从单元格的索引开始和结束。

def nextStep(self):

    newValues = StateContainer()
    for i in range(0, size):
        currentState = m_currentState[i]
        cellNeighborsCount = self.cellNeighborsCount(cellCoordinatesFromIndex(int(i)))
        newValues[i] = currentState == True
                ? cellNeighborsCount == 2 or cellNeighborsCount == 3


    m_currentState = std.move(newValues)
    dataChanged.emit(index(0, 0), index(height - 1, width - 1), {CellRole})

由于它在定义中含有Q_INVOKABLE宏,该函数可以直接从QML代码中调用。它执行游戏的迭代,无论是用户点击Next按钮还是计时器发出triggered()信号。

根据康威生命游戏规则,根据每个单元格的邻居当前状态计算其新状态。当整个网格的新状态计算完成后,它将替换当前状态,并且为整个表格发出一个dataChanged信号。

def loadFile(self, QString fileName):

    file = QFile(fileName)
    if not file.open(QIODevice.ReadOnly):
        return False
    in = QTextStream(file)
    loadPattern(in.readAll())
    return True

def loadPattern(self, plainText):

    clear()
    rows = plainText.split("\n")
    patternSize = QSize(0, rows.count())
    for row in rows:
        if row.size() > patternSize.width():
            patternSize.setWidth(row.size())

    patternLocation = QPoint((width - patternSize.width()) / 2, (height - patternSize.height()) / 2)
    for y in range(0, patternSize.height()):
        line = rows[y]
        for x in range(0, line.length()):
            cellPosition = QPoint(x + patternLocation.x(), y + patternLocation.y())
            m_currentState[cellIndex(cellPosition)] = line[x] == 'O'


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

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

示例项目 @ code.qt.io