Evgenii Legotckoi
6 декабря 2015 г. 20:47

Qt/C++ - Урок 033. Работаем с QGraphicsObject вместо QGraphicsItem

Моё внимание обратили на то, что для работы с сигналами и слотами вместо класса наследованного от QGraphicsItem и от QObject можно использовать класс наследованный от QGraphicsObject . И действительно, если немного покопаться в исходниках QGraphicsObject , то обнаружится, что это класс наследованный от QGraphicsItem и от QObject . То есть также применяется множественное наследование, только в данном случае все велосипеды уже написаны до нас. Поэтому попробуем поработать с данным классом на примере игровой механики.

А именно, предлагаю написать программу, в которой Мы будет перемещать героя кликом мыши по графической сцене, как в любой РПГ наподобие Diablo.

Структура проекта для работы с QGraphicsObject

  • QGraphicsObjectExample.pro - профайл проекта;
  • main.cpp - основной файл исходных кодов;
  • widget.h - заголовочный файл окна приложения;
  • widget.cpp - файл исходных кодов окна приложения;
  • customscene.h - заголовочный файл кастомизированной графической сцены ;
  • customscene.cpp - файл исходных кодов кастомизированной графической сцены;
  • triangle.h - заголовочный файл класса героя-треугольника, который будет передвигаться;
  • triangle.cpp - файл исходных кодов класса героя-треугольника.

mainwindow.ui - QGraphicsObjectExample.pro - main.cpp

Форма главного окна ничем не примечательна. В ней находится только объект QGraphicsView. Файлы QGraphicsObjectExample и main.cpp создаются по умолчанию и не модифицируются.

widget.h

Для реализации задуманного нам понадобится только кастомизированная графическая сцена и больше ничего. Хотя её вполне можно было объявить и локально внутри конструктора класса Widget .

  1. #ifndef WIDGET_H
  2. #define WIDGET_H
  3.  
  4. #include <QWidget>
  5.  
  6. #include "customscene.h"
  7. #include "triangle.h"
  8.  
  9. namespace Ui {
  10. class Widget;
  11. }
  12.  
  13. class Widget : public QWidget
  14. {
  15. Q_OBJECT
  16.  
  17. public:
  18. explicit Widget(QWidget *parent = 0);
  19. ~Widget();
  20.  
  21. private:
  22. Ui::Widget *ui;
  23. CustomScene *scene;
  24. };
  25.  
  26. #endif // WIDGET_H

widget.cpp

В конструкторе класса Widget производим инициализацию графической сцены и объекта треугольника. А также соединяем сигнал signalTargetCoordinate от графической сцены со слотом треугольника. signalTargetCoordinate будет передавать координаты мыши, к которым должен будет двигаться треугольник.

  1. #include "widget.h"
  2. #include "ui_widget.h"
  3.  
  4. Widget::Widget(QWidget *parent) :
  5. QWidget(parent),
  6. ui(new Ui::Widget)
  7. {
  8. // Устанавливаем параметры окна приложения
  9. this->resize(600,600);
  10. this->setFixedSize(600,600);
  11.  
  12. ui->setupUi(this);
  13. scene = new CustomScene();
  14.  
  15. ui->graphicsView->setScene(scene);
  16. ui->graphicsView->setRenderHint(QPainter::Antialiasing); // Устанавливаем сглаживание
  17. ui->graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключаем скроллбар по вертикали
  18. ui->graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключаем скроллбар по горизонтали
  19.  
  20. scene->setSceneRect(0,0,520,520); // Устанавливаем размеры графической сцены
  21.  
  22. Triangle *triangle = new Triangle();
  23. triangle->setPos(260,260);
  24. scene->addItem(triangle);
  25.  
  26. // Соединяем сигнал о положении курсора со слотом для передвижения героя
  27. connect(scene, &CustomScene::signalTargetCoordinate, triangle, &Triangle::slotTarget);
  28. }
  29.  
  30. Widget::~Widget()
  31. {
  32. delete ui;
  33. }

customscene.h

Для передачи координат мыши на графической сцене будет использоваться сигнал, а передаваться он будет из событий mousePressEvent и mouseMoveEvent. При этом передача сигнала с данными будет производиться только в том случае, если была зажата левая кнопка мыши.

  1. #ifndef CUSTOMSCENE_H
  2. #define CUSTOMSCENE_H
  3.  
  4. #include <QObject>
  5. #include <QGraphicsScene>
  6. #include <QGraphicsSceneMouseEvent>
  7.  
  8. class CustomScene : public QGraphicsScene
  9. {
  10. Q_OBJECT
  11. public:
  12. explicit CustomScene(QObject *parent = 0);
  13. ~CustomScene();
  14.  
  15. signals:
  16. // Сигнал для передачи координат положения курсора мыши
  17. void signalTargetCoordinate(QPointF point);
  18.  
  19. public slots:
  20.  
  21. private:
  22. // Функция, в которой производится отслеживание положения мыши
  23. void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
  24. void mousePressEvent(QGraphicsSceneMouseEvent *event);
  25. };
  26.  
  27. #endif // CUSTOMSCENE_H

