Skip to main content

07. Множества точек 2

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

Панель добавления

Чтобы создать несколько выпадающих блоков, нужно добавить команду

    if (!ImGui::CollapsingHeader("Background"))
return;

в метод ShowBackgroundSetting():

// рисование параметров цвета
void ShowBackgroundSetting() {
// если не раскрыт список `Background`
if (!ImGui::CollapsingHeader("Background"))
// заканчиваем выполнение
return;

// Инструмент выбора цвета
if (ImGui::ColorEdit3("Background color", color)) {
// код вызывается при изменении значения
// задаём цвет фона
setColor(color);
}
// конец рисование окна
}

Если нажать на треугольник, помеченный красным

idea

То появится созданная нами панель управления цветом фона:

idea

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

Для начала создадим две кнопки друг за другом:

// ручное добавление элементов
void ShowAddElem() {
// если не раскрыта панель `Add Elem`
if (!ImGui::CollapsingHeader("Add Elem"))
// заканчиваем выполнение
return;


// Инструмент выбора цвета
if (ImGui::DragInt2("Coords", lastAddPosBuf)) {
// никаких действий не требуется, достаточно
// тех изменений буфера, которые imGui выполняет
// автоматически
}

// если нажата кнопка `Set 1`
if (ImGui::Button("Set 1"))
// добавляем то добавляем в список точку, принадлежащую первому множеству
points.emplace_back(Point(sf::Vector2i(lastAddPosBuf[0], lastAddPosBuf[1]), SET_1));

// если нажата кнопка `Set 2`
if (ImGui::Button("Set 2"))
// добавляем то добавляем в список точку, принадлежащую второму множеству
points.emplace_back(Point(sf::Vector2i(lastAddPosBuf[0], lastAddPosBuf[1]), SET_2));

}

И вызовем нашу функцию из главного цикла:

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

// рисование параметров цвета
ShowBackgroundSetting();
// ручное добавление элементов
ShowAddElem();

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

Запустим программу:

idea

Если окно у вас получилось слишком сжатым, потяните, за его край.

У вас должно теперь заработать добавление через кнопку.

idea

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

// ручное добавление элементов
void ShowAddElem() {
// если не раскрыта панель `Add Elem`
if (!ImGui::CollapsingHeader("Add Elem"))
// заканчиваем выполнение
return;


// Инструмент выбора цвета
if (ImGui::DragInt2("Coords", lastAddPosBuf)) {
// никаких действий не требуется, достаточно
// тех изменений буфера, которые imGui выполняет
// автоматически
}

// фиксируем id равный 0 для первого элемента
ImGui::PushID(0);
// если нажата кнопка `Set 1`
if (ImGui::Button("Set 1"))
// добавляем то добавляем в список точку, принадлежащую первому множеству
points.emplace_back(Point(sf::Vector2i(lastAddPosBuf[0], lastAddPosBuf[1]), SET_1));
// восстанавливаем буфер id
ImGui::PopID();

// говорим imGui, что следующий элемент нужно рисовать на той же линии
ImGui::SameLine();
// задаём id, равный одному
ImGui::PushID(1);
// если нажата кнопка `Set 2`
if (ImGui::Button("Set 2"))
// добавляем то добавляем в список точку, принадлежащую второму множеству
points.emplace_back(Point(sf::Vector2i(lastAddPosBuf[0], lastAddPosBuf[1]), SET_2));
// восстанавливаем буфер id
ImGui::PopID();
}

Теперь интерфейс добавления будет выглядеть примерно так:

idea

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

idea

Поэтому нужно преобразовать объявление элемента управления таким образом:

    // Инструмент выбора цвета
if (ImGui::DragInt2("Coords", lastAddPosBuf, 0.5f, 0, std::min(WINDOW_SIZE_X, WINDOW_SIZE_Y))) {
// никаких действий не требуется, достаточно
// тех изменений буфера, которые imGui выполняет
// автоматически
}

Третьим аргументом добавилось значение скорости прокрутки, четвёртым - минимальное значение, пятым - максимальное.

Теперь значение будет ограничено видимой областью экрана:

idea

Если сейчас кликнуть мышью, то значения на панели добавления не изменятся. Чтобы связать клик мышью с ними, добавьте сохранение координат клика в буфер, хранящий координаты последнего добавления:

    // если событие - это клик мышью
