[imgui] 6. Calendar

[imgui] 6. Calendar

2023년 7월 10일
imgui

목표 #

  • 해당하는 날짜에 스트링으로 일정을 메모할 수 있는 캘린더앱
  • 윤년 적용
  • 오늘날짜는 파란색, 선택된날짜는 초록색, 일정이 있다면 빨간색
  • 프로그램 종료 시 자동으로 캘린더 파일 저장, 시작 시 로드

Calendar

구현코드 #

github 링크

메인함수 #

 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}
comments powered by Disqus