customscene.cpp

  1. #include "customscene.h"
  2. #include <QApplication>
  3.  
  4. CustomScene::CustomScene(QObject *parent) :
  5. QGraphicsScene(parent)
  6. {
  7.  
  8. }
  9.  
  10. CustomScene::~CustomScene()
  11. {
  12.  
  13. }
  14.  
  15. void CustomScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
  16. {
  17. if(QApplication::mouseButtons() == Qt::LeftButton){
  18. emit signalTargetCoordinate(event->scenePos()); // Передаём данный о местоположении клика
  19. }
  20. }
  21.  
  22. void CustomScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
  23. {
  24. if(QApplication::mouseButtons() == Qt::LeftButton){
  25. emit signalTargetCoordinate(event->scenePos()); // Передаём данный о местоположении клика
  26. }
  27. }

triangle.h

Данный класс наследуем от класса QGraphicsObject и не применяем наследование от класса QObject, поскольку QGraphicsObject уже наследован от него, а также от QGrapihcsItem. Дальнейшая работа с данным объектом производится также, как если бы мы работали с классом, наследованным от QGrapgicsItem.

Небольшим моментом по сравнению с другими статьями о GameDev , например той, где мы просто отслеживаем перемещение мыши герой, является то, что здесь имеется переменная состояния стоять/идти.

Она необходима для того, чтобы героя остановить в том случае, когда он упрётся в стену или границу территории и точка его прибытия окажется недостижимой. А так здесь также используется игровой таймер с игровым слотом, где обрабатывается процесс движения главного героя.

А вот слот получения координат целевой точки обрабатывает поворот героя в сторону цели.

  1. #ifndef TRIANGLE_H
  2. #define TRIANGLE_H
  3.  
  4. #include <QObject>
  5. #include <QTimer>
  6. #include <QGraphicsObject>
  7.  
  8. #define GO true
  9. #define STOP false
  10.  
  11. class Triangle : public QGraphicsObject
  12. {
  13. Q_OBJECT
  14. public:
  15. explicit Triangle();
  16.  
  17. signals:
  18.  
  19. private:
  20. QRectF boundingRect() const;
  21. void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
  22.  
  23. public slots:
  24. void slotTarget(QPointF point); // Слот для установки координаты, куда нужно идти
  25.  
  26. private:
  27. QTimer *gameTimer; // Игровой таймер
  28. QPointF target; // Положение курсора
  29. bool state; // Статус идти/стоять
  30.  
  31. private slots:
  32. void slotGameTimer(); // Игровой слот
  33. };
  34.  
  35. #endif // TRIANGLE_H

triangle.cpp

  1. #include "triangle.h"
  2. #include <math.h>
  3.  
  4. #include <QPainter>
  5.  
  6. static const double Pi = 3.14159265358979323846264338327950288419717;
  7. static double TwoPi = 2.0 * Pi;
  8.  
  9. static qreal normalizeAngle(qreal angle)
  10. {
  11. while (angle < 0)
  12. angle += TwoPi;
  13. while (angle > TwoPi)
  14. angle -= TwoPi;
  15. return angle;
  16. }
  17.  
  18. Triangle::Triangle()
  19. : QGraphicsObject()
  20. {
  21. setRotation(0);
  22.  
  23. state = STOP;
  24.  
  25. gameTimer = new QTimer(); // Инициализируем игровой таймер
  26. // Подключаем сигнал от таймера и слоту обработки игрового таймера
  27. connect(gameTimer, &QTimer::timeout, this, &Triangle::slotGameTimer);
  28. gameTimer->start(5); // Стартуем таймер
  29. }
  30.  
  31. QRectF Triangle::boundingRect() const
  32. {
  33. return QRectF(-12,-15,24,30);
  34. }
  35.  
  36. void Triangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
  37. {
  38. // Отрисовка треугольника
  39. QPolygon polygon;
  40. polygon << QPoint(0,-15) << QPoint(12,15) << QPoint(-12,15);
  41. painter->setBrush(Qt::red);
  42. painter->drawPolygon(polygon);
  43.  
  44. Q_UNUSED(option);
  45. Q_UNUSED(widget);
  46. }
  47.  
  48. void Triangle::slotGameTimer()
  49. {
  50. if(state){
  51. QLineF lineToTarget(QPoint(0,0), mapFromScene(target));
  52. if(lineToTarget.length() > 2){
  53. setPos(mapToParent(0, -2));
  54. }
  55. /* Проверка выхода за границы поля
  56. * Если объект выходит за заданные границы, то возвращаем его назад
  57. * */
  58. if(this->x() - 30 < 0){
  59. this->setX(30); /// слева
  60. state = STOP; // Останавливаемся
  61. }
  62. if(this->x() + 30 > 520){
  63. this->setX(520 - 30); /// справа
  64. state = STOP; // Останавливаемся
  65. }
  66.  
  67. if(this->y() - 30 < 0){
  68. this->setY(30); /// сверху
  69. state = STOP; // Останавливаемся
  70. }
  71. if(this->y() + 30 > 520){
  72. this->setY(520 - 30); /// снизу
  73. state = STOP; // Останавливаемся
  74. }
  75. }
  76. }
  77.  
  78. void Triangle::slotTarget(QPointF point)
  79. {
  80. // Определяем расстояние до цели
  81. target = point;
  82. QLineF lineToTarget(QPointF(0, 0), mapFromScene(target));
  83. // Угол поворота в направлении к цели
  84. qreal angleToTarget = ::acos(lineToTarget.dx() / lineToTarget.length());
  85. if (lineToTarget.dy() < 0)
  86. angleToTarget = TwoPi - angleToTarget;
  87. angleToTarget = normalizeAngle((Pi - angleToTarget) + Pi / 2);
  88.  
  89. // Поворачиваем героя к цели
  90. if (angleToTarget >= 0 && angleToTarget < Pi) {
  91. // Rotate left
  92. setRotation(rotation() - angleToTarget * 180 /Pi);
  93. } else if (angleToTarget <= TwoPi && angleToTarget > Pi) {
  94. // Rotate right
  95. setRotation(rotation() + (angleToTarget - TwoPi )* (-180) /Pi);
  96. }
  97.  
  98. state = GO; // Разрешаем идти
  99. }

