사례연구: Tkinter

[tkinter]

GUI

지금까지 우리가 본 대부분의 프로그램들은 문자기반입니다만, 많은 프로그램들은 GUI 라고도 알려져 있는 그래픽 사용자 인터페이스(graphical user interfaces) 를 사용합니다.

파이썬은 GUI 기반 프로그램을 작성하는데 여러 가지 선택지를 제공하는데, wxPython, Tkinter, Qt 등이 있습니다. 각각은 장점과 단점을 갖고 있는데, 파이썬이 한가지 표준으로 수렴되지 않은 것은 이 때문입니다.

이 장에서 제가 소개할 것은 Tkinter 인데, 제 생각으로는 시작하기에 가장 쉽기 때문입니다. 이 장에 나오는 대부분의 개념들은 다른 GUI 모듈들에도 적용됩니다.

Tkinter 에 관한 여러 책들과 웹 페이지들이 있습니다. 가장 훌륭한 온라인 리소스들 중 하나는 Fredrik Lundh 의 An Introduction to Tkinter 입니다.

저는 Swampy에 따라오는 Gui.py 라는 모듈을 작성했습니다. Tkinter 에 있는 함수들과 클래스들의 단순화된 인터페이스를 제공합니다. 이 장의 예제들은 이 모듈에 의존합니다.

여기에 Gui 를 만들고 표시하는 간단한 예가 있습니다:

GUI 를 만들려면, Swampy 로부터 Gui를 들여오기 해야 합니다:

from swampy.Gui import *

또는, 여러분이 Swampy 를 어떻게 설치했는지에 따라, 이렇게 될 수도 있습니다:

from Gui import *

그런 다음 Gui 객체를 인스턴스화합니다:

g = Gui()
g.title('Gui')
g.mainloop()

이 코드를 실행하면, 빈 회색 정사각형과 Gui 라는 제목의 창이 나타나야 합니다. mainloop 는 이벤트 루프(event loop)를 실행하는데, 사용자가 무언가 하기를 기다렸다가 거기에 따라 반응합니다. 이 것은 무한 루프입니다; 사용자가 창을 닫거나, Control-C 를 누르거나, 프로그램이 종료할만한 무언가를 할 때까지 실행합니다.

이 Gui 는 그다지 하는 것이 없는데, 위짓(widget) 이 없기 때문입니다. 위빗은 GUI를 구성하는 요소입니다; 이런 것들이 있습니다:

버튼 Button:
텍스트나 이미지를 포함하는 위짓으로, 눌렀을 때 액션(action)을 수행한다.
캔버스 Canvas:
선, 직사각형, 원 과 다른 모양들을 표시할 수 있는 영역.
엔트리 Entry:
사용자가 글을 입력할 수 있는 영역.
스크롤바 Scrollbar:
다른 위짓의 가시 영역을 조절하는 위짓.
프레임 Frame:
다른 위짓들을 담는 컨테이너(container)로 종종 보이지 않는다.

여러분이 Gui를 만들었을 때 본 빈 회색 정사각형이 프레임입니다. 여러분이 새 위짓을 만들면, 이 프레임에 더해집니다.

버튼과 콜백

메쏘드 bu 는 버튼 위짓을 만듭니다:

button = g.bu(text='Press me.')

bu 의 반환 값은 Button 객체입니다. 프레임에 나타나는 버튼은 이 객체의 그래픽 표현입니다. 버튼을 제어하려면 그 것의 메쏘드를 호출하면 됩니다.

bu 는 버튼의 생김새와 기능을 조절하는 32개의 매개변수를 받아들입니다. 이 매개변수들을 옵션(option) 이라 부릅니다. 32개 옵션 모두에 대한 값들을 제공하는 대신, 필요한 옵션들만 키워드 인자를 사용해서, text='Press me.' 과 같은 식으로, 지정하고, 나머지는 내정 값들을 사용합니다.

위짓을 프레임에 더하면, 수축합니다; 즉, 프레임은 버튼의 크기로 줄어듭니다. 위짓을 더 하면, 프레임은 그들을 수용할 만큼 커집니다.

메쏘드 la 는 레이블(Label)위짓을 만듭니다:

label = g.la(text='Press the button.')

기본적으로, Tkinter는 위짓들을 위에서 아래로 쌓고, 가운데 정렬합니다. 곧 이런 동작을 어떻게 바꾸는지 보게 될 것입니다.

버튼을 누르면, 별로 하는 것이 없음을 알게 될 것입니다. “연결”하지 않았기 때문인데, 할 일을 지시하지 않았다는 뜻입니다!

