tkinter 응용 프로그램은 대부분의 시간을 이벤트 루프에서 소모한다. 즉 mainloop()에서 이벤트를 기다리면서 반복 루프를 실행한다. 이것은 이벤트-구동방식이라한다.
이벤트는 다양한 소스에서 발생될 수 있다. 키보드의 키를 누르거나, 마우스 버튼을 누르는 등 여러 방면에서 이벤트가 발생될 수 있다. 각 위젯에 개발자는 파이썬 함수를 붙일 수 있다.
만일 위젯에서 이벤트 지정자와 일치하는 이벤트가 발생하면 주어진 이벤트 처리 함수가 이벤트를 설명하는 객체와 함께 호출된다.
간단한 예로 아래의 코드는 마우스 이벤트를 받아 좌표를 출력한다.
from tkinter import *
# 이벤트 처리 함수
def mouse_event(event):
print(event.x ,event.y, '에서 마우스 이벤트 발생')
window = Tk()
frame = Frame(window,width=300, height=300, bg='blue')
# 이벤트 지정자 <Button-1>는 마우스 좌클릭을 말한다.
# 휠은 2, 우클릭은 3이다.
frame.bind('<Button-1>',mouse_event)
frame.pack()
window.mainloop()
키보드 이벤트는 현재 키보드 포커스를 소유하고 있는 위젯으로 보내진다.
아래의 focus_set() 메소드는 포커스를 원하는 위젯으로 이동시킬 수 있다.
아래의 경우 마우스를 통해 mouseEvent() 함수가 호출되어야 focus_set()을 통해 frame으로 키보드의 포커스가 옮겨진다.
from tkinter import *
def mouseEvent(event):
frame.focus_set()
print('좌표: ', event.x, event.y)
# repr() 메소드는 문자코드를 문자열로 변환시켜준다.
def keyEvent(event):
print(repr(event.char),'가 눌렸습니다.')
window = Tk()
frame = Frame(window, width=300, height=300, bg='green')
frame.bind("<Button-1>", mouseEvent)
frame.bind("<Key>", keyEvent)
frame.pack()
window.mainloop()
이벤트 지정자는 다음과 같은 형식의 문자열로 기술된다.
<수식어 - 타입 - 세부사항>
'타입' 필드는 이벤트 지정자의 가장 중요한 부분이다. '타입'은 우리가 연결하고 싶어하는 이벤트의 종류를 지정한다. 타입의 예로서, Button, Key, Enter, Configure 등이 있다. '수식어' 와 '세부사항' 은 이벤트에 대한 추가적인 정보를 제공하는 필드이다.
이벤트 지정자를 단순화 시키는 많은 방법이 있다. 예를 들어 키보드 키 이벤트를 매칭할 경우 <,> 를 생략하고 단순히 원하는 키를 입력하여도 된다.
<Button-1>
마우스가 버튼 위젯 위에서 눌려졌을 때 발생하는 이벤트이다. "Button 1"이 마우스의 왼쪽 버튼이고 "Button 2"가 중간 버튼, "Button 3"이 오른쪽 버튼이다. 사용자가 위젯 위에서 마우스 버튼을 누르면 tkinter는 자동적으로 마우스 포인터를 독점한다.마우스 버튼이 눌리고 있는 동안에는 모든 마우스 이벤트들은 (즉 마우스 모션이나 마우스 해제 이벤트) 현재의 위젯에 보내진다. 마우스가 위젯을 벗어나더라도 마찬가지이다. 마우스 포인터의 현재 위치는 이벤트 객체의 x와 y멤버에 저장된다.
개발자는 Button 대신에 ButtonPress를 사용하여도 되고 아니면 생략하여도 된다. <Button-1>, <ButtonPress-1>, <1>은 모두 버튼 이벤트를 가리킨다.
<B1-Motion>
마우스 버튼 1이 눌려진 채로 움직일 때 발생한다. 마우스의 중간 버튼이면 B2가 되고 오른쪽 버튼이면 B3가 된다. 마우스 포인터의 현재 위치는 콜백 메소드로 전달되는 이벤트 객체의 x와 y에 저장된다.
<ButtonRelease-1>
사용자가 Button 1에서 손을 뗄 때 발생한다. 마우스 포인터의 현재 위치는 콜백 메소드로 전달되는 이벤트 객체의 x와 y에 저장된다.
<Double-Button-1>
마우스 버튼1이 더블클릭 될때 발생한다. Double, Triple을 접두사로 사용할 수 있다. 만약 더블 클릭과 단일 클릭이 동시에 연결되어 있다면 양쪽 호출 함수가 호출된다.
<Enter>
마우스 포인트가 위젯으로 진입하였을 때 발생한다. 사용자가 Enter 키를 눌렀다는 의미가 아니다.
<Leave>
마우스 포인터가 위젯으로 떠났을 때 발생한다
<FocusIn>
키보드 포커스가 현재의 위젯으로 이동했을때 발생한다.
<FocusOut>
키보드 포커스가 현재의 위젯에서 다른 위젯으로 이동할 때 발생한다.
<Return>
사용자가 엔터키를 입력하였을 때 발생한다.. 개발자는 키보드에 존재하는 어떤 키에도 콜백 메소드를 연결할 수 있다.
<Key>
사용자가 어떤 키라도 누르면 발생한다. 눌러진 키는 이벤트 객체의 char 멤버에 저장된다. 만약 F5와 같은 특수키라면 char 멤버는 비어있다.
a
사용자가 "a"를 입력하였을 때 발생한다. 대부분의 인쇄가능한 문자는 이런 식으로 이벤트를 연결할 수 있다. 예외로는 "(공백)"(<space>)와 "<" (<less>)가 있다. 1은 키보드 바인딩이고 <1>은 버튼 바인딩임을 주의해야한다.
<Shift-Up>
사용자가 쉬프트 키를 누른 상태로 위쪽 화살표를 누르면 발생한다. Alt, Shift, Control 과 같은 수식어를 사용할 수 있다.
<Configure>
위젯이 크기를 변경하였을 때 발생한다. 위젯의 위치나 플랫폼을 변경해도 발생한다. 새로운 크기는 콜백 메서드로 전달되는 이벤트 객체의 width와 height 속성에 저장된다.
# 이벤트 처리를 활용하여 페인트 프로그램 구현
from tkinter import *
from tkinter.colorchooser import *
# 상수 선언
DEFAULT_PEN_SIZE = 1.0
DEFAULT_COLOR = 'BLACK'
# 필요한 변수들의 초기화
mode = 'pen'
old_x = None
old_y = None
myColor = DEFAULT_COLOR
erase_on = False
# 이벤트 처리 함수들의 처리
# 펜을 사용하고자 할 때 모드를 'pen'으로 전환
def use_pen():
global mode
mode = "pen"
print('변경 모드: ',mode)
# 색상 버튼이 선택되면 사용자한테 색상을 선택하는 대화상자를 띄운다.
def choose_color():
global myColor
# askcolor() 의 리턴타입은 튜플인데 인덱스[1]이 16진수로 색상을 표식
myColor = askcolor(color=myColor)[1]
print('변경된 색상: ', myColor)
# 지우개 버튼이 선택되면 모드를 erase으로 바꾼다.
def use_erase():
global mode
mode = 'erase'
print('변경 모드: ', mode)
# 그림을 그리는 paint()를 정의
def paint(event):
global var, erase_on, mode, old_x, old_y
# 모드가 지우개면 fill_color를 white로 설정하고 아니라면 mycolor로 설정
fill_color = 'white' if mode =='erase' else myColor
# old_x, old_y가 값이 있다면 '움직였다'라는 의미다.
# capstyle=ROUND 은 선의 끝을 둥글게 한다는 의미다.
if old_x and old_y:
canvas.create_line(old_x, old_y, event.x, event.y, capstyle=ROUND,
width=var.get(), fill=fill_color)
old_x = event.x
old_y = event.y
# 사용자가 마우스 버튼에서 손을 떼면 이전 점을 삭제한다.
def reset(event):
global old_x, old_y
old_x = None
old_y = None
# 캔버스의 모든 그림을 삭제
def clear_all():
global canvas
canvas.delete(ALL)
#-------------------------------------------------------
# 위젯들 배치
root = Tk()
var = DoubleVar() # 사이즈 값과 연결되어질 객체를 생성
# 펜이라는 버튼을 만들고 이벤트 처리함수를 use_pen 로 연결하였다.
penButton = Button(root, text= '펜', command=use_pen)
penButton.grid(row=0, column=0, sticky=W+E)
# 색상선택이라는 버튼을 만들고 이벤트 처리함수를 choose_color 로 연결하였다.
colorButton = Button(root, text= '색상선택', command=choose_color)
colorButton.grid(row=0, column=1, sticky=W+E)
# 지우개이라는 버튼을 만들고 이벤트 처리함수를 use_erase 로 연결하였다.
eraseButton = Button(root, text= '지우개', command=use_erase)
eraseButton.grid(row=0, column=2, sticky=W+E)
# 모두삭제라는 버튼을 만들고 이벤트 처리함수를 clear_all 로 연결하였다.
clearButton = Button(root, text= '모두삭제', command=clear_all)
clearButton.grid(row=0, column=3, sticky=W+E)
# Sclae 클래스는 값을 설정하기 위한 수치 조정바를 생성하는 클래스
# variable속성은 수치 조정바의 상태를 저장할 제어 변수
# orient 속성은 수치 조정바의 표시방향(horizontal, vertical)
scale = Scale(root, variable=var, orient=VERTICAL)
scale.grid(row=1, column=4, sticky=N+S)
canvas = Canvas(root, bg='white', width=600, height=500)
canvas.grid(row=1, columnspan=4)
# 캔버스에 바인딩 처리
canvas.bind('<B1-Motion>',paint)
canvas.bind('<ButtonRelease-1>',reset)
root.mainloop()
기본 윈도우에 메뉴를 추가할 때의 구상 개념과 형식은 아래와 같다.
위를 기반으로 윈도우에 [파일] 메뉴 아래에 [열기]와 [종류] 하위 메뉴가 있는 코드는 아래와 같다.
from tkinter import *
from tkinter import messagebox
# 함수 정의
def func_open():
messagebox.showinfo('첫 번째', '두 번째')
def func_exit():
window.quit()
window.destroy() # 윈도우를 메모리에서 제거하는 메소드
# 메인 코드
window = Tk()
mainMenu = Menu(window) # mainMenu 변수에 Menu를 생성한다. 소유자는 window이다.
window.config(menu=mainMenu) # 윈도우에 메뉴는 mainMenu 라고 지정한다.
# tearoff 속성은 메뉴의 점선을 없애준다.
fileMenu = Menu(mainMenu, tearoff=False)
# 다른 메뉴가 확장이 되어야 되기 때문에 add_cascade() 사용하여 메뉴의 이름을 '파일' 로 설정하였고
# menu 속성값을 fileMenu 로 지정하였다.
mainMenu.add_cascade(label='파일1', menu=fileMenu)
mainMenu.add_cascade(label='파일2', menu=fileMenu)
mainMenu.add_cascade(label='파일3')
# 하위 메뉴 만들기
fileMenu.add_command(label='열기', command=func_open)
fileMenu.add_separator() # 구분선
fileMenu.add_command(label='종료', command=func_exit)
window.mainloop()
먼저 Menu(부모윈도우)로 mainMenu 변수를 생성한다. mainMenu는 메뉴자체를 나타내는 변수이다. 그리고 생성한 메뉴 자체를 윈도창의 메뉴로 지정한다.
그 다음 상위 메뉴인 [파일]을 생성하고, 메뉴 자체에 부착한다. [파일] 메뉴는 선택하고 끝나는 것이 아니라, 그 아래에 다른 메뉴가 확장되어야 하므로 add_cascade() 함수를 사용한다.
[파일] 메뉴의 하위에 [열기] 메뉴를 준비한다. [열기] 메뉴는 선택할 때 어떤 작동을 해야 하므로, add command() 함수를 사용한다. 메뉴 사이에 구분선을 넣고, 같은 방식으로 하위 메뉴를 생성한다. 또한 함수의 매개변수로 command를 이용해 함수를 연결하고 있다.
파이썬은 대화상자를 몇 개 지원해준다. 기본적인 메세지를 표시해주는 messagebox.showinfo(), 숫자와 문자를 입력받게 하는 tkinter.simpledialog 모듈의 askinteger(), askstring()이 있다.
from tkinter import *
from tkinter.simpledialog import askinteger
window = Tk()
window.geometry('400x300')
labal = Label(window, text='입력된 값',bg='green')
labal.pack()
value = askinteger('첫 번째 입력','주사위 입력(1~6)',minvalue=1, maxvalue= 6)
# 입력 받은 정수값을 문자열로 변환하여 text를 수정한다.
labal.config(text=str(value))
window.mainloop()
위의 코드에서 정수의 값을 입력받기 위한 대화상자가 필요하기에 먼저 tkinter.simpledialog 모듈을 가져왔다.
변수 value에 askinteger 함수를 통해 값을 입력받는다. 만일 실수를 받고자 한다면 askfloat(), 문자열을 입력받고자 한다면 askstring() 함수를 이용한다.
minvalue는 최소 정수값, maxvalue는 최대 정수값을 말한다. 만일 여기 범위 밖의 값을 입력하면 자체적으로 오류가 발생한다.
파일을 열거나 저장을 할 때의 대화상자도 존재하는데 tkinter.filedialog 모듈을 가져온다. 그 후 askopenfilename(), asksaveasfile() 함수를 입력한다.
아래의 코드는 파일을 여는 코드이다.
from tkinter import *
from tkinter.filedialog import *
window = Tk()
window.geometry('400x300')
label = Label(window, text='선택된 파일의 이름')
label.pack()
filename =askopenfilename(parent=window, filetypes=(("GIF 파일", "*.gif"), ("모든 파일", "*.*")))
label.configure(text = str(filename))
window.mainloop()
먼저 filedialog 모듈을 가져온다. 그 후 askopenfile() 함수의 parent의 값으로 부모 윈도우를 지정한다. 그리고 filetype을 지정하는데, 튜플의 형태로 값을 입력받는다.
튜플안에 튜플은 ('표시할 글자', '확장자명') 의 형태로 구성한다. askopenfile() 함수는 경로를 포함해서 선택한 파일의 파일명을 반환시켜준다.
아래는 파일을 저장하는 코드이다.
from tkinter import *
from tkinter.filedialog import *
window = Tk()
window.geometry('400x300')
label = Label(window, text='저장한 파일')
label.pack()
saveFile = asksaveasfile(parent=window, mode='w', defaultextension='.jpg',
filetypes=(("JPG 파일", "*.jpg;*.jpeg"), ("모든 파일", "*.*")))
label.configure(text = saveFile)
saveFile.close()
window.mainloop()
출력 결과:
위의 프로그램을 실행하면 [다른 이름으로 저장] 대화상자가 열린다. 그리고 기존에 있는 파일명을 입력하고 (저장) 버튼을 누르면 기존 파일을 바꿀 것인지 묻는 메시지창을 표시한다.
주의할 점은 askopenfilename() 함수는 파일 경로와 파일명 문자열만 반환하지만, asksaveasfile() 함수는 다른 정보도 함께 반환한다는 것이다.
반환된 값 중에서 name과 관련된 것이 새로 저장할 파일명이 들어 있는 부분이다. asksaveasfile() 함수의 매개변수 중 mode="w”는 쓰기 모드라는 의미이다. 그리고 defaultextension=".jpg"는 특별히 확장명을 지정하지 않으면 확장명을 jpg로 붙인다는 의미이다.
이후 label1에 saveFp의 내용을 text에 대입시켜서 출력한다. 그리고 파일을 닫는다.(saveFile.close( )) 그리고 실행된 이후에는 파일의 기록이 완료된다.