초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 10부(배포판 수정, 최적화 3)

2024. 10. 28. 23:49코딩 연습/아날로그 시계 만들기

이전까지 코드는 시계 테두리와 12시간 표시칸을 매초 갱신하고 있었다.

 

아래 기존 코드를 보자.

    def update_clock(self):
        # 시계를 업데이트하는 함수
        self.canvas.delete("clock")  # 기존 시계 삭제
        # self.canvas.delete("background")  # 기존 배경 삭제
        # if self.background_image:
        #     self.load_background_image(self.background_image_path)

        width = self.canvas.winfo_width()
        height = self.canvas.winfo_height()
        radius = min(width, height) // 2 - 10  # 시계 반지름 설정
        center_x = width // 2  # 시계 중심 X 좌표
        center_y = height // 2  # 시계 중심 Y 좌표

        # 안티앨리어싱을 위한 오프스크린 이미지 생성
        scale_factor = 1.3
        large_size = (int(width * scale_factor), int(height * scale_factor))
        large_image = Image.new("RGBA", large_size, (255, 255, 255, 0))
        draw = ImageDraw.Draw(large_image)

        # 시계 테두리와 시침, 분침, 초침 그리기
        large_center_x = large_size[0] // 2
        large_center_y = large_size[1] // 2
        large_radius = radius * scale_factor

        # 유효한 반지름 값인지 확인 후 그리기
        if large_radius > 0:
            # 시계 테두리 그리기 (검은색과 흰색 윤곽선)
            draw.ellipse(
                (large_center_x - large_radius, large_center_y - large_radius, large_center_x + large_radius, large_center_y + large_radius),
                outline="black",
                width=4
            )
            draw.ellipse(
                (large_center_x - large_radius, large_center_y - large_radius, large_center_x + large_radius, large_center_y + large_radius),
                outline="white",
                width=2
            )

            # 12개의 시간 표시선 그리기
            for i in range(12):
                angle = math.radians(i * 30 - 90)
                x_outer = large_center_x + large_radius * 0.9 * math.cos(angle)
                y_outer = large_center_y + large_radius * 0.9 * math.sin(angle)
                x_inner = large_center_x + large_radius * 0.8 * math.cos(angle)
                y_inner = large_center_y + large_radius * 0.8 * math.sin(angle)
                draw.line((x_inner, y_inner, x_outer, y_outer), fill="black", width=10)
                draw.line((x_inner, y_inner, x_outer, y_outer), fill="white", width=6)

            # 현재 시간 가져오기
            current_time = time.localtime()
            hours = current_time.tm_hour % 12
            minutes = current_time.tm_min
            seconds = current_time.tm_sec

            # 시침 그리기
            hour_angle = math.radians((hours + minutes / 60) * 30 - 90)
            hour_x = large_center_x + large_radius * 0.5 * math.cos(hour_angle)
            hour_y = large_center_y + large_radius * 0.5 * math.sin(hour_angle)
            draw.line((large_center_x, large_center_y, hour_x, hour_y), fill="black", width=12)
            draw.line((large_center_x, large_center_y, hour_x, hour_y), fill="white", width=8)

            # 분침 그리기
            minute_angle = math.radians((minutes + seconds / 60) * 6 - 90)
            minute_x = large_center_x + large_radius * 0.7 * math.cos(minute_angle)
            minute_y = large_center_y + large_radius * 0.7 * math.sin(minute_angle)
            draw.line((large_center_x, large_center_y, minute_x, minute_y), fill="black", width=8)
            draw.line((large_center_x, large_center_y, minute_x, minute_y), fill="white", width=6)

            # 초침 그리기
            second_angle = math.radians(seconds * 6 - 90)
            second_x = large_center_x + large_radius * 0.9 * math.cos(second_angle)
            second_y = large_center_y + large_radius * 0.9 * math.sin(second_angle)
            draw.line((large_center_x, large_center_y, second_x, second_y), fill="red", width=4)

            # 시계 가운데 점 그리기
            draw.ellipse((large_center_x - 7, large_center_y - 7, large_center_x + 7, large_center_y + 7), outline="white", fill="black")

            # 안티앨리어싱을 위해 이미지를 원래 캔버스 크기로 축소
            small_image = large_image.resize((width, height), Image.LANCZOS)
            self.clock_image = ImageTk.PhotoImage(small_image)
            self.canvas.create_image(0, 0, anchor="nw", image=self.clock_image, tags="clock")

        # 다음 업데이트 시간 계산
        current_time = time.time()
        self.next_update_time += 1
        delay = max(0, int((self.next_update_time - current_time) * 1000))
        self.root.after(delay, self.update_clock)

 

어떻게 해야할까? GPT에 해달라고 했다. 그랬더니 update_clock 기존 메서드에서 테두리와 시간 구분선, 가운데 점처럼 움직이지 않는 부분을 떼어서  draw_clock_static_elements 이름의 메서드에 할당했다. 그리고 기존 on_resize() 메서드에 호출되도록 붙여넣었다. GPT가 짜준 코드를 보자. 물론 이 코드를 그대로 붙여 놓으면 제대로 작동 안 할 것이지만 일단 보자.

 

class AnalogClockApp:
    def __init__(self, root):
        self.root = root
        self.root.title("아날로그 시계")

        # 아이콘 설정 및 기타 초기화 코드 생략

        # 캔버스를 생성하여 시계를 그릴 공간을 마련
        self.canvas = tk.Canvas(root, width=400, height=400)
        self.canvas.pack(fill="both", expand=True)

        # 창 크기 변경 이벤트 바인딩
        self.root.bind("<Configure>", self.on_resize)

        # 배경 이미지 및 테두리, 시간 표시선 초기화
        self.load_background_image(self.background_image_path)
        self.draw_clock_static_elements()  # 시계 테두리와 시간 표시선 그리기

        # 다음 업데이트 시간을 초기화
        self.next_update_time = time.time()

        # 시계 바늘 업데이트 시작
        self.update_clock()
        self.root.mainloop()

    def draw_clock_static_elements(self):
        """시계의 테두리와 12개의 시간 표시선, 가운데 점을 그리는 함수 (해상도 변경 시에만 호출)"""
        self.canvas.delete("clock_static")

        # 캔버스 크기 얻기
        width = self.canvas.winfo_width()
        height = self.canvas.winfo_height()
        radius = min(width, height) // 2 - 10  # 시계 반지름 설정
        center_x = width // 2
        center_y = height // 2

        # 시계 테두리 그리기
        self.canvas.create_oval(
            center_x - radius, center_y - radius,
            center_x + radius, center_y + radius,
            outline="black", width=4, tags="clock_static"
        )
        self.canvas.create_oval(
            center_x - radius, center_y - radius,
            center_x + radius, center_y + radius,
            outline="white", width=2, tags="clock_static"
        )

        # 12개의 시간 표시선 그리기
        for i in range(12):
            angle = math.radians(i * 30 - 90)
            x_outer = center_x + radius * 0.9 * math.cos(angle)
            y_outer = center_y + radius * 0.9 * math.sin(angle)
            x_inner = center_x + radius * 0.8 * math.cos(angle)
            y_inner = center_y + radius * 0.8 * math.sin(angle)
            self.canvas.create_line(x_inner, y_inner, x_outer, y_outer, fill="black", width=10, tags="clock_static")
            self.canvas.create_line(x_inner, y_inner, x_outer, y_outer, fill="white", width=6, tags="clock_static")

        # 시계 가운데 점 그리기
        self.canvas.create_oval(center_x - 7, center_y - 7, center_x + 7, center_y + 7, outline="white", fill="black", tags="clock_static")

    def update_clock(self):
        """시계 바늘만 매초 갱신하는 함수"""
        self.canvas.delete("clock")  # 기존 시계 바늘 삭제

        # 캔버스 크기 얻기
        width = self.canvas.winfo_width()
        height = self.canvas.winfo_height()
        radius = min(width, height) // 2 - 10
        center_x = width // 2
        center_y = height // 2

        # 현재 시간 가져오기
        current_time = time.localtime()
        hours = current_time.tm_hour % 12
        minutes = current_time.tm_min
        seconds = current_time.tm_sec

        # 시침, 분침, 초침 그리기
        # 시침
        hour_angle = math.radians((hours + minutes / 60) * 30 - 90)
        hour_x = center_x + radius * 0.5 * math.cos(hour_angle)
        hour_y = center_y + radius * 0.5 * math.sin(hour_angle)
        self.canvas.create_line(center_x, center_y, hour_x, hour_y, fill="black", width=12, tags="clock")
        self.canvas.create_line(center_x, center_y, hour_x, hour_y, fill="white", width=8, tags="clock")

        # 분침
        minute_angle = math.radians((minutes + seconds / 60) * 6 - 90)
        minute_x = center_x + radius * 0.7 * math.cos(minute_angle)
        minute_y = center_y + radius * 0.7 * math.sin(minute_angle)
        self.canvas.create_line(center_x, center_y, minute_x, minute_y, fill="black", width=8, tags="clock")
        self.canvas.create_line(center_x, center_y, minute_x, minute_y, fill="white", width=6, tags="clock")

        # 초침
        second_angle = math.radians(seconds * 6 - 90)
        second_x = center_x + radius * 0.9 * math.cos(second_angle)
        second_y = center_y + radius * 0.9 * math.sin(second_angle)
        self.canvas.create_line(center_x, center_y, second_x, second_y, fill="red", width=4, tags="clock")

        # 다음 업데이트 시간 계산
        current_time = time.time()
        self.next_update_time += 1
        delay = max(0, int((self.next_update_time - current_time) * 1000))
        self.root.after(delay, self.update_clock)

    def on_resize(self, event):
        """창의 크기가 변경될 때 호출되는 함수"""
        if event.widget == self.root:
            self.load_background_image(self.background_image_path)
            self.draw_clock_static_elements()  # 해상도가 변경될 때만 시계 테두리와 시간 표시선 재그리기

 

