使用QAbstractItemModel将2D c++游戏板暴露给QML

Expose 2D C++ Game Board to QML using QAbstractItemModel

本文关键字:暴露 QML 游戏 c++ QAbstractItemModel 2D 使用      更新时间:2023-10-16

我正在用c++编写一个简单的贪吃蛇游戏,游戏板模型包含一个二维状态向量(std::vector<std::vector<board::state>>)。现在我想把这个棋盘暴露给QML,这样它基本上就是某种网格/棋盘,可以访问模型的状态。我已经阅读了很多关于这一主题的内容,但仍然无法理解其机制并解决我的问题。将文档、教程和博客条目应用到我的问题中是我的障碍。

我将QAbstractItemModel子类化为我的游戏板模型,并实现了必要的功能。现在我想进入QML,并在那里使用/显示我的模型的内容。

下面是我的代码:

board.h

#pragma once
#include <vector>
#include <QAbstractItemModel>
class board : public QAbstractItemModel
{
  Q_OBJECT
public:
  enum class state
  {
    empty,
    snake,
    fruit
  };
  board(int x, int y);
  state get_state(int x, int y) const;
  void  set_state(int x, int y, state state);
  QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const;
  QModelIndex parent(const QModelIndex& index) const;
  int rowCount(const QModelIndex& parent = QModelIndex()) const;
  int columnCount(const QModelIndex& parent = QModelIndex()) const;
  QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
private:
  std::vector<std::vector<state>> m_board;
};
Q_DECLARE_METATYPE(board::state)

board.cpp

#include "board.h"
board::board(int x, int y) :
  m_board(x, std::vector<board::state>(y, board::state::empty))
{
}
//----------------------------------------------------------------------------------------------------------------------
board::state board::get_state(int x, int y) const
{
  if((size_t) x >= m_board.size() || x < 0)
    return board::state::empty;
  if((size_t) y >= m_board.at(0).size() || y < 0)
    return board::state::empty;
  return m_board.at(x).at(y);
}
//----------------------------------------------------------------------------------------------------------------------
void board::set_state(int x, int y, state state)
{
  if(get_state(x, y) == state)
    return;
  m_board.at(x).at(y) = state;
}
//----------------------------------------------------------------------------------------------------------------------
QModelIndex board::index(int row, int column, const QModelIndex&) const
{
  if((size_t) row >= m_board.size() || row < 0)
    return QModelIndex();
  if((size_t) column >= m_board.at(0).size() || column < 0)
    return QModelIndex();
  return createIndex(row, column);
}
//----------------------------------------------------------------------------------------------------------------------
QModelIndex board::parent(const QModelIndex& index) const
{
  if((size_t) index.row() >= m_board.size() || index.row() < 0)
    return QModelIndex();
  if((size_t) index.column() >= m_board.at(0).size() || index.column() < 0)
    return QModelIndex();
  return createIndex(index.row(), index.column());
}
//----------------------------------------------------------------------------------------------------------------------
int board::rowCount(const QModelIndex&) const
{
  return m_board.size();
}
//----------------------------------------------------------------------------------------------------------------------
int board::columnCount(const QModelIndex&) const
{
  return m_board.at(0).size();
}
//----------------------------------------------------------------------------------------------------------------------
QVariant board::data(const QModelIndex& index, int role) const
{
  if(!index.isValid())
    return QVariant();
  if(role != Qt::DisplayRole)
    return QVariant();
  if((size_t) index.row() >= m_board.size() || index.row() < 0)
    return QVariant();
  if((size_t) index.column() >= m_board.at(0).size() || index.column() < 0)
    return QVariant();
  return qVariantFromValue(get_state(index.row(), index.column()));
}

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickView>
#include <board.h>
int main(int argc, char *argv[])
{
  QGuiApplication app(argc, argv);

  board game_board(10, 10);
  game_board.set_state(4, 9, board::state::snake);
  game_board.set_state(3, 10, board::state::fruit);
  QQuickView view;
  view.setResizeMode(QQuickView::SizeRootObjectToView);
  QQmlContext *ctxt = view.rootContext();
  ctxt->setContextProperty("myGameBoard", &game_board);
  QQmlApplicationEngine engine;
  engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
  return app.exec();
}

