초간단 아날로그 시계 만들기 - GPT를 이용한 코딩 연습 - 11부(기능 추가, 프로그램 정보)

2024. 11. 4. 10:59코딩 연습/아날로그 시계 만들기

프로그램을 만들었지만 이 프로그램이 누가 만들었는지 버전은 어떤지 표시가 없다.

기존 옵션 메뉴에 프로그램 정보를 추가해본다.

 

우선 GPT에게 물어봤다.

    def __init__(self, root):
        self.options_menu.add_separator() # 구분선       
        self.options_menu.add_command(label="프로그램 정보", command=self.show_program_info) # 프로그램 정보 메뉴 항목 추가

    # 프로그램 정보 표시 함수 추가
    def show_program_info(self):
        tk.messagebox.showinfo("프로그램 정보", "제작자: HeumDuNeo\n버전: 1.0.0")

옵션에 구분선을 추가하고, 프로그램 정보라는 이름의 메뉴를 추가한다. 그리고 그 메뉴의 기능을 정의한다. add_command에 바로 텍스트를 등록하면 되는거 아닌가 궁금해서 GPT에게 물어봤는데 command = 다음은 함수이름이나 호출 가능한 객체를 참조해야한다. 그래서 텍스트를 바로 넣으면 오류가 난다. 그런데 여기서도 호출문은 넣으면 바로 실행되므로 예를 들어 add_command에 messagebox를 바로 넣으면 프로그램을 실행하자 마자 메시지가 떠버린다. 그래서 람다식으로 넣던가 별도의 함수를 지정해야한다.

 

그런데 이 메시지기 프로그램 중간에 정의된 메서드에 있으면 나중에 찾기 힘들므로, 프로그램 상단에 빼고 싶다.

PROGRAM_INFO = ("프로그램 정보", "제작자: HeumDuNeo\n버전: 1.0.0") # 프로그램 정보에 띄울 메시지

    # 프로그램 정보 표시 함수 추가
    def show_program_info(self):
        tk.messagebox.showinfo(*PROGRAM_INFO)

내용을 변수로 정의해서 보기 쉽게 프로그램 코드 상단으로 위치 이동 했다. 원래는 showinfo()에 PROGRAM_INFO를 바로 넣었는데 그렇게 되면 아래처럼 나온다.

뭐가 문제인지 GPT에 물어본다. PROGRAM_INFO에 저장된 값은 튜플인데, tk. messagebox.showinfo() 함수는 첫 번째 인자로 제목 문자열, 두 번째 인자로 메시지 문자열을 받아야 하기 떄문에 저렇게 나오는 것이다.

GPT가 해결방법을 두 개 제시했지만 제일 쉬운 방법은 * 연산자를 써서 언패킹 하는 것이다. 변수 앞에 *을 붙이면 튜플의 요소를 각각의 인자로 전달한다.

이제 제대로 나온다.

 

그런데 앞의 ⓘ 아이콘이 거슬린다. 아이콘을 바꾸기 앞서, 버전이 1.0.0 인데 버전 숫자는 어떻게 관리하는지 알아보자. 역시 GPT에게 물어본다. 아래는 GPT가 정리해준 버전에 대한 숫자 규칙이다.

 

보통 소프트웨어 버전 관리는 Semantic Versioning (SemVer) 규칙을 따릅니다. 이 규칙에 따르면, 버전은 MAJOR.MINOR.PATCH 형식으로 구성됩니다. 일반적으로 다음과 같은 기준으로 버전을 올립니다:

1. Semantic Versioning 규칙

  • MAJOR (주 버전): 기능이 크게 변경되거나 하위 호환성을 깨는 변경 사항이 있을 때 올립니다. 예를 들어 1.0.0에서 2.0.0으로 변경됩니다.
  • MINOR (부 버전): 기능이 추가되지만 하위 호환성은 유지되는 경우에 올립니다. 예를 들어, 1.1.0이나 1.2.0처럼 업데이트합니다.
  • PATCH (패치 버전): 버그 수정, 보안 패치 또는 아주 작은 기능 개선 등이 있을 때 올립니다. 예를 들어 1.0.1, 1.0.2 등으로 버전을 올립니다.

