[imgui] 6. Calendar
2023년 7월 10일
목표 #
- 해당하는 날짜에 스트링으로 일정을 메모할 수 있는 캘린더앱
- 윤년 적용
- 오늘날짜는 파란색, 선택된날짜는 초록색, 일정이 있다면 빨간색
- 프로그램 종료 시 자동으로 캘린더 파일 저장, 시작 시 로드

구현코드 #
메인함수 #
1class WindowClass
2{
3public:
4 // Add Meeting 버튼 윈도우 팝업 설정들
5 static constexpr auto popUpFlags =
6 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
7 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar;
8 static constexpr auto popUpSize = ImVec2(300.0f, 100.0f);
9 static constexpr auto popUpButtonSize = ImVec2(120.0f, 0.0f);
10 static constexpr auto popUpPos = ImVec2(1280.0f / 2.0f - popUpSize.x / 2.0f,
11 720.0f / 2.0f - popUpSize.y / 2.0f);
12
13 // 캘린더 설정들
14 static constexpr auto monthNames =
15 std::array<std::string_view, 12U>{"January",
16 "February",
17 "March",
18 "April",
19 "May",
20 "June",
21 "July",
22 "August",
23 "September",
24 "October",
25 "November",
26 "December"};
27 static constexpr auto minYear = 2000U;
28 static constexpr auto maxYear = 2038U;
29
30 struct Meeting
31 {
32 // name 스트링에는 일정 이름을 작성
33 std::string name;
34
35 // Serialize는 일정하나를 바이너리형태로 out 스트림에 저장
36 // Deserialize는 in 스트림에서 일정하나 불러오기
37 void Serialize(std::ofstream& out) const;
38 static Meeting Deserialize(std::ifstream& in);
39
40 // 같은 일정이 있는지 비교를 위해 연산자 구현
41 // 컴파일 타임에 계산이 가능한경우 미리 계산해둔다.
42 constexpr bool operator==(const Meeting& rhs) const
43 { return name == rhs.name; }
44 };
45
46public:
47 WindowClass() : meetings({}) { }
48 void Draw(std::string_view label);
49
50public:
51 // 일정 불러오기, 저장하기
52 void LoadMeetingsFromFile(std::string_view filename);
53 void SaveMeetingsToFile(std::string_view filename);
54
55private:
56 void DrawDateCombo(); // 날짜선택 콤보박스 출력
57 void DrawCalendar(); // 선택한 날짜에 해당하는 달력 출력
58 void DrawAddMeetingWindow(); // Add Meeting 윈도우 창
59 void DrawMeetingList(); // 미팅리스트를 그려주는창
60
61 void UpdateSelectedDateVariables(); // selectedDate to selected 변수들
62
63private:
64 // selected 변수들
65 int selectedDay = 1;
66 int selectedMonth = 1;
67 int selectedYear = 2023;
68
69 std::chrono::year_month_day selectedDate;
70
71 bool addMeetingWindowOpen = false;
72
73 float calendarFontSize = 2.0F;
74
75 // meeting 리스트
76 std::map<std::chrono::year_month_day, std::vector<Meeting>> meetings;
77};
1
2void WindowClass::Draw(std::string_view label)
3{
4 constexpr static auto window_flags =
5 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
6 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar;
7 constexpr static auto window_size = ImVec2(1280.0F, 720.0F);
8 constexpr static auto window_pos = ImVec2(0.0F, 0.0F);
9
10 ImGui::SetNextWindowSize(window_size);
11 ImGui::SetNextWindowPos(window_pos);
12
13 ImGui::Begin(label.data(), nullptr, window_flags);
14
15 DrawDateCombo();
16 ImGui::Separator();
17 DrawCalendar();
18 ImGui::Separator();
19 DrawMeetingList();
20
21 // 중첩윈도우는 사용할때 주의해야한다. 어차피 윈도우라 아래의 End() 이후로 옮겨도된다.
22 if (addMeetingWindowOpen)
23 DrawAddMeetingWindow();
24
25 ImGui::End();
26}
DrawDateCombo #
콤보박스 버튼 그리기
1void WindowClass::DrawDateCombo()
2{
3 ImGui::Text("Select a date:");
4
5 // 다음 Item의 가로길이 지정
6 ImGui::PushItemWidth(50);
7
8 // 콤보의 label, 콤보에 프리뷰로 보여질 문자열
9 if (ImGui::BeginCombo("##day", std::to_string(selectedDay).data()))
10 {
11 for (std::int32_t day_idx = 1; day_idx <= 31; ++day_idx)
12 {
13 const auto curr_date =
14 std::chrono::year_month_day(std::chrono::year(selectedYear),
15 std::chrono::month(selectedMonth),
16 std::chrono::day(day_idx));
17 // 날짜가 실제로 존재하는지 체크
18 if (!curr_date.ok())
19 break;
20
21 // 클릭할 수 있는 숫자 출력. 선택된 날짜랑 같다면 강조표시
22 if (ImGui::Selectable(std::to_string(day_idx).data(),
23 day_idx == selectedDay))
24 {
25 // 클릭했을때 값 업데이트
26 selectedDay = day_idx;
27 selectedDate = curr_date;
28 }
29 }
30 ImGui::EndCombo();
31 }
32 ImGui::PopItemWidth();
33 ImGui::SameLine();
34 ImGui::PushItemWidth(100);
35
36 /* ... month, year는 day와 동일함 ... */
37
38 // 버튼을 눌렀을때 WindowOpen true로 세팅
39 if (ImGui::Button("Add Meeting"))
40 addMeetingWindowOpen = true;
41}
DrawCalendar #
달력 그리기
1void WindowClass::DrawCalendar()
2{
3 // 내부에 요소가 많아서 하나로 묶고, 가로길이 최대, 세로길이 400으로 지정
4 const auto child_size = ImVec2(ImGui::GetContentRegionAvail().x, 400.0f);
5 ImGui::BeginChild("###calendar", child_size, false);
6 const auto original_font_size = ImGui::GetFontSize(); // 기존 폰트사이즈 백업
7 ImGui::SetWindowFontScale(calendarFontSize); // 폰트사이즈 calendarFontSize = 2.0f 배 증가
8
9 // 현재날짜를 days 기준으로 내림처리 (days 까지만 가져오겠다는 뜻)
10 const auto curr_time =
11 std::chrono::floor<std::chrono::days>(std::chrono::system_clock::now());
12 const auto todays_date = std::chrono::year_month_day(curr_time);
13
14 const auto y = selectedYear;
15 for (std::int32_t m = 1; m <= 12; ++m)
16 {
17 // format 함수로 const char* 타입 문자열을 3글자만 가져오기
18 ImGui::Text("%s", fmt::format("{:.3}", monthNames[m - 1]).data());
19 ImGui::SameLine();
20
21 // 31개의 날짜를 전부 돌면서 존재하는지 체크하고, 있다면 색에 맞게 출력
22 for (std::int32_t d = 1; d <= 31; ++d)
23 {
24 const auto iteration_date =
25 std::chrono::year_month_day(std::chrono::year(y),
26 std::chrono::month(m),
27 std::chrono::day(d));
28 if (!iteration_date.ok())
29 break;
30
31 ImGui::SameLine();
32 if (todays_date == iteration_date)
33 ImGui::TextColored(ImVec4(0.5f, 0.5f, 1.0f, 1.0f), "%d", d);
34 else if (selectedDate == iteration_date)
35 ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "%d", d);
36 else if (meetings.contains(iteration_date))
37 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "%d", d);
38 else
39 ImGui::Text("%d", d);
40
41 // 직전 그려진 요소가 클릭된 경우
42 if (ImGui::IsItemClicked())
43 {
44 selectedDate = iteration_date;
45 UpdateSelectedDateVariables();
46 }
47 }
48 }
49 ImGui::SetWindowFontScale(original_font_size);
50 ImGui::EndChild();
51}
52
53void WindowClass::UpdateSelectedDateVariables()
54{
55 selectedDay = static_cast<int>(selectedDate.day().operator unsigned int());
56 selectedMonth = static_cast<int>(selectedDate.month().operator unsigned int());
57 selectedYear = static_cast<int>(selectedDate.year());
58}
DrawAddMeetingWindow #
Popup 형태로도 구현해도되고, PopupModal 형태도 괜찮다.
Window로 구현하면 다른 Window에 가려질 수 있다는 단점이 있다.
1void WindowClass::DrawAddMeetingWindow()
2{
3 static char meeting_name_buffer[128] = "...";
4
5
6 ImGui::SetNextWindowSize(popUpSize);
7 ImGui::SetNextWindowPos(popUpPos);
8
9 // 윈도우 생성
10 ImGui::Begin("###addMeeting", &addMeetingWindowOpen, popUpFlags);
11
12 ImGui::Text("Add meeting to %d.%s.%d",
13 selectedDay,
14 monthNames[selectedMonth - 1].data(),
15 selectedYear);
16 ImGui::InputText("Meeting Name",
17 meeting_name_buffer,
18 sizeof(meeting_name_buffer));
19 if (ImGui::Button("Save"))
20 {
21 meetings[selectedDate].push_back(Meeting{meeting_name_buffer});
22 std::memset(meeting_name_buffer, 0, sizeof(meeting_name_buffer));
23 addMeetingWindowOpen = false;
24 }
25 ImGui::SameLine();
26 if (ImGui::Button("Cancel"))
27 {
28 addMeetingWindowOpen = false;
29 }
30
31 ImGui::End();
32}
Save / Load 로직 #
그냥 바이너리 파일 입출력이다. write와 read 함수는 주소값을 매개변수로 받기 때문에 둘다 저장할 변수의 주소값을 넘겨야 한다.
1void WindowClass::LoadMeetingsFromFile(std::string_view filename)
2{
3 auto in = std::ifstream(filename.data());
4
5 if (!in.is_open())
6 return;
7
8 // meetings의 size를 먼저 로드해온다.
9 auto num_meetings = std::size_t{0};
10 in.read(reinterpret_cast<char *>(&num_meetings), sizeof(num_meetings));
11
12 // meeting을 저장했던 map의 키 숫자만큼 반복
13 for (std::size_t i = 0; i < num_meetings; ++i)
14 {
15 // 날짜를 로드
16 auto date = std::chrono::year_month_day{};
17 in.read(reinterpret_cast<char *>(&date), sizeof(date));
18
19 // 날짜에 해당하는 meeting들이 몇개가 있는지
20 auto num_meetings_on_that_date = std::size_t{0};
21 in.read(reinterpret_cast<char *>(&num_meetings_on_that_date),
22 sizeof(num_meetings_on_that_date));
23
24 // 날짜에 해당하는 meeting 숫자만큼 반복
25 for (std::size_t j = 0; j < num_meetings_on_that_date; ++j)
26 {
27 // 하나씩 meeting에 꺼내오고 meetings벡터에 push해줌
28 auto meeting = Meeting::Deserialize(in);
29 meetings[date].push_back(meeting);
30 }
31 }
32
33 in.close();
34}
35
36WindowClass::Meeting WindowClass::Meeting::Deserialize(std::ifstream &in)
37{
38 auto meeting = Meeting{};
39 auto name_length = std::size_t{0};
40 in.read(reinterpret_cast<char *>(&name_length), sizeof(name_length));
41
42 meeting.name.resize(name_length);
43 in.read(meeting.name.data(), static_cast<std::streamsize>(name_length));
44
45 return meeting;
46}
DrawMeetingList #
1
2void WindowClass::DrawMeetingList()
3{
4 if (meetings.empty())
5 {
6 ImGui::Text("No meetings at all.");
7 return;
8 }
9
10 ImGui::Text("Meetings on %d.%s.%d: ",
11 selectedDay,
12 monthNames[selectedMonth - 1].data(),
13 selectedYear);
14
15 // 키가 있는지 체크하지 않으면 meetings[selectedDate].empty() 함수를 호출하면서 키가 들어가게된다.
16 if (!meetings.contains(selectedDate) || meetings[selectedDate].empty())
17 {
18 ImGui::Text("No meetings for this day.");
19 return;
20 }
21
22 for (const auto& meeting : meetings[selectedDate])
23 {
24 // 순서없는 점형태 목록에 텍스트 출력
25 ImGui::BulletText("%s", meeting.name.data());
26 if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
27 {
28 std::erase(meetings[selectedDate], meeting);
29 return;
30 }
31 }
32}