main.qml

import QtQuick 2.6
import QtQuick.Window 2.2
Window {
  visible: true
  MouseArea {
    anchors.fill: parent
    onClicked: {
      Qt.quit();
    }
  }
  Text {
    text: qsTr("Hello World")
    anchors.centerIn: parent
  }
  GridView {
    model: myGameBoard
    delegate: Rectangle {
      width: 30
      height: 30
      color: blue
    }
  }
}

我肯定有很多东西是我遗漏的或者是明显错误的。我也知道,将c++模型暴露给QML是经常被问到的问题,但我仍然无法做到。

陈述一些更具体的问题:

  • 如何在QML中显示模型中的数据?我已经看到这是用角色名完成的,但是我的行没有角色名为他们的列
  • 我的模型是否正确实现?
  • 我需要使用什么QML组件来拥有一个瓷砖/棋盘,如果我需要一个自定义组件,它需要哪些属性才能从模型读取数据?

谢谢你看!

  1. QtQuick,使用QML的主要UI框架,基本上只处理列表模型,因此使用角色来模拟表,例如使用TableView

  2. parent()方法是错误的,因为它基本上再次返回相同的索引。这不会造成任何问题,因为你有一个表,而不是一个树。

建议:如果你只需要一个表模型,从QAbstractTableModel派生,让它照顾index()parent()

  • 没有标准的QtQuick元素可以处理表模型,所以你必须建立自己的或使用列表模型,只是在网格中安排"列表项"。
  • 在自定义项的情况下,您可以构建一个与您的模型一起工作的项,甚至可以直接在数据上工作,而不需要模型。

    如果您使用模型并且想要从QML实例化模型,那么您需要一个"指向您的模型类的指针"或"指向QAbstractItemModel的指针"的属性。

    如果你不想使用一个模型或者不需要从QML实例化它,那么你根本不需要任何特定的属性。

    无论哪种情况,您的自定义项都可以使用以下方法之一:

    1. 它可以自己绘制所有东西,即瓷砖的网格和瓷砖本身
    2. 只是提供网格并使用委托来绘制不同类型的tile,例如,ListView允许为列表元素,页眉,页脚等设置委托。

    我有一个适合我的解决方案。下面是我所做的和一些代码:

    • 我使用Q_ENUM作为状态枚举,使其在QML
    • 中可用。
    • 将rootContext连接到QML引擎
    • 编写了一个自定义QML组件,该组件由带有中继器的列组成,其中包含带有中继器的行
    • 保存外中继器和内中继器的索引,编制数据访问索引

    Board.qml

    import QtQuick 2.0
    Column {
      Repeater {
        model: myGameBoard.columnCount()
        Row {
          property int y_pos: index
          Repeater {
            id: repeatr
            model: myGameBoard.rowCount()
            Cell {
              x_cord: index
              y_cord: y_pos
              size: 20
            }
          } //Repeater
        } //Row
      } //Repeater
    } //Column
    

    Cell.qml

    import QtQuick 2.0
    Rectangle {
      id: cell
      property int x_cord: 0
      property int y_cord: 0
      property int size: 10
      width: size
      height: size
      color: getCorrectColor()
      Connections {
        target: myGameBoard
        onDataChanged: {
          cell.color = cell.getCorrectColor()
        }
      }
      function getCorrectColor() {
        switch(myGameBoard.data(myGameBoard.index(cell.x_cord, cell.y_cord)) + 0) {
        case 0 :
          return "honeydew"
        case 1 :
          return "black"
        case 2 :
          return "orangered"
        default:
          return "yellow"
        }
      }
    }
    

    c++端基本保持不变,除了在board.h的状态枚举上使用了Q_ENUM。

    谢谢你的帮助和给复读员的提示!