예시

  • 1.0.0: 최초의 완전한 릴리스
  • 1.1.0: 기존 기능에 새로운 기능을 추가했을 때
  • 1.1.1: 기존 기능에서 작은 버그를 수정했을 때

버전 관리의 추가 포인트

  • 프리릴리스 버전: 아직 완전하지 않은 개발 버전이라면 1.0.0-alpha, 1.0.0-beta처럼 버전에 태그를 붙일 수 있습니다.
  • 자동화된 버전 관리 도구: 코드 저장소에서는 Git 태그를 통해 버전을 관리하거나, GitHub의 release 기능을 이용해 변경 사항에 따라 자동으로 버전을 올릴 수도 있습니다.

이번 건은 맞는 말이고 이런 내용의 단순 정보는 대부분 오류가 없지만 GPT는 그럴싸하게 소설을 쓸 수 있다. 이럴 때는 반드시 검증을 해야한다.

 

버전에 대해 알아봤으니 ⓘ 아이콘을 없애보자. GPT에게 물어보니, Tkinter의 메시지박스는 아이콘을 바꿀 수 없다고 한다. 그래서 Toplevel을 써야한다고 한다. 창을 직접 만들어 나가는 거니 생각보다 코드가 복잡하고 길어진다. 굳이 해야할가 생각하다가 어차피 코딩 연습용이니까 해보려고 한다.

from tkinter import Toplevel

PROGRAM_INFO_TITLE = "프로그램 정보"
PROGRAM_INFO_MESSAGE = "제작자: HeumDuNeo\n버전: 1.0.0"
PROGRAM_ICON_PATH = "path/to/your/icon.ico"  # 프로그램 아이콘 경로

class AnalogClockApp:
# ...... 생략
    def show_program_info(self):
        info_window = Toplevel(self.root)
        info_window.title(PROGRAM_INFO_TITLE)
        info_window.geometry("300x150")  # 창 크기 설정
        info_window.iconbitmap(PROGRAM_ICON_PATH)  # 프로그램 아이콘 설정

        # 메시지와 아이콘 표시
        icon_label = tk.Label(info_window)
        icon = tk.PhotoImage(file=PROGRAM_ICON_PATH)  # 아이콘을 이미지로 로드
        icon_label.config(image=icon)
        icon_label.image = icon  # 이미지 참조 유지
        icon_label.pack(pady=10)

        # 프로그램 정보 메시지 표시
        message_label = tk.Label(info_window, text=PROGRAM_INFO_MESSAGE, font=("Arial", 10))
        message_label.pack(pady=10)

        # 닫기 버튼
        close_button = tk.Button(info_window, text="닫기", command=info_window.destroy)
        close_button.pack(pady=5)

        # 창을 최상위로 유지 (선택 사항)
        info_window.transient(self.root)  # 부모 창 위에 표시
        info_window.grab_set()  # 다른 창 클릭 비활성화
        self.root.wait_window(info_window)

우선 from tkinter import Toplevel 이부분은 어차피 Toplevel 은 한번만 쓰므로 지우고 Toplevel 앞에 tk.를 붙여 tk.Toplevel로 해줬다.

 

그리고 실행하면 오류가 난다. tk.PhotoImage 메서드는 ico 파일을 지원하지 않기 때문이다. PNG나 GIF 파일을 지원하므로 블로그 아이콘 PNG 파일을 새롭게 썼다.

 

그리고 실행해보면

아무것도 없는 거 같다. 하지만 아니다. 화면을 키워보니 다음과 같다.

이미지가 원본 해상도 그대로 출력된다. 이미지를 줄여 쓸까 하다가 어차피 ico 파일을 쓰는게 여러모로 편할 거 같다. 마침 Pillow 라이브러리를 사용하면 ico 파일을 png 형식으로 바꿔 쓸 수 있다고 GPT가 알려준다.

        icon_image = Image.open(DEFAULT_ICON_IMAGE)
        icon = ImageTk.PhotoImage(icon_image)  # 아이콘을 이미지로 로드

 

그리고 실행해본다.

