그래픽 사용자 인터페이스(Graphic User Interface)

(Graphic User Interface)(GUI)는 그래픽 환경으로 프로그래밍을 할 수 있다. 파이썬에서는 tkinter, wxPython(wxWidget), PyQt 등의 GUI 프로그래밍 도구들이 있다.

Tkinter

tkinter(Tk interface)는 Tk를 파이썬에서 사용할 수 있도록하는 표준 파이썬 GUI 모듈이다. Tk는 GUI 프로그래밍을 할 수 있도록 하는 도구이다.

불러오기

import tkinter

간단한 프로그램

다음과 같은 간단한 프로그램을 실행해보자.

import tkinter

# root 위젯을 만든다.
 = tkinter.Tk()

# 창에 관한 모든 이벤트를 처리한다.
.mainloop()

아래와 같은 창이 하나 뜬다.

다음으로 창의 제목을 변경해보자. 최상위 레벨 창에 관한 메소드들 설명은 http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/toplevel.html에서 볼 수 있다.

import tkinter

 = tkinter.Tk()

# 창 제목 설정
.title("엄청 단순한 창!!")

.mainloop()

창이 너무 작아서 제목이 다 보이질 않는다.

창의 크기를 조절하고 아이콘을 지정해주자. 아이콘은 온라인 사이트 http://www.rw-designer.com/online_icon_maker.php에서 만들어 다운로드하여 사용할 수 있다.

import tkinter

 = tkinter.Tk()

.title("엄청 단순한 창!!")

# 창 크기 조절
.geometry("300x300")

# 창 아이콘 설정
.wm_iconbitmap('images/apple.ico')

.mainloop()

창 크기 조정은 geometry(창크기) 메소드를 이용해서 할 수 있고 `창크기 <http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/geometry.html>`__는 '가로x세로'로 정한다.

직접하기

위젯(widgets)

앞에서 빈 창을 만들어 봤다. 창에 무언가를 넣어 보자. 무언가를 위젯이라고 한다. 위젯에는 Button, Entry, Label, Frame, … 등이 있다. 먼저 레이블 위젯을 넣어 보자.

import tkinter

 = tkinter.Tk()

레이블 = tkinter.Label(, text='레이블')

레이블.pack()

.mainloop()

다음과 같은 창이 뜬다.

다음 줄

레이블 = tkinter.Label(, text='레이블')

tkinter Label을 만들고 그것을 레이블 변수에 저장하라는 것이다. 새로운 레이블을 만들 때 첫 번째 나오는 인수는 레이블을 위치 시킬 곳을 지정하는 것이고 다음 인자부터는 레이블의 속성들을 지정하는 것이다.

다음 줄

레이블.pack()

은 창에서 적당한 곳에 위치하게 만들라는 것이다. 텍스트 입력과 버튼을 추가하고 싶으면 다음과 같이 한다.

import tkinter

 = tkinter.Tk()
.title('위젯 예제')
.wm_iconbitmap('images/apple.ico')

레이블 = tkinter.Label(, text='레이블')
텍스트 = tkinter.Entry()
버튼 = tkinter.Button(, text="버튼")

레이블.pack()
텍스트.pack()
버튼.pack()

.mainloop()

아래와 같이 보인다.

위젯은 이 밖에도 많이 있다.

직접하기

  • 아래와 같이 만들어 보자. tkinter_password

위젯 개선하기

http://usingpython.com/making-widgets-look-nice/ 참조.

창의 색깔도 변경할 수 있다. 색깔은 다음 사이트 https://www.tcl.tk/man/tcl8.6/TkCmd/colors.htm 또는 http://wiki.tcl.tk/37701에서 확인할 수 있다.

import tkinter

 = tkinter.Tk()
.configure(background="aquamarine")
.title('환영합니다.')
.wm_iconbitmap('images/apple.ico')

그림 = tkinter.PhotoImage(file='images/apple.png')
 = tkinter.Label(, image=그림)
.pack()

안내레이블 = tkinter.Label(, text='이름과 나이를 입력하시오', font=("Helvetica", 16))
안내레이블.pack()

이름레이블 = tkinter.Label(, text='이름', fg='#383a39', bg='#a1dbcd')
이름입력 = tkinter.Entry()
이름레이블.pack()
이름입력.pack()

나이레이블 = tkinter.Label(, text='나이', fg='#383a39', bg='#a1dbcd')
나이입력 = tkinter.Entry()
나이레이블.pack()
나이입력.pack()

버튼 = tkinter.Button(, text="확인", fg='#a1dbcd', bg='#383a39')

버튼.pack()

.mainloop()

위젯 동적 추가

프로그램을 효율적으로 작성하기 위해서 반복문을 이용했듯이 GUI 프로그래밍에서도 사용할 수 있다. 간단한 계산기 GUI 프로그램을 만들기 위해서 다음과 같이 일일이 작성하는 것은 귀찮은 일이다.

버튼0 = tkinter.Button(, text='0')
버튼0.pack()
버튼1 = tkinter.Button(, text='1')
버튼1.pack()
버튼2 = tkinter.Button(, text='2')
버튼2.pack()
...

대신에 다음과 같이 for 반복문을 이용할 수 있다.

for  in range(10):
    버튼 = tkinter.Button(, text=)
    버튼.pack(side=tkinter.LEFT)

는 0부터 9까지 반복하면서 버튼 위젯을 동적으로 10번 만든다. side=tkinter.LEFT 인자는 창의 왼쪽부터 배치한다. 다음과 같이 보인다.