버튼의 동작을 제어하는 옵션은 command입니다. command의 값은 버튼이 눌릴 때 실행되는 함수입니다. 예를 들어, 여기 새 레이블을 만드는 함수가 있습니다:

def make_label():
    g.la(text='Thank you.')

이제 이 함수를 command 로 갖는 버튼을 만들 수 있습니다:

button2 = g.bu(text='No, press me!', command=make_label)

이 버튼을 누르면, make_label 를 실행해서 새 레이블이 나타나야 합니다.

command 옵션의 값은 함수 객체인데, 콜백(callback)으로 알려져 있습니다. 버튼을 만들기 위해 bu 를 호출한 후에, 사용자가 버튼을 누르면 실행 흐름이 “역 호출(calls back)” 하기 때문입니다.

이런 종류의 흐름은 이벤트 주도 프로그래밍(event-driven programming)의 특징입니다. 버튼 누르기와 키 입력과 같은 사용자 액션(action)들을 이벤트(event) 라고 부릅니다. 이벤트 주도 프로그래밍에서 실행 흐름은 프로그래머보다는 사용자 액션에 의해 결정됩니다.

이벤트 주도 프로그래밍의 문제는 임의의 사용자 액션에 올바르게 동작하는 (또는 적어도 적절한 오류 메시지를 생성하는) 위짓과 콜백 세트를 구성하는 것입니다.

[연습 19.1.]

하나의 버튼으로 구성된 GUI 를 만드는 프로그램을 작성하세요. 버튼을 누르면 두 번째 버튼을 만들어야 합니다. 버튼을 누르면, “Nice job!” 이라고 말하는 레이블을 만들어야 합니다.

버튼을 한 번 이상 누르면 어떻게 될까요? 답: http://thinkpython.com/code/button_demo.py

캔버스 위짓

가장 다재 다능한 위짓 중 하나는 캔버스인데, 선, 원과 다른 모양들을 그기는 공간을 만듭니다. 연습 [canvas] 을 했다면 이미 캔버스에 익숙할 겁니다.

메쏘드 ca 는 캔버스를 만듭니다:

canvas = g.ca(width=500, height=500)

width 와 height 는 픽셀(pixel)로 나타낸 캔버스의 크기입니다.

위짓을 만든 후에도, 여전히 config 메쏘드로 옵션 값들을 바꿀 수 있습니다. 예를 들어, bg 옵션은 배경 색을 바꿉니다:

canvas.config(bg='white')

bg 의 값은 색깔 이름이 담긴 문자열입니다. 사용할 수 있는 색깔 이름들은 파이썬의 구현에 따라 다릅니다만, 모든 구현들은 적어도 이것들을 제공합니다:

white   black
red     green    blue
cyan    yellow   magenta

캔버스에 있는 모양들을 항목(item) 이라고 부릅니다. 예를 들어, 캔버스 메쏘드 circle 은 (예상한 대로) 원을 그립니다:

item = canvas.circle([0,0], 100, fill='red')

첫 번째 인자는 원의 중심을 나타내는 좌표 쌍입니다; 두 번째는 반지름입니다.

Gui.py 는 캔버스의 중심을 원점으로 하고 양의 \(y\) 축이 위를 향하는 표준 직교 좌표 계를 제공합니다. 이는 다른 그래픽 시스템에서 사용하는, 좌 상단을 원점으로 하고 \(y\) 축이 아래를 가리키는 것과는 다릅니다.

fill 옵션은 원이 빨강으로 채워져야 함을 지정합니다.

circle 의 반환 값은 Item 객체인데, 캔버스상의 항목을 수정할 수 있는 메쏘드들을 제공합니다. 예를 들어, config 를 사용해서 원의 옵션들을 바꿀 수 있습니다:

item.config(fill='yellow', outline='orange', width=10)

width 는 픽셀(pixel)로 표현한 윤곽선(outline)의 굵기입니다; outline 은 색깔입니다.

[연습 19.2.] [circle]

Canvas 와 Button 을 만드는 프로그램을 작성하세요. 사용자가 Button 을 누르면, 캔버스에 원을 그려야 합니다.

좌표 시퀀스

rectangle 메쏘드는 직사각형의 마주보는 두 꼭지점들을 지정하는 좌표 시퀀스를 받아들입니다. 이 예는 좌 하단이 원점에 있고 우 상단이 \((200,100)\) 에 있는 녹색 직사각형을 그립니다:

