[imgui] 5. Paint 구현
2023년 7월 8일
목표 #
- 그림판을 만들기
- 색 변경, 펜 두께 변경, Save, Load, CLear 기능

구현코드 #
메인함수 #
1#include <cstdint>
2#include <string_view>
3#include <imgui.h>
4#include <vector>
5
6class WindowClass
7{
8public:
9 // position, color, size 3가지 데이터를 묶어서 하나의 PointData로 사용
10 using PointData = std::tuple<ImVec2, ImColor, float>;
11
12 static constexpr auto popUpFlags =
13 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
14 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar;
15 static constexpr auto popUpSize = ImVec2(300.0f, 100.0f);
16 static constexpr auto popUpButtonSize = ImVec2(120.0f, 0.0f);
17 static constexpr auto popUpPos = ImVec2(1280.0f / 2.0f - popUpSize.x / 2.0f,
18 720.0f / 2.0f - popUpSize.y / 2.0f);
19
20public:
21 WindowClass()
22 : points({}), canvasPos({}), currentDrawColor(ImColor(255, 255, 255)),
23 pointDrawSize(2.0F), filenameBuffer("test.bin"){};
24
25 void Draw(std::string_view label);
26
27private:
28 void DrawMenu(); // 색선택, Size선택, Save, Load 버튼등
29 void DrawCanvas(); // 캔버스 그리기
30 void DrawColorButtons(); // 색션택 버튼을 모듈화
31 void DrawSizeSettings(); // 사이즈 선택을 모듈화
32 void ClearCanvas(); // 캔버스 클리어
33
34 // 바이너리로 PointData 저장 및 복원
35 void SaveToImageFile(std::string_view filename);
36 void LoadFromImageFile(std::string_view filename);
37
38 // 이전에 TextEditor에서 사용한팝업
39 void DrawSavePopup();
40 void DrawLoadPopup();
41
42private:
43 // 캔버스 가로 세로 길이, 컬러채널
44 std::uint32_t numRows = 800; // Row's size;
45 std::uint32_t numCols = 600; // Column's size;
46 std::uint32_t numChannels = 3;
47
48 ImVec2 canvasPos;
49 ImVec2 canvasSize = ImVec2(static_cast<float>(numRows), static_cast<float>(numCols));
50
51 std::vector<PointData> points; // 실제 그림 데이터가 저장되는곳
52 ImColor currentDrawColor; // 선택된 Color
53 float pointDrawSize; // point(Dot) Size;
54
55 char filenameBuffer[256];
56};
void WindowClass::Draw(std::string_view label) { constexpr static auto window_flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar; constexpr static auto window_size = ImVec2(1280.0F, 720.0F); constexpr static auto window_pos = ImVec2(0.0F, 0.0F);
ImGui::SetNextWindowSize(window_size);
ImGui::SetNextWindowPos(window_pos);
ImGui::Begin(label.data(), nullptr, window_flags);
DrawMenu();
DrawCanvas();
ImGui::End();
}
DrawMenu #
버튼을 만들고, save, load 버튼을 눌렀을때 팝업 띄우는건 TextEditor와 중복된다.
DrawMenu 코드 내부에서 ClearCanvas, DrawColorButtons, DrawSizeSettings 를 호출해서 그린다.
DrawColorButtons / DrawSizeSettings #