코드가 길어지고 있어 얼핏 눈에 안 들어올 수도 있다. 하지만 시침 분침 초침 등 자세히 볼 필요 없는 부분을 넘기면 생각처럼 복잡한 코드는 아니다. 변경할 부분을 좀 살펴보면

__init__() 
	self.draw_clock_static_elements() # 시계 테두리와 시간 표시선 그리기

draw_clock_static_elements() 메서드에서 시계의 고정된 부분을 그릴 것이므로 초기화 메서드에서 이 부분을 추가한다.

 

이제  draw_clock_static_elements() 메서드를 정의하는 부분을 보자

기존엔 draw.ellipse() 함수를 써써 시계 테두리인 원을 그렸다. 그런데 이번엔 canvas.create_oval() 함수로 바꿨다. 왜 바꿨는지 GPT에게 물어봤다. Tkinter의 Canvas 자체 메서드로 create_oval()을 쓰면 바로 화면에 그릴 수 있다. draw.ellipse는 Pillow라이브러리에서 제공하는데, ImageDraw 객체를 이용해야하기 때문에 객체를 만들어야 한다. 그래서 성능 향상과 코드 간결성으로 변경하면 장점이 있다고 한다. 마찬가지 이유로 12개의 시간 표시선 역시 기존 draw.line() 함수에서 canvas.create_line()으로 바꿨다.

근데 그럼 기존엔 왜 Pillow라이브러리를 썼을까? 기존 코드를 쓴지 며칠 되어 그런가 나도 잊고 있었다. 이유는 곡선이 매끄럽지 않게 보이는 계단 현상을 줄이기 위함이였다. canvas로 선을 그리면 곡선에서 선이 모니터 화면 도트에 따라 계단처럼 보인다. Pillow의 draw는 더 크게 그렸다가 줄이는 방식으로 계단효과를 줄일 수 있기 때문에 썼던 것이다.

 

다시 GPT에게 고쳐달라고 했다.

