Evgenii Legotckoi
Evgenii Legotckoi12 сентября 2015 г. 12:12

Qt/C++ - Урок 017. QGraphicsScene или как работать с графикой в Qt

Начиная с этой статьи Мы приступаем к изучению графических библиотек Qt, а если быть точнее, QGraphicsScene. Данный класс предоставляет функционал по управлению большим количеством 2D объектов. QGraphicsScene устанавливается в QGraphicsView.

Опишем функционал, который необходимо будет реализовать в нашем приложении:

  • Добавление графической сцены в QGraphicsView.
  • Отрисовка двух объектов на графической сцене с помощью линий, а именно прямоугольника и квадрата.
  • Динамическое изменение размеров графической сцены в зависимости от изменения размеров QGraphicsView.
  • Динамическое изменение объектов на графической сцене в зависимости от размеров самой графической сцены.

Программный код был написан в QtCreator 3.3.1 на основе Qt 5.4.1.

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

Структура проекта для работы с QGraphicsScene К структуре проекта "по умолчанию" добавляется ещё один класс MyGraphicView.

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


mainwindow.ui

Внешний вид приложения представляет из себя главное окно и GridLayout, растянутый на всё это окно. По итогам урокам приложение будет выглядеть следующим образом:

Внешний вид приложения с QGraphicsScene ## mainwindow.h

Всё, что делаем в этом файле, это подключаем заголовочный файл MyGraphicsView.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include <mygraphicview.h>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private:
    Ui::MainWindow  *ui;
    MyGraphicView   *myPicture;     // Наш кастомный виджет
};

#endif // MAINWINDOW_H

mainwindow.cpp

Данный файл также ничем не примечателен. Инициализируем виджет и добавляем его GridLayout окна приложения.

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

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

    /* Инициализируем виджет с графикой */
    myPicture   = new MyGraphicView();
    /* и добавляем его на слой */
    ui->graphicLayout->addWidget(myPicture);
}

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

mygraphicview.h

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

Для динамической перерисовки графической сцены, а также при создании главного окна, параметры ширины и высоты окна устанавливаются не сразу, а после полной отрисовки, поэтому для отрисовки содержимого виджета потребуется некоторая задержка по времени, чтобы получить правильные значения ширины и высоты виджета, в котором будет содержаться QGraphicsScene. Для этого воспользуемся классом QTimer , по переполнению которого будем вызывать СЛОТ, в котором уже будет происходить отрисовка содержимого графической сцены и корректировка её размеров.

#ifndef MYGRAPHICVIEW_H
#define MYGRAPHICVIEW_H

#include <QWidget>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsItemGroup>
#include <QTimer>


// Расширяем класс QGraphicsView
class MyGraphicView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit MyGraphicView(QWidget *parent = 0);
    ~MyGraphicView();

signals:

private slots:
    void slotAlarmTimer();  /* слот для обработчика переполнения таймера
                             * в нём будет производиться перерисовка
                             * виджета
                             * */

private:
    QGraphicsScene      *scene;     // Объявляем сцену для отрисовки
    QGraphicsItemGroup  *group_1;   // Объявляем первую группу элементов
    QGraphicsItemGroup  *group_2;   // Объявляем вторую группу элементов

    /* Таймер для задержки отрисовки.
     * Дело в том, что при создании окна и виджета
     * необходимо некоторое время, чтобы родительский слой
     * развернулся, чтобы принимать от него адекватные параметры
     * ширины и высоты
     * */
    QTimer              *timer;

private:
    /* Перегружаем событие изменения размера окна,
     * чтобы перехватывать его
     * */
    void resizeEvent(QResizeEvent *event);
    /* Метод для удаления всех элементов
     * из группы элементов
     * */
    void deleteItemsFromGroup(QGraphicsItemGroup *group_1);
};

#endif // MYGRAPHICVIEW_H

mygraphicview.cpp

Для перерисовки объектов в QGraphicsScene эти самые объекты необходимо будет удалять, поэтому для удобства работы элементы этих объектов буду сгруппированы, а также будет написан метод для удаления всех элементов группы. Это удобно в том случае, если Вам необходимо перерисовать только один объект из нескольких, который состоит из ряда элементов.