if (event.type == sf::Event::MouseButtonPressed) {
// если мышь не обрабатывается элементами imGui
if (!ImGui::GetIO().WantCaptureMouse) {
// меняем координаты последней добавленной точки
lastAddPosBuf[0] = event.mouseButton.x;
lastAddPosBuf[1] = event.mouseButton.y;
// если левая кнопка мыши
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

Удалите теперь ручное добавление точек до старта главного цикла:

    // задаём цвет фона
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;

Теперь этот участок кода должен выглядеть так:

    // задаём цвет фона
setColor(color);


// переменная таймера
sf::Clock deltaClock;

Теперь точки не добавляются по умолчанию, при этом ручное добавление и добавление мышью работают:

idea

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

Случайные точки

Самый простой способ порождения случайных точек - это статическая функция у структуры:

// Точка
struct Point {
// положение
sf::Vector2i pos;
// номер множества
int setNum;

// конструктор
Point(const sf::Vector2i &pos, int setNum) : pos(pos), setNum(setNum) {
}

// получить случайную точку
static Point randomPoint() {
return Point(sf::Vector2i(
rand() % WINDOW_SIZE_X,
rand() % WINDOW_SIZE_Y),
rand() % 2
);
}
};

Теперь случайную точку можно получить в любом месте такой командой:

Point p = Point::randomPoint();

Теперь напишем функцию добавления заданного числа случайных точек

// добавить заданное кол-во случайных точек
void randomize(int cnt) {
for (int i = 0; i < cnt; i++) {
points.emplace_back(Point::randomPoint());
}
}

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

// буфер кол-ва случайных точек
int lastRandoCntBuf[1] = {10};

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

// панель добавления случайных точек
void ShowRandomize() {
// если не раскрыта панель `Randomize`
if (!ImGui::CollapsingHeader("Randomize"))
// заканчиваем выполнение
return;

// первый элемент в строке
ImGui::PushID(0);

// Инструмент выбора кол-ва
if (ImGui::DragInt("Count", lastRandoCntBuf, 0.1, 0, 100)) {

}
// восстанавливаем буффер id
ImGui::PopID();
// следующий элемент будет на той же строчке
ImGui::SameLine();
// второй элемент
ImGui::PushID(1);
// создаём кнопку добавления
if (ImGui::Button("Add"))
// по клику добавляем заданное число случайных точек
randomize(lastRandoCntBuf[0]);
ImGui::PopID();
}

И вызвать её внутри главного цикла:

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

// рисование параметров цвета
ShowBackgroundSetting();
// ручное добавление элементов
ShowAddElem();
// добавление случайных точек
ShowRandomize();

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

Случайное добавление должно заработать:

idea

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

Работа с файлами

Чтобы работать с файлами, добавим сначала две константы путей до файла ввода и вывода:

// путь к файлу вывода
static const char OUTPUT_PATH[255] = "D:/Programming/Files/out.txt";
// путь к файлу ввода
static const char INPUT_PATH[255] = "D:/Programming/Files/in.txt";
Обратите внимание

Все папки должны быть созданы. Иначе программа просто не создаст файл. Вам нужно указать пути до доступной вам папки. Лучше всего создайте где-нибудь новую.

Чтобы работать с файлами, подключим библиотеку <fstream>:

#include <fstream>

Теперь нам нужно написать функции чтения точек из файла и записи в него:

// загрузка из файла
void loadFromFile() {
// открываем поток данных для чтения из файла
std::ifstream input(INPUT_PATH);
// очищаем массив точек
points.clear();
// пока не достигнут конец файла
while (!input.eof()) {
int x, y, s;
input >> x; // читаем x координату
input >> y; // читаем y координату
input >> s; // читаем номер множества
// добавляем в динамический массив точку на основе прочитанных данных
points.emplace_back(Point(sf::Vector2i(x, y), s));
}
// закрываем файл
input.close();
}

// запись в файл
void saveToFile() {
// открываем поток данных для записи в файл
std::ofstream output(OUTPUT_PATH);

// перебираем точки
for (auto point: points) {
// выводим через пробел построчно: x-координату, y-координату и номер множества
output << point.pos.x << " " << point.pos.y << " " << point.setNum << std::endl;
}

// закрываем
output.close();
}

Теперь напишем функцию, создающую панель работы с файлами:

// работа с файлами
void ShowFiles() {
// если не раскрыта панель `Files`
if (!ImGui::CollapsingHeader("Files"))
// заканчиваем выполнение
return;

// первый элемент в линии
ImGui::PushID(0);
// создаём кнопку загрузки
if (ImGui::Button("Load")) {
// загружаем данные из файла
loadFromFile();
}
// восстанавливаем буфер id
ImGui::PopID();

// следующий элемент будет на той же строчке
ImGui::SameLine();
// второй элемент
ImGui::PushID(1);
// создаём кнопку сохранения
if (ImGui::Button("Save")) {
// сохраняем задачу в файл
saveToFile();
}
// восстанавливаем буфер id
ImGui::PopID();
}

и вызовем её в главном цикле:

    ...
ImGui::Begin("Control");

// рисование параметров цвета
ShowBackgroundSetting();
// ручное добавление элементов
ShowAddElem();
// добавление случайных точек
ShowRandomize();
// работа с файлами
ShowFiles();

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

Запустите программу и создайте 15 случайных точек, а потом нажмите на кнопку сохранения:

idea

По указанному вами пути появится соответствующий файл

idea

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

idea

Теперь скопируйте этот файл

idea

и поменяйте его название на то, которое вы указали в пути файла ввода. У меня - это in.txt

idea

Теперь заново запустите программу и нажмите на кнопку загрузки. Должны появиться те же точки, что и при прошлом запуске программы

idea

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

Решение задачи

Для решения задачи и её отображения будем использовать следующий подход: если точки совпадают по координатам, то просто меняем их множества на SET_CROSSED, потом проходим второй раз по массиву точек и все множества не SET_CROSSED меняем на SET_SINGLE:

// решение задачи
void solve() {
// у совпадающих по координатам точек меняем множество на SET_CROSSED
for (int i = 0; i < points.size(); i++)
for (int j = i + 1; j < points.size(); j++)
if (points[i].pos == points[j].pos)
points[i].setNum = points[j].setNum = SET_CROSSED;

// у всех точек, у которых множество не SET_CROSSED, задаём множество SET_SINGLE
for (auto &point: points)
if (point.setNum != SET_CROSSED)
point.setNum = SET_SINGLE;

}

Теперь создадим функцию рисования панели решения. В ней будет две кнопки: решить задачу и очистить

// решение задачи
void ShowSolve() {
// если не раскрыта панель `Solve`
if (!ImGui::CollapsingHeader("Solve"))
return;
// первый элемент в линии
ImGui::PushID(0);
// создаём кнопку решения
if (ImGui::Button("Solve")) {
solve();
}

// восстанавливаем буфер id
ImGui::PopID();

// следующий элемент будет на той же строчке
ImGui::SameLine();
// второй элемент
ImGui::PushID(1);

// создаём кнопку очистки
if (ImGui::Button("Clear")) {
// удаляем все точки
points.clear();
}
// восстанавливаем буфер id
ImGui::PopID();
}

и вызовем её в главном цикле:

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

// рисование параметров цвета
ShowBackgroundSetting();
// ручное добавление элементов
ShowAddElem();
// добавление случайных точек
ShowRandomize();
// работа с файлами
ShowFiles();
// решение задачи
ShowSolve();

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

Теперь нам осталось только добавить ещё два цвета для множеств из решения задачи:


// рисование задачи на невидимом окне во всё окно приложения
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) {
ImColor clr;
// Устанавливаем цвет по номеру множества
switch (point.setNum) {
case SET_1:
clr = ImColor(200, 100, 150);
break;
case SET_2:
clr = ImColor(100, 200, 150);
break;
case SET_CROSSED:
clr = ImColor(100, 150, 200);
break;
case SET_SINGLE:
clr = ImColor(150, 200, 100);
break;
}
// добавляем в список рисования круг
pDrawList->AddCircleFilled(
sf::Vector2i(point.pos.x, point.pos.y),
3,
clr,
20
);
}
// заканчиваем рисование окна
ImGui::End();
}