Итог

В результате Вы получите приложение, в котором будете управлять перемещением главного героя с помощью одной лишь мышки, будто это РПГ, наподобие Diablo. Демонстрацию работы приложения Вы можете увидеть в видеоуроке.

Ссылка на скачивание проекта в zip-архиве: QGraphicsObjectExample

Видеоурок

Вам это нравится? Поделитесь в социальных сетях!

m
  • 6 марта 2018 г. 19:08

Добрый день! Пожалуйста подскажите, что может означать ошибка - ~\customscene.h:17: ошибка: 'void CustomScene::signalTargetCoordinate(QPointF)' is protected

Evgenii Legotckoi
  • 6 марта 2018 г. 19:47

Добрый день! По какой-то причине у вас данный сигнал оказался в protected секции, а не в секции сигналов, то есть скорее всего он объявлен в заголовочнике так

protected:
    void signalTargetCoordinate(QPointF point);
А не так
public:
    void signalTargetCoordinate(QPointF point);
А вы видимо пытаетесь вызвать его извне объекта. Или что-то типо того...
m
  • 6 марта 2018 г. 20:51

Большое спасибо. Файл был Ваш, но принудительное добавление public: перед void signal эту ошибку устранило. Но есть еще ошибка -

~\widget.cpp:27: ошибка: no matching function for call to 'Widget::connect(CustomScene*&, void (CustomScene::*)(QPointF), Triangle*&, void (Triangle::*)(QPointF))'

Если можно помогите пожалуйста - я в Qt вторую неделю всего.

Evgenii Legotckoi
  • 6 марта 2018 г. 21:01

Скорее всего в одном из классов не объявлена нужная функция (слот или сигнал). Нужно, чтобы сигнатуры сигнала и слота совпадали в данном конкретном примере. А в одном из классов видимо отсутствует нужный метод ( сигнал или слот - почитайте эту статью, чтобы разобраться с теорией сигналов и слотов).

m
  • 6 марта 2018 г. 21:40

Большое спасибо, удачи Вам, здоровья, и всего доброго.

m
  • 7 марта 2018 г. 16:40

Добрый день! Еще раз спасибо за уроки. Все заработало, помог ваш урок 24. Public был ни при чем, нужно было две строки проекта с connect записать в стиле Qt 4.8.5. Правда - стрелками на клавиатуре треугольник двигается с точностью наоборот. Но это мелочи. Всего доброго.

Evgenii Legotckoi
  • 7 марта 2018 г. 16:43

Добрый день!
Что? А почему? Вы используете Qt 4.8?

m
  • 8 марта 2018 г. 13:34

Добрый день! Это требование руководства, увы!

Evgenii Legotckoi
  • 8 марта 2018 г. 14:24

Вы случаем не в ОНИИПе работаете?

m
  • 9 марта 2018 г. 13:20

Добрый день! Точно так, в НИИ, только не О...П. Остальное, к сожалению, не моя информация. Всего доброго.

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь