나를 기록하다
article thumbnail
반응형

이미지 합치기 프로그램 실행과정
오늘 실습한 내용을 실행하는 과정이다. 위의 그림파일도 오늘 실습한 프로그램을 통해 만들었다.

 

문제

여러 이미지를 합 치는 프로그램을 만드시오

[사용자 시나리오]
1. 사용자는 합치려는 이미지를 1개 이상 선택한다.
2. 합쳐진 이미지가 저장될 경로를 지정한다.
3. 가로넓이, 간격, 포맷 옵션을 지정한다.
4. 시작 버튼을 통해 이미지를 합친다.
5. 닫기 버튼을 통해 프로그램을 종료한다.

[기능 명세]
1. 파일추가 : 리스트 박스에 파일 추가
2. 선택삭제 : 리스트 박스에서 선택된 항목 삭제
3. 찾아보기 : 저장 폴더를 선택하면 텍스트 위젯에 입력
4. 가로넓이 : 이미지 넓이 지정(원본유지, 1024, 800, 640)
5. 간격 : 이미지 간의 간격 지정(없음, 좁게, 보통, 넓게)
6. 포맷 : 저장 이미지 포맷 지정(png, jpg, bmp)
7. 시작 : 이미지 합치기 작업 실행
8. 진행상황 : 현재 진행 중인 파일 순서에 맞게 반영
9. 닫기 : 프로그램 종료

 

 

 

 

풀이

import os
import tkinter.ttk as ttk
import tkinter.messagebox as msgbox
from tkinter import *  # __all__
from tkinter import filedialog
from PIL import Image

root = Tk()
root.title("PRAO GUI")


# 파일 추가
def add_file():
    files = filedialog.askopenfilenames(title="이미지 파일을 선택하세요", filetypes=(("PNG 파일", "*.png"), ("모든 파일", "*.*")),
                                        initialdir="/Users/prao/Desktop/DEV.PRAO/pythonworkspace/gui_project")
    # 사용자가 선택한 파일 목록
    for file in files:
        list_file.insert(END, file)


# 선택 삭제
def del_file():
    for index in reversed(list_file.curselection()):
        list_file.delete(index)


# 저장 경로 (폴더)
def browse_dest_path():
    folder_selected = filedialog.askdirectory(initialdir="/Users/prao/Desktop/DEV.PRAO/pythonworkspace/gui_project")
    if folder_selected == '':  # 사용자가 취소를 누를 때
        return
    txt_dest_path.delete(0, END)
    txt_dest_path.insert(0, folder_selected)


# 이미지 통합
def merge_image():
    try:
        # 가로넓이
        img_width = cmb_width.get()
        if img_width == "원본유지":
            img_width = -1  # -1일 떄는 원본 기준으로 유지
        else:
            img_width = int(img_width)

        # 간격
        img_space = cmb_space.get()
        if img_space == "좁게":
            img_space = 30
        elif img_space == "보통":
            img_space = 60
        elif img_space == "넓게":
            img_space = 90
        else:  # 없음
            img_space = 0

        # 포맷
        img_format = cmb_format.get().lower()
				# PNG, JPG, BMP 값을 받아와서 소문자로 변경

        """#################################################################"""

        images = [Image.open(x) for x in list_file.get(0, END)]

        # 이미지 사이즈를 리스트에 넣어서 하나씩 처리
        image_sizes = []  # [(width1, height1), (width2, height2), ...]
        if img_width > -1:
            # width 값 변경
            image_sizes = [(int(img_width), int(img_width * x.size[1] / x.size[0])) for x in images]
        else:
            image_sizes = [(x.size[0], x.size[1]) for x in images]  # 원본 사이즈 사용

        widths, heights = zip(*(image_sizes))

        # 최대 너비, 전체 높이 구하기
        max_width, total_height = max(widths), sum(heights)

        # 스케치북 준비
        if img_space > 0:  # 이미지 간격 옵션 적용
            total_height += (img_space * (len(images) - 1))

        result_img = Image.new("RGB", (max_width, total_height), (255, 255, 255))  # 배경 흰색
        y_offset = 0  # y 위치
        # for img in images:
        #     result_img.paste(img, (0, y_offset))
        #     y_offset += img.size[1]  # height 값 만큼 더해줌
        for idx, img in enumerate(images):
            # width 가 원본유지가 아닐 때에는 이미지 크기 조정
            if img_width > -1:
                img = img.resize(image_sizes[idx])

            result_img.paste(img, (0, y_offset))
            y_offset += (img.size[1] + img_space)  # height 값 + 사용자가 지정한 간격

            progress = (idx + 1) / len(images) * 100  # 실제 percent 정보를 계산
            p_var.set(progress)
            progress_bar.update()

        # 포맷 옵션 처리
        file_name = "prao_photo." + img_format
        dest_path = os.path.join(txt_dest_path.get(), file_name)
        result_img.save(dest_path)
        msgbox.showinfo("알림", "작업이 완료되었습니다.")
    except Exception as err:  # 예외처리
        msgbox.showerror("에러", err)