#include "mygraphicview.h"

MyGraphicView::MyGraphicView(QWidget *parent)
    : QGraphicsView(parent)
{

    /* Немного поднастроим отображение виджета и его содержимого */
    this->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключим скроллбар по горизонтали
    this->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);   // Отключим скроллбар по вертикали
    this->setAlignment(Qt::AlignCenter);                        // Делаем привязку содержимого к центру
    this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);    // Растягиваем содержимое по виджету

    /* Также зададим минимальные размеры виджета
     * */
    this->setMinimumHeight(100);
    this->setMinimumWidth(100);

    scene = new QGraphicsScene();   // Инициализируем сцену для отрисовки
    this->setScene(scene);          // Устанавливаем сцену в виджет

    group_1 = new QGraphicsItemGroup(); // Инициализируем первую группу элементов
    group_2 = new QGraphicsItemGroup(); // Инициализируем вторую группу элементов

    scene->addItem(group_1);            // Добавляем первую группу в сцену
    scene->addItem(group_2);            // Добавляем вторую группу в сцену

    timer = new QTimer();               // Инициализируем Таймер
    timer->setSingleShot(true);
    // Подключаем СЛОТ для отрисовки к таймеру
    connect(timer, SIGNAL(timeout()), this, SLOT(slotAlarmTimer()));
    timer->start(50);                   // Стартуем таймер на 50 миллисекунд
}

MyGraphicView::~MyGraphicView()
{

}

void MyGraphicView::slotAlarmTimer()
{
    /* Удаляем все элементы со сцены,
     * если они есть перед новой отрисовкой
     * */
    this->deleteItemsFromGroup(group_1);
    this->deleteItemsFromGroup(group_2);

    int width = this->width();      // определяем ширину нашего виджета
    int height = this->height();    // определяем высоту нашего виджета

    /* Устанавливаем размер сцены по размеру виджета
     * Первая координата - это левый верхний угол,
     * а Вторая - это правый нижний угол
     * */
    scene->setSceneRect(0,0,width,height);

    /* Приступаем к отрисовке произвольной картинки
     * */
    QPen penBlack(Qt::black); // Задаём чёрную кисть
    QPen penRed(Qt::red);   // Задаём красную кисть

    /* Нарисуем черный прямоугольник
     * */
    group_1->addToGroup(scene->addLine(20,20, width - 20, 20, penBlack));
    group_1->addToGroup(scene->addLine(width - 20, 20, width - 20, height -20, penBlack));
    group_1->addToGroup(scene->addLine(width - 20, height -20, 20, height -20, penBlack));
    group_1->addToGroup(scene->addLine(20, height -20, 20, 20, penBlack));

    /* Нарисуем красный квадрат
     * */
    int sideOfSquare = (height > width) ? (width - 60) : (height - 60);
    int centerOfWidget_X = width/2;
    int centerOfWidget_Y = height/2;

    group_2->addToGroup(scene->addLine(centerOfWidget_X - (sideOfSquare/2),
                                       centerOfWidget_Y - (sideOfSquare/2),
                                       centerOfWidget_X + (sideOfSquare/2),
                                       centerOfWidget_Y - (sideOfSquare/2),
                                       penRed));

    group_2->addToGroup(scene->addLine(centerOfWidget_X + (sideOfSquare/2),
                                       centerOfWidget_Y - (sideOfSquare/2),
                                       centerOfWidget_X + (sideOfSquare/2),
                                       centerOfWidget_Y + (sideOfSquare/2),
                                       penRed));

    group_2->addToGroup(scene->addLine(centerOfWidget_X + (sideOfSquare/2),
                                       centerOfWidget_Y + (sideOfSquare/2),
                                       centerOfWidget_X - (sideOfSquare/2),
                                       centerOfWidget_Y + (sideOfSquare/2),
                                       penRed));

    group_2->addToGroup(scene->addLine(centerOfWidget_X - (sideOfSquare/2),
                                       centerOfWidget_Y + (sideOfSquare/2),
                                       centerOfWidget_X - (sideOfSquare/2),
                                       centerOfWidget_Y - (sideOfSquare/2),
                                       penRed));
}