리스트를 이용해서 위젯의 스타일을 변경할 수 있다. 다음 예제는 색깔과 레이블 텍스트를 사용할 때 리스트를 이용한 것이다.

import tkinter

 = tkinter.Tk()

.title('색상')

.wm_iconbitmap('images/apple.ico')

색깔 = ['red', 'yellow', 'pink', 'green', 'purple', 'orange', 'blue']

for  in 색깔:
     = tkinter.Button(text=, bg=)
    .pack(fill=tkinter.X)

.mainloop()

여기서 다른 것은 색깔 리스트를 textbg에 사용했다는 것이다. pack() 인자로 fill=tkinter.X를 넘겨 주었는데 이것은 창 크기가 변할 때 가로 방향(x축 방향)으로 채워지라는 것이다. 아래쪽 그림과 같이 변한다.

color_list color_list_long

직접하기

  • 다음과 같은 프로그램을 작성하시오. image0

반응하기(이벤트)

이 절에서는 GUI 프로그램이 사용자의 요구에 반응하는 방법에 대해서 배운다.

import tkinter

 = tkinter.Tk()

.title('반응')

.wm_iconbitmap('images/apple.ico')

def 다시부르기():
    print('버튼이 눌렸습니다.')

 = tkinter.Button(, text='눌러주세요.', command=다시부르기)
.pack()

.mainloop()
버튼이 눌렸습니다.
버튼이 눌렸습니다.
버튼이 눌렸습니다.

위 프로그램을 실행하면 아래와 같이 뜨고 버튼을 3번 누르면 명령창에 버튼이 눌렸습니다가 3번 표시된다.

여기서 중요한 것은 버튼을 만들 때 사용된 인자 command=다시부르기이다. 이것은 버튼을 누를 때 다시부르기() 함수가 실행된다는 것이다. 이 메시지가 명령창에 보이는 것은 GUI 코딩이 원하는 것이 아니다. 메시지를 창에 표시해 보자.

import tkinter

 = tkinter.Tk()

.title('반응')

.wm_iconbitmap('images/apple.ico')

def 다시부르기():
    .config(text="버튼이 눌렸습니다.")

 = tkinter.Label(, text="아무것도 안 눌렸어요.")
.pack()

 = tkinter.Button(, text='눌러주세요.', command=다시부르기)
.pack()

.mainloop()

레이블 위젯 를 만들 때 처음 텍스트는 "아무것도 눌렸어요."로 설정 되어 있고 버튼을 누르면 다시부르기 함수가 실행이 되면서 레.config() 메소드가 실행이 되어 "버튼이 눌렸습니다."라는 텍스트가 보이게 된다. config() 메소드는 객체가 만들어진 후 속성들을 바꾸기 위해 사용된다. 버튼을 누르기 전과 후의 그림들이다.

button_click_bef button_click_aft

직접하기

  • 다음 그림과 같이 버튼을 누르면 색깔이 바뀌게 프로그램을 작성하시오. 2개의 함수 빨갛게(), 파랗게()를 만들어 호출하도록 할 수 있다. button_red_blue button_red button_blue

매개변수 있는 콜백 함수

다음과 같이 3개의 상자가 있을 때 어떤 상자에 상금이 들어 있는지를 맞추는 게임을 한다고 하자.

import tkinter
import random

 = tkinter.Tk()

.title('상금 타기')

.wm_iconbitmap('images/apple.ico')

 = random.randint(0, 2)

def 상금확인():
    if  == :
        .config(text="맞췄습니다.")
    else:
        .config(text="틀렸습니다.")

 = tkinter.Label(, text="어느 상자에 상금이 있을까요?")
.pack()

for  in range(3):
     = tkinter.Button(, text='상자'+str(), command=상금확인())
    .pack(side=tkinter.LEFT)

.mainloop()

위와 같이 하면 버튼을 만들면서 상금확인()을 실행하기 때문에 원하는 결과가 나오질 않는다. 따라서 다음과 같이 lambda 함수를 이용해서 적용해야 한다.

for  in range(3):
     = tkinter.Button(, text='상자'+str(), command=lambda num=: 상금확인(num))
    .pack(side=tkinter.LEFT)
import tkinter
import random

 = tkinter.Tk()

.title('상금 타기')

.wm_iconbitmap('images/apple.ico')

 = random.randint(0, 2)

def 상금확인():
    if  == :
        .config(text="맞췄습니다.")
    else:
        .config(text="틀렸습니다.")

 = tkinter.Label(, text="어느 상자에 상금이 있을까요?")
.pack()

for  in range(3):
     = tkinter.Button(, text='상자'+str(), command=lambda num=: 상금확인(num))
    .pack(side=tkinter.LEFT)

.mainloop()

직접하기

  • 다음과 같이 전화번호부에 있는 사람들을 보여주고 버튼을 누르면 자세한 정보를 보이는 프로그램을 작성하시오. phonebook

Tk

Tk 프로그래밍에서 중요한 개념은 위젯(widget), 배치 관리(geometry management), 이벤트 처리(event handling)이다. 위젯은 화면에 보이는 그래픽 성분들이고 배치관리는 이러한 위젯을 어느 부분에 위치시키고, 어떻게 보이게 할지를 관리하는 것이다. 위젯들 간의 상호 작용을 어떻게 할 것인지를 정하는 것이 이벤트 처리이다. 가령, 버튼을 눌렀을 때 무엇을 해야 하는지, 스크롤바를 움직였을 때 어떻게 보여야 하는지 등에 대해서 설정해야 한다. 이러한 기능은 command 또는 event binding을 통해서 정할 수 있다.

위젯(widget)

위젯(window gadget)이란 화면에 보이는 그래픽 성분들 모두를 의미한다고 할 수 있다. 예를 들면 버튼, 텍스튼 입력, 레이블, 첵크 박스, 라디오 버튼, 콤보 버튼, 스크롤바, 프레임 등이 있다. 위젯은 콘트롤(control) 또는 창(window)라고도 불린다.

뿌리 창(root window)

모든 위젯은 계층 구조 형식으로 표현된다. GUI 프로그래밍은 뿌리(root) 위젯(또는 창)이 만들어지고 그 안에 원하는 모든 위젯들을 넣어서 만들어 진다. 뿌리 창은 다음과 같이 생성한다.

import tkinter as tk

뿌리창 = tk.Tk()

뿌리창.mainloop()

위 프로그램을 실행하면 다음과 같은 창이 뜬다.

  • 3줄: tk.Tk()를 이용해서 뿌리 창 인스턴스를 생성한다.

  • 5줄: mainloop() 메소드는 뿌리 창의 이벤트를 처리하는 것으로 이 메소드가 있어야지만 창이 보이게 된다. 창 오른쪽 위에 있는 닫기 버튼을 클릭하면 mainloop()를 끝낸다. 기본 뿌리 창은 아이콘, 제목, 축소, 확대, 닫기 버튼이 달려있는 창이다.

위젯 만들기

위젯을 만들 때 뿌리(root) 창(또는 위젯)을 제외하고는 부모(parent or master) 위젯을 지정해주어야 한다. 모든 위젯은 뿌리 위젯 안에 포함되어 만들어진다. 부모 위젯 안에 포함되는 위젯을 자식(child or slave) 위젯이라고 부른다.

위젯을 만드는 문법은 다음과 같다.

위젯변수이름 = tkinter.위젯이름(부모위젯, **옵션)

다음은 루트 창에 레이블(label)과 버튼을 추가하는 예이다.

import tkinter as tk

뿌리창 = tk.Tk()

 = tk.Label(뿌리창, text='레이블 위젯입니다.')
.pack()

 = tk.Button(뿌리창, text='버튼')
.pack()

뿌리창.mainloop()
  • Label() 첫번째 인자 뿌리창은 레이블의 부모 위젯을 나타내고 두번째 인자 text는 레이블에 나타날 문자열을 의미한다.

  • Button() 메소드의 첫번째 인자는 레이블과 마찬가지로 버튼 위젯이 포함될 부모 위젯을 가리키고 두번째 인자는 버튼에 표시될 문자열을 의미한다.

  • pack() 메소드는 레이블과 버튼 위젯이 표시될 위치를 지정하는 것으로 배치 관리자에서 자세히 설명된다.

모든 위젯은 자신의 외관과 동작을 결정하는 여러 가지 옵션들을 가지고 있다. 예를 들면 문자 표시, 글자 크기, 색깔 및 테두리를 결정하는 옵션들이 있다. 이러한 옵션들은 인스턴스를 만들 때 설정할 수도 있고 인스턴스를 만들어 놓은 후 변수이름.config() 또는 변수이름.configure() 메소드를 통해 필요할 때 설정 또는 변경할 수 있다. config()configure() 이름만 다를 뿐 같은 메소드이다.

중요한 위젯들은 다음과 같다.

Toplevel widget

Label widget

Button widget

Canvas widget

Checkbutton widget

Entry widget

Frame widget

LabelFrame widget

Listbox widget

Menu widget

Menubutton widget

Message widget

OptionMenu widget

PanedWindow widget

Radiobutton widget

Scale widget

Scrollbar widget

Spinbox widget

Text widget

Bitmap Class widget

Image Class widget

import tkinter as tk

뿌리 = tk.Tk()
뿌리.geometry('500x500')
프레임 = tk.Frame(뿌리, width=400, height=300, bg='red')
프레임.pack()

이름라벨 = tk.Label(프레임, text='이름을 입력하세요')
이름라벨.pack()

이름입력 = tk.Entry(프레임)
이름입력.pack(expand=1, fill=tk.BOTH)

확인버튼 = tk.Button(프레임, text='확인')
확인버튼.pack(expand=1, fill=tk.BOTH)

뿌리.mainloop()

배치 관리자(geometry manager)

geometry manager를 이용해서 위젯들을 배치시킨다. grid, pack, place geomerty manager가 있다.

grid() 관리자

Grid 배치 관리자는 위젯들을 2차원 테이블을 이용해서 배치한다. 부모 위젯은 행, 열로 나누어진 구역(셀)에 자식들 위젯을 넣는다. 그리드 관리자는 tkinter 배치관리자 중 이해하기 쉽고 많이 사용된다.

grid() 관리자 사용법

자식 위젯에서 grid() 메소드를 이용해서 행, 열 정보만 넘겨주면 된다. 그리드의 크기를 미리 정할 필요가 없다. 관리자가 자동으로 알아서 결정한다. 아래 예제를 실행하면 아래 그림과 같다.

import tkinter as tk

root = tk.Tk()

tk.Label(root, text='이름:').grid(row=0, column=0)
tk.Label(root, text='암호:').grid(row=1, column=0)

tk.Entry(root).grid(row=0, column=1)
tk.Entry(root).grid(row=1, column=1)

tk.Button(root, text='로그인').grid(row=2, column=1, sticky='e')

root.mainloop()

sticky 옵션을 이용해서 각 셀에서 위젯의 위치를 정할 수 있다. sticky='e|w|s|n|nw|ne|sw|se' 등이 올 수 있다. 기본 설정은 중앙에 위치한다. 또한 we 옵션을 이용해서 가로 전체를 차지하게 할 수 있다. 또한 sticky='nsew'를 이용해서 셀 전체를 차지할 수 있다.

ipadx 또는 ipady 옵션을 이용해서 위젯의 내부 공간을 설정할 수 있다. padx 또는 pady를 이용해서 위젯 외부 공간을 설정한다. 다음 코드를 실행하면 아래 그림과 같이 나온다.

import tkinter as tk

root = tk.Tk()
root.config(bg='orange')

tk.Label(root, text='이름:').grid(row=0, column=0, sticky='wens', padx=10, pady=10, ipadx=10, ipady=10)
tk.Label(root, text='암호:').grid(row=1, column=0, padx=10, pady=10, ipadx=10, ipady=10)

tk.Entry(root).grid(row=0, column=1)
tk.Entry(root).grid(row=1, column=1)

tk.Button(root, text='로그인').grid(row=2, column=1, sticky='ewns')

root.mainloop()

더 복잡한 경우에는 셀들을 합칠 필요가 있다. 이럴 때 rowspan, colspan 옵션을 사용한다. columnspan=4라고 하면 현재 위젯이 위치한 열부터 연속으로 4개의 열을 합쳐서 위젯이 차지한다. 만일 w.grid(row=0, column=1, columnspan=4)라고 하면 0행, 1열부터 0행, 4열까지 위젯 w가 공간을 차지하게 된다. 아래 예제는 rowspan, columnspan의 예를 보인 것이다.

import tkinter as tk

root = tk.Tk()
root.config(bg='red')

for i in range(3):
    root.rowconfigure(i, weight=1)

for j in range(4):
    root.columnconfigure(j, weight=1)

tk.Label(root, text='A').grid(row=0, column=0, sticky='wens', padx=(0, 1), pady=(0, 1))
tk.Label(root, text='B').grid(row=0, column=1, sticky='wens', padx=(0, 1), pady=(0, 1))
tk.Label(root, text='C').grid(row=0, column=2, sticky='wens', padx=(0, 1), pady=(0, 1))
tk.Label(root, text='D').grid(row=0, column=3, sticky='wens', pady=(0, 1))
tk.Label(root, text='E').grid(row=1, column=0, sticky='wens', padx=(0, 1), pady=(0, 1))
tk.Label(root, bg='orange', text='F', anchor='center').grid(row=1, column=1, rowspan=2, columnspan=3, sticky='ewns')
tk.Label(root, text='H').grid(row=1, column=3, rowspan=2, sticky='wens')
tk.Label(root, text='I').grid(row=2, column=0, sticky='wens', padx=(0, 1))

root.mainloop()

위 코드를 실행하면 다음과 같다.

  • root.config(bg='red')에서 루트 창의 배경색을 빨강으로 했다. 3행 4열의 구조를 이루고 있다. 셀의 행, 열번호는 0번부터 시작한다.

  • padx=(0,1)을 이용하여 A, B, C, E, I 셀 오른쪽에 1픽셀의 빈 공간을 주어 배경색인 빨강색이 보이고 있다. 마찬가지로 pady=(0,1)을 이용하여 A, B, C, D, E 셀 아래쪽에 1픽셀의 공간을 두어 배경색이 보이게 하고 있다.

  • rowconfigure(행번호, weight=숫자), columnconfigure(열번호, weight=숫자)를 이용해서 창의 크기가 변할 때 weight의 숫자에 비례해서 공간이 할당되게 했다. weight=1 이기 때문에 동일하게 공간이 나뉘어진다.

  • sticky='wens' 옵션을 이용해 셀의 위젯이 모든 방향으로 확장되게 했다. we(또는 ew)는 왼쪽, 오른쪽 방향으로 확장되고 ns는 위, 아래로 확장된다.

  • F 셀은 rowspan=2, columnspan=3을 이용해서 1번행, 1번열부터 2번행, 3번열에 있는 6개의 셀들의 공간을 차지하게 된다. bg='orange'를 이용해서 배경색을 오렌지색으로 했다. Labelanchor=’center’ 옵션을 이용해 글자의 위치를 중앙에 놓이게 했다. 왼쪽 위에 위치 시키고 싶으면 nw와 같이 옵션을 주면 된다.

  • H 셀은 rowspan=2이므로 1번행, 3번열부터 2번행, 3번열까지 2개의 셀 공간을 차지한다. 그런데 이 공간의 F 셀과 겹치는 부분인데 나중에 차지하는 공간인 H셀이 보이게 된다.

다음 예제는 일반적인 텍스트 편집기 프로그램의 찾기 및 바꾸기 창의 GUI를 구현해 본 것이다. 다음은 배치도를 그린 것이다.

아래는 위 그림을 코드로 구현해 본 것이다.

import tkinter as tk

root = tk.Tk()
root.title('찾기 및 바꾸기')
root.columnconfigure(2, weight=1)

