06. Множества точек
В прошлой главе мы создали демонстрационную программу, углубляться в механику работы не будем, все команды снабжены комментариями.
Приложение, работающее с множествами точек, мы будем писать два урока. Можно использовать репозиторий из прошлого задания.
Пример готового проекта, который должен быть получен
в конце второй части, можно посмотреть здесь.
Fork
репозитория делать запрещается.
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/Window/Event.hpp>
#include <imgui-SFML.h>
#include <imgui.h>
// главный метод
int main() {
// создаём окно для рисования
sf::RenderWindow window(sf::VideoMode(1280, 720), "ImGui + SFML = <3");
// задаём частоту перерисовки окна
window.setFramerateLimit(60);
// инициализация imgui+sfml
ImGui::SFML::Init(window);
// переменная таймера
sf::Clock deltaClock;
// пока окно открыто, запускаем бесконечный цикл
while (window.isOpen()) {
// создаём событие sfml
sf::Event event;
// пока окно принимает события
while (window.pollEvent(event)) {
// отправляем события на обработку sfml
ImGui::SFML::ProcessEvent(event);
// если событие - это закрытие окна
if (event.type == sf::Event::Closed) {
// закрываем окно
window.close();
}
}
// запускаем обновление окна по таймеру с заданной частотой
ImGui::SFML::Update(window, deltaClock.restart());
// рисуем демонстрационное окно
ImGui::ShowDemoWindow();
// очищаем окно
window.clear();
// рисуем по окну средствами imgui+sfml
ImGui::SFML::Render(window);
// отображаем изменения на окне
window.display();
}
// завершаем работу imgui+sfml
ImGui::SFML::Shutdown();
return 0;
}
Событие - это любой сигнал от операционной системы нашему приложению. Например: нажатие кнопок, изменение размера окна, клик по нему. Операционная система не использует впрямую определённый язык, она позволяет обмениваться приложениям при помощи универсальных посылок, а уже менеджеры окон позволят перевести эти универсальные посылки-события в конструкции того или иного языка.
Суть работы программы заключается в запуске бесконечного цикла с заданной задержкой, внутри которого
сначала обрабатываются все события операционной системы, потом подготавливаются служебные переменные
командой ImGui::SFML::Update()
, потом выполняются непосредственные команды рисования. После них
идут команды применения этих команд к непосредственному рисованию на экране.
Фон приложения
Любой цвет можно задать при помощи трёх базовых. В программировании самый распространённый набор базовых цветов - это RGB, ещё этот набор называют моделью RGB.
Базовой единицей imgui
внутри окна является собственное окно. Окна создаются последовательно.
Например, если мы добавим строчки
// создаём второе окно управления
ImGui::Begin("Control");
// значение цвета по умолчанию
float color[3] = {0.12f, 0.12f, 0.13f};
// Инструмент выбора цвета
if (ImGui::ColorEdit3("Background color", color)) {
// код вызывается при изменении значения
}
// конец рисование окна
ImGui::End();
перед
// рисуем демонстрационное окно
ImGui::ShowDemoWindow();
то теперь программа будет выглядеть так:
Если навести курсор на квадрат рядом с Background color
, то отобразится увеличенный индикатор цевта
Если кликнуть по нему, то откроется палитра
Также можно зажать один из ползунков и изменить значение
Если задать Ctrl
и кликнуть по ::ShowDemoWindow
то Clion
откроет исходники этого метода.
Здесь прописаны все элементы демонстрационного окна. Если вам что-то приглянулось, можете просто найти этот элемент в исходниках и скопировать к себе.
Например, элементу HoldToRepeat
(выделен красными скобками)
соответствует выделенный код
Сотрём теперь команду
// рисуем демонстрационное окно
ImGui::ShowDemoWindow();
и поменяем заголовок окна на Geometry Project 10
:
// создаём окно для рисования
sf::RenderWindow window(sf::VideoMode(1280, 720), "Geometry Project 10");
Запустите программу, теперь ваше приложение должно выглядеть примерно так:
Свяжем теперь инструмент управления цвета с фоном.
Для начала сделаем color
глобальной, и создадим глобальную переменную цвета bgColor
, понятную imgui
:
// цвет фона
static sf::Color bgColor;
// значение цвета по умолчанию
float color[3] = {0.12f, 0.12f, 0.13f};
Теперь напишем функцию, задающую значения переменно bgColor
по вещественному
массиву компонент от до .
// задать цвет фона по вещественному массиву компонент
static void setColor(float * pDouble){
bgColor.r = static_cast<sf::Uint8>(pDouble[0] * 255.f);
bgColor.g = static_cast<sf::Uint8>(pDouble[1] * 255.f);
bgColor.b = static_cast<sf::Uint8>(pDouble[2] * 255.f);
}
С помощью структуры sf::Color(100, 200, 150)
задаётся цвет. Все параметры могут
принимать значения от до включительно, каждый параметр - это компонента
цвета - красный, - зелёный - синий.
Теперь нам нужно добавить команду setColor(color)
, задающую цвет фона при старте
приложения:
// задаём частоту перерисовки окна
window.setFramerateLimit(60);
// инициализация imgui+sfml
ImGui::SFML::Init(window);
// задаём цвет фона
setColor(color);
// переменная таймера
sf::Clock deltaClock;
и при изменении параметров цвета:
// создаём окно управления
ImGui::Begin("Control");
// Инструмент выбора цвета
if (ImGui::ColorEdit3("Background color", color)) {
// код вызывается при изменении значения
// задаём цвет фона
setColor(color);
}
// конец рисование окна
ImGui::End();
Теперь каждый раз, когда меняются параметры цвета будет меняться переменная bgColor
.
Чтобы использовать её в качестве цвета фона, нужно переписать команду windows.clear()
:
// конец рисование окна
ImGui::End();
// очищаем окно
window.clear(bgColor);
// рисуем по окну средствами imgui+sfml
ImGui::SFML::Render(window);
Если теперь запустить приложение, то фон изменится
Модель RGB составлена по первым буквам названий базовых цветов: Red(красный), Green(зелёный) и Blue(синий). Чем больше значение соответствующей компоненты, тем значительнее вклад того или иного базового цвета.
Если все компоненты имеют максимальное значение, то мы получим белый цвет, а если минимальное, то чёрный.
При этом, если поменять цвет в окне параметров цвета, то изменится и фон:
Весь код примера:
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/Window/Event.hpp>
#include <imgui-SFML.h>
#include <imgui.h>
// цвет фона
static sf::Color bgColor;
// значение цвета по умолчанию
float color[3] = {0.12f, 0.12f, 0.13f};
// задать цвет фона по вещественному массиву компонент
static void setColor(float * pDouble){
bgColor.r = static_cast<sf::Uint8>(pDouble[0] * 255.f);
bgColor.g = static_cast<sf::Uint8>(pDouble[1] * 255.f);
bgColor.b = static_cast<sf::Uint8>(pDouble[2] * 255.f);
}
// главный метод
int main() {
// создаём окно для рисования
sf::RenderWindow window(sf::VideoMode(1280, 720), "Geometry Project 10");
// задаём частоту перерисовки окна
window.setFramerateLimit(60);
// инициализация imgui+sfml
ImGui::SFML::Init(window);
// задаём цвет фона
setColor(color);
// переменная таймера
sf::Clock deltaClock;
// пока окно открыто, запускаем бесконечный цикл
while (window.isOpen()) {
// создаём событие sfml
sf::Event event;
// пока окно принимает события
while (window.pollEvent(event)) {
// отправляем события на обработку sfml
ImGui::SFML::ProcessEvent(event);
// если событие - это закрытие окна
if (event.type == sf::Event::Closed) {
// закрываем окно
window.close();
}
}
// запускаем обновление окна по таймеру с заданной частотой
ImGui::SFML::Update(window, deltaClock.restart());
// создаём окно управления
ImGui::Begin("Control");
// Инструмент выбора цвета
if (ImGui::ColorEdit3("Background color", color)) {
// код вызывается при изменении значения
// задаём цвет фона
setColor(color);
}
// конец рисование окна
ImGui::End();
// очищаем окно
window.clear(bgColor);
// рисуем по окну средствами imgui+sfml
ImGui::SFML::Render(window);
// отображаем изменения на окне
window.display();
}
// завершаем работу imgui+sfml
ImGui::SFML::Shutdown();
return 0;
}
Создайте новый коммит с названием background control works
и отправьте его на сервер.
Точки
Наше приложение будет решать конкретную геометрическую задачу
Заданы два множества точек в целочисленном двумерном пространстве. Требуется построить пересечения и разность этих множеств.
У каждой точки есть три параметра: -координата, -координата, - множество.
Чтобы хранить множество точки, проще всего хранить целое число, каждому множеству сопоставляется константа с уникальным значением:
// первое множество
static const int SET_1 = 0;
// второе множество
static const int SET_2 = 1;
// пересечение множеств
static const int SET_CROSSED = 2;
// разность множеств
static const int SET_SINGLE = 3;
Можно было бы хранить просто три массива целых чисел, но такой способ довольно неудобен. Лучше объединить три переменных в единую структуру и хранить один массив этих структур:
Положение окна задаётся целочисленными координатами. В математике мы привыкли, что ось Y направлена вверх, т.е. положение по высоте определяется снизу вверх. В компьютерных системах почему-то традиционно сложилась традиция направлять ось вниз. Левая верхняя точка экрана имеет координаты , а правая нижняя , где - ширина окна, а - высота.
// Точка
struct Point {
// положение
sf::Vector2i pos;
// номер множества
int setNum;
// конструктор
Point(const sf::Vector2i &pos, int setNum) : pos(pos), setNum(setNum) {
}
};
структура устроена следующим образом: сначала указывается ключевое слово struct
, потом - название структуры,
затем тело структуры, заключённое в фигурные скобки {...}
.
В теле структуры указываются переменные-поля:
// положение
sf::Vector2i pos;
// номер множества
int setNum;
Положение использует готовую структуру от sfml
- двумерный целочисленный вектор sf::Vector2i
.
Он имеет два поля: x
и y
координаты.
Также в тело структуры добавлен конструктор.
// конструктор
Point(const sf::Vector2i &pos, int setNum) : pos(pos), setNum(setNum) {
}
Конструктор - это специальный метод, который вызывается при создании структуры.
Например, код:
Point p = Point(Vector2i(1, 10), SET_1);
создаст структуру с положением pos
равным структуре двумерного целочисленного
вектора с координатами равной 1 и равным . Номер множества будет
равен .
Для каждой переменной конструктора необходимо написать после скобочек конструкции вида:
Point(tp1 var1, tp2 var2, ... , tpN varN): var1(var1), var2(var2), ...., varN(varN){
...
}
Название конструктора должно совпадать с названием структуры, при этом возвращаемого значения не должно быть.
Теперь создадим глобальную переменную динамического списка точек:
// точки
std::vector<Point> points;
И заполним его до главного цикла тестовыми точками:
...
// инициализация imgui+sfml
ImGui::SFML::Init(window);
// задаём цвет фона
setColor(color);
points.push_back(Point(sf::Vector2i(100, 600), SET_1));
points.push_back(Point(sf::Vector2i(100, 700), SET_1));
points.push_back(Point(sf::Vector2i(200, 500), SET_2));
points.push_back(Point(sf::Vector2i(200, 700), SET_2));
// переменная таймера
sf::Clock deltaClock;
...
Чтобы нарисовать точки, нужно писать новый код. Чтобы код был легко читаемым, перенесём рисование параметров цвета в отдельный метод:
// рисование параметров цвета
void ShowBackgroundSetting() {
// Инструмент выбора цвета
if (ImGui::ColorEdit3("Background color", color)) {
// код вызывается при изменении значения
// задаём цвет фона
setColor(color);
}
// конец рисование окна
}
Теперь рисование окна в главном цикле будет выглядеть так:
...
// создаём окно управления
ImGui::Begin("Control");
// рисование параметров цвета
ShowBackgroundSetting();
// конец рисования окна
ImGui::End();
...
Чтобы рисовать не в окне imGui
, а в главном окне, нужно использовать невидимое окно,
равное ширине и высоте окна. Для этого создадим сначала константы ширины и высоты
окна:
...
static const int WINDOW_SIZE_X = 800;
static const int WINDOW_SIZE_Y = 800;
...
Поменяем числа в параметрах команды окна на эти константы:
....
// создаём окно для рисования
sf::RenderWindow window(sf::VideoMode(WINDOW_SIZE_X, WINDOW_SIZE_Y), "Geometry Project 10");
// задаём частоту перерисовки окна
window.setFramerateLimit(60);
...
Теперь напишем сам метод рисования. Подробно рассматривать его не будем, все команды снабжены комментариями:
// рисование задачи на невидимом окне во всё окно приложения
void RenderTask() {
// задаём левый верхний край невидимого окна
ImGui::SetNextWindowPos(ImVec2(0, 0));
// задаём правый нижний край невидимого окна
ImGui::SetNextWindowSize(ImVec2(WINDOW_SIZE_X, WINDOW_SIZE_Y));
// создаём невидимое окно
ImGui::Begin("Overlay", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground);
// получаем список примитивов, которые будут нарисованы
auto pDrawList = ImGui::GetWindowDrawList();
// перебираем точки из динамического массива точек
for (auto point: points) {
// добавляем в список рисования круг
pDrawList->AddCircleFilled(
sf::Vector2i(point.pos.x, point.pos.y),
3,
point.setNum == SET_1 ? ImColor(200, 100, 150) : ImColor(100, 200, 150),
20
);
}
// заканчиваем рисование окна
ImGui::End();
}
У метода pDrawList->AddCircleFilled()
такие аргументы:
center
- координаты центраradius
- радиусcol
- цвет заливкиnum_segments
- кол-во сегментов при рисовании
С помощью структуры ImColor(100, 200, 150)
задаётся цвет. Все параметры могут
принимать значения от до включительно, каждый параметр - это компонента
цвета - красный, - зелёный - синий.
Добавим перед рисованием окна управления вызов функции RenderTask()
:
...
// запускаем обновление окна по таймеру с заданной частотой
ImGui::SFML::Update(window, deltaClock.restart());
// рисование задания
RenderTask();
// создаём окно управления
ImGui::Begin("Control");
...
Запустим теперь приложение:
Теперь добавим прозрачность окну Control
. Для этого перед рисованием окна
нужно добавить команду ImGui::PushStyleColor()
:
...
// рисование задания
RenderTask();
// делаем окно полупрозрачным
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.12f, 0.12f, 0.13f, 0.8f));
// создаём окно управления
ImGui::Begin("Control");
...
С помощью структуры ImVec4(0.12f, 0.12f, 0.13f, 0.8f)
задаётся цвет окна. Все параметры могут
принимать значения от до включительно, первые три это компоненты цвета - красный,
- зелёный - синий, четвёртая отвечает за прозрачность.
И ImGui::PopStyleColor()
- после:
...
// конец рисования окна
ImGui::End();
// Возвращаем цвет окна к исходному
ImGui::PopStyleColor();
// очищаем окно
window.clear(bgColor);
...
Если теперь перетащить окно Control
на точки, то будет видно, что оно полупрозрачное:
Весь код приложения:
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/Window/Event.hpp>
#include <imgui-SFML.h>
#include <imgui.h>
// первое множество
static const int SET_1 = 0;
// второе множество
static const int SET_2 = 1;
// пересечение множеств
static const int SET_CROSSED = 2;
// разность множеств
static const int SET_SINGLE = 3;
// Ширина окна
static const int WINDOW_SIZE_X = 800;
// Высота окна
static const int WINDOW_SIZE_Y = 800;
// Точка
struct Point {
// положение
sf::Vector2i pos;
// номер множества
int setNum;
// конструктор
Point(const sf::Vector2i &pos, int setNum) : pos(pos), setNum(setNum) {
}
};
// динамический список точек
std::vector<Point> points;
// цвет фона
static sf::Color bgColor;
// значение цвета по умолчанию
float color[3] = {0.12f, 0.12f, 0.13f};
// задать цвет фона по вещественному массиву компонент
static void setColor(float *pDouble) {
bgColor.r = static_cast<sf::Uint8>(pDouble[0] * 255.f);
bgColor.g = static_cast<sf::Uint8>(pDouble[1] * 255.f);
bgColor.b = static_cast<sf::Uint8>(pDouble[2] * 255.f);
}
// рисование параметров цвета
void ShowBackgroundSetting() {
// Инструмент выбора цвета
if (ImGui::ColorEdit3("Background color", color)) {
// код вызывается при изменении значения
// задаём цвет фона
setColor(color);
}
// конец рисование окна
}
// рисование задачи на невидимом окне во всё окно приложения
void RenderTask() {
// задаём левый верхний край невидимого окна
ImGui::SetNextWindowPos(ImVec2(0, 0));
// задаём правый нижний край невидимого окна
ImGui::SetNextWindowSize(ImVec2(WINDOW_SIZE_X, WINDOW_SIZE_Y));
// создаём невидимое окно
ImGui::Begin("Overlay", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground);
// получаем список примитивов, которые будут нарисованы
auto pDrawList = ImGui::GetWindowDrawList();
// перебираем точки из динамического массива точек
for (auto point: points) {
// добавляем в список рисования круг
pDrawList->AddCircleFilled(
sf::Vector2i(point.pos.x, point.pos.y),
3,
point.setNum == SET_1 ? ImColor(200, 100, 150) : ImColor(100, 200, 150),
20
);
}
// заканчиваем рисование окна
ImGui::End();
}
// главный метод
int main() {
// создаём окно для рисования
sf::RenderWindow window(sf::VideoMode(WINDOW_SIZE_X, WINDOW_SIZE_Y), "Geometry Project 10");
// задаём частоту перерисовки окна
window.setFramerateLimit(60);
// инициализация imgui+sfml
ImGui::SFML::Init(window);
// задаём цвет фона
setColor(color);
points.push_back(Point(sf::Vector2i(100, 600), SET_1));
points.push_back(Point(sf::Vector2i(100, 700), SET_1));
points.push_back(Point(sf::Vector2i(200, 500), SET_2));
points.push_back(Point(sf::Vector2i(200, 700), SET_2));
// переменная таймера
sf::Clock deltaClock;
// пока окно открыто, запускаем бесконечный цикл
while (window.isOpen()) {
// создаём событие sfml
sf::Event event;
// пока окно принимает события
while (window.pollEvent(event)) {
// отправляем события на обработку sfml
ImGui::SFML::ProcessEvent(event);
// если событие - это закрытие окна
if (event.type == sf::Event::Closed) {
// закрываем окно
window.close();
}
}
// запускаем обновление окна по таймеру с заданной частотой
ImGui::SFML::Update(window, deltaClock.restart());
// рисование задания
RenderTask();
// делаем окно полупрозрачным
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.12f, 0.12f, 0.13f, 0.8f));
// создаём окно управления
ImGui::Begin("Control");
// рисование параметров цвета
ShowBackgroundSetting();
// конец рисования окна
ImGui::End();
// Возвращаем цвет окна к исходному
ImGui::PopStyleColor();
// очищаем окно
window.clear(bgColor);
// рисуем по окну средствами imgui+sfml
ImGui::SFML::Render(window);
// отображаем изменения на окне
window.display();
}
// завершаем работу imgui+sfml
ImGui::SFML::Shutdown();
return 0;
}
Создаёте новый коммит point rendering works
и отправьте его на сервер.
Обработка мыши
За клик мышью отвечает событие sf::Event::MouseButtonPressed
. Добавим его обработку:
...
// пока окно принимает события
while (window.pollEvent(event)) {
// отправляем события на обработку sfml
ImGui::SFML::ProcessEvent(event);
// если событие - это закрытие окна
if (event.type == sf::Event::Closed) {
// закрываем окно
window.close();
}
// если событие - это клик мышью
if (event.type == sf::Event::MouseButtonPressed) {
// если левая кнопка мыши
if (event.mouseButton.button == sf::Mouse::Button::Left)
points.emplace_back(sf::Vector2i(event.mouseButton.x, event.mouseButton.y), SET_1);
else
points.emplace_back(sf::Vector2i(event.mouseButton.x, event.mouseButton.y), SET_2);
}
}
...
Добавление точек с помощью мыши работает:
Если попробовать перетащить окно, то на его месте останется точка. Это связано с тем,
что мы обрабатываем и те события, которые уже обработал imGui
. Поэтому перед обработкой
клика мыши нужно сделать дополнительную проверку ImGui::GetIO().WantCaptureMouse
.
Этот флаг равен истине, если imGui
обработал клик мыши, т.е. мы кликнули по одному из его окон
...
// если событие - это клик мышью
if (event.type == sf::Event::MouseButtonPressed) {
// если мышь не обрабатывается элементами imGui
if (!ImGui::GetIO().WantCaptureMouse) {
// если левая кнопка мыши
if (event.mouseButton.button == sf::Mouse::Button::Left)
points.emplace_back(sf::Vector2i(event.mouseButton.x, event.mouseButton.y), SET_1);
else
points.emplace_back(sf::Vector2i(event.mouseButton.x, event.mouseButton.y), SET_2);
}
}
...
Теперь при перетаскивании окна не появляются новые точки:
Создайте новый коммит mouse add works
и отправьте его на сервер.
Задание
Вам необходимо создать свой репозиторий, в котором добавлены следующие коммиты (для каждого сначала указывается название, потом требования):
background control works
- приложение, в котором будет окно управления цветом фона как на картинкеpoint rendering works
- приложение, в котором будут рисоваться заданные добавленные точки вручную через код, точка должна описываться структуройmouse add works
- приложение, которое позволяет добавлять точки с помощью клика мышью по экрану
Ссылку на github-репозиторий необходимо отправить в поле ввода задания на сайте mdl.