Evgenii Legotckoi
Evgenii Legotckoi4 декабря 2015 г. 12:18

Qt/C++ - Урок 027. Полиморфизм в Qt на примере геометрических фигур в QGraphicsScene

Полиморфизм является одной из парадигм ООП (объектно-ориентированное программирование). Полиморфизм реализует способность базового класса предоставлять свойства и методы, реализация которых не известна для будущих классов, которые будут наследованы от данного класса. То есть базовый класс в данном случае имеет весь необходимый набор методов для реализации поставленных задач, но конкретная реализация определённых методов возлагается на классы потомки. И, как правило, базовый класс является абстрактным, то есть от него не создаются объекты.

Например, необходимо реализовать программу, в которой имеется возможность рисовать несколько геометрических фигур мышью на графической сцене , выбирая текущую фигур с помощью кнопок в приложении. Для этого можно создать несколько классов, каждый из которых будет реализовывать весь необходимый функционал, и фактически дублировать в каждом классе большую часть кода, усложняя и запутывая разработку. Либо можно создать один базовый класс, например, Figure , который будет наследован от QGraphicsItem , и реализовать весь основной функционал в данном классе, а потом уже создать три класса, которые будут наследованы от данного класса, но реализовать в них только метод отрисовки самой фигуры: Romb, Square, Triangle.

На ниже следующем рисунке показана логика наследования для базового класса Figure . Применено также множественное наследование класса Figure для реализации работы с сигналами и слотами .


Структура проекта - Полиморфизм на примере

  • PaintFigure.pro - профайл проекта;
  • mainwindow.h - заголовочный файл основного окна приложения;
  • mainwindow.cpp - файл исходных кодов основного окна приложения;
  • paintscene.h - заголовочный файл графической сцены;
  • paintscene.cpp - файл исходных кодов графической сцены;
  • figure.h - заголовочный файл базового класса фигур;
  • figure.cpp - файл исходных кодов класса фигур;
  • romb.h - заголовочный файл класса Ромб;
  • romb.cpp - файл исходных кодов класса Ромб;
  • square.h - заголовочный файл класса Квадрат/Прямоугольник;
  • square.cpp - файл исходных кодов класса Квадрат/Прямоугольник;
  • triangle.h - заголовочный файл класса Треугольник;
  • triangle.cpp - файл исходных кодов класса Треугольник;
  • mainwindow.ui - форма главного окна приложения.

mainwindow.ui - PaintFigure.pro - main.cpp

Набрасываем в дизайнере следующую форму главного окна приложения. А файлы PaintFigure.pro и main.cpp остаются созданными по умолчанию и не подвергаются модификациям.

mainwindow.h

В заголовочном файле главного окна приложения необходимо объявить кастомизированную графическую сцену PaintScene, которая будет отвечать за отрисовку геометрических фигур. Также переопределяем метод resizeEvent(), в котором будем пересчитывать размер графической сцены в зависимости от размера окна приложения. Это уже чисто косметический момент.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTimer>
#include <QResizeEvent>

#include "paintscene.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private:
    Ui::MainWindow *ui;
    PaintScene *scene;  // Объявляем кастомную графическую сцену
    QTimer *timer;      /* Определяем таймер для подготовки актуальных размеров
                         * графической сцены
                         * */
private:
    /* Переопределяем событие изменения размера окна
     * для пересчёта размеров графической сцены
     * */
    void resizeEvent(QResizeEvent * event);

private slots:
    // Таймер для изменения размеров сцены при изменении размеров Окна приложения
    void slotTimer();
    void on_pushButton_clicked();       // Включаем отрисовку Ромба
    void on_pushButton_2_clicked();     // Включаем отрисовку Квадрата
    void on_pushButton_3_clicked();     // Включаем отрисовку Треугольника
};

#endif // MAINWINDOW_H

mainwindow.cpp

