초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 5부(1차 완성)
2024. 10. 23. 00:53ㆍ코딩 연습/아날로그 시계 만들기
역시 canvas 버전이 아닌 일반 GPT 4o 버전으로 기존 시침 그리는 코드를 던져주고, 시계 시침을 그리는데 배경색이랑 헷갈릴 때가 있다며 윤곽선을 넣을 수 있냐고 물었다.
답은 간단했다. 시침을 아래처럼 두께만 두껍게 해서 다른색으로 한 번 더 그리라는 것이었다. ㅡ.ㅡ
# Draw hour hand
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, width=8, fill="black", tags="clock")
self.canvas.create_line(center_x, center_y, hour_x, hour_y, width=6, fill="White", tags="clock")
적용하고 나니 확실히 보기가 편하다. 이쯤에서 전체 코드를 한번 올려본다.
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk
import time
import math
import os
import json
CONFIG_FILE = "config.json"
DEFAULT_BACKGROUND_IMAGE = "C:/당신의 사진 경로/시계 배경.jpg" # Path to the default image
class AnalogClockApp:
def __init__(self, root):
self.root = root
self.root.title("Analog Clock with Custom Background")
self.load_config()
self.canvas = tk.Canvas(root, width=400, height=400)
self.canvas.pack(fill="both", expand=True)
self.always_on_top = tk.BooleanVar()
self.always_on_top.set(self.config.get("always_on_top", False))
self.root.attributes("-topmost", self.always_on_top.get())
self.menu = tk.Menu(root)
root.config(menu=self.menu)
self.options_menu = tk.Menu(self.menu, tearoff=0)
self.menu.add_cascade(label="Options", menu=self.options_menu)
self.options_menu.add_command(label="Change Background Image", command=self.change_background_image)
self.options_menu.add_checkbutton(label="Always on Top", onvalue=True, offvalue=False, variable=self.always_on_top, command=self.toggle_always_on_top)
self.background_image = None
if self.config.get("background_image"):
self.load_background_image(self.config["background_image"])
else:
self.load_background_image(DEFAULT_BACKGROUND_IMAGE)
self.update_clock()
self.root.mainloop()
def load_config(self):
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r") as file:
self.config = json.load(file)
else:
self.config = {}
def save_config(self):
with open(CONFIG_FILE, "w") as file:
json.dump(self.config, file)
def load_background_image(self, image_path):
try:
image = Image.open(image_path)
# 캔버스 크기 얻기
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
# 이미지와 캔버스 비율 계산
image_ratio = image.width / image.height
canvas_ratio = canvas_width / canvas_height
# 이미지의 비율에 맞춰 크롭 영역 설정
if image_ratio > canvas_ratio:
# 이미지가 더 넓을 때, 가로 부분을 잘라냄
new_width = int(image.height * canvas_ratio)
left = (image.width - new_width) // 2
image = image.crop((left, 0, left + new_width, image.height))
else:
# 이미지가 더 길 때, 세로 부분을 잘라냄
new_height = int(image.width / canvas_ratio)
top = (image.height - new_height) // 2
image = image.crop((0, top, image.width, top + new_height))
# 캔버스 크기에 맞춰 이미지 축소 (비율 유지)
image.thumbnail((canvas_width, canvas_height), Image.LANCZOS)
# 크롭된 이미지를 캔버스에 표시
self.background_image = ImageTk.PhotoImage(image)
self.canvas.create_image(canvas_width / 2, canvas_height / 2,
anchor="center", image=self.background_image,
tags="background")
except Exception as e:
print(f"Error loading background image: {e}")
def change_background_image(self):
file_path = filedialog.askopenfilename(filetypes=[("Image Files", "*.png;*.jpg;*.jpeg;*.bmp")])
if file_path:
self.config["background_image"] = file_path
self.save_config()
self.load_background_image(file_path)
def toggle_always_on_top(self):
self.root.attributes("-topmost", self.always_on_top.get())
self.config["always_on_top"] = self.always_on_top.get()
self.save_config()
def update_clock(self):
self.canvas.delete("clock")
self.canvas.delete("background")
if self.background_image:
self.load_background_image(self.config.get("background_image", DEFAULT_BACKGROUND_IMAGE))
width = self.canvas.winfo_width()
height = self.canvas.winfo_height()
radius = min(width, height) // 2 - 10
center_x = width // 2
center_y = height // 2
# Draw clock face with hour markers
self.canvas.create_oval(center_x - radius, center_y - radius, center_x + radius, center_y + radius, outline="black", width=4, tags="clock")
self.canvas.create_oval(center_x - radius, center_y - radius, center_x + radius, center_y + radius, outline="white", width=2, tags="clock")
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, width=4, fill="black", tags="clock")
self.canvas.create_line(x_inner, y_inner, x_outer, y_outer, width=2, fill="white", tags="clock")
# Get current time
current_time = time.localtime()
hours = current_time.tm_hour % 12
minutes = current_time.tm_min
seconds = current_time.tm_sec
# Draw hour hand
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, width=8, fill="black", tags="clock")
self.canvas.create_line(center_x, center_y, hour_x, hour_y, width=6, fill="White", tags="clock")
# Draw minute hand
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, width=6, fill="black", tags="clock")
self.canvas.create_line(center_x, center_y, minute_x, minute_y, width=4, fill="White", tags="clock")
# Draw second hand
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, width=4, fill="black", tags="clock")
self.canvas.create_line(center_x, center_y, second_x, second_y, width=2, fill="red", tags="clock")
self.root.after(1000, self.update_clock)
if __name__ == "__main__":
root = tk.Tk()
app = AnalogClockApp(root)
그런데 선이 자연스럽지 않고 계단 현상이 너무 두드러져 보인다. 이를 해결하고자 또 GPT에게 물어보자.
from PIL import Image, ImageDraw, ImageTk
import math
def draw_clock_with_antialiasing(self, center_x, center_y, radius, hours, minutes, seconds):
# 빈 이미지 생성 (안티앨리어싱 지원)
img_size = (center_x * 2, center_y * 2)
image = Image.new("RGBA", img_size, (255, 255, 255, 0))
draw = ImageDraw.Draw(image)
# 시침 계산
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)
draw.line([center_x, center_y, hour_x, hour_y], fill="blue", width=6)
# 분침 계산
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)
draw.line([center_x, center_y, minute_x, minute_y], fill="green", width=4)
# 초침 계산
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)
draw.line([center_x, center_y, second_x, second_y], fill="red", width=2)
# Pillow 이미지를 Tkinter용 이미지로 변환하여 캔버스에 표시
self.clock_image = ImageTk.PhotoImage(image)
self.canvas.create_image(0, 0, image=self.clock_image, anchor="nw", tags="clock")
위와 같이 Pillow 라이브러리를 이용해서 다시 코드를 줬는데, 다시 이걸 보면서 고치기 귀찮으니 내 코드 전체를 다시 던지고 내 코드에 맞게 고쳐달라고 부탁했다.
그랬더니 예기치 않게 자기가 더 최적화를 해주겠다며 전반적으로 코드를 다시 짜줬다. 하지만 기대와 다르게 실행해보면 예상치 못하게 작동한다. 사진도 이상한 크기로 변했고, 어플이 이상하게 버벅거린다. 전체 코드를 주고 받으니 어딜 고쳤는지 코드를 분석하기 귀찮아진다.
다시 ChatGPT 4o with canvas 원래 대화방으로 돌아갔다.
이전의 전체 코드를 다시 GPT에게 던져주고 계단현상을 해결해달라고 부탁했더니 간단한 답이 돌아왔다. 시계를 처음에 크게 그리고 이미지를 축소시켜 안티앨리어싱 효과를 얻자는 것이다. 근데 웃긴 것은 이전 채팅방에선 Pillow에는 안티앨리어싱이 있다고 이걸 쓰자고 해놓고 여기서는 옵션이 없으니 이미지를 크게한 뒤 줄여 쓰자고 한다는 점이다. 어쨌든 따라해본다.
코드를 적용하니 오류가 났다. 다시 어디서 무슨 오류 메시지가 났는지 알려주고 고쳐달라고 했다. 이번엔 제대로 작동한다.
맘에 들어 시계 프로그램을 가만히 쳐다보고 있었다. 초침을 보고 있으니 어떨 때는 초침이 한번에 2초씩 건너 뛰고 있었다. 여태것 몰랐던 매우 중요한 문제였다! GPT에게 다시 물어봤다.
GPT가 전체 코드를 다시 짜주었다.
잘 작동한다. 그런데 화면이 작을 땐 상관 없는데 화면이 커지면 어플이 버벅인다. 컴퓨터의 cpu는 i7-13700k이고, 모니터는 4k 해상도다. 창을 최대화 하면 순간적으로 먹통이 된다. 작업관리자로 확인해보니 창을 최대화 하니까 싱글코어가 거의 풀로 돌아가고 있다. 아무래도 이미지를 너무 크게 그린 게 문제인 듯하다. 스케일링 팩터를 4가 아닌 1.1로 줄여본다. 이것만으로도 선분을 부드럽게 하는데 충분하다.
바늘 두께를 조정하고, 선 끝이 네모라 선분끼리 어긋나는 중앙 부분을 가리고자 중앙에 점을 추가했다. 전체 코드를 아래와 같이 정리한다.
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageDraw, ImageTk
import time
import math
import os
import json
CONFIG_FILE = "config.json"
DEFAULT_BACKGROUND_IMAGE = "C:/당신의 경로/시계 배경.jpg" # 기본 배경 이미지 경로
class AnalogClockApp:
def __init__(self, root):
self.root = root
self.root.title("Analog Clock with Custom Background") # 창 제목 설정
self.load_config() # 설정 파일 불러오기
# 캔버스를 생성하여 시계를 그릴 공간을 마련
self.canvas = tk.Canvas(root, width=400, height=400)
self.canvas.pack(fill="both", expand=True)
# 항상 위에 표시할지 여부 설정
self.always_on_top = tk.BooleanVar()
self.always_on_top.set(self.config.get("always_on_top", False))
self.root.attributes("-topmost", self.always_on_top.get())
# 메뉴 생성 및 옵션 추가
self.menu = tk.Menu(root)
root.config(menu=self.menu)
self.options_menu = tk.Menu(self.menu, tearoff=0)
self.menu.add_cascade(label="Options", menu=self.options_menu)
self.options_menu.add_command(label="Change Background Image", command=self.change_background_image)
self.options_menu.add_checkbutton(label="Always on Top", onvalue=True, offvalue=False, variable=self.always_on_top, command=self.toggle_always_on_top)
# 배경 이미지 설정
self.background_image = None
if self.config.get("background_image"):
self.load_background_image(self.config["background_image"])
else:
self.load_background_image(DEFAULT_BACKGROUND_IMAGE)
# 다음 업데이트 시간을 초기화
self.next_update_time = time.time()
# 시계 업데이트 시작
self.update_clock()
self.root.mainloop()
def load_config(self):
# 설정 파일을 불러오는 함수
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r") as file:
self.config = json.load(file)
else:
self.config = {}
def save_config(self):
# 설정 파일을 저장하는 함수
with open(CONFIG_FILE, "w") as file:
json.dump(self.config, file)
def load_background_image(self, image_path):
# 배경 이미지를 불러와서 캔버스에 표시하는 함수
try:
image = Image.open(image_path)
# 캔버스 크기 얻기
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
# 이미지와 캔버스 비율 계산
image_ratio = image.width / image.height
canvas_ratio = canvas_width / canvas_height
# 이미지의 비율에 맞춰 크롭 영역 설정
if image_ratio > canvas_ratio:
# 이미지가 더 넓을 때, 가로 부분을 잘라냄
new_width = int(image.height * canvas_ratio)
left = (image.width - new_width) // 2
image = image.crop((left, 0, left + new_width, image.height))
else:
# 이미지가 더 길 때, 세로 부분을 잘라냄
new_height = int(image.width / canvas_ratio)
top = (image.height - new_height) // 2
image = image.crop((0, top, image.width, top + new_height))
# 캔버스 크기에 맞춰 이미지 축소 (비율 유지)
image.thumbnail((canvas_width, canvas_height), Image.LANCZOS)
# 크롭된 이미지를 캔버스에 표시
self.background_image = ImageTk.PhotoImage(image)
self.canvas.create_image(canvas_width / 2, canvas_height / 2,
anchor="center", image=self.background_image,
tags="background")
except Exception as e:
print(f"Error loading background image: {e}")
def change_background_image(self):
# 배경 이미지를 변경하는 함수
file_path = filedialog.askopenfilename(filetypes=[("Image Files", "*.png;*.jpg;*.jpeg;*.bmp")])
if file_path:
self.config["background_image"] = file_path
self.save_config()
self.load_background_image(file_path)
def toggle_always_on_top(self):
# 창을 항상 위에 표시할지 여부를 토글하는 함수
self.root.attributes("-topmost", self.always_on_top.get())
self.config["always_on_top"] = self.always_on_top.get()
self.save_config()
def update_clock(self):
# 시계를 업데이트하는 함수
self.canvas.delete("clock") # 기존 시계 삭제
self.canvas.delete("background") # 기존 배경 삭제
if self.background_image:
self.load_background_image(self.config.get("background_image", DEFAULT_BACKGROUND_IMAGE))
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.1
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, minute_x, minute_y), fill="white", width=5)
draw.line((large_center_x, large_center_y, second_x, second_y), fill="red", width=3)
# 시계 가운데 점 그리기
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)
if __name__ == "__main__":
root = tk.Tk()
app = AnalogClockApp(root)
드디어 완성되었다. 하지만 개선할 점이 여전히 많다.
어플 이름도 바꾸고 디버그 실행이 아닌 배포버전으로 만들어야하고, 성능 개선도 좀 더 하고싶다.
다음장에 계속하도록 하겠다.
'코딩 연습 > 아날로그 시계 만들기' 카테고리의 다른 글
초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 9부(배포판 수정, 최적화 2) (1) | 2024.10.28 |
---|---|
초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 8부(배포판 수정, 최적화 1) (2) | 2024.10.27 |
초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 7부(배포판 수정, 레지스트리를 사용해 저장하기) (0) | 2024.10.24 |
초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 6부(배포판 만들기) (3) | 2024.10.24 |
초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 4부 (1) | 2024.10.22 |
초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 3부 (0) | 2024.10.22 |
초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 2부 (1) | 2024.10.21 |
초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 1부 (1) | 2024.10.21 |