그렇게 draw_clock_static_elements() 메서드는 끝내려 했지만 draw.ellipse()함수에서 원의 위치를 잡을 좌표 중에 x1좌표가 x0보다 작아져서 오류를 내뿜는 관계로

large_radius = max(1, radius * scale_factor) 

이부분만 max 함수로 반지름이 1 미만이 되지 않도록 했다.

근데 그 이전에도 같은 코드로 아무 오류가 없었다. 화면 확대 배율 값을 일부로 음수로 넣지 않는 이상 사실 x1이 x0보다 작을 일도 없다. 코드를 눈 씻고 찾아봐도 다른점을 못 찾다가 한참만에 기존 코드 맨 앞 부분에서 아래 부분을 봤다.

        # 유효한 반지름 값인지 확인 후 그리기
        if large_radius > 0:

그렇다. 기존엔 if문을 써서 오류를 막았던 것이다. if문 보다 max()문을 쓰는 게 더 좋을까? GPT에게 물어봤다. GPT가 소설을 쓰고 있을지도 모르지만 max()문은 단순한 비교 연산으로 파이썬에서 내부적으로 빠르게 처리된다고 한다. 조건문은 추가 연산이 필요하므로 더 빠르다고 한다. 코드 가독성도 더 좋으므로 max()문을 쓰라고 추천한다. 그래서 기존 코드도 max()문으로 바꿨다.

 

마지막으로 on_resize() 메서드를 보자. GPT가 짜준 코드는 그럴싸하지만 원래 코드와 비교하면 좀 이상하다.

    def on_resize(self, event):
        # 창의 크기가 변경될 때 호출되는 함수
        if event.widget == self.root:
            # 현재 크기 조정 중임을 표시
            self.is_resizing = True

            # 크기 조정이 완료된 후 1초 뒤에 작업 예약
            self.root.after(200, self.resize_complete)

    def resize_complete(self):
        # 크기 조정이 끝났을 때만 배경 이미지를 로드
        if self.is_resizing:
            self.load_background_image(self.background_image_path)
            # 작업이 완료되었으므로 플래그 초기화
            self.is_resizing = False
            
 # 위에는 기존 코드 ___________아래는 GPT 코드
 
    def on_resize(self, event):
        """창의 크기가 변경될 때 호출되는 함수"""
        if event.widget == self.root:
            self.load_background_image(self.background_image_path)
            self.draw_clock_static_elements()  # 해상도가 변경될 때만 시계 테두리와 시간 표시선 재그리기

 

버벅임을 막기 위해 resize_complete()메서드를 별도로 만들어 is_resizing 변수를 이용해 마지막 한번만 연산하도록 했었다. 그런데 전체 코드를 다 줬음에도 싹 무시하고 GPT 맘대로 on_resize() 메서드에 배경 사진과 시계 테두리를 그리도록 했다. resize_complete()메서드에 draw_clock_static_elements() 메서드를 불러오도록 고친다.

 

그리고 실행하니 아래처럼 작동한다.

 

뭔가 이상한 점을 확인 했는가? 가운데 점은 새로고침 할 이유가 없으므로 draw_clock_static_elements() 메서드로 뺐는데 이 점은 제일 앞에 있어야 하므로 여기에 뺐으면 안 됐다.

다시 이 부분을 수정했다. 실행했다. 의도하진 않았지만 프로그램 창을 끌 때 이전엔 시계가 잠깐 사라졌는데 이젠 그 부분이 해결됐다.

 

다시 전체 코드와 아이콘을 올린다.(아이콘이 없으면 실행 할 때 오류가 난다. 시계 배경은 배경으로 쓸 사진을 "시계 배경.jpg" 로 같은 폴더에 놓으면 된다.)

HeumDuNeo ClockAPP.ico
0.01MB

아이콘

HeumDuNeoClock.py
0.01MB

파이썬 코드

 

뭔가 여기서 끝내려니 아직 아쉬움이 남는다. 다음엔 프로그램 정보에 만든이를 띄우고, 기능을 추가하여 뒤에 배경이 투명해져 뒤에 작업화면이 나오게끔 해보려고 한다.