Skip to main content

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.

w

Базовой единицей imgui внутри окна является собственное окно. Окна создаются последовательно.

Например, если мы добавим строчки

    // создаём второе окно управления
ImGui::Begin("Control");

// значение цвета по умолчанию
float color[3] = {0.12f, 0.12f, 0.13f};
// Инструмент выбора цвета
if (ImGui::ColorEdit3("Background color", color)) {
// код вызывается при изменении значения
}
// конец рисование окна
ImGui::End();

перед

    // рисуем демонстрационное окно
ImGui::ShowDemoWindow();

то теперь программа будет выглядеть так:

idea

Если навести курсор на квадрат рядом с Background color, то отобразится увеличенный индикатор цевта

idea

Если кликнуть по нему, то откроется палитра

idea

Также можно зажать один из ползунков и изменить значение

idea

Если задать Ctrl и кликнуть по ::ShowDemoWindow

idea

то Clion откроет исходники этого метода.

idea

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

Например, элементу HoldToRepeat (выделен красными скобками)

idea

соответствует выделенный код

idea

Сотрём теперь команду

    // рисуем демонстрационное окно
ImGui::ShowDemoWindow();

и поменяем заголовок окна на Geometry Project 10:

    // создаём окно для рисования
sf::RenderWindow window(sf::VideoMode(1280, 720), "Geometry Project 10");

Запустите программу, теперь ваше приложение должно выглядеть примерно так:

idea

Свяжем теперь инструмент управления цвета с фоном.

Для начала сделаем color глобальной, и создадим глобальную переменную цвета bgColor, понятную imgui:

// цвет фона
static sf::Color bgColor;
// значение цвета по умолчанию
float color[3] = {0.12f, 0.12f, 0.13f};

Теперь напишем функцию, задающую значения переменно bgColor по вещественному массиву компонент от 0.00.0 до 1.01.0.

// задать цвет фона по вещественному массиву компонент
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) задаётся цвет. Все параметры могут принимать значения от 00 до 255255 включительно, каждый параметр - это компонента цвета rr - красный, gg - зелёный bb - синий.

Теперь нам нужно добавить команду 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);

Если теперь запустить приложение, то фон изменится

idea

Цвет

Модель RGB составлена по первым буквам названий базовых цветов: Red(красный), Green(зелёный) и Blue(синий). Чем больше значение соответствующей компоненты, тем значительнее вклад того или иного базового цвета.

Если все компоненты имеют максимальное значение, то мы получим белый цвет, а если минимальное, то чёрный.

При этом, если поменять цвет в окне параметров цвета, то изменится и фон:

idea

Весь код примера:

#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 и отправьте его на сервер.

idea

Точки

Наше приложение будет решать конкретную геометрическую задачу

Постановка задачи

Заданы два множества точек в целочисленном двумерном пространстве. Требуется построить пересечения и разность этих множеств.

У каждой точки есть три параметра: xx-координата, yy-координата, ss - множество.

Чтобы хранить множество точки, проще всего хранить целое число, каждому множеству сопоставляется константа с уникальным значением:

// первое множество
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 направлена вверх, т.е. положение по высоте определяется снизу вверх. В компьютерных системах почему-то традиционно сложилась традиция направлять ось вниз. Левая верхняя точка экрана имеет координаты [0,0][0,0], а правая нижняя [w,h][w, h], где ww - ширина окна, а hh - высота.

// Точка
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 равным структуре двумерного целочисленного вектора с координатами xx равной 1 и yy равным 1010. Номер множества будет равен 00.

Для каждой переменной конструктора необходимо написать после скобочек конструкции вида:

    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) задаётся цвет. Все параметры могут принимать значения от 00 до 255255 включительно, каждый параметр - это компонента цвета rr - красный, gg - зелёный bb - синий.

Добавим перед рисованием окна управления вызов функции RenderTask():

    ...
// запускаем обновление окна по таймеру с заданной частотой
ImGui::SFML::Update(window, deltaClock.restart());

// рисование задания
RenderTask();

// создаём окно управления
ImGui::Begin("Control");
...

Запустим теперь приложение:

idea

Теперь добавим прозрачность окну 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) задаётся цвет окна. Все параметры могут принимать значения от 0.00.0 до 1.01.0 включительно, первые три это компоненты цвета rr - красный, gg - зелёный bb - синий, четвёртая отвечает за прозрачность.

И ImGui::PopStyleColor() - после:

    ...
// конец рисования окна
ImGui::End();

// Возвращаем цвет окна к исходному
ImGui::PopStyleColor();

// очищаем окно
window.clear(bgColor);
...

Если теперь перетащить окно Control на точки, то будет видно, что оно полупрозрачное:

idea

Весь код приложения:

#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);
}
}
...

Добавление точек с помощью мыши работает:

idea

Если попробовать перетащить окно, то на его месте останется точка. Это связано с тем, что мы обрабатываем и те события, которые уже обработал 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);
}
}
...

Теперь при перетаскивании окна не появляются новые точки:

idea

Создайте новый коммит mouse add works и отправьте его на сервер.

Задание

Вам необходимо создать свой репозиторий, в котором добавлены следующие коммиты (для каждого сначала указывается название, потом требования):

  1. background control works - приложение, в котором будет окно управления цветом фона как на картинке idea
  2. point rendering works - приложение, в котором будут рисоваться заданные добавленные точки вручную через код, точка должна описываться структурой idea
  3. mouse add works - приложение, которое позволяет добавлять точки с помощью клика мышью по экрану idea

Ссылку на github-репозиторий необходимо отправить в поле ввода задания на сайте mdl.

Ссылка на контест.