canvas.rectangle([[0, 0], [200, 100]],
                 fill='blue', outline='orange', width=10)

이런 방식으로 꼭지점들을 지정하는 것을 경계 상자(bounding box) 라고 하는데, 두 점이 직사각형을 가두기(bound) 때문입니다.

oval 은 경계 상자를 받아서 지정된 직사각형안에 타원을 그립니다:

canvas.oval([[0, 0], [200, 100]], outline='orange', width=10)

line 은 좌표 시퀀스를 받아서 점들을 연결하는 선들을 그립니다. 이 예는 삼각형의 짧은 두 변을 그립니다:

canvas.line([[0, 100], [100, 200], [200, 100]], width=10)

polygon 은 같은 인자를 받아들이지만, (필요하면) 다각형의 마지막 변을 그리고 안을 채웁니다:

canvas.polygon([[0, 100], [100, 200], [200, 100]],
               fill='red', outline='orange', width=10)

위짓 더 보기

Tkinter 는 사용자에게 글을 입력하게 하는 두 개의 위짓을 제공합니다: Entry 는 한 줄이고 Text 위짓은 여러 줄입니다.

en 은 새 Entry 를 만듭니다:

entry = g.en(text='Default text.')

text 옵션은 엔트리가 만들어질 때 글을 미리 입력해 둘 수 있도록 합니다. get 메쏘드는 Entry 의 내용 (사용자가 바꿨을 겁니다) 을 돌려줍니다:

>>> entry.get()
'Default text.'

te 는 Text 위짓을 만듭니다:

text = g.te(width=100, height=5)

width 와 height 은 글자수와 줄 수로 나타낸 위짓의 크기입니다.

insert 는 글을 Text 위짓에 삽입합니다:

text.insert(END, 'A line of text.')

END 는 Text 위짓의 마지막 글자를 가리키는 특별한 지수(index)입니다.

1.1 처럼 소수점이 들어간 지수로 글자를 지정할 수도 있는데, 정수부는 줄 번호를 나타내고, 그 뒤는 행을 뜻합니다. 다음 예는 첫 줄의 첫 글자 뒤에 'nother' 를 더합니다.

>>> text.insert(1.1, 'nother')

get 메쏘드는 위짓의 글을 읽습니다; 시작과 끝 지수를 인자로 받습니다. 다음 예는 위짓의 모든 (개행 문자를 포함한) 글을 돌려줍니다:

>>> text.get(0.0, END)
'Another line of text.\n'

delete 메쏘드는 위짓에서 글을 삭제합니다; 다음 예는 처음 두 글자를 뺀 나머지 모두를 삭제합니다:

>>> text.delete(1.2, END)
>>> text.get(0.0, END)
'An\n'

[연습 19.3.] [circle2]

여러분들의 연습 [circle] 에 대한 답을 수정해서 Entry 위짓과 두 번째 버튼을 추가하세요. 사용자가 두 번째 버튼을 누르면 Entry 에서 색깔 이름을 읽어 들여서 원의 내부를 칠하는데 사용해야 합니다. config 를 사용해서 이미 있는 원을 바꾸세요; 새 원을 만들지 마세요.

여러분의 프로그램은 사용자가 아직 만들어지지 않은 원의 색깔을 바꾸려 하거나, 색깔 이름이 잘못된 경우를 다룰 수 있어야 합니다.

제 답을 http://thinkpython.com/code/circle_demo.py 에서 볼 수 있습니다.

위짓 패킹하기

지금까지는 위짓들을 한 열로 쌓아왔습니다만, 대부분의 GUI 에서 배치(layout)는 더 복잡합니다. 예를 들어, 그림 [fig.turtleworld] 는 TurtleWorld ([turtlechap] 장을 보세요)의 단순화된 버전을 보여줍니다.

TurtleWorld

[fig.turtleworld]

이 절에서는 이 GUI 를 만드는 코드를 단계별로 분해해서 보여줍니다. 완전한 예제는 http://thinkpython.com/code/SimpleTurtleWorld.py 에서 다운로드 할 수 있습니다.

최상위 수준에서, 이 GUI 는 가로로 배치된 두 개의 위짓을 갖고 있습니다: Canvas 와 Frame. 그래서 첫 번째 단계는 행(row)을 만드는 것입니다.

class SimpleTurtleWorld(TurtleWorld):
    """This class is identical to TurtleWorld, but the code that
    lays out the GUI is simplified for explanatory purposes."""

    def setup(self):
        self.row()
        ...

setup 은 위짓을 만들고 배치하는 함수입니다. GUI 에 위짓들을 배치하는 것을 패킹(packing)이라 부릅니다.

row 는 가로 Frame 을 만들어서 “현재 프레임”으로 지정합니다. 이 프레임이 닫히거나 다른 프레임이 만들어질 때 까지는, 뒤따르는 모든 위짓들은 가로로 패킹됩니다.

여기에 캔버스와 다른 위짓들을 담을 세로(comuln) 프레임을 만드는 코드가 있습니다:

self.canvas = self.ca(width=400, height=400, bg='white')
self.col()

열에 들어가는 첫 번째 위짓은 격자(grid) 프레임인데, 2x2 로 배치된 4개의 버튼을 담습니다:

self.gr(cols=2)
self.bu(text='Print canvas', command=self.canvas.dump)
self.bu(text='Quit', command=self.quit)
self.bu(text='Make Turtle', command=self.make_turtle)
self.bu(text='Clear', command=self.clear)
self.endgr()

gr 은 격자를 만듭니다; 인자는 열의 개수입니다. 격자에서 위짓은 왼쪽에서 오른쪽으로, 위에서 아래로 배치됩니다.

첫 번째 버튼은 self.canvas.dump 를 콜백으로 사용합니다; 두 번째는 self.quit를 사용합니다. 이것들은 속박된 메쏘드(bound method) 인데, 특정 객체에 결합되어 있다는 뜻입니다. 호출될 때는, 그 개체에 대해 호출됩니다.

열의 다음 위짓은 Button 과 Entry 를 담는 가로 프레임입니다.

self.row([0,1], pady=30)
self.bu(text='Run file', command=self.run_file)
self.en_file = self.en(text='snowflake.py', width=5)
self.endrow()

row 의 첫 번째 인자는 위짓들 사이의 여유 공간을 어떻게 나눌지 결정하는 가중치들의 리스트입니다. 리스트 는 모든 여유 공간을 두 번째 위짓, Entry 입니다, 에게 준다는 뜻입니다. 이 코드를 실행하고 창의 크기를 변경하면, Entry 는 커지는데 반해 Button 은 그대로 있는 것을 보게 될 것입니다.

옵션 pady 는 이 행을 \(y\) 방향으로 “패딩(padding)”해서, 위와 아래에 30 픽셀 크기의 공간을 추가합니다.

endrow 는 이 위짓들의 행을 끝내서, 이어지는 위짓들은 세로 프레임에 패킹됩니다. Gui.py 는 프레임들의 스택을 유지합니다:

  • 프레임을 만들려고 row, col 또는 gr 를 사용할 때, 스택의 제일 위로 가고 현재 프레임이 됩니다.
  • 프레임을 닫기위해 endrow, endcol 또는 endgr 를 사용할 때, 스택에서 제거되고 이전 프레임이 현재 프레임이 됩니다.

메쏘드 run_file 은 Entry 의 내용을 읽고, 그 값을 파일명으로 사용해서 내용을 읽어 들인 후에 run_code 로 전달합니다. self.inter 는 Interpreter 객체인데 문자열을 받아서 파이썬 코드로 해석해서 실행하는 법을 알고 있습니다.

def run_file(self):
    filename = self.en_file.get()
    fp = open(filename)
    source = fp.read()
    self.inter.run_code(source, filename)

마지막 두 위짓은 Text 위짓과 Button 입니다:

self.te_code = self.te(width=25, height=10)
self.te_code.insert(END, 'world.clear()\n')
self.te_code.insert(END, 'bob = Turtle(world)\n')

self.bu(text='Run code', command=self.run_text)

run_text 는 파일 대신에 Text 위짓으로 부터 코드를 받는다는 것만 빼고는 run_file 과 유사합니다:

def run_text(self):
    source = self.te_code.get(1.0, END)
    self.inter.run_code(source, '<user-provided code>')

불행히도, 위짓 배치의 세부사항들은 다른 언어나 다른 파이썬 모듈들에서는 다릅니다. Tkinter 혼자서만도 위짓을 배치하는 세가지 다른 메커니즘을 제공합니다. 이 메커니즘들을 지오메트리 관리자(geometry manager)라고 합니다. 이 절에서 소개한 것은 “격자(grid)” 지오메트리 관리자입니다; 다른 것들은 “팩(pack)” 과 “플레이스(place)” 라고 불립니다.

다행히도, 이 절에 나오는 대부분의 개념들은 다른 GUI 모듈들과 다른 언어들에도 적용됩니다.

메뉴와 Callable

Menubutton 은 버튼처럼 보이는 위짓이지만, 눌렀을 때 메뉴를 띄웁니다. 사용자가 항목을 선택한 후에, 메뉴는 사라집니다.

여기에 색깔 선택 Menubutton 을 만드는 코드가 있습니다 (http://thinkpython.com/code/menubutton_demo.py에서 다운로드 할 수 있습니다):

g = Gui()
g.la('Select a color:')
colors = ['red', 'green', 'blue']
mb = g.mb(text=colors[0])

mb 는 Menubutton 을 만듭니다. 처음에, 버튼의 글은 내정된 색깔의 이름입니다. 다음 순환은 각 색깔마다 하나의 메뉴 항목을 만듭니다:

for color in colors:
    g.mi(mb, text=color, command=Callable(set_color, color))

mi의 첫 번째 인자는 이 항목이 연결될 Menubutton 입니다.

command 옵션은 Callable 객체인데, 처음 나오는 것입니다. 지금까지 함수와 속박된 메쏘드가 콜백으로 사용되는 것을 봤습니다, 어떤 인자도 함수로 전달할 필요가 없다면 작 동작합니다. 그렇지 않다면 (set_color 같은) 함수와 (color 같은)인자를 담고 있는 Callable 객체를 만들어야 합니다.

Callable 객체는 함수와 인자에 대한 참조를 애트리뷰트로 보관합니다. 후에, 사용자가 메뉴 항목을 클릭하면, 콜백은 함수를 호출하고 저장되어있던 인자를 전달합니다.

set_color 는 이렇게 됩니다:

def set_color(color):
    mb.config(text=color)
    print color

사용자가 메뉴 항목을 선택하고 set_color 가 호출될 때, 새로 선택된 색깔로 Menubutton 을 설정합니다. 색깔을 인쇄하기도 합니다; 이 예를 실행해보면, set_color이 (Callable 객체가 만들어질 때가 아니라) 항목을 선택할 때 호출된다는 것을 확인할 수 있습니다.

바인딩

바인딩(binding) 은 위짓, 이벤트, 콜백 간의 결합입니다: (버튼 누르기와 같은) 이벤트가 위짓에서 발생할 때 콜백이 호출됩니다.

많은 위짓들에는 내정된 바인딩이 있습니다. 예를 들어, 버튼을 누를 때, 내정된 바인딩은 눌린 것처럼 보이게 버튼의 양각을 바꿉니다. 버튼을 떼면 바인딩은 버튼의 모양을 되돌리고 command 옵션으로 지정된 콜백을 호출합니다.

bind 메쏘드를 사용해서 이 내정된 바인딩들을 번복하거나 새로 등록할 수 있습니다. 예를 들어, 이 코드는 캔버스의 바인딩을 만듭니다(이 절의 예제들은 http://thinkpython.com/code/draggable_demo.py에서 다운로드 할 수 있습니다):

ca.bind('<ButtonPress-1>', make_circle)

첫 번째 인자는 이벤트 문자열입니다; 이 이벤트는 사용자가 왼쪽 마우스 버튼을 누를 때 일어납니다. 다른 마우스 이벤트로는 ButtonMotion, ButtonRelease, Double-Button 가 있습니다. Double-Button.

두 번째 인자는 이벤트 처리기(event handler) 입니다. 이벤트 처리기는 콜백처럼 함수나 속박된 메쏘드입니다만, 중요한 차이점은 이벤트 처리기가 Event 객체를 매개변수로 받아들인다는 점입니다. 여기 예가 있습니다:

def make_circle(event):
    pos = ca.canvas_coords([event.x, event.y])
    item = ca.circle(pos, 5, fill='red')

Event 객체는 이벤트의 종류와 마우스 포인터의 좌표와 같은 세부사항들에 대한 정보를 담고 있습니다. 이 예에서 우리가 필요로 하는 정보는 마우스 클릭의 위치입니다. 이 값들은 “픽셀 좌표(pixel coordinates)”로 되어있는데 하위 그래픽 시스템에 의해 정의됩니다. 메쏘드 canvas_coords 는 이 값들을 circle 과 같은 캔버스 메쏘드들과 호환되는 “캔버스 좌표”로 변환합니다.

Entry 위짓의 경우, 흔히 <Return> 이벤트를 바인드(bind)하는데, 사용자가 Return 이나 Enter 키를 누를 때 일어납니다. 예를 들어, 다음 코드는 Button 과 Entry 를 만듭니다.

bu = g.bu('Make text item:', make_text)
en = g.en()
en.bind('<Return>', make_text)

make_text 는 버튼이 눌리거나 사용자가 Entry 에 입력하다가 Return을 누를 때 실행됩니다. 이 것이 동작하게 하려면, (인자 없이) command 로 호출되거나 (인자로 Event 를 받는) 이벤트 처리기로 호출될 수 있는 함수가 필요합니다:

def make_text(event=None):
    text = en.get()
    item = ca.text([0,0], text)

make_text 는 Entry 의 내용을 받아서 캔버스에 Text 항목으로 표시합니다.

캔버스 항목에 대한 바인딩을 만드는 것도 가능합니다. 다음은 Draggable 의 클래스 정의인데, 드랙앤드롭(drag-and-drop) 기능을 구현하는 바인딩을 제공하는 Item 의 자식 클래스입니다.

class Draggable(Item):

    def __init__(self, item):
        self.canvas = item.canvas
        self.tag = item.tag
        self.bind('<Button-3>', self.select)
        self.bind('<B3-Motion>', self.drag)
        self.bind('<Release-3>', self.drop)

init 메쏘드는 Item 을 매개변수로 받아들입니다. Item 의 애트리뷰트를 복사하고 세 이벤트들에 대한 바인딩을 만듭니다: 버튼 누름, 버튼 이동, 버튼 뗌.

이벤트 처리기 select 는 현재 이벤트의 좌표와 항목의 원래 색깔을 저장한 후, 색깔을 노랑으로 바꿉니다:

def select(self, event):
    self.dragx = event.x
    self.dragy = event.y

    self.fill = self.cget('fill')
    self.config(fill='yellow')

cget 은 “설정 조회(get configuration)” 를 뜻하는데, 옵션의 이름을 문자열로 받아서 그 옵션의 현재 값을 돌려줍니다.

drag 은 객체가 출발점으로부터 얼마나 멀리 움직였는지 계산하고, 저장된 좌표들을 갱신한 후 항목을 옮깁니다.

def drag(self, event):
    dx = event.x - self.dragx
    dy = event.y - self.dragy

    self.dragx = event.x
    self.dragy = event.y

    self.move(dx, dy)

이 계산은 픽셀 좌표에서 이루어집니다; 캔버스 좌표로 바꿔야 할 필요가 없습니다.

마지막으로 drop 은 항목의 원래 색깔을 복원합니다:

def drop(self, event):
    self.config(fill=self.fill)

이미 존재하는 항목에 드랙앤드롭 기능을 추가하는데 Draggable 클래스를 사용할 수 있습니다. 예를 들어, 여기 make_circle 의 수정된 버전이 있는데, circle 을 사용해서 Item 을 만든 후에 Draggable로 끌 수 있게 만듭니다:

def make_circle(event):
    pos = ca.canvas_coords([event.x, event.y])
    item = ca.circle(pos, 5, fill='red')
    item = Draggable(item)

이 예는 계승의 혜택 중 한가지를 보여줍니다: 정의를 수정하지 않고도 부모 클래스의 능력을 수정할 수 있습니다. 이것은 여러분이 작성하지 않은 모듈에 정의된 동작을 변경하고자 할 때 특히 유용합니다.

디버깅

GUI 프로그래밍의 과제 중 하나는, GUI 가 구성되는 동안 일어나는 것들과, 나중에 사용자 이벤트에 대한 반응으로 일어날 것들을 구분하는 것입니다.

예를 들어, 콜백을 설정할 때, 흔한 오류는 함수에 대한 참조를 전달하는 대신 함수를 호출하는 것입니다:

def the_callback():
    print 'Called.'

g.bu(text='This is wrong!', command=the_callback())

이 코드를 시행하면, the_callback 을 즉시 호출한 후에 버튼을 만드는 것을 보게 됩니다. 버튼을 눌렀을 때는 아무런 일도 하지 않는데, the_callback 의 반환값이 None이기 때문입니다. 보통 GUI 를 구성하는 동안은 콜백 호출을 원하지 않습니다; 나중에 사용자 이벤트에 대한 반응으로 호출되어야만 합니다.

GUI 프로그래밍의 다른 과제는 여러분이 실행 흐름에 대한 제어 권을 갖고 있지 않다는 것입니다. 프로그램의 어떤 부분이 실행되고 그 순서가 어떻게 될지는 사용자 액션이 결정합니다. 이 것은 어떤 가능한 이벤트 시퀀스에 대해서도 프로그램이 올바로 동작하도록 설계해야만 한다는 뜻입니다.

예를 들어, 연습 [circle2] 의 GUI 에는 두 개의 위짓이 있습니다: 하나는 Circle 항목을 만들고 다른 하나는 Circle 의 색깔을 바꿉니다. 만약 사용자가 원을 만든 후에 색깔을 바꾼다면 아무 문제가 없습니다. 그러나 사용자가 아직 존재하지도 않는 원의 색깔을 바꾸려 한다면 어떻게 될까요? 또는 하나 이상의 원을 만든다면?

위짓의 개수가 늘어남에 따라, 모든 가능한 이벤트 시퀀스를 상상하는 것은 점점 힘들어집니다. 이 복잡도를 다루는 한가지 방법은 시스템의 상태를 객체에 캡슐화한 다음 이런 것들을 고려하는 것입니다:

  • 가능한 상태들은 무엇입니까? Circle 예에서, 두 개의 상태를 고려할 수 있습니다: 사용자가 첫 번째 원을 만든 전 과 후.
  • 각 상태에서 어떤 이벤트가 일어날 수 있습니까? 앞의 예에서, 사용자는 버튼들 중 하나를 누르거나 종료할 수 있습니다.
  • 각 상태-이벤트 쌍에 대해, 원하는 결과가 뭡니까? 두 개의 상태와 두 개의 버튼이 있기 때문에, 고려해야 할 네 개의 상태-이벤트 쌍이 있습니다.
  • 무엇이 한 상태에서 다른 상태로 이동하게 만듭니까? 이 경우에, 사용자가 첫 번째 원을 만들 때 이동합니다.

이벤트의 시퀀스에 관계없이 항상 성립할 불변 조건을 정의하고 검사하는 것이 도움이 될 수도 있습니다.

GUI 프로그래밍에 대한 이런 접근법은, 사용자 이벤트의 모든 가능한 시퀀스를 검사하는데 시간을 들이지 않고서도 올바른 코드를 작성하도록 도울 수 있습니다!

용어

GUI:
그래픽 사용자 인터페이스(graphical user interface).
위짓 widget:
GUI 를 구성하는 요소의 하나로, 버튼, 메뉴, 글자 입력난 등을 포함한다.
옵션 option:
위짓의 생김새나 기능을 제어하는 값.
키워드 인자 keyword argument:
함수 호출의 일부로 매개변수의 이름을 표시하는 인자.
콜백 callback:
위짓과 결합되어 사용자가 액션을 수행할 때 호출되는 함수.
속박된 메쏘드 bound method:
특정한 인스턴스와 결합된 메쏘드.
이벤트 주도 프로그래밍 event-driven programming:
실행 흐름이 사용자 액션에 의해 결정되는 프로그래밍 스타일..
이벤트 event:
GUI 가 반응하게 만드는 마우스 클릭이나 키 누름과 같은 사용자 액션.
이벤트 루프 event loop:
사용자 액션을 기다렸다가 반응하는 무한 루프.
항목 item:
캔버스 위짓 내의 그래픽 요소.
경계 상자 bounding box:
항목들의 집합을 둘러싸는 직사각형으로, 보통 마주보는 두 꼭지점으로 지정된다.
팩 pack:
GUI 의 요소들을 배치하고 표시하는 것.
지오메트리 관리자 geometry manager:
위짓들을 패킹하는 시스템.
바인딩 binding:
위짓, 이벤트, 이벤트 처리기들 간의 결합. 이벤트 처리기는 위짓에서 이벤트가 일어날 때 호출된다.

연습

[연습 19.4.]

이 연습을 위해, 여러분은 이미지 뷰어(image viewer)를 작성할 것입니다. 여기 간단한 예가 있습니다:

g = Gui()
canvas = g.ca(width=300)
photo = PhotoImage(file='danger.gif')
canvas.image([0,0], image=photo)
g.mainloop()

PhotoImage 는 파일을 읽어서 Tkinter 가 표시할 수 있는 PhotoImage 객체를 돌려줍니다. Canvas.image 는 이미지를 캔버스에 넣는데, 주어진 좌표를 중심이 되게 합니다. 레이블, 버튼이나 다른 위짓들에도 이미지를 넣을 수 있습니다:

g.la(image=photo)
g.bu(image=photo)

PhotoImage 는 GIF 나 PPM 같은 몇 가지 이미지 형식들만 다룰 수 있습니다만, 다른 파일들을 읽으려면 Python Imaging Library (PIL) 를 사용할 수 있습니다.

PIL 모듈의 이름은 Image 이지만, Tkinter 가 같은 이름의 객체를 정의합니다. 충돌을 피하기 위해, 이런 식으로 import...as 를 사용할 수 있습니다:

import Image as PIL
import ImageTk

첫 번째 줄은 Image 를 들여오기 해서 지역적인 이름 PIL을 부여합니다. 두 번째 줄은 ImageTk 를 들여오기 하는데, PIL 이미지를 Tkinter PhotoImage 로 변환할 수 있습니다. 여기 예가 있습니다:

image = PIL.open('allen.png')
photo2 = ImageTk.PhotoImage(image)
g.la(image=photo2)
  1. http://thinkpython.com/code 에서 image_demo.py, danger.gif, allen.png 를 다운로드 하세요. image_demo.py 를 실행하세요. PIL 과 ImageTk 를 설치해야 할 수도 있습니다. 아마 여러분들의 소프트웨어 저장소에 있겠지만, 그렇지 않은 경우 pythonware.com/products/pil/ 에서 얻을 수 있습니다.

  2. image_demo.py 에서 두 번째 PhotoImage 의 이름을 photo2 에서 photo로 바꾼 후 다시 프로그램을 실행하세요. 두 번째 PhotoImage 를 볼 수 있지만 첫 번째는 볼 수 없어야 합니다.

    문제는 여러분이 photo 를 다시 대입할 때, 첫 번째 PhotoImage 에 대한 참조를 덮어쓰게 되어, 첫 번째 PhotoImage 가 사라지게 됩니다. PhotoImage 를 지역 변수에 대입하면 같은 일이 일어납니다; 함수가 종료할 때 사라집니다.

    이 문제를 피하기 위해서는, 유지하고 싶은 각 PhotoImage 에 대한 참조를 저장해야만 합니다. 전역 변수를 쓰거나 PhotoImages 를 자료 구조나 객체의 애트리뷰트로 저장할 수 있습니다.

    이 동작은 실망스러울 수 있는데, 제가 경고하고 있는 이유입니다(그리고 예로 든 이미지가 “Danger!”라고 말하고 있는 이유입니다).

  3. 이 예로 시작해서, 디렉토리의 이름을 받아서 모든 파일들을 순환하고, PIL 이 이미지로 인식하는 모든 것들을 표시하는 프로그램을 작성하세요. PIL 이 인식하지 못하는 파일들을 잡아내는데 try 문을 사용할 수 있습니다.

    사용자가 이미지를 클릭하면, 프로그램은 다음 것을 보여줘야 합니다.

  4. PIL 은 이미지를 다루는 다양한 메쏘드들을 제공합니다. 이에 대한 것들을 http://pythonware.com/library/pil/handbook 에서 읽을 수 있습니다. 도전 과제로, 이 중 몇 가지 메쏘드들을 선택해서 이미지에 적용하는 GUI 를 제공하세요.

답: http://thinkpython.com/code/ImageBrowser.py.

[연습 19.5.]

벡터 그래픽 편집기는 사용자가 화면에서 모양을 그리고 편집하며 포스트스크립트(Postscript) 나 SVG 같은 벡터그래픽 형식의 출력 파일을 생성하도록 하는 프로그램입니다.

Tkinter 로 간단한 벡터 그래픽 편집기를 작성하세요. 최소한, 사용자가 선, 원, 직사각형을 그릴 수 잇게 해야 하고, 캔버스 내용의 포스트스크립트 표현을 생성하기 위해 Canvas.dump 를 사용해야 합니다.

도전 과제로, 사용자가 캔버스의 항목들을 선택해서 크기를 바꿀 수 있게 할 수 있습니다.

[연습 19.6.]

Tkinter 를 사용해서 기본적인 웹 브라우저를 작성하세요. 사용자가 URL 을 입력할 수 있는 Text 위짓과 페이지의 내용을 보여줄 수 있는 Canvas 가 있어야 합니다.

파일을 다운로드 하는데 urllib 모듈을, HTML 태그(http://docs.python.org/2/library/htmlparser.html를 보세요)를 해석하는데 HTMLParser 모듈을 사용할 수 있습니다.

최소한 여러분의 브라우저는 단순 텍스트와 하이퍼링크(hyperlink)를 처리해야 합니다. 도전 과제로 배경색들, 텍스트 형식 태그들, 이미지들을 다룰 수 있습니다.