Qt Quick TableView 예제 - Conway의 인생 게임

Conway's Game of Life 예제에서는 QML TableView 유형을 사용하여 사용자가 이동할 수 있는 C++ 모델을 표시하는 방법을 보여 줍니다.

예제 실행하기

에서 예제를 실행하려면 Qt Creator에서 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 예제 빌드 및 실행하기를 참조하세요.

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 컴포넌트를 사용하여 셀 그리드를 표시합니다. 이러한 각 셀은 직사각형 QML 컴포넌트인 TableView의 델리게이트에 의해 화면에 그려집니다. 셀의 값을 읽고 사용자가 클릭하면 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;
}

여기에서는 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;
}

setData 메서드는 QML 인터페이스에서 속성 값을 설정할 때 호출되며, 이 예제에서는 셀을 클릭했을 때 셀의 상태를 전환합니다. data() 함수와 동일한 방식으로 이 메서드는 indexrole 매개 변수를 받습니다. 또한 새 값은 toBool 함수를 사용하여 부울로 변환하는 QVariant 로 전달됩니다.

모델 객체의 내부 상태를 업데이트할 때 TableView 컴포넌트에 표시된 데이터를 업데이트해야 한다는 것을 알리기 위해 dataChanged 신호를 보내야 합니다. 이 경우 클릭한 셀만 영향을 받으므로 업데이트해야 하는 테이블의 범위는 셀의 인덱스에서 시작하여 셀의 인덱스에서 끝납니다.

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() 신호를 보낼 때 게임의 반복을 재생합니다.

콘웨이의 게임 오브 라이프 규칙에 따라 이웃 셀의 현재 상태에 따라 각 셀에 대해 새로운 상태가 계산됩니다. 전체 그리드에 대해 새 상태가 계산되면 현재 상태를 대체하고 전체 테이블에 대해 데이터 변경 신호가 방출됩니다.

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

© 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.