В главном окне приложения имеется три кнопки, по нажатию которых Мы будем устанавливать необходимый тип фигуры для отрисовки мышью. Также присутствует слот для обработки изменения размера окна по сигналу от таймера для перерисовки.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

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

    timer = new QTimer();       // Инициализируем таймер
    connect(timer, &QTimer::timeout, this, &MainWindow::slotTimer);
    timer->start(100);          // Запускаем таймер
}

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

void MainWindow::slotTimer()
{
    /* Переопределяем размеры графической сцены в зависимости
     * от размеров окна
     * */
    timer->stop();
    scene->setSceneRect(0,0, ui->graphicsView->width() - 20, ui->graphicsView->height() - 20);
}

void MainWindow::resizeEvent(QResizeEvent *event)
{
    timer->start(100);
    QMainWindow::resizeEvent(event);
}

// Ромб
void MainWindow::on_pushButton_clicked()
{
    scene->setTypeFigure(PaintScene::RombType);
}

// Квадрат
void MainWindow::on_pushButton_2_clicked()
{
    scene->setTypeFigure(PaintScene::SquareType);
}

// Треугольник
void MainWindow::on_pushButton_3_clicked()
{
    scene->setTypeFigure(PaintScene::TriangleType);
}

paintscene.h

Заголовочный файл кастомной графической сцены. В данной сцене присутствует свойство Q_PROPERTY, которое отвечает за хранение текущего типа фигуры для отрисовки. Список типов задаётся с помощью enum FigureTypes. Также присутствует объявление объекта tempFigure , который является экземпляром класса Figure . Данный класс является базовым в соответствии с парадигмой полиморфизма, поэтому он используется для временного хранения фигуры, которая отрисовывается на графической сцене, поскольку все основные методы для задания размеров фигуры с помощью мыши реализованы в базовом классе Figure.

Также переопределены методы событий от мыши. В методе mousePressEvent создаётся геометрическая фигура для отрисовки, а в методе mouseMoveEvent производится перерисовка фигуры в зависимости от положения курсора мыши до тех пор, пока кнопка мыши не будет отпущена.

#ifndef PAINTSCENE_H
#define PAINTSCENE_H

#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include "figure.h"

class PaintScene : public QGraphicsScene
{
    Q_OBJECT
    // Свойство текущего типа используемой фигуры
    Q_PROPERTY(int typeFigure
               READ typeFigure WRITE setTypeFigure
               NOTIFY typeFigureChanged)

public:
    explicit PaintScene(QObject *parent = 0);
    ~PaintScene();

    int typeFigure() const;                 // Возвращение текущего типа
    void setTypeFigure(const int type);     // Установка текущего типа

    // Перечисление типов используемых фигур
    enum FigureTypes {
        SquareType,
        RombType,
        TriangleType
    };

signals:
    void typeFigureChanged();               // Сигнал об изменении типа текущей фигуры

private:
    /* Объект для временного хранения рисуемой фигуры
     * Является объектом базового класса для всех трёх типов фигур в примере
     * */
    Figure *tempFigure;
    int m_typeFigure;   // текущий тип фигуры

private:
    // Для рисования используем события мыши
    void mousePressEvent(QGraphicsSceneMouseEvent * event);
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event);

};

#endif // PAINTSCENE_H

paintscene.cpp

#include "paintscene.h"
#include "romb.h"
#include "triangle.h"
#include "square.h"

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

}

PaintScene::~PaintScene()
{

}

int PaintScene::typeFigure() const
{
    return m_typeFigure;
}

void PaintScene::setTypeFigure(const int type)
{
    m_typeFigure = type;
}

void PaintScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    /* Устанавливаем конечную координату положения мыши
     * в текущую отрисовываемую фигуру
     * */
    tempFigure->setEndPoint(event->scenePos());
    /* Обновляем содержимое сцены,
     * необходимо для устранения артефактов при отрисовке фигур
     * */
    this->update(QRectF(0,0,this->width(), this->height()));
}