tk.Label(root, text="찾을 내용:").grid(row=0, column=0, sticky='e')
tk.Entry(root, width=40).grid(row=0, column=1, padx=2, pady=2, sticky='we', columnspan=4)

tk.Label(root, text="바꿀 내용:").grid(row=1, column=0, sticky='e')
tk.Entry(root).grid(row=1, column=1, padx=2, pady=2, sticky='we', columnspan=4)

tk.Button(root, text="찾기").grid(row=0, column=5, sticky='e' + 'w', padx=2, pady=2, ipadx=3)
tk.Button(root, text="다음 찾기").grid(row=1, column=5, sticky='e' + 'w', padx=2, ipadx=3)
tk.Button(root, text="바꾸기").grid(row=2, column=5, sticky='e' + 'w', padx=2, ipadx=3)
tk.Button(root, text="모두 바꾸기").grid(row=3, column=5, sticky='e' + 'w', padx=2, ipadx=3)

tk.Checkbutton(root, text='단어 완전 일치').grid(row=2, column=1, sticky='w')
tk.Checkbutton(root, text='대소문자 구분').grid(row=3, column=1, sticky='w')
tk.Checkbutton(root, text='되돌이 검색').grid(row=4, column=1, sticky='w')

tk.Label(root, text="방향:").grid(row=2, column=3, sticky='w')
tk.Radiobutton(root, text='위', value=1).grid(row=3, column=3, padx=2, sticky='w')
tk.Radiobutton(root, text='아래', value=2).grid(row=3, column=4, padx=2, sticky='w')

root.update_idletasks()
root.minsize(width=root.winfo_reqwidth(), height=root.winfo_reqheight())
root.maxsize(root.winfo_screenwidth(), root.winfo_reqheight())

root.mainloop()
  • root.columnconfigure(2, weight=1)를 이용해서 창의 크기가 변할 때 2번열만 확장을 시킨다.

  • root.update_idletasks()은 창의 크기가 변경된 것을 감지하는 메소드이다.

  • root.winfo_reqwidth(), root.winfo_reqheight()를 이용해서 변경된 창의 너비 및 높이를 구하고, root.minsize(), maxsize() 메소드는 창의 최소 크기 및 최대 크기를 설정한다. minsizemaxsize의 높이를 같게함으로 변경될 창의 높이를 고정시키고 너비만 변하게 했다.

위 코드를 실행하면 다음과 같다.

자세한 그리드 알고리즘은 https://www.tcl.tk/man/tcl8.4/TkCmd/grid.htm#M31를 참조한다.

pack()

위젯.pack(부모위젯) 메소드를 이용해서 부모위젯에서 위젯의 배치와 모양을 결정할 수 있다. pack 관리자는 신축성 있는 고무 판에 작은 직사각형 구멍이 하나 있다고 생각한다. 그 구멍 안에 위젯을 추가하면서 옵션으로 주어진 면에 위젯을 맞춘다. 즉, side='top|bottom|left|right' 옵션에 따라 위젯을 위치 시킨다. 또 다른 위젯은 남은 사각형 공간에 똑같은 방법으로 밀어 넣는 것을 반복한다. 여기서 'top|bottom|left|right'에서 |또는을 의미한다. 즉, top 또는 bottom 또는 left 또는 right이 될 수 있다는 뜻이다.

pack 관리자는 주로 다음과 같을 경우 많이 사용된다.

  • 하나의 위젯이 창 전체를 차지하는 경우

  • 위젯들을 수평 또는 수직으로 나열하는 경우

예를 들면 다음과 같이 라벨 위젯을 루트 창에 넣는다고 가정하자. 처음 상태는 다음과 같이 가상의 빈 직사각형(빨간색 사각형)이 있다고 생각한다.

여기에 라벨1 이름의 라벨을 왼쪽에 위치시키려고 한다고 하자. 즉, tkinter.Label(side='left', text='라벨1')이라고 하면 다음과 같은 상태가 된다.

가상의 직사각형은 라벨이 차지하고 남은 공간에 위치하고 있다. 다음에 라벨2를 상단에 위치시키려고 다음과 같이 입력하면 tkinter.Label(side='top', text='라벨2') 아래 그림과 같은 상태가 된다.

위젯을 추가할 때마다 남은 가상의 직사각형 공간에 위와 같은 과정을 반복하게 된다. 위젯의 실제 크기는 기본적으로 위젯의 내용에 따라 조절된다. 라벨의 경우는 텍스트의 길이에 맞게 크기가 정해진다.

pack(fill='x')fill='x|y|both' 옵션을 이용해 위젯의 남는 공간을 채울 수 있다. 부모 위젯의 크기가 변할 때 함께 변한다. side='top|bottom'fill='x'에 대해서 반응하고 side='left|right'fill='y'에 대해서 반응한다.

expand=True|1을 이용해서 다른 방향으로도 확장시킬 수 있다. expand=True 옵션은 남은 공간을 차지하고 fill옵션은 그 공간을 채우는 역할을 한다. side='left|right'인 위젯은 남은 직사각형을 균등하게 세로로 나누고, side='top|bottom'일 때는 균등하게 가로로 나눈다. 세부적인 것은 직접 보면서 대응해나가야 할 것이다. 자세한 packer 알고리즘은 https://www.tcl.tk/man/tcl/TkCmd/pack.htm#M26을 참조한다.

아래 그림은 다음 코드를 실행시킨 것이다.

import tkinter as tk