아이콘 모양이 여전히 과하게 큰 거 같다. 이전에 자주 썼던 resize를 이용해 아이콘 크기를 줄여본다.

        # 메시지와 아이콘 표시
        icon_label = tk.Label(info_window)
        icon_image = Image.open(DEFAULT_ICON_IMAGE)
        icon_image = icon_image.resize((100,100), Image.LANCZOS)
        icon = ImageTk.PhotoImage(icon_image)  # 아이콘을 이미지로 로드
        icon_label.config(image=icon)
        icon_label.image = icon  # 이미지 참조 유지
        icon_label.pack(pady=10)

참고로 icon_image 단계에서 resize해야 한다. icon 단계에서 PhotoImage 객체에는 resize 메서드를 쓸 수 없기 때문에 resize를 사용하면 오류가 난다.

 

폰트는 맑은 고딕으로 바꿔주고, 텍스트는 왼쪽 정렬로 한다. 왼쪽 정렬은 GPT에 물어보니 다음과 같다.

justify="left": 텍스트의 줄을 왼쪽으로 정렬하도록 설정합니다.

        # 프로그램 정보 메시지 표시
        message_label = tk.Label(info_window, text=PROGRAM_INFO_MESSAGE, font=("맑은 고딕", 10), justify="left")
        message_label.pack(pady=10)

 

여기에 창 크기를 적당히 고쳐주고 문구도 적당히 바꿔서 실행시키니 다음과 같다.

근데 여기서 블로그 링크도 걸고 싶어졌다. 어떻게 해야할지 GPT에게 물어본다.

우선 webbrowser 모듈을 import 해야한다.

import webbrowser

 

그리고 기존 show_program_info() 메서드에 하이퍼링크 텍스트를 추가한다. 참고로 진짜 하이퍼링크는 아니고 하이퍼링크처럼 보이게 하는 것이다.(파란 글자, 손가락 커서)

        # 하이퍼링크 텍스트 추가
        link_label = tk.Label(
            info_window, text="제작자: https://heumdoneo.tistory.com/", font=("맑은 고딕", 10),
            fg="blue", cursor="hand2"
        )

원래는 이거였는데 주소가 반복되어 아래처럼 고쳤다.

        # 하이퍼링크 텍스트 추가
        link_label = tk.Label(
            info_window, text=f"제작자: {LINK_URL}", font=("맑은 고딕", 10),
            fg="blue", cursor="hand2"
        )

물론 코드 상단에 LINK_URL = "https://heumdoneo.tistory.com/"  # 링크 URL  변수에 링크를 저장해야한다.

 

 

라벨을 추가했으면 이제 클릭하면 해당 주소로 가게 해보자

        # 링크 클릭 시 브라우저로 열기
        def open_link(event):
            webbrowser.open(LINK_URL)

        link_label.bind("<Button-1>", open_link)  # 클릭 이벤트 바인딩

link_label 은 참고로 그냥 객체 변수이다. link_label에 <button-1>이 감지되면, open_link 메서드가 호출되게 바인딩한다. 참고로 button-1은 마우스 좌클릭이다. 버튼2는 휠클리이고, 버튼3은 마우스 우클릭이다.

정리하면 아까 생성한 링크처럼 보이는 텍스트인 이름이 link_label 인 객체에 좌클릭하면 open_link를 실행하게 하는 것이다.

 

실행해보자.

 

잘 된다.

 

좀 더 다듬어 봤다.

        	# 하이퍼링크 텍스트 추가
        link_label_1 = tk.Label(info_window, text=f"이 프로그램 만드는 과정 소개    ", font=("맑은 고딕", 10), fg="blue", cursor="hand2", justify="left")
        link_label_1.pack(pady=1)
        link_label_2 = tk.Label(info_window, text=f"이 프로그램 만드는 과정 첫 페이지", font=("맑은 고딕", 10), fg="blue", cursor="hand2", justify="left")
        link_label_2.pack(pady=1)
        
                # 닫기 버튼
        close_button = tk.Button(info_window, text="닫기", command=info_window.destroy, justify="right")
        close_button.pack(pady=5)

 

