soma0sd

코딩 & 과학 & 교육

Tkinter: 제목표시줄 교체

반응형

파이썬(Python)의 내장 패키지 중 하나인 Tkinter 그래픽 유저 인터페이스를 작성할 수 있도록 해줍니다. 비슷한 기능을 하는 유명한 패키지로는 PyQt가 있습니다. Tkinter는 PyQt에 비해 자유도가 낮지만 그만큼 빠르게 개발할 수 있고, 파이썬 라이센스를 따르기 때문에 배포와 이용이 비교적 자유롭습니다.

tkinter의 윈도우

아래의 스크립트를 사용하기 전에 app.ico를 준비해서 같은 디렉토리에 넣어두어야 제대로 작동합니다. "png to ico"등으로 구글링 하여 온라인 파일 변환 서비스를 이용하면 그림을 아이콘 파일(*.ico)로 쉽게 만들 수 있습니다.

from tkinter import Tk, ttk


class app(Tk):
  def __init__(self, **args):
    super().__init__(**args)
    # 윈도우 설정
    self.title("제목표시줄 교체")
    self.iconbitmap("app.ico")
    self.minsize(300, 200)

    # 요소 정의
    widget = ttk.Frame(self)
    label = ttk.Label(widget, text="위젯영역")

    # 요소 배치
    widget.pack(side='bottom', fill='both', expand='yes')
    label.pack()

if __name__ == "__main__":
  app().mainloop()

그림 1. 윈도우10의 Tk 윈도우

클래스(class)를 사용해도 안해도 괜찮지만, 객체단위의 관리가 없다면 조금만 복잡해져도 관리에 문제가 생길 수 있기 때문에 가능하면 클래스를 사용해서 GUI를 구성하는 것을 추천합니다. 윈도우 10의 다른 프로그램과 비교했을 때 파란색 제목표시줄이 상당히 거슬립니다.

Tk.overrideredirect

제목표시줄이 마음에 들지 않는다는 내용으로 구글링하면 가장 많이 나오는 답변이 Tk.overrideredirect(True)를 이용하라는 것입니다.

overrideredirect 값에 따른 GUI의 변화

기본 윈도우 프레임과 제목표시줄이 사라졌지만, 작업표시줄에서도 사라졌습니다. 이런 경우 GUI가 포커스를 잃어 비활성화 상태가 되어버리면 찾기가 힘들어집니다.

windll을 사용하여 작업표시줄에 아이콘을 만들고 연결하기

from tkinter import Tk, ttk
from ctypes import windll


class app(Tk):
  def __init__(self, **args):
    # ...생략...
    self.after(0, self._set_window)

  def _set_window(self):
    GWL_EXSTYLE = -20
    WS_EX_APPWINDOW = 0x00040000
    WS_EX_TOOLWINDOW = 0x00000080
    hwnd = windll.user32.GetParent(self.winfo_id())
    style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
    style = style & ~WS_EX_TOOLWINDOW
    style = style | WS_EX_APPWINDOW
    res = windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style)
    self.wm_withdraw()
    self.after(0, lambda: self.wm_deiconify())

여기서 Tk.after(시간, 함수)는 현재 GUI를 방해하지 말고 숨어서 작동하라는 의미 정도로 이해하시면 될 듯 합니다. windll을 쓰는 이상 윈도우에서만 정상적으로 작동합니다.

제목표시줄을 새로 만들고 기능 구현하기

원래 존재하던 제목표시줄의 기능에는 제목표시줄을 드래그했을 때의 창 이동과 더블클릭 했을 때의 최대화와 크기복원이 있습니다. 닫기와 같은 기능버튼도 있지만 여기서는 닫기버튼만 다시 만들도록 하겠습니다.

from tkinter import Tk, ttk
from ctypes import windll


class app(Tk):
  def __init__(self, **args):
    super().__init__(**args)
    # init: 윈도우 기본 설정
    self.title("제목표시줄 교체")
    self.overrideredirect(True)
    self.iconbitmap("app.ico")
    self.minsize(300, 200)
    self.after(0, self._set_window)

    # init: 요소 정의하기
    titlebar = ttk.Frame(self, style="titlebar.TFrame")
    widget = ttk.Frame(self)
    title = ttk.Label(
        titlebar, text=self.title(), style="titlebar.TLabel")
    close = ttk.Button(
        titlebar, text='X', takefocus=False, command=self.on_exit)
    label = ttk.Label(
        widget, text="위젯영역")

    # init: 요소 배치하기
    titlebar.pack(side='top', fill='x', expand='no')
    widget.pack(side='bottom', fill='both', expand='yes')
    title.pack(side='left', fill='x', expand='yes', pady=4)
    close.pack(side='right')
    label.pack()

    # 요소에 함수 바인딩하기
    # <ButtonPress-1>: 마우스 왼쪽 버튼을 누름
    # <ButtonRelease-1>: 마우스 왼쪽 버튼을 뗌
    # <Double-Button-1>: 마우스 왼쪽 더블클릭
    # <B1-Motion>: 마우스를 클릭한 상태로 움직임
    title.bind("<ButtonPress-1>", self.start_move)
    title.bind("<ButtonRelease-1>", self.stop_move)
    title.bind("<Double-Button-1>", self.on_maximise)
    title.bind("<B1-Motion>", self.on_move)

  def on_maximise(self, event):
    # 창의 제목을 더블클릭
    # 최대화와 복원을 토글
    if self.state() == 'normal':
      self.state("zoomed")
    else:
      self.state("normal")

  def start_move(self, event):
    # 창의 제목을 클릭
    # 위치 변수 등록
    self.x = event.x
    self.y = event.y

  def stop_move(self, event):
    # 마우스를 뗌
    # 변수 초기화
    self.x = None
    self.y = None

  def on_move(self, event):
    # 마우스 드래그
    # 윈도우를 이동
    deltax = event.x - self.x
    deltay = event.y - self.y
    x = self.winfo_x() + deltax
    y = self.winfo_y() + deltay
    self.geometry("+%s+%s" % (x, y))

  def on_exit(self):
    # 종료 버튼 클릭
    # GUI를 끝냄
    self.destroy()

  def _set_window(self):
    GWL_EXSTYLE = -20
    WS_EX_APPWINDOW = 0x00040000
    WS_EX_TOOLWINDOW = 0x00000080
    hwnd = windll.user32.GetParent(self.winfo_id())
    style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
    style = style & ~WS_EX_TOOLWINDOW
    style = style | WS_EX_APPWINDOW
    res = windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style)
    self.wm_withdraw()
    self.after(0, lambda: self.wm_deiconify())


if __name__ == "__main__":
  app().mainloop()

주요 내용과 기능은 스크립트의 주석을 참고하시기 바랍니다. bind(이벤트, 함수)의 이벤트는 tkinter 내부에서 약속한 문구들이기 때문에 다른 이벤트와 함수를 연결하고자 하는 경우에는 tkinter의 기술문서를 참고하시기 바랍니다.

반응형
태그:

댓글

End of content

No more pages to load