tkinter-design <Palette>

Source code: github

 

GitHub - Denev6/tkinter-design: Python tkinter GUI Design

Python tkinter GUI Design. Contribute to Denev6/tkinter-design development by creating an account on GitHub.

github.com

< 코드 > < 이미지 > < 설명 > 순서로 제작되어 있으며,
상황에 따라 일부 내용이 생략되어 있습니다.


기본 정보

Contributors

  • 코드: DeneV
    • 아래 작성된 코드는 전체 프로젝트 중 일부를 편집하여 작성되었습니다.
  • 프로젝트: Team Palette

Licenses & Copyrights

  • Code
    • 코드는 수정 / 배포 / 사적 목적으로 사용할 수 있습니다.
    • 단, 프로젝트에 사용된 이미지 및 폰트의 저작권은 외부에 있으며 이에 대한 사용 책임은 코드 사용자에게 있습니다. 
  • img/logo.png
    • Image by Cha_cha
    • "tvN 즐거운이야기"가 적용되어 있습니다.
    • "tvN 즐거운이야기체"의 지적 재산권은 CJ E&M에 있습니다.
  • img/sample.png
    • Photo by DeneV
  • img/icon/*.png
    • (c) 2013-2017 Cole Bemis, The MIT License

Files

Palette
    |- main.py
    |- palette
    |   |- __init__.py
    |   |- data.py
    |   |- functions.py
    |- img
        |- icon.ico
        |- error.png
        |- logo.png
        |- design
        |   |- *.png
        |- icon
        |   |- *.png
        |- photo

Version

  • python: 3.9.7
  • pillow: 8.4.0 (보안문제로 인해 9.0.1 권장)

main.py

from tkinter import *
import tkinter.ttk as ttk
from tkinter.tix import *

from PIL import ImageTk

from palette.functions import *

tkinter

class App(Tk):
    def __init__(self):
        Tk.__init__(self)
        Tk.title(self, "Palette")
        Tk.geometry(self, "360x480+500+200")
        Tk.resizable(self, 0, 0)
        ico_path = get_path("img\icon.ico")  # functions.py 참고
        Tk.iconbitmap(self, ico_path)
        self._frame = None
        self.refresh_frame(IntroPage)

    def refresh_frame(self, frame, *parameters):
        if self._frame is not None:
            self._frame.destroy()
        self._frame = frame(self, *parameters)
        self._frame.pack(expand=YES)

Tk를 상속받은 App 클래스를 생성하여 페이지를 만듭니다.

  • title: 해당 UI 상단에 표시되는 제목을 설정합니다.
  • geometry: '가로x세로' 크기를 설정하고 '+가로+세로'를 통해 화면에 생성될 위치를 지정합니다.
  • resizable: 화면 크기를 사용자가 늘릴 수 있는 범위를 지정합니다.
    • (0, 0)은 사용자가 화면 크기를 조정할 수 없도록 합니다.
  • iconbitmap: 상단 좌측에 표시될 아이콘을 설정합니다.

__init__의 refresh_frame을 통해 코드를 실행할 때 보여질 첫 페이지를 설정합니다.
refresh_frame을 통해 window 창을 유지한 상태에서 화면의 내용을 변경할 수 있도록 합니다.


기본 설정

class MainFrame(Frame):
    def __init__(self, master):
        super().__init__(master)
        # 기본 디자인 정보
        self._font = (None, 13)
        self._bg = "#ffffff"
        self._green = "#349E71"
        self._green2 = "#CDEFE0"
        self.pack(fill="both", expand=True)
        self._trend_text = crawl_naver_datalab()  # functions.py 참고
        style = ttk.Style()
        # 스타일 설정
        style.theme_use("clam")
        style.map("TCombobox", fieldbackground=[("readonly", "#ffffff")])
        style.map("TCombobox", selectbackground=[("readonly", "#ffffff")])
        style.map("TCombobox", selectforeground=[("readonly", "#000000")])
        style.map("TCombobox", background=[("readonly", "#ffffff")])
        style.configure("TNotebook", background=self._green)
        style.configure("TNotebook.Tab", font=(None, 10))
        style.map("TNotebook.Tab", background=[("selected", "#FEDF51")])

페이지에서 사용할 기본 정보를 설정합니다.

  • self._font = (폰트, 크기)
  • self._bg: 배경색
  • style.theme_use: tkinter에서 제공하는 위젯 스타일을 변경할 수 있습니다.
    • "TCombobox": 선택 페이지의 combobox를 커스텀합니다.
    • "TNotebook.Tab": 결과 페이지의 notebook 상단 tab을 커스텀합니다.

세부 기능

정보 제공

class InfoPage(Toplevel):
    def __init__(self, master):
        super().__init__(master)
        self.title("INFO")
        self.geometry("300x360+680+270")
        Label(self, text="TEXT").pack()

새로운 window 창을 생성해 텍스트 정보를 제공합니다.


이미지 표시

class ImgPage(Toplevel):
    def __init__(self, master, img_file_name, folder=None, extension="png"):
        super().__init__(master)
        image = get_img(img_file_name, folder, extension)  # functions.py 참고
        photo = ImageTk.PhotoImage(image)
        width, height = image.size
        self.title("img_file_name")
        self.geometry(f"{width}x{height}+680+270")
        self.resizable(0, 0)
        label = Label(self, image=photo)
        label.image = photo
        label.pack()

새로운 window 창을 생성해 이미지 정보를 제공합니다.
image.size를 이용해 이미지의 가로, 세로 정보를 받고 geometry를 통해 자동으로 창의 크기가 바뀌도록 설계하였습니다.


마우스 오버

class InfoBalloon(Balloon):
    def __init__(self, master):
        super().__init__(master, initwait=60)
        for i in self.subwidgets_all():
            i.config(bg="#ffffff")
        self.message.config(wraplength=100)

마우스를 지정한 위젯에 올렸을 때 정보창을 제공합니다.


표지

class IntroPage(MainFrame):
    def __init__(self, master):
        super().__init__(master)
        self.config(bg="#ffffff")
        Label(self, bg=self._bg, height=4).pack(fill="x")  # 상단 여백을 위해 추가
        # 로고 설정
        image = get_img("logo")
        photo_logo = ImageTk.PhotoImage(image)
        label_logo = Label(self, image=photo_logo, bg=self._bg)
        label_logo.image = photo_logo
        label_logo.pack()
        # 시작 버튼
        image = get_img("button-start", "design")
        photo_button = ImageTk.PhotoImage(image)
        button = Button(
            self,
            image=photo_button,
            relief="flat",
            bg=self._bg,
            command=lambda: master.refresh_frame(StartPage),
        )
        button.image = photo_button
        button.pack(ipadx=3, ipady=2)

코드를 실행했을 때 보여질 첫 화면을 설정합니다.


선택 페이지

class StartPage(MainFrame):
    def __init__(self, master):
        super().__init__(master)
        self.config(bg=self._bg)
        combo_var = StringVar()
        entry_1_var = DoubleVar()
        entry_2_var = DoubleVar()
        entry_3_var = DoubleVar()

해당 페이지에서 사용자로부터 입력 받을 내용을 선언합니다.

  • 자료형에 따라 StringVar, IntVar, DoubleVar 등을 사용합니다.
f_title = Frame(self, bg=self._bg)
f_title.pack(fill="x", pady=10)
    ...
# 제목 옆 이미지
image = get_img("icon", extension="ico").resize((25, 25), Image.ANTIALIAS)
photo_icon = ImageTk.PhotoImage(image)
icon = Label(f_title, image=photo_icon, bg=self._bg)
icon.image = photo_icon
icon.pack(side="left")
# 제목 텍스트
Label(f_title, text="Basic Info").pack()

상단 아이콘 및 제목을 설정합니다.

f_left = Frame(self, bg=self._bg)
f_right = Frame(self, bg=self._bg)
f_btn = Frame(self, bg=self._bg)
f_btn.pack(side="bottom")
f_left.pack(side="left")
f_right.pack(side="right")

"좌측: 텍스트, 우측: 입력 창, 하단: 버튼" 형태로 제작하였으며, 제작하는 디자인에 따라 다양하게 변형할 수 있습니다.

Label(f_left, bg=self._bg, text="OPTION").pack()
Label(f_left, bg=self._bg, text="ENTRY_1").pack()
    ...

좌측 텍스트를 설정합니다.

# Combobox
combo = ttk.Combobox(
    f_right, textvariable=combo_var, state="readonly"
)  # readonly를 통해 사용자가 정보를 수정할 수 없도록 합니다.
combo["values"] = ["OPTION_A", "OPTION_B", "OPTION_C"]
combo.current(0)
combo.pack()
# Entry
entry_1 = Entry(f_right, textvariable=entry_1_var)
entry_1.pack()
    ...
entry_1.delete(0, END)  # 내용 초기화
    ...
  • Combobox: 사용자 정해진 선택지 내에서 선택할 수 있도록 합니다.
  • Entry: 사용자가 자유롭게 내용을 입력할 수 있도록 합니다.
    • 해당 값의 자료형에 따라 예외처리를 할 필요가 있습니다.
    • tkinter.messagebox.showwarning('Title', 'message')를 사용하면 경고창을 화면에 띄울 수 있습니다.
# File Save 기능
image = get_img("save-mint", "icon").resize((25, 25), Image.ANTIALIAS)
photo_save = ImageTk.PhotoImage(image)
width_save, height_save = image.size
btn_save = Button(
    f_btn,
    bg=self._bg,
    image=photo_save,
    relief="flat",
    width=width_save,
    height=height_save,
    activebackground=self._bg,
    command=lambda: save_new_csv(entry_1, entry_2, entry_3),
)
btn_save.image = photo_save
btn_save.pack()

# File Open 기능
image = get_img("file-text-mint", "icon").resize((25, 25), Image.ANTIALIAS)
photo_file = ImageTk.PhotoImage(image)
width_file, height_file = image.size
btn_open = Button(
    f_btn,
    bg=self._bg,
    image=photo_file,
    relief="flat",
    width=width_file,
    height=height_file,
    activebackground=self._bg,
    command=lambda: open_csv(entry_1, entry_2, entry_3),
)
btn_open.image = photo_file
btn_open.pack()

Entry에 입력한 데이터를 저장하거나 불러오는 기능을 제공합니다.

# Menu
self._menu = Menu(master)
# Menu 1
self._menu_data = Menu(self._menu, tearoff=0)
self._menu_data.add_command(
    label="OPEN", command=lambda: open_csv(entry_1, entry_2, entry_3)
)
self._menu_data.add_command(
    label="SAVE", command=lambda: save_new_csv(entry_1, entry_2, entry_3)
)
self._menu_data.add_separator()
self._menu_data.add_command(label="INFO", command=lambda: InfoPage(master))
self._menu.add_cascade(label="DATA", menu=self._menu_data)
# Menu 2
self._menu_sample = Menu(self._menu, tearoff=0)
self._menu_sample.add_command(label="INFO", command=lambda: InfoPage(master))
self._menu.add_cascade(label="SAMPLE", menu=self._menu_sample)
master.config(menu=self._menu)


상단의 menu를 사용할 수 있습니다.

def disable_menu_file(self):
    self._menu_data.entryconfig("OPEN", state="disable")
    self._menu_data.entryconfig("SAVE", state="disable")
    self._menu_data.entryconfig("INFO", state="disable")

더 이상 사용하지 않는 메뉴를 비활성화합니다.

  • 특정 페이지에서만 사용가능한 기능을 다른 페이지에서 사용할 수 없도록 합니다.
def switch_to_ResultPage(self, master):
    # Get data from ComboBox and Entry
        ...
    self.disable_menu_file()
    master.refresh_frame(ResultPage, texts, imgs, urls)

사용자가 입력한 정보를 가져올 수 있도록 코드를 설계합니다.
disable_menu_file을 통해 일부 메뉴를 비활성화합니다.
refresh_frame을 통해 다음 페이지로 넘어갈 수 있도록 합니다.


결과 페이지

class ResultPage(MainFrame):
    def __init__(self, master, texts, imgs, urls):
        super().__init__(master)
        self.config(bg=self._green)
        notebook = ttk.Notebook(self, width=300, height=370)
        notebook.pack(expand=True)
        f_1 = Frame(self, bg="#ffffff")
        f_2 = Label(self, bg="#ffffff")
        f_3 = Frame(self, bg="#ffffff")

notebook: 책갈피 형태로 frame을 선택하여 볼 수 있습니다.

notebook.add(f_1, text="sample_1")
Label(f_1, bg="#ffffff", height=2).pack(fill="x")  # 상단 여백을 위해 입력
Button(
    f_1, text=texts[0], command=lambda: show_ImgPage(ImgPage, master, imgs[0])
).pack()
    ...
# 하단 이미지와 텍스트 정보
f_ref_1 = Frame(f_1, bg="#ffffff")
f_ref_1.pack(side="bottom")
image = get_img("image-mint", "icon")
photo_img = ImageTk.PhotoImage(image)
f_image = Label(f_ref_1, image=photo_img, bg="#ffffff")
f_image.image = photo_img
f_image.pack(side="left")
Label(f_ref_1, text="* click to \nshow images.", bg="#ffffff").pack(side="left")

텍스트를 클릭하면 지정된 이미지 파일을 보여줍니다.

notebook.add(f_2, text="sample_2")
Label(f_2, bg="#ffffff", height=2).pack(fill="x")
Button(f_2, text=texts[0], command=lambda: open_url(urls[0])).pack()
    ...
# 하단 이미지와 텍스트 정보
f_ref_tip = Frame(f_2, bg=self._bg)
f_ref_tip.pack(side="bottom")
image = get_img("link-mint", "icon")
photo_link = ImageTk.PhotoImage(image)
f_image = Label(f_ref_tip, image=photo_link, bg="#ffffff")
f_image.image = photo_link
f_image.pack(side="left")
Label(f_ref_tip, text="* click to \nopen urls.", bg="#ffffff").pack(side="left")

텍스트를 클릭하면 지정된 url을 기본 브라우저를 통해 보여줍니다.

notebook.add(f_3, text="sample_3")
# y-Scrollbar
scrollbar = Scrollbar(f_3, orient="vertical")
scrollbar.pack(side="right", fill="y")
f_text = Text(f_3, yscrollcommand=scrollbar.set)
f_text.pack(side="left", fill="both")
scrollbar.config(command=f_text.yview)
# Text 초기화 & 입력
f_text.delete(1.0, END)
f_text.insert(1.0, self._trend_text)
f_text.configure(state="disabled")  # 시용자가 내용을 수정할 수 없도록 합니다.

Naver DataLab 크롤링을 통해 최근 패션/의류 인기 검색어를 크롤링한 결과를 보여줍니다.

y축으로 스크롤할 수 있는 scrollbar를 제공합니다.


추가 입력 페이지

class OptionalPage(MainFrame):
    def __init__(self, master, texts, imgs, urls):
        super().__init__(master)
        self.config(bg=self._bg)
        combo_1 = StringVar()
        combo_2 = StringVar()
        combo_3 = StringVar()

해당 페이지에서 사용자로부터 받을 내용을 선언합니다.

# Question-Mark 이미지 불러오기
image = get_img("help-circle-black", "icon").resize((12, 12), Image.ANTIALIAS)
photo_help = ImageTk.PhotoImage(image)
tip_mark = Label(f_color, image=photo_help, bg=self._bg)
tip_mark.image = photo_help
tip_mark.pack(side="right")
    ...
# 마우스 오버 정보 제공
tip_box = InfoBalloon(master)
tip_box.bind_widget(tip_mark, balloonmsg="Here we have Info")

이미지(Label) 위에 마우스를 올리면 텍스트가 담긴 박스를 보여줍니다.

  • 클릭하면 작동하는 Button과 달리, 마우스 오버로 작동합니다.

추가 결과 페이지

notebook.add(f_color, text="sample_2")
Label(f_color, text="Colors").pack()
Label(f_color, bg=colors[0]).pack()
    ...


입력 받는 색상을 확인할 수 있도록 합니다.

  • 사용자가 색상을 입력하지 않을 경우, 배경색과 같은 색으로 설정합니다.
btn_quit = Button(f_btn, ... command=master.quit)

quit을 이용해 별도의 함수를 설정하지 않고 프로그램을 종료할 수 있습니다.


실행

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

위에서 제작한 코드가 화면에 실행되도록 합니다.


functions.py

from tkinter import filedialog
from tkinter.constants import END
import tkinter.messagebox as msgbox
import os
import webbrowser

from PIL import Image

if __name__ == "__main__":
    from data import *
else:
    from palette.data import *

이미지

def get_path(path):
    return os.path.join(os.getcwd(), path)

입력한 path의 절대경로를 반환합니다.

def get_img(img_file_name, folder=None, extension="png"):
    if folder is None:
        # 폴더가 없을 때
        img_path = get_path(f"img\{img_file_name}.{extension}")
    elif folder:
        # 폴더가 있을 때
        img_path = get_path(f"img\{folder}\{img_file_name}.{extension}")
    try:
        image = Image.open(img_path)
    except:
        # 오류 발생
        img_path = get_path("img\error.png")
        image = Image.open(img_path)
    return image

"파일 이름 / 파일 경로(선택) / 확장자"를 입력 받아 이미지를 반환합니다.
오류 발생 시, 지정한 이미지(error.png)를 반환합니다.

def show_ImgPage(class_name, master, *parameters):
    # class_name을 ImgPage로 설정
    return class_name(master, *parameters)

class_name을 ImgPage로 설정하여 이미지를 보여줍니다.

  • 이미지 표시를 참고해주세요.

URL 열기

def open_url(url):
    try:
        webbrowser.open(url)
    except webbrowser.Error as exp:
        msgbox.showerror("오류", f"브라우저를 정상적으로 열 수 없습니다.\n{exp}")
    except Exception:
        return

기본 브라우저를 통해 지정한 url을 보여줍니다.
오류 발생 시, 지정한 메시지를 보여줍니다.


data.py

sample = {
    "key": "value"
}

데이터를 저장해 functions에서 사용할 수 있습니다.