/* Этим методом перехватываем событие изменения размера виджет
 * */
void MyGraphicView::resizeEvent(QResizeEvent *event)
{
    timer->start(50);   // Как только событие произошло стартуем таймер для отрисовки
    QGraphicsView::resizeEvent(event);  // Запускаем событие родителького класса
}


/* Метод для удаления всех элементов из группы
 * */
void MyGraphicView::deleteItemsFromGroup(QGraphicsItemGroup *group)
{
    /* Перебираем все элементы сцены, и если они принадлежат группе,
     * переданной в метод, то удаляем их
     * */
    foreach( QGraphicsItem *item, scene->items(group->boundingRect())) {
       if(item->group() == group ) {
          delete item;
       }
    }
}

Итог

Результат работы приложения продемонстрирован на следующем видео с момента 5:27. До этого момента в видео присутствуют пояснения проекта.

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

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

D
  • 28 марта 2017 г. 8:42

Добрый день, Евгений! Спасибо за ваши уроки и за ваш исходный код к ним. Только небольшой вопрос - а где исходный код главной функции main()? без нее ничего не запускается. Напишите, пожалуйста, ответ, ну или, исходный код функции main(). Заранее спасибо.

Evgenii Legotckoi
  • 28 марта 2017 г. 8:55

Добрый день, Doug.
Я обычно прикладываю код функции main тогда, когда она отличается от файла в проекте созданном по умолчанию.

Например здесь, используется класс mainwindow для окна приложения. Он будет выглядеть так, если создадите в Qt Creator приложение на Qt, у которого базовым классом будет класс QMainWindow. 

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

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow w;
    w.show();

    return a.exec();
}
D
  • 28 марта 2017 г. 9:09

Спасибо, Евгений!! А файл main.cpp во всех других уроках останется таким же? ( я имею ввиду уроки, где вы рассказываете о работе с 2d и QGraphicsScene ? ... и, если есть какие-то изменения, не могли бы вы тоже добавить в другие уроки файл main.cpp c главной функцией main()? Еще раз спасибо.

Evgenii Legotckoi
  • 28 марта 2017 г. 9:17

Там может быть разница в том, что класс окна может быть Widget вместо MainWindow . Но это должно быть очевидно из класса главного окна приложения. Поэтому просто вместо MainWindow пишите Widget . Код этой функции приводится в том случае, если он отличается от кода по умолчанию при создании нового проекта.

D
  • 28 марта 2017 г. 10:44

Понял. Хорошо.Спасибо. Удачи Вам!

D
  • 29 марта 2017 г. 0:52

Добрый день! Можно такой вопрос - как организовать перемещение / поворот фигур ПРАВОЙ кнопкой мыши? И как сделать, так чтобы фигуру в фигуре (например, квадрат в треугольнике), можно было перемещать / вращать как единое целое (монолит)? Спасибо.

Evgenii Legotckoi
  • 29 марта 2017 г. 1:11

Что касается перемещения, то можно организовать либо как сделано в этой статье

Либо можно воспользоваться флагами:

  1. ItemIsMovable - включает возможность перемещения
  2. ItemIsSelectable - включает возможность выделения объектов

Флаги применяются непосредственно к QGraphicsItem . Можно сразу в конструкторе настраивать.

setFlags(QGraphicsItem::ItemIsMovable|QGraphicsItem::ItemIsSelectable);

Что касается поворота объектов, то встроенными средствами Qt это не реализуется, в том смысле, что там нет волшебного флага наподобие ItemIsMovable , который просто возьмёт и включит данную возможность. Я как-то реализовывал подобное в одном тестовом задании, но нужно искать исходники. Там много строчек написано. Поэтому ответить сходу затрудняюсь.

P/S/ Этот ваш последний вопрос имеет косвенное отношение к статье, поэтому не могли бы Вы задавать такие вопросы сразу на форуме ? Чтобы была уже отдельная ветка обсуждения.

D
  • 29 марта 2017 г. 1:27

Спасибо, Евгений за Ваш такой оперативный ответ. Не знал, что есть форум. Спасибо за ссылку.

МК
  • 27 сентября 2017 г. 13:12

Благодарю за уроки!
Сделал вроде бы всё как написано, но при изменении размеров окна не происходит изменения размеров виджета, и соответственно геометрических фигур. В чём причина, подскажите.
Кстати, можно сделать подключение сигнала к слоту в новом синтаксисе:

// Подключаем СЛОТ для отрисовки к таймеру
//connect(timer, SIGNAL(timeout()), this, SLOT(slotAlarmTimer()));
connect(timer, &QTimer::timeout, this, &MyGraphicView::slotAlarmTimer);

Нашёл ответ на просторах интернета:
В Дизайнере ПКМ по MainWindow: Компоновка - Скомпоновать по сетке.
Нужно писать о таких простых, но совсем не очевидных для новичка вещах.
ЗЫ: увидел у вас 24-ый урок про сигналы и слоты, так что вторая часть предыдущего комментария также снимается ))

