Рисование на QGraphicsScene

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 12 мес. назад.

  • Автор
    Сообщения
  • #3159

    При разработке приложений, особенно игровых, достаточно удобно использовать графическую сцену Qt. В частности, QGraphicsScene использовался при разработке этой игрушки под Android. Графическая сцена позволяет размещать на себе элементы (эффективно работает с тысячами объектов), использовать слои, группировать объекты и т.п. Для отображения сцены используется QGraphicsView, причем вы можете отображать не всю сцену целиком, а ее часть (это очень здорово для игрушек типа Марио). Более подробно про графическую сцену можно прочитать тут: Работа с графической сценой [Qt], а более практичный пример будет сейчас рассмотрен.

    Мы напишем приложение, позволяющее рисовать на сцене линии, подобно тому, как это делается в MS Paint:
    qgraphicsscene-example

    Работа с QGraphicsView и QGraphicsScene

    Итак, объекты мы будем добавлять на графическую сцену. Для отображения сцены – использоваться QGraphicsView, который, помимо прочего, является виджетом (наследует QWidget). Мы определим класс-наследник от QGraphicsView, в котором будем обрабатывать события мыши:

    #include <QGraphicsView>
    
    class Line;
    
    class Canvas : public QGraphicsView {
        Q_OBJECT
    public:
        explicit Canvas(int h, int w, QWidget *parent = 0);
    public slots:
        void resizeEvent(QResizeEvent *);
    protected slots:
        void mousePressEvent(QMouseEvent *event);
        void mouseMoveEvent(QMouseEvent *event);
        void mouseReleaseEvent(QMouseEvent *event);
    protected:
        QGraphicsScene m_scene;
        Line *m_line;
    
        bool m_isKeyPressed;
    };

    Наш класс кроме сцены содержит указатель на объект текущей линии (которая рисуется сейчас, пока зажата кнопка мыши) и переменную m_isKeyPressed, показывающую происходит ли сейчас рисование. Класс Line – это не стандартный класс, мы рассмотрим его дальше.

    Конструктор класса принимает размер сцены, вызывает конструктор базового класса (QGraphicsView), создает объект сцены и изменяет свой размер так, чтобы отобразить всю сцену, растянув во все окно без искажения (KeepAspectRatio):

    Canvas::Canvas(int h, int w, QWidget *parent)
        : QGraphicsView(parent),
          m_scene(0, 0, w, h, this),
          m_isKeyPressed(false) {
        setScene(&m_scene);
        fitInView(m_scene.sceneRect(), Qt::KeepAspectRatio);
    }

    Если нажата кнопка мыши, срабатывает mousePressEvent. Однако, он сработает для любой кнопки мыши, нас интересует только левая кнопка. Аргумент функции – QMouseEvent содержит позицию, в которой было произведено нажатие, однако это позиция в координатах QGraphicsView. Нам нужно добавить объект на сцену, поэтому позицию преобразуем в координаты сцены методом mapToScene(). Тут же создается новый объект линии (ведь мы начали рисование) и в нее добавляется первая точка:

    void Canvas::mousePressEvent(QMouseEvent *event) {
        if (event->button() == Qt::LeftButton) {
            QPointF pos = mapToScene(event->pos());
    
            m_line = new Line(QColor(123, 127, 0), &m_scene);
            m_scene.addItem(m_line);
    
            m_line->addPoint(pos);
            m_isKeyPressed = true;
        }
    }

    При перемещении мыши по QGraphicsView срабатывает метод mouseMoveEvent(). В нем нам достаточно получить координату сцены и добавить ее в линию. Выполнять это нужно только если в настоящий момент зажата клавиша мыши:

    void Canvas::mouseMoveEvent(QMouseEvent *event) {
        if (m_isKeyPressed) {
            QPointF pos = mapToScene(event->pos());
            m_line->addPoint(pos);
        }
    }

    Если отпущена левая кнопка мыши мы вызовем у линии метод removePoint();. Класс Line изображает последнюю добавленную точку в форме круга (мы сами сделали так). Чтобы после рисования осталась только линия, этот круг нужно удалить:

    void Canvas::mouseReleaseEvent(QMouseEvent *event) {
        if (event->button() == Qt::LeftButton) {
            m_isKeyPressed = false;
            m_line->removePoint();
        }
    }

    При изменении размера виджета управление передается методу resizeEvent(), который вызывает fitInView чтобы перерисовать сцену с опцией Qt::KeepAspectRatio в новых размерах виджета:

    void Canvas::resizeEvent(QResizeEvent *) {
        fitInView(m_scene.sceneRect(), Qt::KeepAspectRatio);
    }

    Класс линии – использование QGraphicsItemGroup

    В библиотеке Qt есть встроенный класс для отображения на графической сцене линий – QGraphicsLineItem, однако он предназначен для рисования прямых, а нам требуется рисовать ломаные линии. Ломаную линию мы будем рассматривать как набор прямых. Для работы с группой объектов (прямых) можно использовать QGraphicsItemGroup. При этом мы создаем элементы сцены, указывая для них QGraphicsItemGroup в качестве родительского объекта.

    #include <QObject>
    #include <QGraphicsItemGroup>
    #include <QPen>
    #include <QBrush>
    
    class Line : public QGraphicsItemGroup, public QObject {
        const int EllipseRadius = 3;
        const int LineWidth = 1;
    
        QPen m_pen;
        QBrush m_brush;
    public:
        Line(const QColor &color, QObject *parent = 0);
        void removePoint();
    public slots:
        void addPoint(QPointF point);
    protected:
        QGraphicsEllipseItem *m_lastPoint;
    };

    Интерфейс класса линии позволяет добавить точку и удалить эллипс (изображающий последнюю точку линии). Обратите внимание, что в этом примере используется множественное наследование – наш класс является не только группой графической объектов, но и QObject (это необходимо для использования механизма сигналов и слотов Qt).
    Конструктор Line вызывает конструкторы базовых классов (QGraphicsItemGroup и QObject):

    Line::Line(const QColor &color, QObject* parent)
        : QGraphicsItemGroup(), QObject(parent),
        m_pen(color), m_brush(color), m_lastPoint(nullptr) {
        m_pen.setWidth(LineWidth);
    }

    При добавлении точки проверяет значение m_lastPoint – если оно равно nullptr, то добавлять линию не требуется – достаточно добавить лишь конечную точку (QGraphicsEllipseItem). Если же точка не является первой, то выполняется добавление линии (QGraphicsLineItem), соединяющей предыдущую и текущую точки, а также удаление старой конечной точки:

    void Line::addPoint(QPointF point) {
        QPointF radius(EllipseRadius, EllipseRadius);
    
        if (m_lastPoint) {
            QGraphicsLineItem *line = new QGraphicsLineItem(
                        QLineF(m_lastPoint->pos(), point), this);
            line->setPen(m_pen);
    
            delete m_lastPoint;
        }
        m_lastPoint = new QGraphicsEllipseItem(QRectF(-radius, radius), this);
        m_lastPoint->setBrush(m_brush);
        m_lastPoint->setPos(point);
    }

    Удаление последней точки выполняется когда рисование завершено:

    void Line::removePoint() {
        if (m_lastPoint)
            delete m_lastPoint;
        m_lastPoint = nullptr;
    }

    Функция main()

    Чтобы использовать все, что мы тут написали – достаточно создать экземпляр класса Canvas и вызвать для него метод show():

    #include <QApplication>
    #include <QDebug>
    #include "canvas.h"
    
    int main(int argc, char *argv[]) {
      try {
        QApplication a(argc, argv);
    
        Canvas canvas(300, 400);
        canvas.show();
    
        return a.exec();
      }
      catch (QString err) {
        qDebug() << err;
      }
    }

    Исходники можно взять в репозитории.

Для ответа в этой теме необходимо авторизоваться.