Evgenii Legotckoi
12 вересня 2015 р. 22: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.

  1. #ifndef MAINWINDOW_H
  2. #define MAINWINDOW_H
  3.  
  4. #include <QMainWindow>
  5.  
  6. #include <mygraphicview.h>
  7.  
  8. namespace Ui {
  9. class MainWindow;
  10. }
  11.  
  12. class MainWindow : public QMainWindow
  13. {
  14. Q_OBJECT
  15.  
  16. public:
  17. explicit MainWindow(QWidget *parent = 0);
  18. ~MainWindow();
  19.  
  20. private:
  21. Ui::MainWindow *ui;
  22. MyGraphicView *myPicture; // Наш кастомный виджет
  23. };
  24.  
  25. #endif // MAINWINDOW_H

mainwindow.cpp

Цей файл також нічим не примітний. Ініціалізуємо віджет та додаємо його GridLayout вікна програми.

  1. #include "mainwindow.h"
  2. #include "ui_mainwindow.h"
  3.  
  4. MainWindow::MainWindow(QWidget *parent) :
  5. QMainWindow(parent),
  6. ui(new Ui::MainWindow)
  7. {
  8. ui->setupUi(this);
  9.  
  10. /* Инициализируем виджет с графикой */
  11. myPicture = new MyGraphicView();
  12. /* и добавляем его на слой */
  13. ui->graphicLayout->addWidget(myPicture);
  14. }
  15.  
  16. MainWindow::~MainWindow()
  17. {
  18. delete ui;
  19. }

mygraphicview.h

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

Для динамічного перемальовування графічної сцени, а також при створенні головного вікна, параметри ширини і висоти вікна встановлюються не відразу, а після повного відмальовування, тому для відображення вмісту віджету потрібна деяка затримка часу, щоб отримати правильні значення ширини та висоти віджета, в якому буде утримуватися QGraphicsScene. Для цього скористаємося класом QTimer , по переповненню якого викликатимемо СЛОТ, у якому вже відбуватиметься малювання вмісту графічної сцени та коригування її розмірів.

  1. #ifndef MYGRAPHICVIEW_H
  2. #define MYGRAPHICVIEW_H
  3.  
  4. #include <QWidget>
  5. #include <QGraphicsView>
  6. #include <QGraphicsScene>
  7. #include <QGraphicsItemGroup>
  8. #include <QTimer>
  9.  
  10.  
  11. // Расширяем класс QGraphicsView
  12. class MyGraphicView : public QGraphicsView
  13. {
  14. Q_OBJECT
  15. public:
  16. explicit MyGraphicView(QWidget *parent = 0);
  17. ~MyGraphicView();
  18.  
  19. signals:
  20.  
  21. private slots:
  22. void slotAlarmTimer(); /* слот для обработчика переполнения таймера
  23. * в нём будет производиться перерисовка
  24. * виджета
  25. * */
  26.  
  27. private:
  28. QGraphicsScene *scene; // Объявляем сцену для отрисовки
  29. QGraphicsItemGroup *group_1; // Объявляем первую группу элементов
  30. QGraphicsItemGroup *group_2; // Объявляем вторую группу элементов
  31.  
  32. /* Таймер для задержки отрисовки.
  33. * Дело в том, что при создании окна и виджета
  34. * необходимо некоторое время, чтобы родительский слой
  35. * развернулся, чтобы принимать от него адекватные параметры
  36. * ширины и высоты
  37. * */
  38. QTimer *timer;
  39.  
  40. private:
  41. /* Перегружаем событие изменения размера окна,
  42. * чтобы перехватывать его
  43. * */
  44. void resizeEvent(QResizeEvent *event);
  45. /* Метод для удаления всех элементов
  46. * из группы элементов
  47. * */
  48. void deleteItemsFromGroup(QGraphicsItemGroup *group_1);
  49. };
  50.  
  51. #endif // MYGRAPHICVIEW_H

mygraphicview.cpp