root = tk.Tk()

root.config(bg='blue')

label_a = tk.Label(root, text='A', relief='sunken')
label_a.pack(side='left', fill='both', expand=1)

label_b = tk.Label(root, text='B', relief='sunken')
label_b.pack(side='right', fill='x', expand=0, anchor='center')

label_c = tk.Label(root, text='C', relief='sunken')
label_c.pack(side='top', fill='both', expand=1)

label_d = tk.Label(root, text='D', relief='sunken')
label_d.pack(side='left', fill='both', expand=1)

label_e = tk.Label(root, text='E', relief='sunken')
label_e.pack(side='top', fill='both', expand=1)

label_f = tk.Label(root, text='F', relief='sunken')
label_f.pack(side='right', fill='both', expand=1)

label_g = tk.Label(root, text='G', relief='sunken')
label_g.pack(side='right', fill='both', expand=1)

root.mainloop()

label_a, label_d, label_g, label_fside 옵션의 값이 left 또는 right이고 expand=True 이기 때문에 너비가 균등하게 되고, label_c, label_eside 옵션의 값이 top 또는 bottom이기 때문에 높이가 같게 된 것이다. label_bside='right' 이지만 expand=0 이기 때문에 너비를 균등하게 분할받지 못했다. 또한 fill='x' 이기 때문에 y 방향으로 크기가 확장되지 않은 것을 알 수 있다. 만일 fill='y' 이었다면 y 방향으로 커졌을 것이다.

place()

place 배치 관리자는 많이 사용되지 않는 관리자이다. 부모 위젯에서 (x, y) 좌표를 지정함으로써 자식 위젯의 위치를 지정한다. place() 메소드를 이용해서 설정할 수 있으며 다음과 같이 두 가지 중요한 옵션을 이용한다.

  • 절대 위치: place(x=숫자, y=숫자). x, y의 위치는 부모 위젯으로부터 anchor 위치를 나타내는 것이다. anchor='nw'가 기본설정이다. 앵커(anchor)란 자식 위젯의 기준점을 의미한다. 위 그림 참조.

  • 상대 위치: place(relx=숫자, rely=숫자, relwidth=숫자, relheight=숫자). relx(또는 rely)는 0.0부터 1.0까지 올 수 있고 1.0은 부모 위젯의 너비(높이)와 같은 것이고 0.5는 부모 위젯의 너비(높이)의 1/2 위치이다. relwidthrelheight도 마찬가지이다.

  • 부모 위젯의 창의 크기가 변할 때 절대 위치로 설정하면 자식 위젯의 위치와 크기는 변하지 않고, 상대 위치로 설정하면 자식 위젯의 위치와 크기가 함께 변한다.

아래 코드는 절대, 상대 위치의 예를 보여준다.

import tkinter as tk

root = tk.Tk()
root.config(bg='orange')

tk.Label(root, text='절대 위치 설정').place(x=50, y=50)
tk.Label(root, text='상대 위치 설정').place(anchor='center', relx=0.5, rely=.5)

root.mainloop()

위 코드를 실행하면 다음과 같다.

  • 절대 위치 설정의 anchor는 기본 설정이 nw이므로 라벨의 왼쪽 상단 모서리의 좌표가 (50, 50)이 된다.

  • 상대 위치 설정의 anchor='center'이므로 라벨의 중앙의 좌표가 relx=0.5가 된다. 즉, 부모 위젯의 너비의 1/2 부분이 라벨의 중앙의 x 좌표가 되는 것이다. y 좌표도 마찬가지이다.

  • 창의 크기를 변경하면 절대 위치 설정의 위치는 변하지 않지만 상대 위치 설정은 크기에 따라 항상 중앙 위치에 놓이게 된다.

같은 부모 아래에서 pack, grid 관리자는 혼용해서 사용할 수 없지만 place 관리자는 pack 또는 grid 관리자와 함께 사용할 수 있다.

최상위 레벨 창 메소드

최상위 레벨 창(top level window) 메소드를 이용해서 창의 상태를 정할 수 있다. 최상위 레벨 창은 루트창 tkinter.Tk() 또는 최상위 레벨 위젯 tkinter.Toplevel(master)을 통해서 얻을 수 있다. Toplevel() 위젯도 루트창을 부모로 갖는다. 따라서 모든 위젯의 뿌리는 뿌리창이다.

  • minsize(width, height) 메소드를 이용해 창의 최소 크기를 정한다.

  • geometry('widthxheight+xoffset+yoffset') 메소드는 창의 처음 크기를 정한다. 창의 크기를 정하는 문자열의 형식은 '너비x높이+(-)x+(-)y'와 같이 사용한다. 너비와 높이는 창의 크기를 의미하고 +x는 창이 시작되는 위치로 창의 왼쪽 변이 스크린의 왼쪽 끝으로부터 x만큼 떨어져서 위치하라는 의미이고 -x이면 창의 오른쪽 변이 스크린의 오른쪽 끝으로부터 x만큼 떨어져 위치하라는 뜻이다. +y는 창의 윗변이 스크린의 맨 위로부터 y 만큼 떨어져 시작하라는 것이고 -y는 창의 아랫변이 스크린의 맨 아랫쪽으로부터 y 만큼 떨어져 위치하라는 뜻이다.

이벤트(event)와 콜백(callback)