Запустите программу, сгенерируйте случайные точки, потом несколько раз кликните сначала одной кнопкой мыши, потом - другой. Это позволит создать точки с одними и теми же координатами:

idea

После этого нажмите на кнопку Solve:

idea

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

Если теперь нажать на Clear, то все точки удалятся:

idea

Создайте новый коммит solve and clear work и отправьте его на сервер

Панель Help

Нам осталось создать панель помощи. Для этого напишем метод порождения строк подсказки:

// помощь
void ShowHelp() {
if (!ImGui::CollapsingHeader("Help"))
return;

// первый заголовок
ImGui::Text("ABOUT THIS DEMO:");
// первый элемент списка
ImGui::BulletText("Author Ivanov Ivan 10-1");
// второй элемент списка
ImGui::BulletText("Powered by SFML+ImGui");
// разделитель
ImGui::Separator();

// второй заголовок
ImGui::Text("TASK:");
// первый элемент списка(многострочный)
ImGui::BulletText("Two sets of points are given\n"
"in an integer two-dimensional space.\n"
"It is required to build intersections and\n"
"the difference between these sets.");
// разделитель
ImGui::Separator();
}

Добавляем вызов созданной нами функции в главный цикл:

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

// рисование параметров цвета
ShowBackgroundSetting();
// ручное добавление элементов
ShowAddElem();
// добавление случайных точек
ShowRandomize();
// работа с файлами
ShowFiles();
// решение задачи
ShowSolve();
// помощь
ShowHelp();

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

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

idea

Вам нужно поменять данные автора на свои фамилию, имя и класс.

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

Пример готового проекта можно посмотреть здесь. Fork репозитория делать запрещается.

Задание

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

  1. manual point add works - приложение, в которое добавлено ручное добавление точек с помощью соответствующей панели idea
  2. random add works - приложение, в которое добавлено ручное добавление случайных точек с помощью соответствующей панели idea
  3. files ready - приложение, в которое добавлена панель для работы с файлами idea
  4. solve and clear work - приложение, в которое добавлена панель для решения задачи и очистки данных idea
  5. complete - приложение, в которое добавлена панель Helpidea

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

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