/* Как только нажали кнопку мыши, создаём фигуру одного из трёх типов
 * и помещаем её на сцену, сохранив указатель на неё в переменной
 * tempFigure
 * */
void PaintScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    switch (m_typeFigure) {
    case SquareType: {
        Square *item = new Square(event->scenePos());
        item->setPos(event->pos());
        tempFigure = item;
        break;
    }
    case RombType: {
        Romb *item = new Romb(event->scenePos());
        item->setPos(event->pos());
        tempFigure = item;
        break;
    }
    case TriangleType: {
        Triangle *item = new Triangle(event->scenePos());
        item->setPos(event->pos());
        tempFigure = item;
        break;
    }
    default:{
        Square *item = new Square(event->scenePos());
        item->setPos(event->pos());
        tempFigure = item;
        break;
    }
    }
    this->addItem(tempFigure);
}

figure.h

Заголовочный файл базового класса для создания геометрических фигур. Он предоставляет основной функционал для определения границ отрисовки геометрической фигуры вне зависимости от того, какая фигура будет отрисовываться, что обеспечивает полиморфизм. Для отрисовки используются две точки: стартовая и конечная . Отрисовка начинается из стартовой точки и она не меняется своего местоположения, а вот конечная точка является координатой противоположного угла прямоугольной области в которой отрисовывается фигура. В зависимости от этих точек перерисовываются и все остальные ключевые точки фигур, а соответственно и границы этих фигур.

#ifndef FIGURE_H
#define FIGURE_H

#include <QObject>
#include <QGraphicsItem>
#include <QDebug>

class Figure : public QObject, public QGraphicsItem
{
    Q_OBJECT
    // Свойство стартовой точки, относительно которой отрисовываем фигуру
    Q_PROPERTY(QPointF startPoint
               READ startPoint WRITE setStartPoint
               NOTIFY pointChanged)
    // Свойство конечной точки, до куда отрисовываем фигуру
    Q_PROPERTY(QPointF endPoint
               READ endPoint WRITE setEndPoint
               NOTIFY pointChanged)
public:
    explicit Figure(QPointF point, QObject *parent = 0);
    ~Figure();

    QPointF startPoint() const; // Стартовая точка
    QPointF endPoint() const;   // Конечная точка

    void setStartPoint(const QPointF point);    // Установка стартовой точки
    void setEndPoint(const QPointF point);      // Установка конечной точки

signals:
    void pointChanged();    // Сигнал об изменении точки

private:
    QPointF m_startPoint;   // Стартовая точка
    QPointF m_endPoint;     // Конечная точка

    QRectF boundingRect() const;    // Область, в которой содержится фигура

public slots:
    void updateRomb();     // Слот обновления области, в которой содержится фигура
};

#endif // FIGURE_H

figure.cpp

#include "figure.h"
#include <QPainter>

Figure::Figure(QPointF point, QObject *parent) :
    QObject(parent), QGraphicsItem()
{
    // Устанавливаем стартовую координату для отрисовки фигуры
    this->setStartPoint(mapFromScene(point));
    this->setEndPoint(mapFromScene(point));
    /* Подключаем сигнал изменения координат к слоту запуска обновления содержимого объекта
     * Сигнал и слот присутствуют в базовом классе
     * */
    connect(this, &Figure::pointChanged, this, &Figure::updateRomb);
}

Figure::~Figure()
{

}

QRectF Figure::boundingRect() const
{
    /* Возвращаем область, в которой лежит фигура.
     * Обновляемая область зависит от стартовой точки отрисовки и от конечной точки
     * */
    return QRectF((endPoint().x() > startPoint().x() ? startPoint().x() : endPoint().x()) - 5,
                  (endPoint().y() > startPoint().y() ? startPoint().y() : endPoint().y()) - 5,
                  qAbs(endPoint().x() - startPoint().x()) + 10,
                  qAbs(endPoint().y() - startPoint().y()) + 10);
}

