들어가며
오늘은 저번에 했던 이미지 배치를 이어서 마우스 이벤트 받는 것을 해보겠습니다. 한 아이템에서 드래그했을 때 드래그 방향의 아이템과 위치가 바뀌는 것을 목표로 합니다.
마우스 이벤트 받기
우선 기존의 QGraphicsPixmapItem을 프로젝트 성격에 맞게 이용하기 위해 상속해서 Item 클래스를 구현합니다. Item클래스는 자신의 좌표와 이미지를 갖고 있고 마우스 이벤트를 받습니다. 그리고 아이템 클래스에 중첩 추상 클래스로 EventListener를 정의합니다. Item 클래스는 생성 시 EventListener를 상속한 객체의 주소를 받고 이를 통해 가상함수 itemDragEvent를 호출합니다.
Item.h
#pragma once
#include <string>
#include <QtWidgets/QGraphicsPixmapItem>
class Item : public QGraphicsPixmapItem
{
class EventListener; // 전방선언
public:
Item(EventListener* listener, const std::string& path, int row, int column, QGraphicsItem* parent); // 생성자
std::string path() const; // Getter
int row() const;
int column() const;
void setRow(int row); // Setter
void setColumn(int column);
protected:
virtual void mousePressEvent(QGraphicsSceneMouseEvent* event) override; // 마우스 누를 때 호출
virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; // 마우스 뗄 때 호출
private:
const std::string _path; // 이미지 종류
int _row; // 좌표
int _column;
EventListener* _listener;
QPointF _pressPos;
public:
enum class Direction
{
Left,
Right,
Up,
Down
};
class EventListener // 인터페이스
{
public:
virtual void itemDragEvent(Item* item, Item::Direction dir) = 0;
};
};
Item.cpp
#include "Item.h"
#include "Consts.h"
#include <QtWidgets/QGraphicsSceneMouseEvent>
Item::Item(EventListener* listener, const std::string& path, int row, int column, QGraphicsItem* parent)
: QGraphicsPixmapItem(
QPixmap(path.c_str()).scaled(Consts::BOARD_ITEM_SIZE, Consts::BOARD_ITEM_SIZE)
, parent)
, _listener(listener)
, _path(path)
, _row(row)
, _column(column)
{
}
std::string Item::path() const
{
return _path;
}
int Item::row() const
{
return _row;
}
int Item::column() const
{
return _column;
}
void Item::setRow(int row)
{
_row = row;
}
void Item::setColumn(int column)
{
_column = column;
}
void Item::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
_pressPos = event->pos();
}
void Item::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
QPointF releasePos = event->pos();
QPointF diff = releasePos - _pressPos;
Direction dir;
if (diff.x() == 0 && diff.y() == 0) // 움직이지 않았다면
{
return;
}
if (qAbs(diff.x()) > qAbs(diff.y())) // x변화량과 y변화량의 절대값 비교를 통해 이동방향 결정
{
if (diff.x() > 0)
{
dir = Direction::Right;
}
else
{
dir = Direction::Left;
}
}
else
{
if (diff.y() > 0)
{
dir = Direction::Down;
}
else
{
dir = Direction::Up;
}
}
_listener->itemDragEvent(this, dir); // 현재 객체와 방향을 전달
}
그 후 Board 클래스를 바뀐 Item클래스에 맞게 수정하고 addItem에서 Item객체를 생성할 때 Board객체를 EventListener로써 전달하여 Board에서 오버라이딩한 가상함수가 호출될 수 있게 합니다.
Board.h
#pragma once
#include <QtWidgets/QGraphicsScene>
#include <QtWidgets/QGraphicsPixmapItem>
#include <QtWidgets/QGraphicsRectItem>
#include <vector>
#include <random>
#include "Item.h"
class Board : public Item::EventListener // EventListener로 제어하기 위해 인터페이스 상속
{
private:
QGraphicsScene* _scene;
QGraphicsRectItem _root; // 보드판 묶음
std::vector<std::vector<Item*>> _items; // item 배열
std::random_device _device; // 난수 생성
std::mt19937 _gen;
public:
Board(QGraphicsScene* scene);
~Board();
void addItem(int row, int column);
void removeItem(int row, int column);
void moveItem(Item* item, int toRow, int toColumn); // 아이템 위치 이동
void exchange(int row0, int column0, int row1, int column1); // 아이템 교환
virtual void itemDragEvent(Item* item, Item::Direction dir); // 오버라이드
};
/*
items
1 2 3 -> _items[0], row0
4 5 6 -> _items[1], row1
7 8 9 -> _items[2], row2
_items[0][0] -> 1
_items[0][1] -> 2
_items[0][2] -> 3
*/
Board.cpp
#include <random>
#include "Board.h"
#include "Consts.h"
#include "Item.h"
Board::Board(QGraphicsScene* scene)
: _scene(scene) // scene 설정
, _gen(_device()) // 난수 생성
{
_scene->addItem(&_root); // scene에 보드판 묶음 추가
_root.setX(_scene->sceneRect().width() / 2 // 보드판 위치 설정
- Consts::BOARD_ITEM_SIZE * Consts::BOARD_LENGTH / 2);
_root.setY(_scene->sceneRect().height() / 2
- Consts::BOARD_ITEM_SIZE * Consts::BOARD_LENGTH / 2);
for (int row = 0; row < Consts::BOARD_LENGTH; ++row) // vector에 item 넣기
{
std::vector<Item*> rowItems(Consts::BOARD_LENGTH);
_items.push_back(rowItems); // 행 삽입
for (int column = 0; column < Consts::BOARD_LENGTH; ++column)
{
addItem(row, column); // 각 열에 item 추가
}
}
}
Board::~Board()
{
for (int row = 0; row < _items.size(); ++row)
{
for (int column = 0; column < _items[row].size(); ++column)
{
removeItem(row, column);
}
}
}
void Board::addItem(int row, int column)
{
std::uniform_int_distribution<int> dis(0, 13); // 난수를 0 ~ 13 까지 균등하게 생성
const std::string& path = Consts::paths[dis(_gen)];
Item* item = new Item(this, path, row, column, &_root);
item->setPos(column * Consts::BOARD_ITEM_SIZE, row * Consts::BOARD_ITEM_SIZE); // 부모위치를 기준으로 item 위치 설정
_items[row][column] = item; // 배열에 item 삽입
}
void Board::removeItem(int row, int column)
{
auto* item = _items[row][column];
if (item == nullptr) // 존재하지 않는다면 종료
return;
_items[row][column] = nullptr; // 벡터 원소 해제
item->setParentItem(nullptr); // 부모 정보 해제
_scene->removeItem(item); // scene에서 해제
delete item; // 동적할당 해제
}
void Board::moveItem(Item* item, int toRow, int toColumn)
{
item->setRow(toRow); // 좌표값 변경
item->setColumn(toColumn);
item->setPos(toColumn * Consts::BOARD_ITEM_SIZE, toRow * Consts::BOARD_ITEM_SIZE); // 보이는 위치 변경
_items[toRow][toColumn] = item; // 벡터내에서 변경
}
void Board::exchange(int row0, int column0, int row1, int column1)
{
Item* item0 = _items[row0][column0];
Item* item1 = _items[row1][column1];
moveItem(item0, row1, column1); // 교환
moveItem(item1, row0, column0);
}
void Board::itemDragEvent(Item* item, Item::Direction dir)
{
int row0 = item->row(); // 기존 아이템
int column0 = item->column();
int row1 = row0; // 교환할 아이템
int column1 = column0;
switch (dir) // 방향
{
case Item::Direction::Left:
column1--;
break;
case Item::Direction::Right:
column1++;
break;
case Item::Direction::Up:
row1--;
break;
case Item::Direction::Down:
row1++;
break;
}
if (row1 < 0 || column1 < 0) // 보드판 바깥 예외처리
return;
if (row1 >= Consts::BOARD_LENGTH || column1 >= Consts::BOARD_LENGTH)
return;
Item* item1 = _items[row1][column1];
if (item1 == nullptr)
return;
exchange(row0, column0, row1, column1); // 교환
}
main.cpp와 Consts.h 는 변화가 없었습니다.
교환되는 모습
![]() |
![]() |
교환되지 않은 모습 | 1행 1열과 1행 2열이 교환되었다. |
마치며
꽤나 복잡했던 부분은 Item의 중첩 클래스로 EventListener 추상 클래스를 만들고, Item은 이 클래스의 포인터만 소유합니다. 그리고 Board가 추상 클래스를 상속하고 오버라이딩 함으로써 Item이 추상클래스 포안터를 통해 Board가 오버라이딩한 함수를 호출할 수 있게 됩니다. Item이 Board와 상속관계가 되는 것보다 의존성을 낮출 수 있습니다.
'게임개발 > Qt' 카테고리의 다른 글
[QT] 애니팡 - 점수 UI 만들기 (완성) (2) | 2024.08.28 |
---|---|
[QT] 애니팡 - 애니메이션 넣기 (2) | 2024.08.28 |
[QT] 애니팡 - 게임 규칙 구현하기 (0) | 2024.08.27 |
[QT] 애니팡 - 이미지 배치하기 (0) | 2024.08.16 |