# 시작
def start():
    # 각 옵션들 값을 확인
    # print("가로넓이 : ", cmb_width.get())
    # print("간격 : ", cmb_space.get())
    # print("포맷 : ", cmb_format.get())

    # 파일 목록 확인
    if list_file.size() == 0:
        msgbox.showwarning("경고", "이미지 파일을 추가하세요")
        return

    # 저장 경로 확인
    if len(txt_dest_path.get()) == 0:
        msgbox.showwarning("경고", "저장 경로를 선택하세요")
        return

    # 이미지 통합 작업
    merge_image()


# 파일 프레임 (파일 추가, 선택 삭제)
file_frame = Frame(root)
file_frame.pack(fill="x", padx=5, pady=5)

btn_add_file = Button(file_frame, text="파일 추가", padx=5, pady=5, width=12, command=add_file)
btn_add_file.pack(side="left")

btn_del_file = Button(file_frame, text="선택 삭제", padx=5, pady=5, width=12, command=del_file)
btn_del_file.pack(side="right")

# 리스트 프레임
list_frame = Frame(root)
list_frame.pack(fill="both", padx=5, pady=5)

scrollbar = Scrollbar(list_frame)
scrollbar.pack(side="right", fill="y")

list_file = Listbox(list_frame, selectmode="extended", height=15, yscrollcommand=scrollbar.set)
list_file.pack(side="left", fill="both", expand=True)
scrollbar.config(command=list_file.yview)

# 저장 경로 프레임
path_frame = LabelFrame(root, text="저장경로")
path_frame.pack(fill="x", padx=5, pady=5, ipady=5)

txt_dest_path = Entry(path_frame)
txt_dest_path.pack(side="left", fill="x", expand=True, padx=5, pady=5, ipady=4)

btn_dest_path = Button(path_frame, text="찾아보기", width=10, command=browse_dest_path)
btn_dest_path.pack(side="right", padx=5, pady=5)

# 옵션 프레임
frame_option = LabelFrame(root, text="옵션")
frame_option.pack(padx=5, pady=5)

# 1. 가로 넓이 옵션
# 가로 넓이 레이블
lbl_width = Label(frame_option, text="가로넓이", width=8)
lbl_width.pack(side="left", padx=5, pady=5)

# 가로 넓이 콤보박스
opt_width = ["원본유지", "1024", "800", "640"]
cmb_width = ttk.Combobox(frame_option, state="readonly", values=opt_width, width=10)
cmb_width.current(0)
cmb_width.pack(side="left", padx=5, pady=5)

# 2. 간격 옵션
# 간격 옵션 레이블
lbl_space = Label(frame_option, text="간격", width=8)
lbl_space.pack(side="left", padx=5, pady=5)

# 간격 옵션 콤보박스
opt_space = ["없음", "좁게", "보통", "넓게"]
cmb_space = ttk.Combobox(frame_option, state="readonly", values=opt_space, width=10)
cmb_space.current(0)
cmb_space.pack(side="left", padx=5, pady=5)

# 3. 파일 포맷 옵션
# 파일 포맷 옵션 레이블
lbl_format = Label(frame_option, text="포맷", width=8)
lbl_format.pack(side="left", padx=5, pady=5)

# 파일 포맷 옵션 콤보
opt_format = ["PNG", "JPG", "BMP"]
cmb_format = ttk.Combobox(frame_option, state="readonly", values=opt_format, width=10)
cmb_format.current(0)
cmb_format.pack(side="left", padx=5, pady=5)

# 진행 상황 progress bar
frame_progress = LabelFrame(root, text="진행상황")
frame_progress.pack(fill="x", padx=5, pady=5, ipady=5)

p_var = DoubleVar()
progress_bar = ttk.Progressbar(frame_progress, maximum=100, variable=p_var)
progress_bar.pack(fill="x", padx=5, pady=5)

# 실행 프레임
frame_run = Frame(root)
frame_run.pack(fill="x", padx=5, pady=5)

btn_close = Button(frame_run, padx=5, pady=5, text="닫기", width=12, command=root.quit)
btn_close.pack(side="right", padx=5, pady=5)

btn_start = Button(frame_run, padx=5, pady=5, text="시작", width=12, command=start)
btn_start.pack(side="right", padx=5, pady=5)