GUI 프로그램밍의 3대 성분 중 위젯, 배치관리자를 알아보았고 마지막 3번째 요소로 이벤트를 들 수 있다. 이벤트란 위젯들을 마우스로 클릭 하거나 키보드를 누르는 행동을 말한다. 콜백(또는 콜백함수)은 이벤트가 발생했을 때 실행되는 함수를 의미한다.

명령 바인딩(command binding)

바인딩이란 이벤트와 콜백을 연결해주는 것을 의미한다. 버튼 위젯에 반응하는 가장 간단한 방법은 버튼 위젯이 가지고 있는 command 옵션을 이용하는 것이다.

import tkinter as tk

def 함수():
    print('함수 불림.')

root = tk.Tk()

tk.Button(root, text='버튼', command=함수).pack()

root.mainloop()

버튼을 클릭하면 command에 할당된 콜백함수 함수가 실행되어 명령창에 함수 불림.이라는 메시지가 출력된다.

콜백함수가 매개변수를 필요로할 때는 lambda 함수를 이용한다. 콜백함수를 호출할 당시의 값으로 고정시키려면 다음과 같이 매개변수에 기본값을 설정한다.

import tkinter as tk

def 콜백함수(매개):
    print(매개)

root = tk.Tk()

매개 = '초기값'
tk.Button(root, text='버튼', command=lambda =매개: 콜백함수()).pack()

매개 = '나중 값'
root.mainloop()

lambda 매=매개: 콜백함수(매)는 매개변수에 기본값 매개를 설정한 함수를 만들었기 때문에 버튼 생성 후에 매개 = '나중 값'은 영향을 미치지 않는다. 그렇지 않고 인자의 값이 특정 상황에 맞게 대입하려면 매개변수가 없는 람다함수를 사용한다.

import tkinter as tk

def 콜백함수(매개):
    print(매개)

root = tk.Tk()

매개 = '초기값'
tk.Button(root, text='버튼', command=lambda : 콜백함수(매개)).pack()

매개 = '나중 값'
root.mainloop()

버튼이 생성되고 매개 = '나중 값'으로 변경되었기 때문에 변경된 매개 값이 버튼을 클릭할 때 콜백함수에 전달이 된다.

command 옵션은 버튼 위젯과 같은 몇 개의 위젯에만 포함되어 있다. 모든 위젯이 command 옵션을 가지고 있는 것이 아니다. 또한 버튼 위젯에 있는 command 옵션은 마우스 왼쪽 버튼 클릭과 키보드 스페이스 키에만 작동을 하고 다른 것에는 반응을 하지 않는다. 이런 불편함을 해소하기 위해 Tkinter는 일반적인 이벤트와 모든 위젯에서 사용할 수 있는 메소드인 bind() 를 제공한다.

이벤트 연동(event binding)

위젯과 이벤트 연동 문법은 다음과 같다.

위젯.bind(이벤트형식, 처리자, add=None)

이벤트 형식

이벤트형식은 어떤 종류의 이벤트인지를 지정하는 문자열이다. 예를 들면 왼쪽 버튼 클릭은 '<Button-1>'이 된다. 이벤트형식은 다음과 같다.

<[제한자-] ...이벤트유형[-세부사항]>
  • 모든 형식은 <...>로 둘러싸여 있어야 한다.

  • 이벤트유형(type)은 반드시 입력해야 하며, 키보드 눌림 또는 마우스 클릭과 같은 형태를 말한다.

  • 제한자(modifier)는 선택 사항으로 Alt, Shift, Control 키들 같이 이벤트 유형 앞에 붙어서 사용되는 것을 말한다.

  • 세부사항(detail)은 선택 사항으로 마우스 버튼이 눌렸을 때 왼쪽(1), 오른쪽(3), 가운데(2) 버튼인지를 지정할 때 사용된다. 또는 키보드 어떤 문자의 키가 눌렸는지를 지정할 때 사용된다.

  • 제한자, 이벤트유형, 세부사항 사이는 - 또는 한 칸 띄움(스페이스)을 이용해 연결한다.

다음은 이벤트 형식의 예이다.

이벤트 형식

설명

<Button-3>

마우스 오른쪽 버튼이 눌렸을 때 이벤트 발생.

<Control-Shift-KeyPress-H>

콘트롤(Control)+시프트(Shift)+영문자 H키가 동시에 눌렸을 때 이벤트가 발생한다. 여기서 Control, Shiftmodifier이고 KeyPress이벤트유형이며 H세부사항이 된다.

처리자(handler)와 콜백 함수(callback)

처리자(handler)는 이벤트가 발생할 때 실행될 콜백함수를 지정한다. 동일한 이벤트가 여러 개 있으면 가장 나중에 정의된 콜백함수만 실행이 된다. 그런데 add='+' 옵션을 지정하면 같은 이벤트에 대해서 앞에 있는 콜백함수가 실행되고 이어서 나중에 있는 콜백함수도 실행이 된다.

import tkinter as tk

root = tk.Tk()

def 함수1(event):
    print('함수1:', event.x, event.y)

def 함수2(event):
    print('함수2:', event.x, event.y)

버튼1 = tk.Button(root, text='버튼1')
버튼1.pack()

버튼1.bind('<Button-1>', 함수1)
버튼1.bind('<Button-1>', 함수2, add='+')

root.mainloop()

위 코드를 실행하면 다음과 같이 함수1이 실행이 되고 이어서 함수2가 실행이 되는 것을 알 수 있다.

함수1: 38 14
함수2: 38 14