Для перемальовування об'єктів у QGraphicsScene ці об'єкти необхідно буде видаляти, тому для зручності роботи елементи цих об'єктів буду згруповані, а також буде написаний метод для видалення всіх елементів групи. Це зручно в тому випадку, якщо Вам необхідно перемалювати лише один об'єкт із кількох, який складається з низки елементів.

  1. #include "mygraphicview.h"
  2.  
  3. MyGraphicView::MyGraphicView(QWidget *parent)
  4. : QGraphicsView(parent)
  5. {
  6.  
  7. /* Немного поднастроим отображение виджета и его содержимого */
  8. this->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключим скроллбар по горизонтали
  9. this->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключим скроллбар по вертикали
  10. this->setAlignment(Qt::AlignCenter); // Делаем привязку содержимого к центру
  11. this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // Растягиваем содержимое по виджету
  12.  
  13. /* Также зададим минимальные размеры виджета
  14. * */
  15. this->setMinimumHeight(100);
  16. this->setMinimumWidth(100);
  17.  
  18. scene = new QGraphicsScene(); // Инициализируем сцену для отрисовки
  19. this->setScene(scene); // Устанавливаем сцену в виджет
  20.  
  21. group_1 = new QGraphicsItemGroup(); // Инициализируем первую группу элементов
  22. group_2 = new QGraphicsItemGroup(); // Инициализируем вторую группу элементов
  23.  
  24. scene->addItem(group_1); // Добавляем первую группу в сцену
  25. scene->addItem(group_2); // Добавляем вторую группу в сцену
  26.  
  27. timer = new QTimer(); // Инициализируем Таймер
  28. timer->setSingleShot(true);
  29. // Подключаем СЛОТ для отрисовки к таймеру
  30. connect(timer, SIGNAL(timeout()), this, SLOT(slotAlarmTimer()));
  31. timer->start(50); // Стартуем таймер на 50 миллисекунд
  32. }
  33.  
  34. MyGraphicView::~MyGraphicView()
  35. {
  36.  
  37. }
  38.  
  39. void MyGraphicView::slotAlarmTimer()
  40. {
  41. /* Удаляем все элементы со сцены,
  42. * если они есть перед новой отрисовкой
  43. * */
  44. this->deleteItemsFromGroup(group_1);
  45. this->deleteItemsFromGroup(group_2);
  46.  
  47. int width = this->width(); // определяем ширину нашего виджета
  48. int height = this->height(); // определяем высоту нашего виджета
  49.  
  50. /* Устанавливаем размер сцены по размеру виджета
  51. * Первая координата - это левый верхний угол,
  52. * а Вторая - это правый нижний угол
  53. * */
  54. scene->setSceneRect(0,0,width,height);
  55.  
  56. /* Приступаем к отрисовке произвольной картинки
  57. * */
  58. QPen penBlack(Qt::black); // Задаём чёрную кисть
  59. QPen penRed(Qt::red); // Задаём красную кисть
  60.  
  61. /* Нарисуем черный прямоугольник
  62. * */
  63. group_1->addToGroup(scene->addLine(20,20, width - 20, 20, penBlack));
  64. group_1->addToGroup(scene->addLine(width - 20, 20, width - 20, height -20, penBlack));
  65. group_1->addToGroup(scene->addLine(width - 20, height -20, 20, height -20, penBlack));
  66. group_1->addToGroup(scene->addLine(20, height -20, 20, 20, penBlack));
  67.  
  68. /* Нарисуем красный квадрат
  69. * */
  70. int sideOfSquare = (height > width) ? (width - 60) : (height - 60);
  71. int centerOfWidget_X = width/2;
  72. int centerOfWidget_Y = height/2;
  73.  
  74. group_2->addToGroup(scene->addLine(centerOfWidget_X - (sideOfSquare/2),
  75. centerOfWidget_Y - (sideOfSquare/2),
  76. centerOfWidget_X + (sideOfSquare/2),
  77. centerOfWidget_Y - (sideOfSquare/2),
  78. penRed));
  79.  
  80. group_2->addToGroup(scene->addLine(centerOfWidget_X + (sideOfSquare/2),
  81. centerOfWidget_Y - (sideOfSquare/2),
  82. centerOfWidget_X + (sideOfSquare/2),
  83. centerOfWidget_Y + (sideOfSquare/2),
  84. penRed));
  85.  
  86. group_2->addToGroup(scene->addLine(centerOfWidget_X + (sideOfSquare/2),
  87. centerOfWidget_Y + (sideOfSquare/2),
  88. centerOfWidget_X - (sideOfSquare/2),
  89. centerOfWidget_Y + (sideOfSquare/2),
  90. penRed));
  91.  
  92. group_2->addToGroup(scene->addLine(centerOfWidget_X - (sideOfSquare/2),
  93. centerOfWidget_Y + (sideOfSquare/2),
  94. centerOfWidget_X - (sideOfSquare/2),
  95. centerOfWidget_Y - (sideOfSquare/2),
  96. penRed));
  97. }
  98.  
  99. /* Этим методом перехватываем событие изменения размера виджет
  100. * */
  101. void MyGraphicView::resizeEvent(QResizeEvent *event)
  102. {
  103. timer->start(50); // Как только событие произошло стартуем таймер для отрисовки
  104. QGraphicsView::resizeEvent(event); // Запускаем событие родителького класса
  105. }
  106.  
  107.  
  108. /* Метод для удаления всех элементов из группы
  109. * */
  110. void MyGraphicView::deleteItemsFromGroup(QGraphicsItemGroup *group)
  111. {
  112. /* Перебираем все элементы сцены, и если они принадлежат группе,
  113. * переданной в метод, то удаляем их
  114. * */
  115. foreach( QGraphicsItem *item, scene->items(group->boundingRect())) {
  116. if(item->group() == group ) {
  117. delete item;
  118. }
  119. }
  120. }