root.resizable(False, False)  # x(너비), y(너비) 값 변경 불가 (창 크기 변경 불가)
root.mainloop()
  • enumerate

    enumerate는 파이썬의 내장 함수로, 이터러블(iterable) 객체를 입력으로 받아 인덱스와 해당 인덱스의 값을 함께 반환해주는 기능을 합니다. 이를 통해 반복문에서 현재 순환 중인 항목의 인덱스와 값을 쉽게 확인할 수 있습니다.

    enumerate는 다음과 같이 사용할 수 있습니다.

    for index, value in enumerate(iterable, start=0):
        # 코드 작성

    iterable: 순회할 수 있는 객체 (예: 리스트, 튜플, 문자열 등) start: 인덱스 시작 번호 (기본값은 0)

    예를 들어, 리스트의 요소들을 출력하면서 인덱스도 함께 출력하려면 다음과 같이 사용할 수 있습니다.

    fruits = ['사과', '바나나', '포도', '딸기']
    for index, fruit in enumerate(fruits):
        print(f"{index}: {fruit}"

    위 코드를 실행하면 다음과 같은 결과가 출력됩니다.

    # 결과
    0: 사과
    1: 바나나
    2: 포도
    3: 딸기

     

 

필요한 모듈 import하기

  1. os 모듈: 파일 경로 처리 등의 기능을 제공하는 모듈
  1. tkinter 모듈: GUI 프로그램을 만들기 위한 모듈
  1. tkinter.ttk 모듈: tkinter의 더 다양한 위젯을 제공하는 모듈
  1. tkinter.messagebox 모듈: 메시지 박스를 제공하는 모듈

 

 

PIL 모듈: 이미지 처리 등의 기능을 제공하는 모듈

  1. tkinter를 이용한 GUI 창 만들기
  1. Tk() 함수를 이용해 루트 윈도우 생성
  1. title() 함수를 이용해 윈도우 제목 설정

 

 

필요한 함수 만들기

  1. add_file(): 파일 추가 버튼을 눌렀을 때 실행되는 함수. 파일 탐색기 창을 열어 이미지 파일을 선택하고, 선택된 파일 목록을 리스트 박스에 추가함.
  1. del_file(): 선택 삭제 버튼을 눌렀을 때 실행되는 함수. 리스트 박스에서 선택된 항목을 삭제함.
  1. browse_dest_path(): 찾아보기 버튼을 눌렀을 때 실행되는 함수. 파일 탐색기 창을 열어 저장 경로를 선택하고, 선택된 경로를 텍스트 박스에 입력함.
  1. merge_image(): 시작 버튼을 눌렀을 때 실행되는 함수. 선택된 이미지 파일을 하나로 합치고, 지정된 포맷으로 저장함. 이 과정에서 각종 옵션(가로넓이, 간격, 포맷)을 적용할 수 있음.
  1. start(): 시작 버튼을 눌렀을 때 실행되는 함수. 파일 목록과 저장 경로를 확인하고, 이미지 통합 작업을 실행함.

 

 

GUI에 필요한 위젯 추가하기

  1. 파일 추가, 선택 삭제 버튼을 담을 파일 프레임 생성 후, 각 버튼 추가
  1. 리스트 박스와 스크롤바를 담을 리스트 프레임 생성 후, 리스트 박스와 스크롤바 추가
  1. 저장 경로를 입력할 텍스트 박스와 찾아보기 버튼을 담을 경로 프레임 생성 후, 각 위젯 추가
  1. 가로넓이, 간격, 포맷 등의 옵션을 담을 옵션 프레임 생성 후, 각종 콤보 박스, 레이블 추가
  1. 프로그램 실행 상황을 나타내는 진행 상황 프레임 생성 후, 프로그레스 바 추가
  1. 시작, 닫기 버튼을 담을 실행 프레임 생성 후, 각 버튼 추가

 

 

함수와 위젯 연결하기

  1. add_file(), del_file(), browse_dest_path() 함수는 각각 파일 추가, 선택 삭제, 저장 경로 찾아보기 버튼의 command 옵션으로 설정됨
  1. merge_image() 함수는 시작 버튼의 command 옵션으로 설정됨
  1. 리스트 박스와 텍스트 박스에 스크롤바를 연결함. 이를 위해 scrollbar.set() 함수를 사용함.
  1. cmb_width, cmb_space, cmb_format 콤보 박스에서 선택한 값을 가져와 merge_image() 함수에서 사용함.

 

 

실행하기

  1. root.mainloop() 함수를 호출하여 GUI 창을 실행함.

 

이런 식의 그림을 합친 파일이 생성됨.

 

본 게시글은 나도코딩님의 파이썬 코딩 무료 강의 (활용편2) - GUI 프로그래밍을 배우고 '여러 이미지 합치기' 프로그램을 함께 만들어요. 을 따라 실습한 내용입니다.

블로그 포스팅할 때 사진 여러개 올리기 귀찮은 경우가 많은데 이 프로그램 꽤나 쓸만할지도?

 

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...