void Figure::updateRomb()
{
    // Вызываем обновление области, в которой лежит фигура
    this->update((endPoint().x() > startPoint().x() ? startPoint().x() : endPoint().x()) - 5,
                 (endPoint().y() > startPoint().y() ? startPoint().y() : endPoint().y()) - 5,
                 qAbs(endPoint().x() - startPoint().x()) + 10,
                 qAbs(endPoint().y() - startPoint().y()) + 10);
}

void Figure::setStartPoint(const QPointF point)
{
    m_startPoint = mapFromScene(point);
    emit pointChanged();
}

void Figure::setEndPoint(const QPointF point)
{
    m_endPoint = mapFromScene(point);
    emit pointChanged();
}

QPointF Figure::startPoint() const
{
    return m_startPoint;
}

QPointF Figure::endPoint() const
{
    return m_endPoint;
}

romb.h

Всё, что нужно сделать в заголовочной файле класса необходимой нам фигуры - это наследоваться от базового класса Figure и переопределить метод paint(), который наследован от предка класса Figure, то есть от QGraphicsItem. В этом методе будет реализована логика отрисовки данной геометрической фигуры.

#ifndef ROMB_H
#define ROMB_H

#include <QObject>
#include <QGraphicsItem>
#include "figure.h"

/* Наследуемся от класса Figure,
 * в котором реализован общий для всех фигур функционал
 * */
class Romb : public Figure
{
    Q_OBJECT

public:
    explicit Romb(QPointF point, QObject *parent = 0);
    ~Romb();

private:
    // Для Ромба реализуем только саму отрисовку
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
};

#endif // ROMB_H

romb.cpp

#include "romb.h"
#include <QPainter>

Romb::Romb(QPointF point, QObject *parent) :
    Figure(point,parent)
{
    Q_UNUSED(point)
}

Romb::~Romb()
{

}

// Реализуем метод отрисовки
void Romb::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    painter->setPen(QPen(Qt::black, 2));

    QPolygonF polygon;

    polygon << QPointF(startPoint().x() + (endPoint().x() > startPoint().x() ? + 1 : - 1)*
                           abs((endPoint().x() - startPoint().x())/2), startPoint().y())
                << QPointF(endPoint().x(), startPoint().y() + (endPoint().y() > startPoint().y() ? + 1 : - 1)*
                           abs((endPoint().y() - startPoint().y())/2))
                << QPointF(startPoint().x() + (endPoint().x() > startPoint().x() ? + 1 : - 1)*
                           abs((endPoint().x() - startPoint().x())/2), endPoint().y())
                << QPointF(startPoint().x(), startPoint().y() + (endPoint().y() > startPoint().y() ? + 1 : - 1)*
                           abs((endPoint().y() - startPoint().y())/2));

    painter->drawPolygon(polygon);

    Q_UNUSED(option)
    Q_UNUSED(widget)
}

Классы Triangle и Square

Структура файлов данных классов аналогична классу Romb , разница лишь в логике реализации отрисовки фигуры.

Итог

В результате умелого использования парадигмы Полиморфизма можно значительно сократить объём программного кода, а также увеличить частоту его повторного использования, особенно если подразумевается наличие множества подобных объектов, как, например, геометрические фигуры.

Демонстрация полученного приложения приведена в видеоуроке.

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

Видеоурок

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

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

L
  • 20 февраля 2018 г. 17:10

Здравствуйте!
В программировании новичок и есть пара вопросов. Буду очень благодарен за ответ.

Не совсем понимаю как:
1) реализовать подобным образом рисование эллипса(конкретно выбор точек по типу как 22-29 строка в romb.cpp);
2) выбор цвета фигур через диалоговое окно выбора цвета (присваиваю переменной Color цвет, после его выбора в QColorDialog и вместо Qt::black пишу Color, но цвет остается черным).