Evgenii Legotckoi
  • 27 сентября 2017 г. 13:39

Добрый день!
О всех очевидных вещах не напишешь. Молодца, что нашли решение самостоятельно.

a
  • 10 мая 2018 г. 1:34

Евгений, добрый день!

А где можно исходники найти?

a
  • 10 мая 2018 г. 3:25

Взял код отсюда. Приделал graphicLayout. Всё, заработало, спасибо.

Правда, так и не понятно зачем этот таймер, зачем эта задержка при отрисовки. Почему бы сразу мгновенно не отрисовывать?
Evgenii Legotckoi
  • 10 мая 2018 г. 3:47

Я уже не помню какая там проблема была.

r
  • 25 сентября 2018 г. 6:37

В статье написано, что таймер сработает один раз. Но это не так. Было бы хорошо добавить

timer->setSingleShot(true);

После инициализации таймера.

Evgenii Legotckoi
  • 25 сентября 2018 г. 6:43

Прямо так не написано.

Хотя соглашусь, что в качестве улучшения вызов данного метода здесь к месту.

G.
  • 31 января 2019 г. 7:48

Где вызывается наш метод resizeEvent() ?

Evgenii Legotckoi
  • 31 января 2019 г. 8:01

Он вызывается в очереди событий в рамках обработки событий взаимодействия с окном. Это всё крутится под капотом Qt. У виджетов есть методы Event, они вызываются в зависимости от срабатывания событий. Если хотите знать, где конкретно, то лезьте в исходники Qt.

G.
  • 31 января 2019 г. 10:38

Спасибо большое! Разобрался.

МА
  • 26 февраля 2019 г. 7:07
  • (ред.)

В файле "mainwindow.cpp" есть строчка:
ui->graphicLayout->addWidget(myPicture);

Возникает вопрос: что такое "graphicLayout"? Где/Как его найти/создать?

upd
Уже разобрался. Т.к. работаю с C++ всего пару часов, не сразу понял, что это просто объект QGridLayout.

Evgenii Legotckoi
  • 26 февраля 2019 г. 7:27
  • (ред.)

Объект был создан через графический дизайнер. ui создаётся в графическом дизайнере.

Комментарии

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

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

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

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

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

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

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
ИМ
Игорь Максимов22 ноября 2024 г. 19:51
Django - Урок 017. Кастомизированная страница авторизации на Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 октября 2024 г. 21:37
Django - Урок 064. Как написать расширение для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 октября 2024 г. 15:19
Читалка fb3-файлов на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов5 октября 2024 г. 14:51
Django - Урок 064. Как написать расширение для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55 июля 2024 г. 18:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Сейчас обсуждают на форуме
m
moogo22 ноября 2024 г. 15: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 г. 22:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 ноября 2024 г. 14:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject4 июня 2022 г. 10:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

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