Підсумок

Результат роботи програми продемонстровано на наступному відео з 5:27. До цього моменту у відео присутні пояснення проекту.

Вам це подобається? Поділіться в соціальних мережах!

D
  • 28 березня 2017 р. 18:42

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

Evgenii Legotckoi
  • 28 березня 2017 р. 18: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 р. 19:09

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

Evgenii Legotckoi
  • 28 березня 2017 р. 19:17

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

D
  • 28 березня 2017 р. 20:44

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

D
  • 29 березня 2017 р. 10:52

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

Evgenii Legotckoi
  • 29 березня 2017 р. 11:11

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

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

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

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

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

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

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

D
  • 29 березня 2017 р. 11:27

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

МК
  • 27 вересня 2017 р. 23:12

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

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

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

Evgenii Legotckoi
  • 27 вересня 2017 р. 23:39

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

a
  • 10 травня 2018 р. 11:34

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

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

a
  • 10 травня 2018 р. 13:25

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

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

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

r
  • 25 вересня 2018 р. 16:37

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

  1. timer->setSingleShot(true);

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

Evgenii Legotckoi
  • 25 вересня 2018 р. 16:43

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

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

G.
  • 31 січня 2019 р. 18:48

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

Evgenii Legotckoi
  • 31 січня 2019 р. 19:01

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

G.
  • 31 січня 2019 р. 21:38

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

МА
  • 26 лютого 2019 р. 18:07
  • (відредаговано)

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

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

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

Evgenii Legotckoi
  • 26 лютого 2019 р. 18:27
  • (відредаговано)

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

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
  • Останні коментарі
  • Evgenii Legotckoi
    16 квітня 2025 р. 17:08
    Благодарю за отзыв. И вам желаю всяческих успехов!
  • IscanderChe
    12 квітня 2025 р. 17:12
    Добрый день. Спасибо Вам за этот проект и отдельно за ответы на форуме, которые мне очень помогли в некоммерческих пет-проектах. Профессиональным программистом я так и не стал, но узнал мно…
  • AK
    01 квітня 2025 р. 11:41
    Добрый день. В данный момент работаю над проектом, где необходимо выводить звук из программы в определенное аудиоустройство (колонки, наушники, виртуальный кабель и т.д). Пишу на Qt5.12.12 поско…
  • Evgenii Legotckoi
    09 березня 2025 р. 21:02
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    09 березня 2025 р. 16:14
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…