Evgenii Legotckoi
  • 21 февраля 2018 г. 3:37

Добрый день!


1) Эллипс можно реализовать так
void Ellipse::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    painter->setPen(QPen(Qt::black, 2));


    QRectF rect(endPoint().x() > startPoint().x() ? startPoint().x() : endPoint().x(),
                endPoint().y() > startPoint().y() ? startPoint().y() : endPoint().y(),
                qAbs(endPoint().x() - startPoint().x()),
                qAbs(endPoint().y() - startPoint().y()));

    painter->drawEllipse(rect);

    Q_UNUSED(option)
    Q_UNUSED(widget)
}
2) Это уже надо смотреть тот участок кода, который вы используете для установки цвета. Возможно там мелкая ошибка. Задайте пожалуйста этот вопрос на форуме и приведите там тот код, который Вы написали.

L
  • 21 февраля 2018 г. 3:53

Огромное спасибо!

ЕЕ
  • 20 августа 2018 г. 13:02

Добрый день, начал только изучать Qt C++.

Никак не могу понять, как удалять последний созданный элемент.
Заранее спасибо.
Evgenii Legotckoi
  • 23 августа 2018 г. 5:56

Здесь скорее нужно использовать стек из указателей, чтобы хранить поочерёдно добавляемые элементы. Например, QVector в конец которого будете добавлять элементы, которые были добавлены на графическую сцену.

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

К
  • 22 мая 2022 г. 6:32

Крутой урок! Но как только захотел добавить функцию выделение объекта ( подразумевается перемещение и изменение размера) ничего не получается . Читал что нужно включить флаги ItemIsSelected и ItemIsMovable , и сделал точно также ка к в вашем примере при перетаскивании мышью(23 урок) , но ничего не заработало.Не подскажите как это должно выглядить?

Evgenii Legotckoi
  • 23 мая 2022 г. 7:23

Ну не видя вашего кода, сказать, где у вас ошибка невозможно.
А что касается перетаскивания и разных манипуляций, то можете распотрошить код векторного редактора из этой статьи Qt/C++ - Урок 072. Пример векторного редактора на Qt .
Там достаточно примеров кода по интересующим вас вопросам.
А писать какие-то ещё примеры на эту тему у меня сейчас очень сильно нет времени.

f
  • 9 декабря 2022 г. 19:46

Здравствуйте! Очень нужна помощь, как сделать так чтобы выбранный пользователем цвет границ фигуры применялся только к новой фигуре(которую только собираешься нарисовать), а не ко всем фигурам(уже нарисованным)? Пожалуйста помогите, никак не могу наладить

Evgenii Legotckoi
  • 12 декабря 2022 г. 3:45

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

Комментарии

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

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:50баллов,
  • Очки рейтинга-4
m
  • molni99
  • 26 октября 2024 г. 1:37

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80баллов,
  • Очки рейтинга4
m
  • molni99
  • 26 октября 2024 г. 1:29

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
ИМ
Игорь Максимов22 ноября 2024 г. 11:51
Django - Урок 017. Кастомизированная страница авторизации на Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 октября 2024 г. 14:37
Django - Урок 064. Как написать расширение для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 октября 2024 г. 8:19
Читалка fb3-файлов на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов5 октября 2024 г. 7:51
Django - Урок 064. Как написать расширение для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55 июля 2024 г. 11:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Сейчас обсуждают на форуме
m
moogo22 ноября 2024 г. 7:17
Mosquito Spray System Effective Mosquito Systems for Backyard | Eco-Friendly Misting Control Device & Repellent Spray - Moogo ; Upgrade your backyard with our mosquito-repellent device! Our misters conce…
Evgenii Legotckoi
Evgenii Legotckoi24 июня 2024 г. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 ноября 2024 г. 6:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject4 июня 2022 г. 3:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

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