그런데 배치가 맘에 들지 않는다. 나는 글자는 왼쪽 정렬 하고 싶어서 왼쪽 정렬 했지만, 하이퍼 링크 부분을 보면 중앙 정렬되어 있다. 또 닫기 버튼도 우측이 아닌 가운데 붙어있다. 이거는 Tkinter에서 pack()메서드가 기본적으로 가운데 정렬이기 때문이다. justify="left"는 이 가운데 정렬 내에서 왼쪽으로 정렬하란 뜻이다. 말하자면 pack이 있고, 그 안에 내용물이 있는데 내용물만 왼쪽 정렬이고 pack은 중앙정렬이라 중앙 정렬처럼 보이는 것이다.

해결법은 pack(side="left")처럼 바꾸거나 grid()메서드를 사용하는 것이다. pack(side="left")은 여유 없이 너무 붙어버려 보기가 좋지 않다. 그래서 pack()을 버리고 grid()를 사용하겠다.

gird()를 사용하면 또 좋은 것이 바둑판처럼 행렬을 사용해서 위치를 자유롭게 쓸 수 있다.

        link_label_2.grid(row=3, column=0, columnspan=2, padx=30, sticky="w")

link_label_2는 객체 변수 명

row는 배치하고자 하는 행

column은 배치하고자 하는 열

columnspan은 몇 개 열을 차지할지를 정한다. 생략하면 1이다. 2면 열 두칸을 차지한다는 뜻이다.

padx, pady 각각 x축(가로), y축(세로)로 여백을 얼마나 둘 지 정한다.

sticky="w"  는 정렬이다. 동e서w남s북n으로 방향을 정하는데 w는 서쪽인 왼쪽 정렬이란 뜻이다. 응용하면 we라 적으면 왼쪽과 오른쪽으로 확장된다. nsew는 전체로 확장된다.

정리했다.

그리고 이제 배포판을 만들어서 켜보자. 그런데 배포판에서 이상하게 프로그램 정보가 백지로 나온다.

로고 아이콘을 PyInstaller로 패키지로 만들 때 _MEIPASS 경로에 저장되는 것을 기억 할 것이다. 그런데 show_program_info() 메서드에서 이 경로의 아이콘을 쓰지 않고 그냥 기본값인 DEFAULT_ICON_IMAGE 변수로 썼기 때문이다.

if hasattr(sys, '_MEIPASS'):
    # PyInstaller로 패키징된 경우 '_MEIPASS' 경로 사용
    self.icon_path = os.path.join(sys._MEIPASS, DEFAULT_ICON_IMAGE)
    self.background_image_path = self.load_background_image_from_registry() or os.path.join(sys._MEIPASS, DEFAULT_BACKGROUND_IMAGE)
else:
    # 개발 중일 때는 현재 디렉토리의 아이콘 파일 사용
    self.icon_path = DEFAULT_ICON_IMAGE
    self.background_image_path = self.load_background_image_from_registry() or DEFAULT_BACKGROUND_IMAGE

# 아이콘 설정
self.root.iconbitmap(self.icon_path)

이렇게 아이콘 경로를 self. 를 붙여 show_program_info() 메서드에서도 사용 가능하게 변경해준다.

    def show_program_info(self):
        info_window = tk.Toplevel(self.root)
        info_window.title(PROGRAM_INFO_TITLE)
        info_window.iconbitmap(self.icon_path)  # 프로그램 아이콘 설정

        # 메시지와 아이콘 표시
        icon_label = tk.Label(info_window)
        icon_image = Image.open(self.icon_path)
        # ......

그리고 show show_program_info() 메서드에서 기존 DEFAULT_ICON_IMAGE 를 self.icon_path으로 고친다.

 

실행해 봤다!  완벽하다(???) 그런데 그동안 삼각형 로고 아이콘에 투명이 아닌 하얀 색을 넣는 걸 깜빡해왔다는 것을 발견했다. 프로그래밍에 완벽은 없는 것 같다. 다시 고쳤다.

 


이제 프로그램을 완성했다. 완성했으니 사진도 바꾸고 약간 다듬어 전체 코드와 프로그램을 업로드 한다.

 

 

HeumDuNeoClock.exe
15.51MB

실행 파일

 

HeumDuNeo ClockAPP.ico
0.01MB

아이콘

 

HeumDuNeoClock.py
0.02MB

파이썬 코드

 

다음에는 할 수 있다면 투명모드 기능을 추가해보도록 한다.