1void WindowClass::DrawColorButtons()
2{
3 const auto selected_red = currentDrawColor == ImColor(255, 0, 0);
4 const auto selected_green = currentDrawColor == ImColor(0, 255, 0);
5 const auto selected_blue = currentDrawColor == ImColor(0, 0, 255);
6 const auto selected_white = currentDrawColor == ImColor(255, 255, 255);
7 const auto none_preset_color =
8 !selected_red && !selected_green && !selected_blue && !selected_white;
9
10 constexpr static auto orange = ImVec4(1.0F, 0.5F, 0.0F, 1.0F);
11
12 // 만약 선택됐다면, 버튼 색상을 orange로 변경
13 if (selected_red)
14 ImGui::PushStyleColor(ImGuiCol_Button, orange);
15 if (ImGui::Button("Red"))
16 currentDrawColor = ImColor(255, 0, 0);
17 if (selected_red)
18 ImGui::PopStyleColor();
19 // 나갈땐 스택에서 pop 해줘야 다른버튼에 영향을 주지 않는다.
20
21 ImGui::SameLine();
22
23 /* ... selected_green, selected_blue, selected_white 중복코드 생략 ... */
24
25 if (none_preset_color)
26 ImGui::PushStyleColor(ImGuiCol_Button, orange);
27 if (ImGui::Button("Choose"))
28 ImGui::OpenPopup("Color Picker");
29 // 일반 Popup으로 호출
30 if (ImGui::BeginPopup("Color Picker"))
31 {
32 ImGui::ColorPicker3("##picker", reinterpret_cast<float *>(¤tDrawColor));
33 ImGui::EndPopup();
34 }
35 if (none_preset_color)
36 ImGui::PopStyleColor();
37}
38
39void WindowClass::DrawSizeSettings()
40{
41 ImGui::Text("Draw Size");
42 ImGui::SameLine();
43
44 // 다음 요소의 최대 가로길이를 지정. canvas를 넘지않도록 계산.
45 ImGui::PushItemWidth(canvasSize.x - ImGui::GetCursorPosX());
46
47 // Float 값범위를 사용하는 한개짜리 Slider이며, 1~10 까지 설정가능하다.
48 ImGui::SliderFloat("##drawSize", &pointDrawSize, 1.0F, 10.0F);
49 ImGui::PopItemWidth();
50}
Save / Load ImageFile #
내 맘대로 자체적인 포맷을 만들어서 바이너리 파일 저장, 로드
1void WindowClass::SaveToImageFile(std::string_view filename)
2{
3 // 이진 파일은 2번째 인자로 std::ios::binary 를 넣어줘야한다.
4 auto out = std::ofstream(filename.data(), std::ios::binary);
5
6
7 // is_open 만 사용해도 된다. !연산자는 failbit, badbit가 설정된경우 true를 리턴해준다.
8 if (!out || !out.is_open())
9 return;
10
11 // 첫번째 값은 size_t (내환경에선 64bit) 크기의 PointData 벡터의 길이 쓰기
12 // write가 const char* 기준으로 작성되어있다.
13 // 메모리상 숫자는 거꾸로 저장되어있지만, 어차피 앞바이트부터 1byte씩 쓰기 때문에 순서에 맞게 저장된다
14 const auto point_count = points.size();
15 out.write(reinterpret_cast<const char *>(&point_count),
16 sizeof(point_count));
17
18 // 포인트 벡터에서 그린 점들에 대한 정보를 하나씩(1px) 꺼내서 저장
19 for (const auto& [point, color, size] : points)
20 {
21 // 파일에 하나씩 point, color, size를 차곡차곡 저장
22 out.write(reinterpret_cast<const char *>(&point), sizeof(point));
23 out.write(reinterpret_cast<const char *>(&color), sizeof(color));
24 out.write(reinterpret_cast<const char *>(&size), sizeof(size));
25 }
26 out.close();
27}
28
29void WindowClass::LoadFromImageFile(std::string_view filename)
30{
31 auto in = std::ifstream(filename.data(), std::ios::binary);
32
33 if (!in || !in.is_open())
34 return;
35
36 auto point_count = std::size_t{0};
37 in.read(reinterpret_cast<char *>(&point_count), sizeof(point_count));
38
39 for (std::size_t i = 0; i < point_count; ++i)
40 {
41 auto point = ImVec2{};
42 auto color = ImColor{};
43 auto size = float{};
44
45 in.read(reinterpret_cast<char *>(&point), sizeof(point));
46 in.read(reinterpret_cast<char *>(&color), sizeof(color));
47 in.read(reinterpret_cast<char *>(&size), sizeof(size));
48
49 points.push_back(std::make_tuple(point, color, size));
50 }
51 in.close();
52
53}
DrawCanvas #
1void WindowClass::DrawCanvas()
2{
3 canvasPos = ImGui::GetCursorPos(); // canvas 위치
4 const auto border_thickness = 1.5F; // boorder 두께
5
6 // 캔버스의 실제 사이즈. 원하는 사이즈 + border 4방향 추가한 크기
7 const auto button_size = ImVec2(canvasSize.x + 2.0F * border_thickness,
8 canvasSize.y + 2.0F * border_thickness);
9 // 사실 canvas는 Button이다.
10 ImGui::InvisibleButton("##canvas", button_size);
11
12 // mouse 좌표를 얻어오고, 마우스가 이전요소(InvisibleButton)에 Hovering되어있는지 확인
13 const auto mouse_pos = ImGui::GetMousePos();
14 const auto is_mouse_hovering = ImGui::IsItemHovered();
15
16 // 버튼 위에서 좌클릭한 경우
17 if (is_mouse_hovering && ImGui::IsMouseDown(ImGuiMouseButton_Left))
18 {
19 // points에 들어가는 값은 border 등에 영향받지 않은 0,0부터 시작되게 하기위함
20 const auto point = ImVec2(mouse_pos.x - canvasPos.x - border_thickness,
21 mouse_pos.y - canvasPos.y - border_thickness);
22 // 점 하나 저장. 이 점들은 push_back되어 위치순이 아니라 시간순으로 정렬된다.
23 points.push_back(
24 std::make_tuple(point, currentDrawColor, pointDrawSize));
25 }
26
27 // 나중에 렌더링 과정에서 그려줄 작업 리스트를 가져온다.
28 auto *draw_list = ImGui::GetWindowDrawList();
29 for (const auto& [point, color, size] : points)
30 {
31 const auto pos = ImVec2(canvasPos.x + border_thickness + point.x,
32 canvasPos.y + border_thickness + point.y);
33 // 작업 리스트에 색이 채워진 원을 추가한다.
34 draw_list->AddCircleFilled(pos, size, color);
35 }
36
37 // border 그리기
38 const auto border_min = canvasPos;
39 const auto border_max =
40 ImVec2(canvasPos.x + button_size.x - border_thickness,
41 canvasPos.y + button_size.y - border_thickness);
42 // 작업 리스트에 비어있는 사각형을 추가한다.
43 draw_list->AddRect(border_min,
44 border_max,
45 IM_COL32(255, 255, 255, 255),
46 0.0F,
47 ImDrawCornerFlags_All,
48 border_thickness);
49}