두개 이상의 인자가 필요한 콜백 함수

bind 함수에서 콜백함수에게 넘겨주는 인자는 이벤트 하나밖에 넘겨줄 수 없다. 따라서 2개 이상의 인자들을 갖는 콜백함수가 필요한 경우는 lambda 함수를 이용해서 넘겨줄 수 있다.

import tkinter as tk

root = tk.Tk()

def 함수(event, 숫자):
    print('함수1:', event.x, event.y, '숫자=', 숫자)

버튼1 = tk.Button(root, text='버튼1')
버튼1.pack()

버튼1.bind('<Button-1>', lambda 이벤트, =10: 함수(이벤트, ))

root.mainloop()

처리자(handler)에 사용될 lambda 함수도 위치 매개변수는 항상 한 개 밖에 필요하지 않으므로 나머지 매개변수는 키워드 매개변수로 지정해야 한다. 그렇지 않으면 콜백함수의 인자를 전역변수로 설정한다. 아래와 같이 람다 함수의 매개변수는 이벤트 하나이고, 콜백함수 함수의 나머지 인자 는 전역변수로 설정한다.

import tkinter as tk

root = tk.Tk()

def 함수(event, 숫자):
    print('함수1:', event.x, event.y, '숫자=', 숫자)

버튼1 = tk.Button(root, text='버튼1')
버튼1.pack()

버튼1.bind('<Button-1>', lambda 이벤트: 함수(이벤트, ))
 = 100

root.mainloop()

위 코드를 실행하면 숫자=100이 출력이 된다.

이벤트 연동 범위

이벤트가 발생할 때 콜백함수(handler)와 연동하는 방식은 3가지가 있다.

  • 인스턴스 연동(instance binding): 이벤트를 지정된 위젯에만 연동하는 것이다. 위에서 설명한 것과 같이 위젯.bind() 메소드를 이용한다.

  • 같은 종류의 위젯 연동(class binding): 같은 종류의 위젯에 이벤트가 발생하게 하려면 위젯.bind_class() 메소드를 사용한다. 여기서 위젯은 어떤 위젯이 와도 된다. 예를 들어 각각의 레이블 위에서 버튼을 눌렀을 때 같은 콜백함수가 불릴 수 있다.

import tkinter as tk

def label_fun(event):
    lab = event.widget
    lab.config(text='변경됨')

root = tk.Tk()

lab1 = tk.Label(root, text='레이블 1')
lab2 = tk.Label(root, text='레이블 2')
lab1.pack()
lab2.pack()

lab1.bind_class('Label', '<Button-1>', label_fun)

root.mainloop()

위 코드를 실행하면 처음에는 레이블 1, 레이블 2라는 문구가 뜨지만 각각의 레이블을 클릭하면 변경됨이라는 문구로 바뀌는 것을 알 수 있다.

  • 모든 위젯 연동(application binding): 위젯에 상관없이 이벤트가 연동되는 것을 말한다. 위젯.bind_all() 메소드를 이용하며 위젯은 어떤 위젯이 와도 된다. 예를 들면, PrtSc 키를 누르면 어떤 위젯이든지 상관없이 화면이 출력되도록 할 필요가 있을 때 사용할 수 있다.

위젯.bind_all('<Key-Print>', 콜백함수)

와 같이 사용할 수 있다.

사용자 정의 이벤트(virtual event)

<<이벤트이름>>로 둘러싸서 이벤트 이름을 원하는대로 정할 수 있다. 예를 들어 <<panic>>이라는 이벤트 이름을 Button-3 또는 pause 키가 눌렸을 때 작동하게 하려면 위젯.event_add() 메소드를 이용해 이벤트 이름을 등록해서 사용하면 된다.

위젯.event_add('<<panic>>', '<Button-3>', '<KeyPress-Pause>')
위젯.bind('<<panic>>', 콜백함수)

위젯 특화 변수(widget-specific variables)

위젯을 사용하다보면 다양한 용도의 변수들이 필요할 때가 있다. 엔트리 위젯 또는 텍스트 위젯의 내용이 바뀐 것을 추적할 때 문자열 변수가 필요할 수 있다. 첵크 박스 위젯에서 첵크가 되어 있는지 아닌지를 알기 위한 논리 변수가 필요할 수도 있다. 스핀 박스 또는 슬라이더 위젯의 값들이 어떻게 변경되었는지를 알 필요가 있을 수도 있다. 이러한 위젯에 특화된 변수들을 다루기 위해 Tkinter는 몇 가지 주로 사용되는 변수 클래스들을 정의하고 있다. StringVar, IntVar, BooleanVar, DoubleVar등이 이것이다. 변수를 사용하는 방법은 다음과 같다.

내문자열 = StringVar()
첵크 = BooleanVar()
다중선택 = IntVar()
음량 = DoubleVar()

변수가 만들어 졌으면 다음과 같이 위젯에서 사용할 수 있다.

tk.Label(root, textvariable=내문자열)
tk.Entry(root, textvariable=내문자열)
tk.Checkbutton(root, text="선택해주세요", variable=첵크)
tk.Radiobutton(root, text="선택1", variable=다중선택, value="선택1")
tk.Scale(root, label="음량 조절", variable=음량, from =0, to=10)

만들어진 변수의 값을 조회 및 변경은 다음과 같이 set, get 메소드를 이용한다.

내문자열.set('안녕하세요. 문자열이 변경되었습니다.')
내문자열.get()

참조