Evgenii Legotckoi
Evgenii Legotckoi6 декабря 2015 г. 9: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 .

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

#include "customscene.h"
#include "triangle.h"

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private:
    Ui::Widget *ui;
    CustomScene *scene;
};

#endif // WIDGET_H

widget.cpp

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

#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    // Устанавливаем параметры окна приложения
    this->resize(600,600);
    this->setFixedSize(600,600);

    ui->setupUi(this);
    scene = new CustomScene();

    ui->graphicsView->setScene(scene);
    ui->graphicsView->setRenderHint(QPainter::Antialiasing);    // Устанавливаем сглаживание
    ui->graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключаем скроллбар по вертикали
    ui->graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключаем скроллбар по горизонтали

    scene->setSceneRect(0,0,520,520);   // Устанавливаем размеры графической сцены

    Triangle *triangle = new Triangle();
    triangle->setPos(260,260);
    scene->addItem(triangle);

    // Соединяем сигнал о положении курсора со слотом для передвижения героя
    connect(scene, &CustomScene::signalTargetCoordinate, triangle, &Triangle::slotTarget);
}

Widget::~Widget()
{
    delete ui;
}

customscene.h

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

#ifndef CUSTOMSCENE_H
#define CUSTOMSCENE_H

#include <QObject>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>

class CustomScene : public QGraphicsScene
{
    Q_OBJECT
public:
    explicit CustomScene(QObject *parent = 0);
    ~CustomScene();

signals:
    // Сигнал для передачи координат положения курсора мыши
    void signalTargetCoordinate(QPointF point);

public slots:

private:
    // Функция, в которой производится отслеживание положения мыши
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
    void mousePressEvent(QGraphicsSceneMouseEvent *event);
};

#endif // CUSTOMSCENE_H

customscene.cpp

#include "customscene.h"
#include <QApplication>

CustomScene::CustomScene(QObject *parent) :
    QGraphicsScene(parent)
{

}

CustomScene::~CustomScene()
{

}

void CustomScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if(QApplication::mouseButtons() == Qt::LeftButton){
        emit signalTargetCoordinate(event->scenePos()); // Передаём данный о местоположении клика
    }
}

void CustomScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    if(QApplication::mouseButtons() == Qt::LeftButton){
        emit signalTargetCoordinate(event->scenePos()); // Передаём данный о местоположении клика
    }
}

triangle.h

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

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

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

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

#ifndef TRIANGLE_H
#define TRIANGLE_H

#include <QObject>
#include <QTimer>
#include <QGraphicsObject>

#define GO true
#define STOP false

class Triangle : public QGraphicsObject
{
    Q_OBJECT
public:
    explicit Triangle();

signals:

private:
    QRectF boundingRect() const;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);

public slots:
    void slotTarget(QPointF point); // Слот для установки координаты, куда нужно идти

private:
    QTimer *gameTimer;      // Игровой таймер
    QPointF target;         // Положение курсора
    bool state;             // Статус идти/стоять

private slots:
    void slotGameTimer();   // Игровой слот
};

#endif // TRIANGLE_H

triangle.cpp

#include "triangle.h"
#include <math.h>

#include <QPainter>

static const double Pi = 3.14159265358979323846264338327950288419717;
static double TwoPi = 2.0 * Pi;

static qreal normalizeAngle(qreal angle)
{
    while (angle < 0)
        angle += TwoPi;
    while (angle > TwoPi)
        angle -= TwoPi;
    return angle;
}

Triangle::Triangle()
    : QGraphicsObject()
{
    setRotation(0);

    state = STOP;

    gameTimer = new QTimer();   // Инициализируем игровой таймер
    // Подключаем сигнал от таймера и слоту обработки игрового таймера
    connect(gameTimer, &QTimer::timeout, this, &Triangle::slotGameTimer);
    gameTimer->start(5);   // Стартуем таймер
}

QRectF Triangle::boundingRect() const
{
    return QRectF(-12,-15,24,30);
}

void Triangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    // Отрисовка треугольника
    QPolygon polygon;
    polygon << QPoint(0,-15) << QPoint(12,15) << QPoint(-12,15);
    painter->setBrush(Qt::red);
    painter->drawPolygon(polygon);

    Q_UNUSED(option);
    Q_UNUSED(widget);
}

void Triangle::slotGameTimer()
{
    if(state){
        QLineF lineToTarget(QPoint(0,0), mapFromScene(target));
        if(lineToTarget.length() > 2){
             setPos(mapToParent(0, -2));
        }
        /* Проверка выхода за границы поля
         * Если объект выходит за заданные границы, то возвращаем его назад
         * */
        if(this->x() - 30 < 0){
            this->setX(30);         /// слева
            state = STOP;           // Останавливаемся
        }
        if(this->x() + 30 > 520){
            this->setX(520 - 30);   /// справа
            state = STOP;           // Останавливаемся
        }

        if(this->y() - 30 < 0){
            this->setY(30);         /// сверху
            state = STOP;           // Останавливаемся
        }
        if(this->y() + 30 > 520){
            this->setY(520 - 30);   /// снизу
            state = STOP;           // Останавливаемся
        }
    }
}

void Triangle::slotTarget(QPointF point)
{
    // Определяем расстояние до цели
    target = point;
    QLineF lineToTarget(QPointF(0, 0), mapFromScene(target));
    // Угол поворота в направлении к цели
    qreal angleToTarget = ::acos(lineToTarget.dx() / lineToTarget.length());
    if (lineToTarget.dy() < 0)
        angleToTarget = TwoPi - angleToTarget;
    angleToTarget = normalizeAngle((Pi - angleToTarget) + Pi / 2);

    // Поворачиваем героя к цели
    if (angleToTarget >= 0 && angleToTarget < Pi) {
        // Rotate left
        setRotation(rotation() - angleToTarget * 180 /Pi);
    } else if (angleToTarget <= TwoPi && angleToTarget > Pi) {
        // Rotate right
        setRotation(rotation() + (angleToTarget - TwoPi )* (-180) /Pi);
    }

    state = GO; // Разрешаем идти
}

Итог

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

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

Видеоурок

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

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

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

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

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

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

protected:
    void signalTargetCoordinate(QPointF point);
А не так
public:
    void signalTargetCoordinate(QPointF point);
А вы видимо пытаетесь вызвать его извне объекта. Или что-то типо того...
m
  • 6 марта 2018 г. 9: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 г. 10:01

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

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

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

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

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

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

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

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

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

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

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

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

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

Комментарии

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

C++ - Тест 002. Константы

  • Результат:16баллов,
  • Очки рейтинга-10
B

C++ - Тест 001. Первая программа и типы данных

  • Результат:46баллов,
  • Очки рейтинга-6
FL

C++ - Тест 006. Перечисления

  • Результат:80баллов,
  • Очки рейтинга4
Последние комментарии
k
kmssr9 февраля 2024 г. 5:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 12:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 21:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 19:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 декабря 2023 г. 8:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
P
Pisych27 февраля 2023 г. 15:04
Как получить в массив значения из связанной модели? Спасибо, разобрался:))
AC
Alexandru Codreanu19 января 2024 г. 22:57
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…
BlinCT
BlinCT27 декабря 2023 г. 19:57
Растягивать Image на парент по высоте Ну и само собою дял включения scrollbar надо чтобы был Flickable. Так что выходит как то так Flickable{ id: root anchors.fill: parent clip: true property url linkFile p…
Дмитрий
Дмитрий10 января 2024 г. 15:18
Qt Creator загружает всю оперативную память Проблема решена. Удалось разобраться с помощью утилиты strace. Запустил ее: strace ./qtcreator Начал выводиться весь лог работы креатора. В один момент он начал считывать фай…
Evgenii Legotckoi
Evgenii Legotckoi12 декабря 2023 г. 17:48
Побуквенное сравнение двух строк Добрый день. Там случайно не высылается этот сигнал textChanged ещё и при форматировани текста? Если решиать в лоб, то можно просто отключать сигнал/слотовое соединение внутри слота и …

Следите за нами в социальных сетях