Qt Quick TableView examples - Conway’s Game of Life¶
The Conway’s Game of Life example shows how the QML TableView type can be used to display a C++ model that the user can pan around.
Running the Example¶
To run the example from Qt Creator , open the Welcome mode and select the example from Examples. For more information, visit Building and Running an Example.
The QML User Interface¶
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 } }
The example uses the TableView component to display a grid of cells. Each of these cells is drawn on the screen by the TableView ’s delegate, which is a Rectangle QML component. We read the cell’s value and we change it using model.value
when the user clicks it.
contentX: (contentWidth - width) / 2; contentY: (contentHeight - height) / 2;
When the application starts, the TableView is scrolled to its center by using its contentX
and contentY
properties to update the scroll position, and the contentWidth
and contentHeight
to compute where the view should be scrolled to.
model: GameOfLifeModel { id: gameOfLifeModel }
The C++ Model¶
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) rowCount = int(QModelIndex parent = QModelIndex()) columnCount = int(QModelIndex parent = QModelIndex()) data = QVariant(QModelIndex index, int role = Qt.DisplayRole) setData(QModelIndex = bool() 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)
The GameOfLifeModel
class extends QAbstractTableModel
so it can be used as the model of our TableView component. Therefore, it needs to implement some functions so the TableView component can interact with the model. As you can see in the private
part of the class, the model uses a fixed-size array to store the current state of all the cells. We also use the QML_ELEMENT
macro in order to expose the class to 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
Here, the rowCount
and columnCount
methods are implemented so the TableView component can know the size of the table. It simply returns the values of the width
and height
constants.
def data(self, QModelIndex index, int role): if (not index.isValid() or role not = CellRole) def QVariant(): def QVariant(m_currentState[cellIndex({index.column(),index.row()})]):
This method is called when the TableView component requests some data from the model. In our example, we only have one piece of data by cell: whether it is alive or not. This information is represented by the CellRole
value of the Roles
enum in our C++ code; this corresponds to the value
property in the QML code (the link between these two is made by the roleNames()
function of our C++ class).
The GameOfLifeModel
class can identify which cell was the data requested from with the index
parameter, which is a QModelIndex
that contains a row and a column.
Updating the Data¶
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
The setData
method is called when a property’s value is set from the QML interface: in our example, it toggles a cell’s state when it is clicked. In the same way as the data()
function does, this method receives an index
and a role
parameter. Additionally, the new value is passed as a QVariant
, that we convert to a boolean using the toBool
function.
When we update the internal state of our model object, we need to emit a dataChanged
signal to tell the TableView component that it needs to update the displayed data. In this case, only the cell that was clicked is affected, thus the range of the table that has to be updated begins and ends at the cell’s index.
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})
This function can be called directly from the QML code, because it contains the Q_INVOKABLE
macro in its definition. It plays an iteration of the game, either when the user clicks the Next button or when the Timer emits a triggered()
signal.
Following the Conway’s Game of Life rules, a new state is computed for each cell depending on the current state of its neighbors. When the new state has been computed for the whole grid, it replaces the current state and a dataChanged signal is emitted for the whole table.
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})
When the application opens, a pattern is loaded to demonstrate how Conway’s Game of Life works. These two functions load the file where the pattern is stored and parse it. As in the nextStep
function, a dataChanged
signal is emitted for the whole table once the pattern has been fully loaded.
© 2022 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.