[원문] http://coreapython.hosting.paran.com/GUI/Thinking%20in%20Tkinter.htm

Tkinter로 생각하기

작성 스티픈 퍼그(Stephen Ferg) (steve@ ferg.org)
revised: 2005-07-17
한글판 johnsonj 2008.04.10

이 파일에는 Tkinter로 생각하기 시리즈에 필요한 파일이 모두 들어있다.

이 파일을 작은 폰트로 인쇄하면, 세로 모드로 인쇄해도 코드 줄 끝이 잘리지 않는다. 줄 끝이 잘림을 감수하고 인쇄하려면, 더 작은 폰트로 인쇄하든가 아니면, 가로 모드로 인쇄하시기를 바란다.

인쇄 페이지는 인쇄 설정에 따라 다르지만 대략 40에서 60 페이지 사이이다. 인쇄 모양이 어떨지 그리고 페이지가 어느 정도 되는지 알아 보려면, 실제로 파일을 인쇄하기 전에 인쇄 미리보기를 하시기를 바란다.

GUI 프로그래밍의 기본 개념

  • 주제: "Tkinter로 생각하기"
  • 저자 : 스티픈 퍼그(Stephen Ferg) (steve@ferg.org)

"Tkinter로 생각하기"에 관하여

본인은 여러 책을 통해 Tkinter를 배우고 있는 중이다. 그런데 생각보다 어려웠다.

문제는 책의 저자들이 성급하게 Tkinter 도구상자에 있는 위젯들을 모두 설명하려고 한다는 것이다. 실제로는 기본적인 개념을 전혀 설명해 주지 않았다. 그들은 "Tkinter로 생각하는 법"을 가르쳐 주지 않았다.

다음에 Tkinter로 생각하는 법을 설명해주는 몇가지 짧은 프로그램을 소개하려고 한다. 그 프로그램들을 통하여 Tkinter에서 사용할 수 있는 모든 유형의 창부품과 속성 그리고 메쏘드를 낱낱히 나열하지는 않겠다. 또 Tkinter 전반에 관한 개론을 제공하지도 않겠다. 단지 Tkinter의 기본적인 개념을 이해시킬 정도로만 여러분을 이끌 생각이다.

주로 Tkinter 꾸리기 (즉"꾸림자(packer)") 위치 관리자(geometry manager)에 대한 연구에 주력하겠다. 격자관리자(grid)나 배치관리자(place)에 관한 연구는 제외한다.

네개의 기본적인 구이-프로그래밍 작업

사용자 인터페이스를 개발하려면 꼭 해야 할 표준적인 작업이 있다..

1) 어떻게 UI가 보여야 하는지 지정해야 한다. 다시 말해, 사용자의 컴퓨터 스크린에 무엇이 보일지 결정하는 코드를 작성해야 한다.

2) UI가 어떤 일을 하기를 원하는지 지정해야 한다. 다시 말해, 프로그램이 할 일을 루틴으로 작성해야 한다.

3) "모습"과 "할 일"을 연관지워 주어야 한다. 다시 말해, 코드를 작성해야 한다. 사용자의 컴퓨터에 보이는 것과 그 프로그램이 할 일을 지정한 루틴과 연관시켜야 한다.

4) 마지막으로, 사용자로부터 입력을 앉아서 기다리는 코드를 작성해야 한다.

몇가지 Gui-프로그래밍 용어

GUI (그래픽 사용자 인터페이스) 프로그래밍은 특별한 용어가 다음과 같은 기본적인 작업과 연관된다.

1) GUI의 모습을 지정하기 위해 "창부품(widgets)"의 모습과 그의 공간적 위치 관계를 기술한다 (즉, 한 위젯과 다른 위젯과의 상하 관계나, 좌우 관계). "창부품(widget)"이라는 용어는 "그래픽 사용자 인터페이스 구성부품"에 대한 일반적인 용어가 된 의미 없는 단어다. 창부품에는 창, 버튼, 메뉴, 메뉴 항목, 아이콘, 드롭-다운 리스트, 스크롤 바, 등등이 포함된다.

2) GUI의 처리를 실제적으로 담당하는 루틴을 "역호출 처리자(callback handlers)" 또는 "사건 처리자(event handlers)"라고 부른다. "사건(Events)"이란 키보드 누름이나 마우스 클릭 같은 입력 사건을 말한다. 이런 루틴을 "처리자(handlers)"라고 부르는데 그 이유는 그런 이벤트를 "처리하기" (즉, 응답하기) 때문이다.

3) 사건 처리자를 한 위젯과 연관짓는 것을 "묶기(binding)"라고 부른다. 대략적으로 말해, 묶기 과정에는 세가지 다른 일이 관련되어 있다:

  • (a) 사건의 유형 (예를 들어, 왼쪽 마우스 버튼 클릭, 또는 키보드에서 ENTER 키 누름)과
  • (b) 창부품 (예, 버튼) 그리고
  • (c) 사건-처리자 루틴.

예를 들어, (a) 왼쪽 마우스 버튼의 한번-클릭이 (b) 화면 위의 "CLOSE" 버튼/창부품에 일어나는 것을 (c) "closeProgram" 루틴에 묶을 수 있다. 이 루틴은 창을 닫고 프로그램을 끝낸다.

4) 앉아서 입력을 기다리는 코드를 "사건 회돌이(event loop)"라고 부른다.

사건 회돌이에 관하여

영화를 보면, 작은 마을이면 어디나 창문가에서 그저 바라 보면서 하루 종일 시간을 보내는 여유로운 할머니가 있기 마련이다. 그 할머니는 지나가는 모든 것들을 본다. 물론 본 것들이 모두 관심의 대상은 아니다 -- 그저 사람들이 거리에서 이리 저리 움직일 뿐이다. 그러나 그 중에 어떤 것은 관심을 끈다 -- 거리 건너 신혼 부부 집에서 큰 싸움이 일어 나거나 한다면 관심이 안 갈 수 없다. 관심의 사건이 일어나면, 그 할머니는 즉시 전화를 들고 그 소식을 경찰이나 이웃에게 전한다.

사건 회돌이는 이 할머니와 많이 닮았다. 사건 회돌이는 사건이 일어나는 것을 낱낱이 모두 보면서 시간을 보낸다. 대부분의 사건은 관심의 대상이 아니다. 그런 사건을 보더라도, 아무 일도 하지 않는다. 그러나 뭔가 흥미로운 사건을 보면 -- 사건 처리자가 그 사건에 묶여 있기 때문에, 관심의 대상인 사건을 보면 -- 즉시 사건 처리자를 불러서 해당 사건이 일어났음을 알린다.

프로그램의 행위

다음 프로그램을 보면 쉽게 사용자-인터페이스의 세계로 들어 갈 수 있다. 이런 기본적인 개념이 아주 간단한 프로그램에 어떻게 구현되어 있는지 보실 수 있다. 다음 프로그램은 Tkinter나 기타 어떤 형태의 GUI 프로그래밍도 사용하지 않는다. 그냥 콘솔에 메뉴를 띄워 간단한 키보드 입력을 받을 뿐이다. 그렇지만, 보시다시피, 사용자-인터페이스 프로그래밍에서 요구하는 네 개의 기본적인 일을 하고 있다.

[revised: 2003-02-23]

프로그램 소스 코드 tt000.py

#----- task 2:  사건 처리자 루틴을 정의한다 ---------------------def handle_A():    print "틀렸군요! 다시 해보세요!"def handle_B():    print "정답입니다!  트릴리엄(Trillium)은 꽃 종류입니다!"def handle_C():    print "틀렸어요! 다시 해보세요!"# ------------ task 1: 화면의 모습을 정의한다 ------------print "\n"*100   # 화면을 깨긋하게 정리하라print "            아주 도발적인 추측 게임"print "========================================================"print "해답을 기호로 누른다음, ENTER 키를 치세요."printprint "    A.  동물"print "    B.  식물"print "    C.  광물"printprint "    X.  이 프로그램 종료"printprint "========================================================"print "'Trillium'이란 어떤 종류의 것입니까?"print# ---- task 4: 사건 회돌이. 영원히 회돌이하면서, 사건들을 관찰한다. ---while 1:    # 다음 사건을 관찰한다    answer = raw_input().upper()    # -------------------------------------------------------    # Task 3: 흥미로운 키보드 사건을 그의 사건 처리자에    # 연관시킨다.  간단한 형태의 묶기임.    # -------------------------------------------------------    if answer == "A": handle_A()    if answer == "B": handle_B()    if answer == "C": handle_C()    if answer == "X":        # 화면을 정리하고 사건 회돌이를 빠져 나간다        print "\n"*100        break    # 다른 사건의 모두 관심의 대상이 아니며, 모두 무시된다

가장 단순한 Tkinter 프로그램

가장 단순한 Tkinter 프로그램 -- 서술문 단 세개!

앞서 지난 프로그램에서 언급한 네 개의 기본적인 GUI 작업중에서, 이 프로그램은 오직 한 가지 일을 한다 -- 사건 회돌이를 실행한다.

(1) 첫 번째 서술문에서 Tkinter를 반입하여, 사용가능하도록 만든다. 반입의 형태("from Tkinter import *")를 주목하자. Tkinter로 부터 오는 어떤 것이든 접두사 "Tkinter"로 자격을 부여하지 않겠다는 뜻이다.

(2) 두 번째 서술문에서 "최상위(toplevel)" 창이 생성된다. 기술적으로, 두 번째 서술문이 하는 일은 클래스 실체 "Tkinter.Tk"를 만드는 것이다.

이 최상위 창은 Tkinter 어플리케이션에서 제일 높은- 수준의 GUI 구성요소이다. 관례적으로, 최상위 창은 보통 이름이 "root"이다.

(3) 세 번째 서술문에서 "root" 객체의 "mainloop" (즉, 사건 회돌이) 메쏘드가 실행된다. 주 회돌이가 실행되면, 루트에 사건이 일어나기를 기다린다. 사건이 일어나면, 바로 처리되고 회돌이는 계속해서 진행하면서, 다음 사건이 일어나기를 기다린다. 회돌이는 루트 창에 "소멸(destroy)" 사건이 일어날 때까지 계속된다. "destroy" 사건은 창을 닫는 사건이다. 루트가 소멸되면, 창은 닫히고 사건 회돌이는 종료한다.

프로그램의 행위

다음 프로그램을 실행하면, (Tk 고마워) 최상위 창이 자동으로 나타나는데 창을 최소화하거나 최대화하며 닫는 창부품으로 장식되어 있다. 시험해 보자 -- 실제로 작동할 것이다.

"close" 창부품을 (제목 막대의 오른 쪽에서, 상자 안의 "x"를) 클릭하면 "destroy" 사건이 일어난다. 소멸 사건은 주 사건 회돌이를 종료시킨다. "root.mainloop()" 이후로는 서술문이 없으므로, 프르그램은 더 이상 아무 일도 하지 않고, 끝난다.

[revised: 2003-02-23]

프로그램 소스 코드 tt010.py

from Tkinter import * ### (1)root = Tk()           ### (2)root.mainloop()       ### (3)

꾸려 넣기(packing)

이제 네가지 주 GUI 작업 중에서 또 하나를 건드려 보자 -- GUI의 모습을 지정해 보자.

다음 프로그램에서는 Tkinter 프로그래밍의 세 가지 주요 개념을 소개한다:

  • * GUI 객체를 만들어서 그의 부모 객체와 연관시키기
  • * 꾸려 넣기(packing)
  • * 그릇(containers) 대 창부품(widgets)

이제부터는 그릇 구성요소와 창푸품을 구별하겠다. 앞으로 쓸 용어에서, "창부품(widget)"이란 (보통) 눈에 보이고 일을 하는 GUI 구성요소이다. "그릇(container)"은 대조적으로 그냥 -- 바구니 같은 -- 그릇으로서 거기에다 창부품을 넣을 수 있다.

Tkinter에서는 수 많은 그릇이 제공된다. "그림판(Canvas)"은 그리기 어플리케이션을 위한 그릇이다. 가장 많이 사용되는 그릇은 "틀(frame)"이다.

틀(Frames)은 Tkinter에서 "Frame"이라고 부르는 클래스로 제공된다. 다음과 같은 표현은:

	Frame(myParent)

Frame 클래스의 실체가 생성되고 (다시 말해, 틀을 만든다), 그 프레임 실체를 그의 부모인 myParent에 연관시킨다. 또다른 방식으로 바라 본다면: 자손 프레임을 myParent 구성요소에 덧붙이는 표현식으로 보아도 좋다.

그래서 이 프로그램에서 다음 서술문은 (1):

	myContainer1 = Frame(myParent)

그의 부모가 myParent (즉, root)인 프레임을 생성하고, 거기에 "myContainer1"이라는 이름을 부여한다. 다시 말해, 그릇을 만들어 주어서 거기에 창부품을 넣을 수 있다. (이 프로그램에서는 아무 창부품도 넣지 않겠다. 다음 프로그램에서 그렇게 해 보자.)

부모/자손 관계는 여기에서 시각적 관계가 아니라 논리적 관계임에 주목하자. 이 관계는 소멸 사건 같은 것들을 지원하기 위해 존재한다 -- (루트 같은) 부모 구성요소가 소멸할 때, 누가 자기의 자손인지 알아서 자신이 소멸하기 전에 그 자손을 죽일 수 있도록 하기 위해서다.

(2) 다음 서술문은 myContainer1을 "꾸려 넣는다(packs)" .

     myContainer1.pack()

간단히 말해, "꾸려넣기(packing)"란 한 GUI 구성요소와 그의 부모 사이의 시각적 관계를 설정하는 처리과정이다. 꾸려 넣지 않으면, 보이지 않는다.

"Pack"에는 Tkinter "pack" 위치 관리자에 관련되어 있다. 위치 관리자(geometry manager)란 본질적으로 시각적으로 그릇과 창부품이 어떻게 보여야 하는지 Tkinter에게 말해주는 API이다. Tkinter는 세가지 위치 관리자를 지원하는데: pack과 grid 그리고 place가 그것이다. 사용하기 쉽기 때문에, Pack과 (약간 범위는 작지만) grid가 널리 사용된다. "Tkinter로 생각하기"에서 모든 예제는 pack 위치 관리자만 사용한다.

그래서 다음에 Tkinter 프로그래밍에서 자주 나타나는 기본적인 패턴이 있다.

  • (1) (창부품이나 그릇의) 실체는 생성되면, 그의 부모와 연관된다.
  • (2) 그 실체는 꾸려넣어진다(packed).

프로그램 행위

다음 프로그램은 앞의 예제와 아주 흡사하다. 단 보기가 좀 어려운데, 그 이유는...

틀은 신축적이다

틀은 기본적으로 그릇이다. 그릇의 안쪽을 -- 말 그대로 그릇 안쪽의 "공간"인 -- "cavity"라고 불리운다. ("Cavity"는 Tkinter가 Tk로부터 가져온 기술적인 용어이다.)

이 cavity는 "잘 늘어난다". 즉 고무 줄처럼 신축성이 좋다. 틀에 최소나 최대 크기를 지정하지 않는 한, 공간(cavity)은 늘어나거나 줄어들어서 그 틀 안에 무엇이 놓이든 잘 적응한다.

앞 프로그램에서는 그 안에 아무 것도 넣지 않았기 때문에, 루트는 기본 크기로 자신을 화면에 표시했다.

그러나 다음 프로그램에서는 "무언가"를 루트의 cavity 안에 집어 넣겠다 -- Container1을 그 안에 집어 넣었다. 그래서 루트 틀은 줄어들어서 Container1의 크기에 적응한다. 그러나 Container1 안에 아무 창부품도 넣지 않았기 때문에, 그리고 Container1에 대해 최소 크기도 지정하지 않았기 때문에, 루트의 공간은 줄어들어 하나도 없게 된다. 그 때문에 아래 제목 막대에서는 아무것도 볼 수 없다.

다음 프로그램에서는 창부품과 기타 그릇들을 Container1에 집어 넣어 보겠다. 어떻게 Container1이 늘어나서 적응하는지 보실 수 있다.

[revised: 2003-02-24]

프로그램 소스 코드 tt020.py

from Tkinter import *root = Tk()myContainer1 = Frame(root)  ### (1)myContainer1.pack()         ### (2)root.mainloop()

창부품 꾸리기

다음 프로그램에서는 처음으로 창부품을 만들어서, 그것을 myContainer1에 집어 넣는다.

(1) 만든 그 창부품은 버튼이다 -- 다시 말해, Tkinter의 "Button" 클래스의 실체이다. 다음 서술문은:

		button1 = Button(myContainer1)

버튼을 생성하고, 거기에 "button1"이라는 이름을 부여하고, 그것을 그의 부모인 myContainer1이라고 부르는 그릇 객체에 연관시킨다.

(2)(3) 창부품들은 속성이 여러개 있는데, 지역 이름공간 사전에 저장되어 있다. 버튼 창부품이 가진 속성으로 크기, 전경색과 배경색, 표시할 텍스트, 테두리의 모양 등등을 제어한다. 이 예제에서는 button1에 딱 두개의 속성만 설정하겠다: 배경색과 텍스트를 설정해 보겠다. 버튼 사전에 "text"와 "background" 키로 값을 설정하면 된다.

		button1["text"]= "Hello, World!"		button1["background"] = "green"

(4) 물론, button1을 꾸려 넣어야 한다.

		button1.pack()

몇가지 유용한 기술적 용어

그릇과 그 안에 담긴 창부품 사이의 관계를 종종 "부모/자손" 관계로 지칭한다. 또 "주인/노예" 관계라고 부르기도 한다.

프로그램의 행위

이 프로그램을 실행하면, Container1에 이제 "Hello, World!"라는 텍스트가 붙은 녹색 버튼이 담겨 있을 것이다. 거기를 클릭하더라도, 아무 일도 일어나지 않는데, 왜냐하면 버튼이 클릭되었을 때 해 줄 일을 아직 지정하지 않았기 때문이다. (나중에 그렇게 해 보자.)

지금까지는 예전과 같이, 제목 막대에 있는 CLOSE을 눌러서 창을 닫아야 한다.

myContainer1가 늘어나서 button1에 어떻게 적응하는지 주목하자.

[revised: 2002-10-01]

프로그램 소스 코드 tt030.py

from Tkinter import *root = Tk()myContainer1 = Frame(root)myContainer1.pack()button1 = Button(myContainer1)      ### (1)button1["text"]= "Hello, World!"    ### (2)button1["background"] = "green"     ### (3)button1.pack()	                    ### (4)root.mainloop()

클래스 구조의 사용

클래스 구조 사용하기

다음 프로그램에서 Tkinter 어플리케이션을 클래스 집합으로 구성하는 법을 보여주겠다.

다음 프로그램에서 MyApp라는 클래스를 추가하고 앞 프로그램에서 코드의 일부를 그의 구성자 (__init__) 메쏘드 안으로 이동시켰다. 다음과 같이 구조화된 프로그램에서는 세가지 다른 일을 한다:

(1) 코드에서 정의된 클래스(MyApp)에는 보여질 구이의 모습이 정의된다. 원하는 GUI의 모습과 더불어 그것으로 하고자 하는 일이 정의된다. 다음 코드는 클래스의 구성자 (__init__) 메쏘드로 이동되었다. (1a)

(2) 프로그램이 실행되면, 제일 먼저 하는 일은 클래스의 실체를 하나 만드는 일이다. 실체를 만들어내는 서술문은 다음과 같다.

   myapp = MyApp(root)

클래스의 이름이 "MyApp"임에 주목하자 (대문자에 주목) 그리고 실체의 이름은 "myapp"이다 (소문자에 주목).

또 다음 서술문은 "root"를 MyApp의 구성자 메쏘드(__init__)에 인자로 건네고 있음을 주목하자. 구성자 메쏘드는 "myParent" 이름 아래의 루트를 인식한다. (1a)

(3) 마지막으로, 루트에서 주회돌이를 실행한다.

왜 어플리케이션을 클래스로 구성하는가?

프로그램에 클래스 구조를 사용하는 이유 하나는 프로그램을 제어하기가 더 좋기 때문이다. 클래스로 구축되어 들어간 프로그램은 보통 -- 특히 아주 큰 프로그램이라면 -- 그렇지 않은 프로그램에 비해 훨씬 더 이해하기가 쉽다.

더 중요하게 고려해야 할 점은 어플리케이션을 클래스로 구축하면 전역 변수를 사용하지 않아도 된다는 것이다. 결과적으로, 프로그램이 커질수록, 사건 처리자가 서로 정보를 공유하기를 바랄 것이다. 한 가지 방법은 전역 변수를 사용하는 것이지만, 이것은 아주 난잡한 테크닉이다. 좀 더 좋은 방법은 실체 변수 (즉, "self." 변수를) 사용하는 것이다. 그러기 위해서는 어플리케이션을 클래스로 구조화시켜야 한다. 이 문제를 나중에 프로그램으로 탐험해 보겠다.

언제 클래스 구조를 도입해야 할까

앞에서 Tkinter 프로그램을 위한 클래스 구조 표기법을 소개하였다. 설명하기 위해 그리고 다른 주제로 나아가기 위해서 말이다. 그러나 실제 개발에서는 다르게 처리하고 싶을 수도 있다.

많은 경우, Tkinter 프로그램은 단순한 스크립트로 시작한다. 모든 코드는 줄 단위이다. 앞서 우리 프로그램 같이 말이다. 그러다가, 어플리케이션을 새롭게 이해할수록, 프로그램은 자라난다. 얼마 지나지 않아 코드가 넘쳐나고 전역 변수를 사용하기 시작할 수 있는데... 아마도 전역 변수가 엄청 많아질 것이다. 프로그램은 이해하고 수정하기에 점점 더 어려워진다. 그런 일이 일어나면, 프로그램을 분해할 때가 된 것이다. 다시 말해 클래스를 사용하여 재구조화해야 한다.

반면에, 클래스에 익숙하다면, 그리고 프로그램의 최종 모습을 잘 이해하고 있다면, 처음부터 클래스를 사용하여 프로그램을 구조화해도 좋다.

그러나 한편으로 (다시 처음으로 되돌아 가 본다면?), 개발 과정에서 초기에는 (게릿 뮐러(Gerrit Muller)가 지적한 바와 같이) 종종 사용해야 할 최적의 클래스 구조를 알지도 못한다 -- 초기 개발 과정에서는 문제와 해결책을 충분히 이해조차 하지 못한다. 클래스를 너무 일찍 사용하면 이해가 점점 어려워지고 결국 분해가 더욱 요구되며 그저 코드만 어지럽히는 불필요한 구조가 도입될 수 있다.

그래서 그 문제는 개인적인 취향과 경험 그리고 환경에 많이 관련된다. 당신에게 맞다고 느껴지는 대로 하자. 그리고 -- 어떤 방법을 사용하든 -- 두려워하지 말고 필요할 때 신중하게 분해하자.

프로그램 행위

다음 프로그램을 실행하면, 앞의 프로그램과 모습이 정확하게 똑 같다. 기능에 전혀 변화가 없다 -- 단지 코드가 구조화 되었을 뿐이다.

[revised: 2003-02-23]

프로그램 소스 코드 tt035.py

from Tkinter import *class MyApp:                         ### (1)    def __init__(self, myParent):      ### (1a)        self.myContainer1 = Frame(myParent)        self.myContainer1.pack()        self.button1 = Button(self.myContainer1)        self.button1["text"]= "Hello, World!"        self.button1["background"] = "green"        self.button1.pack()root = Tk()myapp = MyApp(root)  ### (2)root.mainloop()      ### (3)

속성 설정하기

(1) 앞의 프로그램에서 버튼 객체 button1을 만들어서, 그의 텍스트와 배경색을 아주 직접적인 방법으로 설정한다.

		self.button1["text"]= "Hello, World!"		self.button1["background"] = "green"

다음 프로그램에서 세개의 버튼을 더 Container1에 추가하는데, 약간 방법이 다르다.

(2) button2에 대하여, 처리과정은 본질적으로 button1과 같지만, 버튼의 사전에 접근하는 대신에 버튼에 내장된 "configure" 메쏘드를 사용한다.

(3) button3에서는 method가 여러 키워드 인자를 받을 수 있어서 하나의 서술문에 여러 옵션을 설정할 수 있음을 볼 수 있다.

(4) 앞의 예제에서는 버튼을 설정하는 일이 두-단계의 과정을 거쳤다: 첫 단계로 버튼을 만든 다음 그의 특성을 설정한다. 그러나 버튼을 만들 때 바로 특성을 지정하는 것이 가능하다. "Button" 창부품은 (다른 모든 창부품과 마찬가지로) 첫 인자가 그의 부모라고 생각한다. 이는 위치 인자이지, 키워드 인자가 아니다. 그러나 그 다음부터는 원한다면 여러개의 키워드 인자를 추가해서 창 부품의 특성을 지정할 수 있다.

프로그램의 행위

다음 프로그램을 실행하면, Container1에 이제 원래의 초록색 버튼 말고도 세개의 버튼이 더 들어 있을 것이다.

myContainer1가 늘어나서 어떻게 이런 버튼에 적응하는 것에 주목하자.

버튼이 차곡차곡 쌓이는 것에도 주목하자. 다음 프로그램에서는 왜 이런 식으로 서로 정렬되는지 알아보고 그리고 다르게 정렬하는 법을 살펴 보겠다.

프로그램 소스 코드 tt040.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        self.button1 = Button(self.myContainer1)        self.button1["text"] = "Hello, World!"   ### (1)        self.button1["background"] = "green"     ### (1)        self.button1.pack()        self.button2 = Button(self.myContainer1)        self.button2.configure(text="Off to join the circus!") ### (2)        self.button2.configure(background="tan")               ### (2)        self.button2.pack()        self.button3 = Button(self.myContainer1)        self.button3.configure(text="Join me?", background="cyan")  ### (3)        self.button3.pack()        self.button4 = Button(self.myContainer1, text="Goodbye!", background="red") ### (4)        self.button4.pack()root = Tk()myapp = MyApp(root)root.mainloop()

정렬하기

지난 프로그램에서는 두 개의 버튼이 서로 위아래로 쌓여 있는 것을 보았다. 그렇지만, 옆으로 나란히 보이는게 좋겠다. 다음 프로그램에서는 pack()으로 할 수 있는 일을 살펴보겠다.

(1) (2) (3) (4)

꾸리기는 구성요소의 시각적 관계를 제어하는 방법이다. 그래서 이제 pack "side" 옵션을 사용하여 버튼을 나란히 배치해보자. "side" 인자를 pack() 서술문에 건네면 되는데, 예를 들면:

		self.button1.pack(side=LEFT)

LEFT는 (RIGHT와 TOP 그리고 BOTTOM과 마찬가지로) 사용자에게 친숙한 상수로서 Tkinter에 정의되어 있다. 다시 말해, "LEFT"는 실제로는 "Tkinter.LEFT"이다 -- 그러나 Tkinter를 반입했던 방식 때문에, 접두사로 "Tkinter."를 공급할 필요가 없다.

지난 프로그램에서 왜 버튼은 수직으로 쌓이는가

기억하시겠지만, 지난 프로그램에서는 "side" 옵션을 전혀 지정하지 않고 그냥 버튼을 꾸려 넣었고, 때문에 버튼은 서로 위아래로 쌓여 꾸려졌다. 그것은 기본 "side" 옵션이 "side=TOP"이기 때문이다.

그래서 button1을 꾸려 넣으면, myContainer1의 안쪽 공간 위쪽에 배치된다. 그 때문에 myContainer1에 대하여 남는 공간이 button1 아래에 위치한다. 다음 button2를 꾸려 넣었다. 공간의 위쪽에 꾸려 넣어졌는데, 이것은 공간이 button1 바로 아래에 위치한다는 뜻이다. 그리고 공간은 이제 button2 아래에 위치한다.

버튼을 다른 순서로 꾸려 넣었다면 -- 예를 들어, button2를 먼저 꾸린다음, button1을 꾸려 넣었다면 -- 위치가 반대로 되어서, button2가 위에 배치되었을 것이다.

그래서, 보시다시피, GUI의 겉모습을 통제하는 한 가지 방법은 그릇 안에 창부품을 꾸려 넣는 순서를 제어하는 것이다.

약간 기술적인 용어 -- "동선(orientation)"

"수직적" 동선은 TOP과 BOTTOM이 포함된다. "수평적" 동선은 LEFT와 RIGHT가 포함된다.

창부품과 그릇을 꾸려 넣을 때, 두 가지 동선을 섞는 것도 가능하다. 예를 들어, 한 버튼은 수직적 동선으로 (예, TOP) 꾸려 넣고 다른 버튼은 수평적 동선 (즉, LEFT)로 꾸려 넣을 수 있다.

그러나 한 그릇 안에서 이런 식으로 동선을 섞어 쓰는 것은 좋은 생각이 아니다. 동선을 섞어 쓰면, 최종적으로 어떻게 보일지 예측하기 힘들고, 창크기를 조절할 때 GUI가 일그러지는 모습을 보고 놀랄 수도 있다.

그래서 좋은 디자인 습관은 같은 그릇 안에서 절대로 동선을 섞어 쓰지 않는 것이다. 복잡한 GUI를 다루는 방법은, 실제로 여러 동선을 사용하고 싶으면, 그릇 안에 그릇을 내포시키는 것이다. 나중 프로그램에서 이 주제를 다루어 보겠다.

프로그램의 행위

다음 프로그램을 실행하면, 이제 버튼 두 개가 나란히 보일 것이다.

[revised: 2002-10-01]

프로그램 소스 코드 tt050.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        self.button1 = Button(self.myContainer1)        self.button1["text"]= "Hello, World!"        self.button1["background"] = "green"        self.button1.pack(side=LEFT)	### (1)        self.button2 = Button(self.myContainer1)        self.button2.configure(text="Off to join the circus!")        self.button2.configure(background="tan")        self.button2.pack(side=LEFT)	 ### (2)        self.button3 = Button(self.myContainer1)        self.button3.configure(text="Join me?", background="cyan")        self.button3.pack(side=LEFT)	  ### (3)        self.button4 = Button(self.myContainer1, text="Goodbye!", background="red")        self.button4.pack(side=LEFT)	  ### (4)root = Tk()myapp = MyApp(root)root.mainloop()

사건 묶기

이제 버튼에게 일을 시킬 시간이다. 지난 두 개의 또는 (최초의 네가지) 기본적인 GUI 작업에 관심을 돌려보자 -- 하나는 프로그램에게 실제 작업을 시킬 사건 처리자 루틴을 작성하는 것이고, 다른 하나는 그 사건 처리자 루틴을 창부품과 사건에 묶는 것이다.

다음 프로그램에서는 앞 프로그램에서 만든 버튼을 모두 포기하고 오직 두 개의 버튼만 담긴 단순한 상황으로 돌아갔음에 주목하자: "OK" 버튼과 "Cancel"만 있다.

첫 프로그램을 연구할 때 기억하시겠지만, 기본적인 GUI 작업중의 하나는 "묶기(binding)"이다. "묶기(Binding)"란 다음과 같은 객체들 사이의 관계 또는 연결을 정의하는 과정이다 (보통 다음과 같은데):

  • * 창 부품
  • * 사건 유형과
  • * "사건 처리자"

"사건 처리자(event handler)"는 사건이 일어났을 때 처리하는 메쏘드 또는 서브루틴이다. [자바에서 사건 처리자는 "청취자(listeners)"라고 부르는데, 나는 이게 마음에 든다. 왜냐하면 그것이 무엇을 하는지 정확하게 제시하기 때문이다. -- 사건을 "청취한다" 그리고 그에 반응한다.]

Tkinter에서 이런 묶기를 만드는 방법은 bind() 메쏘드를 이용하는 것인데 이 메쏘드는 모든 Tkinter 창부품에 갖추어져 있다. bind() 메쏘드를 다음과 같은 서술문의 형태로 사용한다:

	widget.bind(event_type_name, event_handler_name)

이런 종류의 묶기를 "사건 묶기(event binding)"라고 부른다.

[사건 처리자를 창 부품에 묶는 다른 방법이 있는데 이를 "명령어 묶기(command binding)"라고 부르며 앞으로 두 프로그램에서 살펴 보겠다. 그러나 지금 당장은 사건 묶기를 살펴보자. 사건 묶기가 무엇인지 이해했으면, 명령어 묶기도 설명하기가 쉽다.]

시작하기 전에 혼란스러운 점 하나를 지적할 필요가 있다. "버튼"이라는 단어는 완전히 다른 두 가지를 지칭하기 위해 사용될 수 있다: (1) 하나는 버튼 창부품이고 -- 컴퓨터 화면에 표시되는 GUI 구성요소이다 -- (2) 또 하나는 마우스의 버튼이다 -- 손가락으로 누르는 버튼을 말함. 혼란을 피하기 위해서 그냥 "버튼"이라고 하기 보다 "버튼 창부품" 또는 "마우스 버튼"이라고 특칭해서 둘을 구별하겠다.

(1) button1 창부품에 일어난 "<Button-1>" 사건(왼쪽 마우스 버튼의 클릭)을 "self.button1Click" 메쏘드에 묶는다. button1에 왼쪽 마우스 버튼 클릭이 일어나면 self.button1Click() 메쏘드가 요청되어 그 사건을 처리한다.

(3) "bind" 연산에 지정되지는 않았지만, 두 개의 인자가 self.button1Click()에 건네지는 것에 주목하자. 물론 첫 인자는 "self"인데, 이는 파이썬의 모든 클래스 메쏘드에 언제나 첫 인자로 건네진다. 두 번째 인자는 사건 객체이다. 이런 식으로 (즉, bind() 메쏘드를 사용하여) 사건을 묶는 테크닉은 언제나 묵시적으로 사건 객체를 인자로 건넨다.

Python/Tkinter에서 사건이 일어나면 사건 객체의 형태를 취한다. 사건 객체는 대단히 유용한데, 그 이유는 모든 종류의 유용한 정보와 메쏘드를 함께 가지고 다니기 때문이다. 사건 객체를 조사하면 어떤 종류의 사건이 일어났는지와 그 사건이 일어난 객체 그리고 기타 유용한 정보들을 알아낼 수 있다.

(4) 그래서, button1이 클릭되면 어떤 일이 일어나기를 원하는가? 이 경우 아주 간단한 일을 시킨다. 그냥 그의 색상을 초록색에서 노란색으로 그리고 다시 원래대로 바꾼다.

(2) button2 ("Goodbye!" 버튼)에는 실제로 좀 그럴 듯한 일을 시켜보자. 그에게 창을 닫도록 시켜 보겠다. 그래서 button2에 일어나는 왼쪽-마우스 클릭을 button2Click() 메쏘드에 묶었고, 그리고

(6) button2Click() 메쏘드에게 myapp의 부모창인 루트 창을 파괴하도록 시켰다. 이렇게 하면 루트 아래의 모든 자식과 자손이 물결치듯이 연달아 파괴된다. 요약하면, GUI의 모든 부분이 소멸된다.

물론 이렇게 하려면 myapp는 자신의 자손이 누구인지 알아야 한다. 그래서 (7) 코드를 그의 구성자에 추가하여 myapp가 그의 부모를 기억하도록 만든다.

프로그램의 행위

다음 프로그램을 실행하면 버튼이 두 개 보인다. "OK" 버튼을 클릭하면 색깔이 바뀐다. "Cancel" 버튼을 클릭하면 어플리케이션이 닫힌다.

GUI가 열리고, 키보드에서 TAB 키를 누르면, 키보드 초점이 두 버튼 사이를 오가는 것이 보일 것이다. 그러나 키보드에서 ENTER/RETURN 키를 눌러봐야 아무 일도 일어나지 않는다. 그 이유는 키보드 사건이 아니라 오직 마우스 클릭만을 버튼에 묶었기 때문이다. 다음으로 키보드 사건을 버튼에도 묶어 보겠다.

마지막으로, 한 버튼의 텍스트가 다른 버튼의 텍스트보다 짧기 때문에, 두 버튼이 크기가 다르다는 것에 주목하자. 이는 약간 보기에 좋지 않다. 다음 프로그램에서 이 문제를 고쳐 보겠다.

[revised: 2002-10-01]

프로그램 소스 코드 tt060.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myParent = parent  ### (7) 부모, 즉 루트를 기억시킨다        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        self.button1 = Button(self.myContainer1)        self.button1.configure(text="OK", background= "green")        self.button1.pack(side=LEFT)        self.button1.bind("<Button-1>", self.button1Click) ### (1)        self.button2 = Button(self.myContainer1)        self.button2.configure(text="Cancel", background="red")        self.button2.pack(side=RIGHT)        self.button2.bind("<Button-1>", self.button2Click) ### (2)    def button1Click(self, event):    ### (3)        if self.button1["background"] == "green": ### (4)            self.button1["background"] = "yellow"        else:            self.button1["background"] = "green"    def button2Click(self, event):  ### (5)        self.myParent.destroy()     ### (6)root = Tk()myapp = MyApp(root)root.mainloop()

초점이란 무엇인가

앞 프로그램에서는 마우스로 클릭하면 버튼에게 일을 시킬 수 있었는데, 키보드에서 키를 눌러서는 일을 시킬 수 없었다. 다음 프로그램에서는 마우스 사건뿐만 아니라 키보드 사건에도 반응시키는 법을 다루어 보겠다.

먼저, "입력 초점(input focus)" 또는 그냥 단순하게 "초점(focus)"이라는 개념이 필요하다.

그리스 신화를 잘 알고 계시다면 (디즈니사의 만화 영화 "허큘레스"를 보셨다면) 아마도 운명의 여신(Fates)을 기억하실 것이다. 운명의 여신은 세명의 여신으로서 인간의 운명을 주관한다. 인간의 운명은 그 여신들의 손아귀에 놓인 실타래였다. 줄을 끊으면, 사망한다.

운명의 여신에 관하여 인상적인 것은 셋 사이에 오직 하나의 눈 만을 공유한다는 것이다. 그 하나의 눈을 가진 여신은 모든 것들을 보고서, 다른 두 여신에게 본 것을 말해 주어야 했다. 만약 그 눈을 서로 주고 받을 수 있었다면, 번갈아서 볼 수 있었을 텐데 말이다. 물론, 그 눈을 훔칠 수 있다면, 다른 여신과 교섭을 할 때 확실한 패를 가진 셈이다.

"초점(Focus)"은 GUI 위의 창부품들에게 키보드 사건을 볼 수 있도록 해준다. 그 한 눈과 운명의 여신들과의 관계는 초점과 GUI 위의 창부품들과의 관계와 같다.

한 순간에 오직 한 창부품만 초점을 가진다. 그리고 "초점을 가진" 창부품만 키보드 사건을 보고 반응할 수 있다. 창 부품에 "초점을 설정하는 일"은 그 초점을 창부품에 부여하는 것이다.

예를 들어, 다음 프로그램에서는 GUI에 버튼이 두 개 있는데: "OK"와 "Cancel"이 그것이다. 키보드에서 RETURN 버튼을 쳤다고 해보자. "Return" 키눌림 사건이 "OK" 버튼에 보여지고 (즉 전송되고), 사용자가 선택을 승인했음을 알려줄 것인가? 그렇지 않으면 "Return" 사건이"Cancel" 버튼에 보여지고 (즉 전송되고), 사용자가 처리를 취소했음을 보여줄 것인가? 그것은 "초점"이 어디에 있는가에 달려있다. 다시 말해, 어느 버튼에 "초점"이 있는가에 달려있다.

한 여신에서 다른 여신에게로 건네지는, 운명의 여신의 눈처럼 초점은 한 GUI 창부품에서 다른 창부품으로 건네질 수 있다. 초점을 한 창부품에서 다른 창부품으로 이동시키거나 건네는 방법은 여러가지가 있다. 한가지 방법은 마우스로 이동시키는 것이다. 한 창부품에 "초점을 설정하려면" 그 위젯에 왼쪽 마우스 버튼을 클릭하면 된다. (최소한 "click to type" 모델이라고 불리우는 이 모델은 윈도우즈와 매킨토시 Tkinter에서 작동하는 방식이다. "focus follows mouse" 관례를 사용하는 시스템도 있는데 이 시스템에서는 마우스 아래에 있기만 하면 그 창부품은 자동으로 초점을 가지며, 클릭할 필요가 없다. Tk에서 같은 효과를 얻으려면 tk_focusFollowsMouse 프로시저를 사용하면 된다.)

초점을 설정하는 또다른 방법은 키보드로 하는 것이다. 초점을 받을 수 있는 창부품들은 만들어진 순서대로 ("순회 순서") 모두 환형 리스트 에 저장된다. 키보드에서 TAB 키를 치면 초점이 현재 위치(위치가 없을 수도 있음)에서 리스트에 있는 다른 창부품으로 이동한다. 리스트의 끝에서, 초점은 다시 처음으로 이동한다. SHIFT+TAB을 치면 초점은 앞으로가 아니라 반대로 이동한다.

GUI 버튼이 초점을 가지면, 초점을 가졌다고 점선 상자가 그 버튼의 텍스트 둘레에 보인다. 다음은 그것을 보는 방법이다. 앞의 프로그램을 실행하자. 프로그램이 시작되면, GUI가 나타나고, 두 버튼 어느 곳에도 초점이 없으므로, 점선 상자가 보이지 않는다. 이제 TAB 키를 쳐 보자. 점선 상자가 왼쪽 버튼 둘레에 보일 것이다. 초점이 주어졌다는 뜻으로 말이다. 이제 TAB 키를 치고 또 쳐보자. 초점이 다른 버튼으로 어떻게 이동하는지 보일 것이다. 마지막 버튼에 도달하면, 다시 처음 버튼으로 빙 둘러서 되돌아 온다. (프로그램에서는 오직 버튼이 두 개만 보이므로, 그 효과는 초점이 두 버튼 사이를 왔다갔다 점프하는 것이 전부이다.)

(0) 다음 프로그램에서는 OK 버튼에게 처음부터 초점을 부여할 생각이다. 그래서 "focus_force()" 메쏘드를 사용하는데, 이 메쏘드는 초점이 OK 버튼에 가도록 만든다. 다음 프로그램을 실행하면 OK 버튼에 시작할 때부터 초점이 주어진 것을 보실 것이다.

지난 프로그램에서는 버튼이 키보드 사건에만 반응했다 -- TAB 키 누름 -- 이로서 초점이 두 버튼 사이를 왔다갔다 이동했다. 그러나 키보드에서 ENTER/RETURN 키를 쳐보면 아무일도 일어나지 않는다. 그것은 키보드 사건이 아니라 오직 마우스 클릭만을 버튼에 묶었기 때문이다.

다음 프로그램에서는 키보드 사건을 버튼에 묶어 보겠다.

(1) (2) 키보드 사건을 버튼에 묶는 서술문은 아주 간단하다 -- 마우스 사건을 묶는 서술문과 형태가 똑같다. 유일한 차이점은 사건의 이름이 키보드 사건의 이름이라는 것이다 (이 경우에는 "<Return>").

키보드에서 RETURN 키를 누르거나 왼쪽 버튼을 클릭해도 똑 같은 효과를 얻고 싶다. 그래서 같은 사건 처리자를 두 가지 다른 종류의 사건에 묶었다.

다음 프로그램은 여러 유형의 사건을 (버튼 같은) 하나의 창부품에 묶을 수 있음을 보여준다. 또 여러 <창부품, 사건>을 조합하여 같은 사건 처리자에 묶을 수 있다.

(3) (4) 버튼 위젯은 여러 종류의 사건에 응답하며, 이제 사건 객체로부터 정보를 열람하는 법을 보여줄 수 있다. 사건 객체들을 (5) "report_event" 루틴에 건네겠다. 이 루틴은 (6) 그 사건 객체의 속성으로부터 얻은 정보를 인쇄해 줄 것이다.

이 정보를 콘솔에 인쇄하려면 이 프로그램을 콘솔 창에서 (pythonw가 아니라) python으로 실행해야 함을 주의하자.

프로그램의 행위

이 프로그램을 실행하면, 버튼이 두 개가 보인다. 왼쪽 버튼을 클릭하거나, 또는 왼쪽 버튼에 키보드 초점이 있을 때 RETURN 키를 누르면 색깔이 바뀐다. 오른쪽 버튼을 클릭하거나, 오른쪽 버튼이 키보드 초점을 가지고 있을 때 RETURN 키를 누르면 어플리케이션이 종료된다. 키보드 사건이나 마우스 사건 어느 것이라도 발생한 시간과 그 사건을 설명해주는 메시지가 인쇄된다.

[Revised: 2002-09-26]

프로그램 소스 코드 tt070.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myParent = parent        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        self.button1 = Button(self.myContainer1)        self.button1.configure(text="OK", background= "green")        self.button1.pack(side=LEFT)        self.button1.focus_force()         ### (0)        self.button1.bind("<Button-1>", self.button1Click)        self.button1.bind("<Return>", self.button1Click)  ### (1)        self.button2 = Button(self.myContainer1)        self.button2.configure(text="Cancel", background="red")        self.button2.pack(side=RIGHT)        self.button2.bind("<Button-1>", self.button2Click)        self.button2.bind("<Return>", self.button2Click)  ### (2)    def button1Click(self, event):        report_event(event)        ### (3)        if self.button1["background"] == "green":            self.button1["background"] = "yellow"        else:            self.button1["background"] = "green"    def button2Click(self, event):        report_event(event)   ### (4)        self.myParent.destroy()def report_event(event):     ### (5)    """그의 속성에 기반하여, 사건의 설명을 인쇄한다.    """    event_name = {"2": "KeyPress", "4": "ButtonPress"}    print "Time:", str(event.time)   ### (6)    print "EventType=" + str(event.type), \        event_name[str(event.type)],\        "EventWidgetId=" + str(event.widget), \        "EventKeySymbol=" + str(event.keysym)root = Tk()myapp = MyApp(root)root.mainloop()

명령어 묶기

앞의 프로그램에서 "사건 묶기(event binding)"를 소개했다. 사건 처리자를 창 부품에 묶는 방법이 한 가지 더 있다. 이른바 "명령어 묶기(command binding)"라는 것으로서 다음 프로그램에서 살펴보자.

명령어 묶기(Command Binding)

기억하시겠지만 지난 프로그램에서는 "<Button-1>" 마우스 사건을 버튼 창부품에 묶었다. "버튼"은 "ButtonPress" 마우스 사건에 대한 또다른 이름이다. 그리고 "ButtonPress" 마우스 사건은 "ButtonRelease" 마우스 사건과 다르다. "ButtonPress" 사건은 마우스 버튼을 누르는 행위지, 그 버튼에서 손을 떼는 행위가 아니다. "ButtonRelease" 사건은 마우스 버튼에서 손을 떼는 행위이다 -- 버튼이 다시 올라 온다.

마우스 ButtonPress와 마우스 ButtonRelease를 구별할 필요가 있는데 "끌어 떨구기(drag and drop)"같은 특징을 지원하기 위해서이다. ButtonPress를 GUI 구성요소에 두고, 그 구성요소를 다른 곳으로 끌고 간 다음, 마우스 버튼을 놓아서 새로운 위치에 "떨구어" 놓는다.

그러나 버튼 창부품은 끌어 떨구기가 되는 종류의 것이 아니다. 버튼을 끌어 떨굴 수 있다고 생각한다면, 버튼 창부품에 ButtonPress를 하고, 마우스 포인터를 화면 위 다른 곳에 끌고 와서, 마우스 버튼을 놓으면 된다. 이는 우리가 버튼 창부품이 눌렸다고 "push" (또는 -- 기술적 용어로 -- "요청(invocation)")으로 간주하는 종류의 행위가 전혀 아니다. 버튼 창부품이 눌렸다고 간주하려면, 사용자가 버튼 창부품에 ButtonPress를 행하고, -- 마우스 포인터를 그 창부품으로부터 전혀 옮기지 않고 -- ButtonRelease가 행해져야 한다. *그것이* 바로 버튼 눌림이라고 간주될 것이다.

이는 앞의 프로그램에서 사용했던 것보다 좀 더 복잡하게 버튼 요청을 표기하는 법이다. 앞에서는 그냥 단순하게 사건 묶기를 사용하여 "Button-1" 사건을 버튼 창부품에 묶었다.

다행스럽게도 이렇게 좀 더 복잡하게 창부품 요청을 표기하는 법을 지원하는 또다른 형태의 묶기가 있다. 이른바 "명령어 묶기(command binding)"라고 부르는데 창부품의 "command" 옵션을 사용하기 때문이다.

다음 프로그램에서는 주석이 달린 줄들을 잘 보시고 (1) 그리고 (2) 어떻게 명령어 묶기가 이루어지는지 살펴보자. "command" 옵션을 사용하여 button1을 "self.button1Click" 사건 처리자에 묶었고, button2를 "self.button2Click" 사건 처리자에 묶었다.

(3) (4)

사건 처리자의 정의를 살펴보자. 주목할 것은 -- 앞 프로그램의 사건 처리자들과는 다르게 -- 사건 객체를 인자로 예상하지 않는다는 것이다. 그것은 사건 묶기와는 다르게 명령어 묶기가 자동으로 사건 객체를 인자로 건네지 않기 때문이다. 물론, 이것은 의미가 있다. 명령어 묶기는 하나의 사건을 처리자에 묶지 않고 여러 사건을 한 처리자에 묶는다. 예를 들면, 버튼 창부품에 대하여 ButtonPress 다음에 ButtonRelease가 따르는 패턴을 처리자에게 묶었을 때, 사건을 그의 사건 처리자에게 건넨다면, 어느 사건이 건네질 것인가: ButtonPress인가 아니면 ButtonRelease인가? 어느 쪽도 정답이 아니다. 이는 명령어 묶기가 사건 묶기와는 다르게 사건 객체를 건네지 않기 때문이다.

이 차이점을 다음 프로그램에서 좀 자세하게 살펴보겠다. 그러나 지금은 프로그램을 실행해보자.

프로그램의 행위

다음 프로그램을 실행하면, 앞의 프로그램에서와 버튼은 모습이 똑 같이 보이겠지만, 행위는 다르다.

버튼 하나에 마우스 ButtonPress을 실행하고 그 행위를 비교해보자. 예를 들어, 화면 위에 마우스 포인터를 이동시켜서 "OK" 버튼 창부품이 위치한 곳으로 가보자. 다음 왼쪽 마우스 버튼을 눌러보자. 절대로 마우스 버튼을 놓으면 안된다.

앞의 예제에서 이렇게 하면, button1Click 처리자가 즉시 실행되고 메시지가 인쇄되었다. 그러나 이 프로그램에서는 아무 일도 일어나지 않는다... 마우스 버튼을 놓기 전까지는 말이다. 마우스 버튼을 놓으면, 메시지가 인쇄된다.

차이점이 또 있다. 스페이스바, 그리고 RETURN 키를 눌러서 행위를 비교해 보자. 예를 들어, TAB 키를 사용하여 초점을 "OK" 버튼에 둔 다음, 스페이스바나 RETURN 키를 눌러보자.

("OK" 버튼이 "Return" 키눌림 사건에 묶여 있는) 앞의 프로그램에서는 스페이스바를 누르면 아무 효과도 없지만 RETURN 키를 누르면 색깔이 바뀌었다. 다음 프로그램에서는 반면에 그 행위가 정반대이다 -- 스페이스바를 누르면 버튼이 색깔을 바꾸지만, RETURN 키를 누르면 아무 효과도 없다.

다음 프로그램에서 이런 행위들을 살펴보겠다.

[revised: 2002-10-01]

프로그램 소스 코드 tt070.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myParent = parent        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        self.button1 = Button(self.myContainer1, command=self.button1Click) ### (1)        self.button1.configure(text="OK", background= "green")        self.button1.pack(side=LEFT)        self.button1.focus_force()        self.button2 = Button(self.myContainer1, command=self.button2Click)  ### (2)        self.button2.configure(text="Cancel", background="red")        self.button2.pack(side=RIGHT)    def button1Click(self):  ### (3)        print "button1Click event handler"        if self.button1["background"] == "green":            self.button1["background"] = "yellow"        else:            self.button1["background"] = "green"    def button2Click(self): ### (4)        print "button2Click event handler"        self.myParent.destroy()root = Tk()myapp = MyApp(root)root.mainloop()

명령어 묶기와 사건 묶기의 차이

앞의 프로그램에서는 "명령어 묶기(command binding)"를 소개하고 사건 묶기와 명령어 묶기 사이의 차이점을 지적하였다. 다음 프로그램에서는 이런 차이점들을 좀 더 상세하게 탐험해보자.

"command"는 어떤 사건에 묶이는가?

앞 프로그램에서는 TAB 키를 사용하여 초점을 "OK" 버튼에 두면, 스페이스바를 눌러서 버튼이 색깔을 바꾸도록 할 수는 있지만, RETURN 키를 누르면 아무 효과도 없었다.

그 이유는 버튼 창부품에 대하여 "command" 옵션에 마우스-사건 인지 뿐만 아니라 키보드-사건 인지가 제공되기 때문이다. 창 부품이 기다리는 키보드 사건은 RETURN 키가 아니라 스페이스바이다. 그래서, 명령어 묶기로, 스페이스바를 누르면 OK 버튼이 색깔을 바꾸지만, RETURN 키는 아무 효과가 없다.

이 행위는 (적어도 윈도우즈가 배경인 나에게는) 이상해 보인다. 그래서 여기에서 교훈은 명령어 묶기를 사용할 생각이라면 정확하게 무엇에 묶고 싶은지 잘 이해하는 것이 좋다는 것이다. 다시 말해, 무슨 키보드/마우스 사건이 명령어를 호출하는지 정확하게 이해하는 것이 좋은 생각이다.

불행하게도 이 정보의 믿을 만한 근원지는 Tk 소스 코드 그 자체이다. 좀 더 정보가 필요하다면 Tk (브렌 웰치(Brent Welch)의 "Practical Programming in Tcl and Tk"가 특별히 좋다) 또는 Tkinter에 관한 책을 보자. Tk 문서는 산만하지만, 온라인으로 볼 수 있다. Tcl의 8.4 버전에 대한 온라인 문서는 다음에서 볼 수 있다:

      http://www.tcl.tk/man/tcl8.4/TkCmd/contents.htm

또 모든 창부품이 "command" 옵션을 제공하는 것은 아니라는 것을 알아야 한다. 다양한 버튼 창부품들 (라디오버튼이나 체크 버튼 등등)은 모두 제공된다. 다른 것들은 비슷한 옵션 (예를 들어 scrollcommand)을 제공하기도 한다. 그러나 실제로 각각의 창부품을 조사해야만 명령어 묶기가 지원되는지 알 수 있다. 그러나 사용할 창부품에 대하여 "command" 옵션을 잘 배워두도록 하자. GUI의 행위가 개선되고, 코더로서의 여러분의 삶이 더 편안해질 것이다.

사건 묶기와 명령어 묶기를 함께 사용하기

지난 프로그램에서 명령어 묶기가 사건 묶기와는 다르게 자동으로 사건 객체를 인자로 건네지 않는다는 것을 알았다. 이 때문에 삶이 조금 더 고단해질 수 있다. 사건 묶기와 명령어 묶기를 *모두* 사용하여 사건 처리자를 창부품에 묶고 싶다면 말이다.

예를 들어, 다음 프로그램에서 버튼이 스페이스바 뿐만 아니라 RETURN 키를 눌러도 반응하면 정말 좋겠다. 그러나 그렇게 하려면, <Return> 키보드 사건에 사건 묶기를 사용해야 한다. 앞의 프로그램에서 했던 것처럼 말이다. (1)

문제는 명령어 묶기가 사건 객체를 인자로 건네지 않는다는 것이다. 그러나 사건 묶기는 건네준다. 그래서 어떻게 사건 처리자를 작성해야 하는가?

이 문제에는 여러가지 해결책이 있다. 그러나 가장 간단한 방법은 두 개의 이벤트 처리자를 작성하는 것이다. "진짜" 사건 처리자(2)는 명령어 묶기가 사용하는 것이 될 것이다. 명령어 묶기는 사건 객체를 예상하지 않는다.

다른 사건 처리자(3)는 그냥 진짜 사건-처리자에 대한포장자가 될 것이다. 이 포장자는 사건-객체 인자를 예상하지만, 그것을 무시하고 진짜 사건-처리자를 호출한다. 포장자에게 진짜 사건-처리자와 똑같은 이름을 부여하지만, 단 뒤에 "_a" 접미사를 덧붙였다.

프로그램의 행위

이 프로그램을 실행하면, 그 행위는 앞의 프로그램과 똑 같다. 단 이제 버튼이 스페이스바 뿐만 아니라 RETURN 키에도 반응한다는 점만 빼고 말이다.

[revised: 2002-10-01]

프로그램 소스 코드 tt075.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myParent = parent        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        self.button1 = Button(self.myContainer1, command=self.button1Click)        self.button1.bind("<Return>", self.button1Click_a)    ### (1)        self.button1.configure(text="OK", background= "green")        self.button1.pack(side=LEFT)        self.button1.focus_force()        self.button2 = Button(self.myContainer1, command=self.button2Click)        self.button2.bind("<Return>", self.button2Click_a)    ### (1)        self.button2.configure(text="Cancel", background="red")        self.button2.pack(side=RIGHT)    def button1Click(self):  ### (2)        print "button1Click event handler"        if self.button1["background"] == "green":            self.button1["background"] = "yellow"        else:            self.button1["background"] = "green"    def button2Click(self): ### (2)        print "button2Click event handler"        self.myParent.destroy()    def button1Click_a(self, event):  ### (3)        print "button1Click_a event handler (a wrapper)"        self.button1Click()    def button2Click_a(self, event):  ### (3)        print "button2Click_a event handler (a wrapper)"        self.button2Click()root = Tk()myapp = MyApp(root)root.mainloop()

정보 공유하기

지난 프로그램에서는 사건 처리자에게 실제로 일을 시키는 방법들을 알아보았다.

다음 프로그램에서는 사건 처리자 사이에 정보를 공유하는 법에 관하여 잠깐 살펴보자.

사건-처리자 함수 사이에 정보 공유하기

사건 처리자에게 어떤 일을 시키고 그 결과를 다른 사건 처리자와 공유하고 싶은 다양한 상황이 있다.

일반적인 패턴은 어플리케이션에 두 세트의 창부품이 있다는 것이다. 한 세트의 창부품은 일정한 정보를 만들고 선택하며 다른 세트의 창부품은 그 정보를 가지고 일을 한다.

예를 들어, 한 창부품에서 사용자는 파일 리스트로부터 파일을 선택하고, 다른 창부품들은 고른 그 파일에 대하여 다양한 연산을 할 수 있다 -- 파일을 열고 삭제하거나 복사하고 이름 바꾸기 등등.

또는 한 세트의 창부품은 어플리케이션에 다양한 환경 구성을 설정하고, 또다른 세트는 (SAVE와 CANCEL 옵션을 제공하는 버튼들) 디스크에 그런 설정을 저장하거나 또는 저장하지 않고 버릴 수 있다.

또는 한 세트의 창부품은 실행하고자 하는 프로그램에 대하여 매개변수들을 설정하고 또다른 창부품은 (RUN이나 EXECUTE 같은 이름을 가진 버튼들) 그런 매개변수를 가지고 프로그램을 시작시킬 수 있다.

또는 나중에 같은 함수를 호출할 때 정보를 건네기 위하여 사건-처리자 함수를 요청할 필요가 있을 수 있다. 그냥 두 가지 다른 값으로 변수를 이리 저리 바꾸는 사건 처리자를 생각해 보자. 변수에 새로 값을 할당하려면, 사건 처리자는 지난 번 실행될 때 그 변수에 어떤 값이 할당되었는지 알아야 한다.

문제점

여기에서 문제는 각 사건 처리자가 별도의 함수라는 것이다. 각 사건 처리자는 자신만의 지역 변수가 있고 이 변수들은 다른 사건-처리자 함수와 공유하지 않으며, 심지어 나중에 호출되는 자신과도 공유하지 않는다. 그래서 문제는 이것이다 -- 자신의 지역 변수를 공유할 수 없다면, 어떻게 사건-처리자 함수가 다른 처리자와 데이터를 공유할 수 있는가?

물론 해결책은 공유될 변수들이 그 사건 처리자 함수에 지역적이지 않으면 된다. 즉, 그 변수들을 사건 처리자 함수의 *바깥에* 저장해야 한다.

해결책 1 -- 전역 변수를 사용하기

공유를 위한 한가지 테크닉은 (공유하고자 하는) 변수들을 전역적으로 만드는 것이다. 예를 들면, 각 처리자에서 myVariable1와 myVariable2를 바꾸거나 볼 필요가 있다면, 다음과 같은 서술문을 배치하면 된다:

		global myVariable1, myVariable2

그러나 전역 변수를 사용하는 것은 잠재적으로 위험하다. 그리고 일반적으로 지저분한 프로그래밍이라고 눈총을 받는다.

해결책 2 -- 실체 변수를 사용하기

좀 더 깔끔한 테크닉은 "실체 변수" (즉, "self." 변수)를 사용하여 사건 처리자 사이에 공유할 정보를 유지하는 것이다. 이렇게 하려면 물론 어플리케이션이 그냥 인-라인 코드가 아니라 클래스로 구현되어야 한다.

이것이 바로 (앞서 이 시리즈 글에서) 어플리케이션을 클래스로 바꾼 까닭이다. 앞에서 이미 해 두었기 때문에, 이 시점에서 우리의 어플리케이션은 이미 실체 변수를 사용할 기반구조를 갖추고 있다.

다음 프로그램에서는 아주 단순한 정보를 기억하고 공유해 보겠다: 요청된 마지막 버튼의 이름을 말이다. 그 정보를 "self.myLastButtonInvoked"라고 부르는 한 실체 변수에 저장한다. [### 1 주석 참조]

그리고 실제로 이 정보를 기억하고 있는지 보여주기 위해, 버튼 처리자가 호출될 때마다, 다음 정보를 인쇄한다. [### 2 주석 참조]

프로그램의 행위

다음 프로그램은 버튼을 세 개 보여준다. 이 프로그램을 실행하고, 버튼을 아무거나 클릭하면, 그 이름과 클릭되었던 앞의 버튼 이름이 화면에 나타난다.

어떤 버튼도 어플리케이션을 닫지 않음에 주목하자. 그래서 닫을 생각이라면 CLOSE 창부품(제목 막대 오른쪽 상자 안의 "X"아이콘)을 눌러야 된다).

[revised: 2002-10-05]

프로그램 소스 코드 tt076.py

from Tkinter import *class MyApp:    def __init__(self, parent):        ### 1 -- 시작시에, 아직 어떤 버튼 처리자도 요청하지 않았다.        self.myLastButtonInvoked = None        self.myParent = parent        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        self.yellowButton = Button(self.myContainer1, command=self.yellowButtonClick)        self.yellowButton.configure(text="YELLOW", background="yellow")        self.yellowButton.pack(side=LEFT)        self.redButton = Button(self.myContainer1, command=self.redButtonClick)        self.redButton.configure(text="RED", background= "red")        self.redButton.pack(side=LEFT)        self.whiteButton = Button(self.myContainer1, command=self.whiteButtonClick)        self.whiteButton.configure(text="WHITE", background="white")        self.whiteButton.pack(side=LEFT)    def redButtonClick(self):        print "RED    button clicked.  Previous button invoked was", self.myLastButtonInvoked  ### 2        self.myLastButtonInvoked = "RED"  ### 1    def yellowButtonClick(self):        print "YELLOW button clicked.  Previous button invoked was", self.myLastButtonInvoked ### 2        self.myLastButtonInvoked = "YELLOW" ### 1    def whiteButtonClick(self):        print "WHITE  button clicked.  Previous button invoked was", self.myLastButtonInvoked ### 2        self.myLastButtonInvoked = "WHITE" ### 1print "\n"*100 # 화면을 정리하는 간단한 방법print "시작..."root = Tk()myapp = MyApp(root)root.mainloop()print "... 완료!"

명령어 묶기를 더 자세히

다음 프로그램에서 탐험해 볼 것은 ...

명령어 묶기의 고급 특징을 더 자세히

tt075.py 프로그램에서는 "command" 옵션을 사용하여 사건 처리자를 창부품에 묶었다. 예를 들어 그 프로그램에서 다음 서술문은

    self.button1 = Button(self.myContainer1, command=self.button1Click)

button1Click 함수를 button1 창부품에 묶는다.

그리고 사건 묶기를 사용하여 버튼을 <Return> 키보드 사건에 묶었다.

    self.button1.bind("", self.button1Click_a)

앞의 프로그램에서는 사건 처리자들이 두 개의 버튼에 대하여 완전히 다른 기능을 수행했다.

그러나 상황이 다르다고 가정해 보자. 버튼이 여러개이고, 그 모든 버튼은 본질적으로 *같은* 유형의 행위를 촉발시켜야 된다고 가정해 보자. 그런 상황을 처리하는 가장 좋은 방법은 모든 버튼에 대하여 사건들을 단 하나의 사건 처리자에 묶는 것이다. 각 버튼은 같은 처리자 루틴을 호출한다. 그러나, 서로 다른 인자들을 건네어 무엇을 할지 지시한다.

그것이 바로 다음 프로그램에서 하고자 하는 것이다.

명령어 묶기

다음 프로그램에서는 보시다시피 두 개의 버튼이 있으며, "command" 옵션을 사용하여 그 버튼 모두를 같은 사건 처리자에 묶는다 -- 즉, "buttonHandler" 루틴에 말이다. 그 buttonHandler 루틴에 인자를 세 개 건넨다: (button_name 변수에) 버튼의 이름과 숫자 하나 그리고 문자열을 건넨다.

    self.button1 = Button(self.myContainer1,    	command=self.buttonHandler(button_name, 1, "Good stuff!")    	)

무거운 어플리케이션이라면 buttonHandler 루틴은 물론 무거운 일을 하겠지만, 이 프로그램에서는 그냥 받은 인자들을 인쇄한다.

사건 묶기

명령어 묶기는 이 정보로 충분하다. 사건 묶기는 어떤가?

아마도 <Return> 사건에 사건 묶기를 하는 두 줄에 주석 처리를 한 것을 눈치채셨을 것이다.

  # self.button1.bind("", self.buttonHandler_a(event, button_name, 1, "Good stuff!"))

다음은 문제의 첫 징조이다. 사건 묶기는 자동으로 사건 인자를 건넨다. -- 그러나 그 사건 인자를 인자 리스트에 포함시키는 방법은 쉽지 않다.

나중에 이 문제로 다시 돌아 와야 할 것이다. 지금은 그냥 프로그램을 실행하고 무슨 일어 나는지 지켜보자.

프로그램의 행위

코드를 살펴보면, 다음 프로그램은 아주 그럴듯 해 보인다. 그러나 실행해 보면 제대로 일을 하지 않는다. buttonHandler 루틴이 GUI가 화면에 표시되기도 전에 요청된다. 실제로, 두 번이나 호출된다!

아무 버튼이나 왼쪽-마우스-클릭을 하면, 아무 일도 일어나지 않는다 -- "eventHandler" 루틴이 호출되지 *않는다*.

이 프로그램을 닫는 유일한 방법은 제목 막대 오른쪽에 있는 (상자 안의 "X") "close" 아이콘을 누르는 것이다.

그래서 이제 프로그램을 실행하고 무슨 일이 일어나는지 지켜보자. 그러면 다음 프로그램에서는 왜 그런 일이 일어나는지 보실 수 있다.

[revised: 2003-02-23]

프로그램 소스 코드 tt077.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myParent = parent        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        button_name = "OK"        self.button1 = Button(self.myContainer1,            command=self.buttonHandler(button_name, 1, "Good stuff!"))        # self.button1.bind("<Return>", self.buttonHandler_a(event, button_name, 1, "Good stuff!"))        self.button1.configure(text=button_name, background="green")        self.button1.pack(side=LEFT)        self.button1.focus_force()  # Put keyboard focus on button1        button_name = "Cancel"        self.button2 = Button(self.myContainer1,            command=self.buttonHandler(button_name, 2, "Bad  stuff!"))        # self.button2.bind("<Return>", self.buttonHandler_a(event, button_name, 2, "Bad  stuff!"))        self.button2.configure(text=button_name, background="red")        self.button2.pack(side=LEFT)    def buttonHandler(self, arg1, arg2, arg3):        print "    buttonHandler routine received arguments:", arg1.ljust(8), arg2, arg3 	def buttonHandler_a(self, event, arg1, arg2, arg3):        print "buttonHandler_a received event", event        self.buttonHandler(arg1, arg2, arg3)print "\n"*100 # 화면을 정리하라print "tt077 프로그램 실행시작."root = Tk()myapp = MyApp(root)print "사건 회돌이를 시작할 준비."root.mainloop()print "사건 회돌이 실행 완료."

역호출 함수

지난 프로그램에서 실행의 흐름을 살펴보면, 이런 의문이 제기된다: "도대체 무슨 일이 일어나고 있는거야??!! "buttonHandler" 루틴이 버튼마다 실행되잖아, 심지어는 사건 회돌이가 시작되기도 전에 말이야!!"

그 이유는 다음과 같은 서술문에서

  self.button1 = Button(self.myContainer1,       command = self.buttonHandler(button_name, 1, "Good stuff!"))

역호출 함수로 사용할 것이 무엇인지 요구하지 않고, buttonHandler 함수를 호출하고 있기 때문이다. 의도한 바는 아니지만, 실제로 그렇게 하고 있는 것이다.

주목할 것

  • * buttonHandler는 함수 객체이다. 그리고 역호출 묶기로 사용 가능하다.
  • * buttonHandler() (반괄호에 주의)는 반면에 "buttonHandler" 함수를 실제로 호출하는 것이다.

다음과 같은 서술문이 실행되면

  self.button1 = Button(self.myContainer1,       command = self.buttonHandler(button_name, 1, "Good stuff!"))

실제로 "buttonHandler" 루틴이 호출된다. buttonHandler 루틴이 실행되어, 메시지가 인쇄되고, 호출의 결과가 반환된다 (이 경우에는 None 객체임). 그러면 버튼의 "command" 옵션은 그 호출의 결과에 묶인다. 간단히 말해, 그 명령어가 "None" 객체에 묶인다. 그 때문에 버튼을 클릭하더라도, 아무일도 일어나지 않는 것이다.

해결책은 있는가?

그래서... 해결책은 무엇인가? 사건-처리자 함수를 매개변수로 재사용할 방법이 있는가?

물론 있다. 일반적으로 알려진 테크닉은 두 가지이다. 하나는 파이썬에 내장된 "람다(lambda)" 함수를 사용하는 것이다. 다른 하나는 "함수내포기법(currying)"이라고 부른다.

다음 프로그램에서는 람다로 작업하는 법을 살펴보겠다. 그리고 그 다음 프로그램에서 함수내포기법(currying- 역주: 하스켈 카레에서 따옴)을 살펴보겠다.

람다와 함수내포기법이 어떻게 작동하는지 설명하지 않는다 -- 너무 복잡하고 우리의 목표와 너무 동떨어져 있다. 우리의 목표는 Tkinter 프로그램을 작동시키는 것이다. 그래서 그냥 그것들을 블랙 박스로 취급하고자 한다. 그 작동 방식에 관해서는 언급하지 않는다 -- 오직 그것들과의 작동 방식에 관해서만 다루겠다.

그래서 람다를 살펴보자.

명령어 묶기(Command Binding)

처음에, 다음 서술문은 작동할 것이라고 생각했지만:

  self.button1 = Button(self.myContainer1,  	command = self.buttonHandler(button_name, 1, "Good stuff!")  	)

... 알고보니 생각대로 작동하지 않았다.

원하는 방식대로 하려면 서술문들을 다음과 같이 재작성해야 한다:

  self.button1 = Button(self.myContainer1,       command = lambda       arg1=button_name, arg2=1, arg3="Good stuff!" :       self.buttonHandler(arg1, arg2, arg3)       )

사건 묶기

람다는 사건 묶기를 매개변수로도 만들어주니 즐겁다. 다음과 같이 하는 대신에:

     self.button1.bind("",     	self.buttonHandler_a(event, button_name, 1, "Good stuff!"))

(이는 작동하지 않는데, 사건 인자를 인자 리스트에 포함시킬 방법이 없기 때문이다), 람다를 사용하여 다음과 같이 작성할 수 있다:

		# 사건 묶기 -- 사건을 인자로 건넨다		self.button1.bind("",			lambda			event, arg1=button_name, arg2=1, arg3="Good stuff!" :			self.buttonHandler_a(event, arg1, arg2, arg3)			)

[여기에서 "event"는 변수 이름이 아니다 -- 파이썬의 키워드나 기타 어떤 것도 아니다. 이 예제에서는 사건 인자에 대하여"event"라는 이름을 사용한다. 그러나 이 테크닉을 연구하면서 "e"라는 이름은 사건 인자에 사용하고, 원한다면 그냥 쉽게 "event_arg"로 불러도 좋다.]

람다를 사용하면 얻는 멋진 특징중 하나는 (원한다면) 그냥 사건 인자를 건네지 않아도 된다는 것이다. 사건 인자를 건네지 않으면, self.buttonHandler 함수를 간접적으로 self.buttonHandler_a 함수를 통하여 호출하는 대신에 직접 호출할 수 있다.

이 테크닉을 보여주기 위해 button1에 했던 것과는 다르게 button2에 사건 묶기 코드를 작성해 보겠다. 다음은 button2로 하고자 하는 것이다:

		# 사건 묶기 -- 사건을 인자로 건네지 않음		self.button2.bind("",			lambda			event, arg1=button_name, arg2=2, arg3="Bad  stuff!" :			self.buttonHandler(arg1, arg2, arg3)			)

프로그램의 행위

프로그램을 실행하면 원하는 그대로 행위한다.

키보드에서 TAB 키를 눌러서 키보드 초점을 OK 버튼에서 CANCEL 버튼으로 다시 반대로 옮길 수 있다는 것에 주목하자.

특히, 키보드에서 <Return> 키를 눌러서 OK 버튼에 요청해 보아야 한다. OK 버튼을 <Return> 키를 눌러서 요청하면, buttonHandler_a 함수를 거치고, 그로부터 메시지도 받는데, 거기에 건네진 사건에 관한 정보가 인쇄된다.

어떤 경우든, 버튼 창부품을 클릭하든 키보드에서 <Return> 키를 눌러서 창부품에 요청하든, buttonHandler 함수에 건네진 인자들이 멋지게 인쇄된다.

[revised: 2003-02-23]

프로그램 소스 코드 tt078.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myParent = parent        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        #------------------ BUTTON #1 ------------------------------------        button_name = "OK"        # 명령어 묶기        self.button1 = Button(self.myContainer1,            command = lambda            arg1=button_name, arg2=1, arg3="Good stuff!" :            self.buttonHandler(arg1, arg2, arg3)            )        # 사건 묶기 -- 사건을 인자로 건넴        self.button1.bind("<Return>",            lambda            event, arg1=button_name, arg2=1, arg3="Good stuff!" :            self.buttonHandler_a(event, arg1, arg2, arg3)            )        self.button1.configure(text=button_name, background="green")        self.button1.pack(side=LEFT)        self.button1.focus_force()  # Put keyboard focus on button1        #------------------ BUTTON #2 ------------------------------------        button_name = "Cancel"        # 명령어 묶기        self.button2 = Button(self.myContainer1,            command = lambda            arg1=button_name, arg2=2, arg3="Bad  stuff!":            self.buttonHandler(arg1, arg2, arg3)            )        # 사건 묶기 -- 사건을 인자로 건네지 않음        self.button2.bind("<Return>",            lambda            event, arg1=button_name, arg2=2, arg3="Bad  stuff!" :            self.buttonHandler(arg1, arg2, arg3)            )        self.button2.configure(text=button_name, background="red")        self.button2.pack(side=LEFT)    def buttonHandler(self, argument1, argument2, argument3):        print "    buttonHandler routine received arguments:" \            , argument1.ljust(8), argument2, argument3 	def buttonHandler_a(self, event, argument1, argument2, argument3):        print "buttonHandler_a received event", event        self.buttonHandler(argument1, argument2, argument3)print "\n"*100 # 화면 정리print "tt078 프로그램 시작."root = Tk()myapp = MyApp(root)print "사건 회돌이 실행 준비."root.mainloop()print "사건 회돌이 실행 완료."

함수내포기법

앞의 프로그램에서는 인자를 사건-처리자 함수에 건네기 위해 람다에 관련된 테크닉을 살펴보았다. 다음 프로그램에서는 "함수내포기법(currying)"이라고 부르는 다른 테크닉을 살펴보겠다.

함수내포기법(Curry)에 관하여

가장 단순한 의미에서, 함수내포기법은 함수를 사용하여 다른 함수를 구성하는 테크닉이다.

함수내포기법은 기능적 프로그래밍에서 빌려온 테크닉이다. 더 자세히 알고 싶다면, "파이썬 요리책"에서 여러 요리법을 보실 수 있다:

	http://aspn.activestate.com/ASPN/Python/Cookbook/

다음 프로그램에 사용된 curry 클래스는 스코트 데이비드 다니엘스(Scott David Daniels)의 요리법으로서 "curry -- associating parameters with a function"이라는 이름으로 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549에서 보실 수 있다.

람다를 연구하던 때와 같이 어떻게 함수내포기법이 작동하는지 설명하지 않는다. 그냥 카레 클래스를 블랙 박스로 취급하겠다. 작동 방식에 관해서는 자세히 언급하지 않는다 -- 오로지 그것으로 작업하는 법에 관해서만 연구한다.

Curry -- 사용하는 법

카레(그 테크닉)를 사용하는 방법은 "curry" 클래스를 프로그램에 포함시키거나, 파이썬 파일에서 반입하는 것이다. 다음 프로그램에서는 카레 코드를 직접 프로그램에 포함시키겠다.

처음에는 다음 서술문이 self.buttonHandler를 self.button1의 명령어 옵션에 묶을 수 있다고 생각했겠지만, 생각대로 작동하지 않음을 알았다.

  self.button1 = Button(self.myContainer1,       command = self.buttonHandler(button_name, 1, "Good stuff!"))

카레를 사용하여, 다음과 같이 서술문을 다시 작성하면 된다:

  self.button1 = Button(self.myContainer1,       command = curry(self.buttonHandler, button_name, 1, "Good stuff!"))

보시다시피, 코드는 보이는 그대로 이해된다. self.buttonHandler 함수를 호출하는 대신에, 카레 객체 (즉, curry 클래스의 실체)를 만들어서, self.buttonHandler 함수를 그의 첫 인자로 건넨다. 기본적으로 카레 객체는 건넨 함수의 이름을 기억한다. 그래서 (카레 객체가) 호출되면, -- 이번에는 -- 생성될 때 주어진 함수를 호출한다.

사건 묶기

채드 네쩌(Chad Netzer)는 사건 묶기를 매개변수화 하는데 사용할 수 있는, 함수내포기법과 비슷한 테크닉을 고안했다. [이 코딩 테크닉은 파이썬 버전이 2.0 이상이어야 한다는 것에 주의하자.] 여기에는 "event_lambda" 함수가 사용된다.

카레에서와 같이 event_lambda를 사용하려면, 프로그램에 "event_lambda" 함수에 대한 코드를 포함시키든지, 파이썬 파일에서 반입해야 한다. 다음 프로그램에서는 프로그램에 event_lambda 함수 코드를 직접 포함시키겠다.

	# ----------  함수를 위한 코드: event_lambda --------	def event_lambda(f, *args, **kwds ):		"람다를 더 예쁜 인터페이스로 싸 넣은 도움자 함수."		return lambda event, f=f, args=args, kwds=kwds : f( *args, **kwds )

event_lambda 함수를 사용할 수 있으면, 이제 self.buttonHandler를 <Return> 키보드 사건에 묶고, 거기에 몇가지 인자를 건넬 수 있다. 다음은 그 방법이다:

	self.button1.bind("",		event_lambda( self.buttonHandler, button_name, 1, "Good stuff!" ) )

event_lambda의 작동 방식에 대하여 너무나 호기심이 넘친다면, button2에 대한 코드를 보는 것이 좀 더 보기에 쉽다.

button2에 대하여, 두-단계의 처리과정이 있다. 첫째는 event_lambda 함수를 호출하는 것이다.

	event_handler = event_lambda( self.buttonHandler, button_name, 2, "Bad  stuff!" )

event_lambda 함수가 호출되면, 람다를 사용하여 새로운 이름없는 ("익명의") 함수 객체를 만든다.

		lambda event, f=f, args=args, kwds=kwds : f( *args, **kwds )

그 이름 없는 함수 객체는 실제로 우리가 요청하고자 하는 함수( 다음 프로그램에서 "self.buttonHandler"인 "f")와 event_lambda 함수를 호출할 때 지정했던 인자들을 위한 포장자이다.

그러면 event_lambda 함수는 새로운 익명 함수를 돌려준다.

event_lambda가 익명 함수를 돌려주면, 거기에 이름을 붙인다: "event_handler"라고 말이다.

	event_handler = event_lambda( self.buttonHandler, button_name, 2, "Bad  stuff!" )

그 다음, 두 번째 단계에서 <Return> 사건을 "event_handler" 함수에 묶는다.

	self.button2.bind("", event_handler )

익명 함수에 대하여 '사건'은 그저 위치 보유자 인자일 뿐이며 버려져서 사용되지 않는다. 오직 위치 인자들 (args)과 키워드 인자들(kwds)만 버튼-처리자 루틴에 건네진다.

아이고! 머리털이 하얕게 다 세어 버렸어요!!

다음은 꼼수이다. 그러나 머리텉이 하얕게 다 세도록 그 작동 방식을 모조리 이해할 필요가 있다고 생각하지 말자. 사용하기 위해서 어떻게 "curry"와 "event_lambda"가 작동하는지 이해하려고 하지 말자. 그저 그것들을 블랙박스처럼 간주하고... 작동 방식에 신경쓰지 말고 그냥 쓰자.

Lambda 대 Curry 그리고 Event_lambda -- 어느 것을 사용해야 하는가?

자...

  • * curry와 event_lambda를 요청하는 코드가 상대적으로 직관적이며 짧고 간단하다.
    나쁜점은 그것들을 사용하려면 프로그램에 코드를 삽입하거나 반입해야 한다는 것이다.
  • * 대조적으로 람다는 파이썬에 내장되어 있다 -- 반입하기 위해 특별히 해야 할 것이 없다; 그냥 거기에 있을 뿐이다.
    약점은 그것을 사용하는 코드가 길고 약간 혼란스럽다는 것이다.

그래서 선택은 여러분의 몫이다. "자기돈 돈 가지고 자기가 사는데" 누가 뭐라고 할까. 가장 친숙한 것을 사용하자. 그리고 작업에 가장 적당하다고 여겨지는 것을 사용하자.

이 이야기가 주는 진정한 교훈은 다음과 같다...

파이썬은 강력한 언어이다. 파이썬은 수 많은 도구를 제공해 주어서 사건을 처리하는 역호출 함수를 만드는데 사용할 수 있다. "Tkinter로 생각하기"는 기본적인 개념을 소개하는 것이 목표이지, 테크닉의 백과사전이 되고자 하는 것이 아니다. 그래서 여기에서는 그런 방법들 중에 몇가지 만을 탐험해 볼 수 있다. 그러나 파이썬으로 기술이 익어갈 수록 자신감이 더 해지고 좀 더 유연성이 필요해짐에 따라, 파이썬에서 좀 고급의 특징을 사용하다 보면, 꼭 필요한 역호출 함수를 만들 수 있을 것이다.

프로그램의 행위

다음 프로그램을 실행하면, 앞의 프로그램과 정확하게 똑같이 행위한다. 프로그램의 행위를 하나도 바꾸지 않았다. 그냥 프로그램의 코딩 방식만 바뀌었을 뿐이다.

[revised: 2003-02-23]

프로그램 소스 코드 tt079.py

from Tkinter import *# ---------- code for class: curry (begin) ---------------------class curry:    """from Scott David Daniels'recipe    "curry -- associating parameters with a function"    in the "Python Cookbook"    http://aspn.activestate.com/ASPN/Python/Cookbook/    """    def __init__(self, fun, *args, **kwargs):        self.fun = fun        self.pending = args[:]        self.kwargs = kwargs.copy()    def __call__(self, *args, **kwargs):        if kwargs and self.kwargs:            kw = self.kwargs.copy()            kw.update(kwargs)        else:            kw = kwargs or self.kwargs        return self.fun(*(self.pending + args), **kw)# ---------- code for class: curry (end) ---------------------# ---------- code for function: event_lambda (begin) --------def event_lambda(f, *args, **kwds ):    """A helper function that wraps lambda in a prettier interface.    Thanks to Chad Netzer for the code."""    return lambda event, f=f, args=args, kwds=kwds : f( *args, **kwds )# ---------- code for function: event_lambda (end) -----------class MyApp:    def __init__(self, parent):        self.myParent = parent        self.myContainer1 = Frame(parent)        self.myContainer1.pack()        button_name = "OK"        # 명령어 묶기 -- 함수내포기법 사용        self.button1 = Button(self.myContainer1,           command = curry(self.buttonHandler, button_name, 1, "Good stuff!"))        # 사건 묶기 -- event_lambda 도움자 함수 사용        self.button1.bind("<Return>",            event_lambda( self.buttonHandler, button_name, 1, "Good stuff!" ) )        self.button1.configure(text=button_name, background="green")        self.button1.pack(side=LEFT)        self.button1.focus_force()  # Put keyboard focus on button1        button_name = "Cancel"        # 명령어 묶기 -- 함수내포기법 사용        self.button2 = Button(self.myContainer1,            command = curry(self.buttonHandler, button_name, 2, "Bad  stuff!"))        # 사건 묶기 -- event_lambda 도움자 함수를 두 단계로 사용        event_handler = event_lambda( self.buttonHandler, button_name, 2, "Bad  stuff!" )        self.button2.bind("<Return>", event_handler )        self.button2.configure(text=button_name, background="red")        self.button2.pack(side=LEFT)    def buttonHandler(self, argument1, argument2, argument3):        print "    buttonHandler routine received arguments:", \            argument1.ljust(8), argument2, argument3    def buttonHandler_a(self, event, argument1, argument2, argument3):        print "buttonHandler_a received event", event        self.buttonHandler(argument1, argument2, argument3)print "\n"*100 # 화면 정리print "tt079 프로그램 실행 준비."root = Tk()myapp = MyApp(root)print "사건 회돌이 시작 준비."root.mainloop()print "사건 회돌이 실행 완료."

조감 테크닉

지난 몇 프로그램에서, 사건 처리자를 창부품에 묶는 테크닉을 많은 시간을 들여 살펴 보았다.

다음 프로그램에서는 다시 GUI의 겉모습을 만드는 주제로 돌아가겠다 -- 창부품을 설정하고 그이 겉모습과 위치를 제어하는 문제로 되돌아 가보자.

Gui의 조감을 제어하는 세가지 테크닉

GUI의 일반적으로 조감을 통제하는데 세 가지 테크닉이 있다.

  • * 창부품 속성
  • * pack() 옵션
  • * 그릇(틀)의 내포

다음 프로그램에서는 창 속성과 pack() 옵션의 설정을 통하여 겉모습을 통제해보자.

수 많은 버튼과 작업할 생각이다. 그 버튼들을 담고 있는 틀과 함께 말이다. 앞 프로그램에서는 그 틀을 "myContainer1"이라고 불렀다. 여기에서는 그 이름을 약간 설명적인 이름인 "buttons_frame"으로 바꾸겠다.

다음 섹션에서 숫자는 소스 코드에 달린 주석의 번호를 가리킨다.

(1) 먼저, 모든 버튼이 너비가 같도록 하기 위해, "width" 속성에 모두 같은 값을 설정한다. "width" 속성은 Tkinter의 "Button" 창부품에 한정되는 것에 주목하자 -- 모든 창부품이 width 속성을 가지는 것은 아니다. 또 주목할 것은 width 속성이 문자 단위로 지정된다는 것이다 (예를 들어, 픽셀이나 인치 또는 밀리미터 단위가 아니다). 가장 긴 라벨인 ("Cancel")에 문자가 여섯개 담기므로, 버튼들에 대하여 그 너비를 "6"으로 지정했다. (1)

(2) 이제 버튼들에 패딩을 덧댄다. 패딩(Padding)은 텍스트와 버튼의 테두리 사이에 텍스트 주위를 둘러싼 여백이다. 버튼의 "padx" 속성과 "pady" 속성을 지정하면 된다. "padx"는 X-축을 따라 수평적으로 왼쪽에서 오른쪽으로 덧댄다. "pady"는 Y-축을 따라 수직적으로 위에서 아래로 덧댄다.

수평 패딩을 3 밀리리터로 지정하고 (padx="3m") 수직 패딩을 1 밀리미터로 지정하겠다 (pady="1m"). 주목하자. (수치인) "width" 속성과는 다르게, 이 속성들은 따옴표로 둘러 싸인다. 그 때문에 뒤에 접두사로 "m"을 붙여서 패딩 단위를 지정하는 것이다. 그래서 패딩 길이를 숫자보다는 문자열로 지정해야 한다.

(3) 마지막으로, 버튼이 든 그릇(buttons_frame)에 약간 패딩을 덧대어 보겠다. 그릇에는 네개의 패딩 속성을 지정할 수 있다. "padx"와 "pady"는 그 틀 주위 (바깥)을 둘러싼 패딩을 지정한다. "ipadx"와 "ipady"는 ("internal padx" 그리고 "internal pady") 내부의 패딩을 지정한다. 내부 패딩은 그 그릇 안에 든 각 창부품을 둘러싼 패딩이다.

틀에 속성으로 패딩을 지정하지 않고, 꾸림자에게 옵션을 건네고 지정하고 있음에 주목하자. (4). 보시다시피, 패딩은 약간 혼란스럽다. 틀은 내부 패딩이 있지만 버튼 같은 창부품은 내부 패딩이 없다. 어떤 경우에는 패딩이 창부품의 속성이지만, 다른 경우에는 pack()에 대한 옵션으로 지정된다.

프로그램의 행위

다음 프로그램을 실행하면, 두 개의 버튼이 보인다. 그러나 이제는 크기가 같다. 버튼의 옆면이 버튼-텍스트에 꽉 끼지 않는다. 그리고 버튼은 멋지게 테두리가 여백으로 둘러 싸여 있다.

[revised: 2003-02-23

프로그램 소스 코드 tt080.py

from Tkinter import *class MyApp:    def __init__(self, parent):        #------ 조감을 제어하기 위한 상수들 ------        button_width = 6      ### (1)        button_padx = "2m"    ### (2)        button_pady = "1m"    ### (2)        buttons_frame_padx =  "3m"   ### (3)        buttons_frame_pady =  "2m"   ### (3)        buttons_frame_ipadx = "3m"   ### (3)        buttons_frame_ipady = "1m"   ### (3)        # -------------- 상수 끝 ----------------        self.myParent = parent        self.buttons_frame = Frame(parent)        self.buttons_frame.pack(    ### (4)            ipadx=buttons_frame_ipadx,  ### (3)            ipady=buttons_frame_ipady,  ### (3)            padx=buttons_frame_padx,    ### (3)            pady=buttons_frame_pady,    ### (3)            )        self.button1 = Button(self.buttons_frame, command=self.button1Click)        self.button1.configure(text="OK", background= "green")        self.button1.focus_force()        self.button1.configure(            width=button_width,  ### (1)            padx=button_padx,    ### (2)            pady=button_pady     ### (2)            )        self.button1.pack(side=LEFT)        self.button1.bind("<Return>", self.button1Click_a)        self.button2 = Button(self.buttons_frame, command=self.button2Click)        self.button2.configure(text="Cancel", background="red")        self.button2.configure(            width=button_width,  ### (1)            padx=button_padx,    ### (2)            pady=button_pady     ### (2)            )        self.button2.pack(side=RIGHT)        self.button2.bind("<Return>", self.button2Click_a)    def button1Click(self):        if self.button1["background"] == "green":            self.button1["background"] = "yellow"        else:            self.button1["background"] = "green"    def button2Click(self):        self.myParent.destroy()    def button1Click_a(self, event):        self.button1Click()    def button2Click_a(self, event):        self.button2Click()root = Tk()myapp = MyApp(root)root.mainloop()

그릇의 내포

다음 프로그램에서는 그릇(틀)의 내포를 살펴보겠다. 일련의 틀을 만들고 서로 안에 내포시켜 넣어 보겠다: bottom_frame과 left_frame 그리고 big_frame을 말이다.

다음 틀에는 아무 것도 담기지 않는다 -- 어떤 창부품도 없다. 보통 틀은 신축적이기 때문에 줄어들어서 크기가 없다. 그러나 "height" 속성과 "width" 속성을 제공하면 최초 크기를 지정할 수 있다.

프레임 모두에 너비나 높이 아무 것도 지정하고 있지 않다. 예를 들면 myContainer1에 대하여, 아무 것도 지정하지 않았다. 그러나 그의 자손에는 너비와 높이를 제공했으므로, 그 그릇은 늘어나서 자손들의 축적된 높이와 너비에 맞게 적응한다.

나중 프로그램에서는 창부품들을 이런 틀에 배치하는 법을 살펴보겠다. 그러나 이 프로그램에서는 그냥 틀을 만들고 거기에 서로 다르게 크기와 위치 그리고 배경색을 주어보겠다.

나중에 다시 계속해서 나올 세 개의 틀에는 테두리를 툭 튀어 나오게 만들었다: bottom_frame과 left_frame 그리고 right_frame. 다른 틀들은 (예를 들면 top_frame과 buttons_frame에는) 테투리가 주어지지 않는다.

프로그램의 행위

이 프로그램을 실행하면, 서로 다른 틀들이 다른 배경색을 가지고 나타난다.

[revised: 2005-07-15]

프로그램 소스 코드 tt090.py

from Tkinter import *class MyApp:    def __init__(self, parent):        self.myParent = parent        ### Our topmost frame is called myContainer1        self.myContainer1 = Frame(parent) ###        self.myContainer1.pack()        #------ 조감을 제어하는데 필요한 상수들 ------        button_width = 6      ### (1)        button_padx = "2m"    ### (2)        button_pady = "1m"    ### (2)        buttons_frame_padx =  "3m"   ### (3)        buttons_frame_pady =  "2m"   ### (3)        buttons_frame_ipadx = "3m"   ### (3)        buttons_frame_ipady = "1m"   ### (3)        # -------------- 상수 끝 ----------------        ### myContainer1 안에 수직적 (위/아래) 동선을 사용하겠다.        ### myContainer1 안에 먼저 buttons_frame을 만든 다음.        ### top_frame과 bottom_frame을 만든다.        ### 다음이 그 데모 틀이다.        # 버튼 틀        self.buttons_frame = Frame(self.myContainer1) ###        self.buttons_frame.pack(            side=TOP,   ###            ipadx=buttons_frame_ipadx,            ipady=buttons_frame_ipady,            padx=buttons_frame_padx,            pady=buttons_frame_pady,            )        # 상단 틀        self.top_frame = Frame(self.myContainer1)        self.top_frame.pack(side=TOP,            fill=BOTH,            expand=YES,            )  ###        # 하단 틀        self.bottom_frame = Frame(self.myContainer1,            borderwidth=5,  relief=RIDGE,            height=50,            background="white",            ) ###        self.bottom_frame.pack(side=TOP,            fill=BOTH,            expand=YES,            )  ###        ### left_frame와 right_frame이라는 틀을 두 개 더         ### top_frame 안에 배치하겠다.        ### top_frame 안에 수평적 (좌/우) 동선을 사용한다.        # left_frame        self.left_frame = Frame(self.top_frame, background="red",            borderwidth=5,  relief=RIDGE,            height=250,            width=50,            ) ###        self.left_frame.pack(side=LEFT,            fill=BOTH,            expand=YES,            )  ###        ### right_frame        self.right_frame = Frame(self.top_frame, background="tan",            borderwidth=5,  relief=RIDGE,            width=250,            )        self.right_frame.pack(side=RIGHT,            fill=BOTH,            expand=YES,            )  ###        # 이제 버튼들을 buttons_frame에 추가한다        self.button1 = Button(self.buttons_frame, command=self.button1Click)        self.button1.configure(text="OK", background= "green")        self.button1.focus_force()        self.button1.configure(            width=button_width,  ### (1)            padx=button_padx,    ### (2)            pady=button_pady     ### (2)            )        self.button1.pack(side=LEFT)        self.button1.bind("<Return>", self.button1Click_a)        self.button2 = Button(self.buttons_frame, command=self.button2Click)        self.button2.configure(text="Cancel", background="red")        self.button2.configure(            width=button_width,  ### (1)            padx=button_padx,    ### (2)            pady=button_pady     ### (2)            )        self.button2.pack(side=RIGHT)        self.button2.bind("<Return>", self.button2Click_a)    def button1Click(self):        if self.button1["background"] == "green":            self.button1["background"] = "yellow"        else:            self.button1["background"] = "green"    def button2Click(self):        self.myParent.destroy()    def button1Click_a(self, event):        self.button1Click()    def button2Click_a(self, event):        self.button2Click()root = Tk()myapp = MyApp(root)root.mainloop()

꾸림자

창 크기를 조절하면 Tkinter와 작업할 때 곤혹스런 경험을 한다. 이런 상황을 상상해 보자. 반복적으로 개발할 것이라고 생각하고서, 처음에는 조심스럽게 틀의 너비와 높이를 지정한다. 테스트해 보고 작동하는지 확인한다. 그러면 다음 단계로 나아가 그 틀에 버튼을 몇개 붙인다. 다시 테스트해 보지만, 이제는 놀랍게도 Tkinter가 마치 틀에 처음부터 "높이"와 "너비"가 지정되지 않았다는 듯이 행동한다. 그리고 그 틀은 버튼을 짝 달라 붙어 감싼다.

무슨 일이 일어난 걸까 ???!!!

음, 꾸림자의 행위에 일관성이 없다. 또는 다음과 같이 말할 수도 있겠다: 꾸림자의 행위는 수 많은 상황적 요인에 따라 달라진다. 꾸림자는 그릇이 비어 있다면 크기 요청을 존중하지만, 그릇에 창부품이 담기면, 그릇의 신축성이 전면으로 나오게 된다 -- 그릇에 대한 "높이"와 "너비" 설정은 무시되며 그릇의 크기는 가능하면 밀접하게 창부품을 둘러싸도록 조정된다.

실제로는 창부품이 담기면 그릇의 크기를 제어할 수 없다.

제어할 수 있는 것은 전체 루트 창의 최초 크기이며, 이것은 창 관리자의 "geometry" 옵션으로 제어한다.

(1)

다음 프로그램에서는 geometry 옵션을 사용하여 작은 틀을 둘러싼 멋지고 큰 창을 만들어 보겠다.

(2) 다음 프로그램에서 사용하는 "title" 옵션이 창 관리자 메쏘드이기도 하다는 사실에 주목하자. "Title"은 창의 제목 막대에 있는 텍스트를 제어한다.

또 창 관리자 옵션은 선택적으로 앞에 "wm_" 접두사를 두어 지정할 수도 있다는 것에 주목하자. 예를 들어 "wm_geometry"와 "wm_title"과 같이 말이다. 다음 프로그램에서는 단지 두 방식 모두가 가능함을 보여주기 위해 "geometry"와 "wm_title"을 사용한다.

프로그램의 행위

다음 프로그램은 네 개의 창을 연이어서 띄운다.

주목하자. 창을 닫으려면 제목 막대의 오른쪽에 있는 상자 안에서 "X"라는 "close" 창부품을 눌러야 한다.

1 번의 경우, 높이와 너비가 지정되면 틀이 어떻게 보이는지 보여준다 -- 그리고 주목 -- 아무 창부품도 담겨 있지 않다.

2 번의 경우, 창부품이 추가되면 정확하게 같은 객체가 어떻게 보이는지 보여준다 (우리의 경우 버튼이 세개 붙는다). 틀이 짝 줄어들어서 세개의 버튼에 들러 붙는다.

3 번의 경우, 빈 틀이 어떻게 보이는지 또 보여준다. 단 이번에는 geometry 옵션을 사용하여 창의 크기를 전체적으로 제어한다. 창의 커다란 회색 필드 안에 틀의 작은 파란색 배경이 보인다.

4 번의 경우, 버튼을 세 개 가진 틀을 볼 수 있는데, 이 번에는 틀의 크기를 geometry 옵션으로 지정한다. 창의 크기가 3 번의 경우와 같음에 주목하자. 그러나 (2 번의 경우와 같이) 틀은 버튼 주위로 들러 붙고, 틀의 파랑 배경이 전혀 보이지 않는다.

[revised: 2002-10-01]

프로그램 소스 코드 tt095.py

from Tkinter import *class App:    def __init__(self, root, use_geometry, show_buttons):        fm = Frame(root, width=300, height=200, bg="blue")        fm.pack(side=TOP, expand=NO, fill=NONE)        if use_geometry:            root.geometry("600x400")  ### (1) Note geometry Window Manager method        if show_buttons:            Button(fm, text="Button 1", width=10).pack(side=LEFT)            Button(fm, text="Button 2", width=10).pack(side=LEFT)            Button(fm, text="Button 3", width=10).pack(side=LEFT)case = 0for use_geometry in (0, 1):    for show_buttons in (0,1):        case = case + 1        root = Tk()        root.wm_title("Case " + str(case))  ### (2) Note wm_title Window Manager method        display = App(root, use_geometry, show_buttons)        root.mainloop()

꾸림 옵션

다음 프로그램에서는 틀 안에서 조감을 통제하는 여러 pack() 옵션을 살펴보겠다:

  • * side
  • * fill
  • * expand
  • * anchor

다음 프로그램은 이 시리즈에서의 다른 프로그램과 다르다. 다시 말해, 어떤 특징을 어떻게 코딩했는지 이해하기 위해 소스 코드를 읽을 필요가 없다. 단지 프로그램을 실행하기만 하면 된다.

이 프로그램의 목적은 꾸림 옵션의 결과를 보여주는 것이다. 프로그램을 실행하면 다양한 꾸림 옵션을 설정하고 다양하게 옵션을 조합한 효과를 관찰하실 수 있다.

꾸림 옵션 아래에 숨겨진 개념들

그릇 안에서 (즉, 틀로) 창부품의 모습을 어떻게 제어하는지 알고 싶다면, 꾸림 위치 관리자가 정렬 방법으로 "cavity" 모델을 사용한다는 것을 기억해야 한다. 다시 말해, 각 그릇에는 공간이 담겨 있고, 그 그릇에 창부품들을 꾸려 넣는다.

한 그릇 안에서 구성요소의 표현과 위치에 관하여 언급하려면, 세 가지 개념을 이해하는게 좋다:

  • * 요구되지 않은 공간 (즉, cavity)
  • * 요구되었지만 사용되지 않은 공간
  • * 요구되었고 사용된 공간

버튼 같은 창부품을 꾸려 넣으면, 언제나 공간의 네 모서리중 하나를 따라서 꾸러진다. pack "side" 옵션은 어느 모서리를 사용할지 지정한다. 예를 들어, "side=LEFT"라고 지정했다면, 그 창부품은 공간의 왼쪽 모서리에 꾸려 넣어진다 (즉 위치를 잡는다).

창부품이 한 모서리를 따라 꾸려 넣어질 때, 전체 모서리를 요구한다. 실제로 요구한 공간을 모두 사용하지는 않을 지라도 말이다. X라고 부르는 작은 버튼을 다음 다이어그램과 같이 커다란 공간의 왼쪽 모서리를 따라 꾸려 넣어 보자.

요구 미사용공간 (미요구)
요구 사용됨(X)
요구 미사용

(요구되지 않은 지역인) 공간은 이제 그 창부품의 오른쪽으로 간다. 창부품 X는 왼쪽 모서리 전체를 요구한다. 한 줄이면 들어가기에 충분히 넓은 공간이다. 그러나 창부품 X는 작기 때문에, 실제로는 오직 요구한 전체 영역에서 작은 부분만 사용한다. 그 작은 부분만 자신을 나타내기 위해 사용한다.

보시다시피, 창부품 X는 자신을 표시하기에 필요한 만큼만 공간을 요구했다. 옵션으로 "expand=YES"을 지정하면, 가능한 모든 지역을 요구한다. 공간의 어느 부분도 요구되지 않은 지역으로 남지 않는다. 그렇다고 하더라도 그것이 창부품 X가 전 영역을 *사용할 것이라는* 뜻은 아니다. 여전히 필요한 만큼만 작은 부분을 사용할 것이다.

창부품이 자신이 사용할 공간보다 더 많이 요구하면, 두 가지 선택이 있다:

  • * 요구되지 않은 공간으로 이동할 수 있거나,
  • * 또는 요구되지 않은 공간으로 자랄 수 있다.

요구되지 않은 공간을 사용하도록 창부품을 키우고 싶다면, "fill" 옵션을 사용하면 되는데, 이 옵션은 창부품에게 사용되지 않은 공간을 채울만큼 커질 수 있는지 그리고 어느 방향으로 자랄 수 있는지 알려준다.

  • * "fill=NONE" 커질 수 없다는 뜻이다.
  • * "fill=X" X-축을 따라 (즉, 수평적으로) 클 수 있다는 뜻이다.
  • * "fill=Y" Y-축을 따라 (즉, 수직적으로) 클 수 있다는 뜻이다.
  • * "fill=BOTH" 수직 수평으로 모두 자랄 수 있다는 뜻이다.

요구되지 않은 공간에 이동시키고 싶다면, "anchor" 옵션을 사용하면 된다. 이 옵션은 창부품에게 요구된 공간에서 어디에 위치할지 알려준다. anchor 옵션의 값은 나침반과 같다. "N"은 "north"를 의미하며 (즉, 요구된 지역의 상단 중앙에 위치한다). "NE"는 "northeast"를 의미하며 (즉 요구된 지역의 우상 모서리에 위치함), "CENTER"는 요구된 지역의 바로 중앙에 위치한다는 뜻이다. 등등.

프로그램 실행하기

이제, 프로그램을 실행하자. 코드를 읽을 필요가 없다. 그냥 프로그램을 실행하고 세 개의 데모 버튼으로 다양하게 꾸림 옵션을 실험해 보자.

버튼 A의 틀에서는 수평으로 공간이 흐른다 -- 틀이 버튼보다 높이가 크지 않다.

버튼 B의 공간은 수직으로 흐른다 -- 틀이 버튼보다 너비가 넓지 않다.

그리고 버튼 C의 틀에서는 공간이 거대하다 -- 버튼보다 높이 너비 모두 크다 -- 놀기에 넓다.

특정 설정 아래에서 버튼의 모습이 조금이라도 이상하게 보인다면, 왜 그 버튼이 그렇게 보이는지 추측해 보자.

그리고 마지막으로....

유용한 디버깅 팁

꾸려 넣기는 복잡한 일이다. 다른 창부품과 관련하여 창부품의 위치를 결정하는 일은 부분적으로 먼저 꾸려진 다른 창부품들이 어떻게 꾸려 넣어졌는가에 달려 있기 때문이다. 다시 말해, 다른 창부품들이 왼쪽을 기준으로 꾸려 넣어졌으면, 다음 창부품 안에 꾸려 넣어질은 공간은 오른쪽에 꾸려 넣어질 것이다. 그러나 공간의 위쪽에 꾸려 넣어졌다면, 다음 창부품이 꾸려 넣어질 공간은 그 아래가 될 것이다. 모두 아주 혼란스럽다.

다음은 유용한 디버깅 팁이다. 조감을 배치하다가 문제에 부딛쳤다면 -- 즉 예상대로 일이 진행되지 않으면 -- 각 그릇에 (즉, 각 틀에) 배경색을 다르게 지정하자. 예를 들어:

  • bg="red" 이나
  • 또는 bg="cyan"
  • 또는 bg="tan"

...또는 노랑이나 파랑, 또는 빨강 등등으로 말이다.

이렇게 하면 틀들이 실제로 어떻게 정렬되는지 볼 수 있다. 눈에 보이는 것들이 문제를 해결하는 실마리가 되는 경우가 많다.

[revised: 2004-04-26]

프로그램 소스 코드 tt100.py

from Tkinter import *class MyApp:    def __init__(self, parent):        #------ 버튼의 조감을 제어하는데 사용되는 상수들 ------        button_width = 6        button_padx = "2m"        button_pady = "1m"        buttons_frame_padx =  "3m"        buttons_frame_pady =  "2m"        buttons_frame_ipadx = "3m"        buttons_frame_ipady = "1m"        # -------------- 상수 끝 ----------------        # Tkinter 변수들을, 라디오 버튼이 통제하도록 설정한다        self.button_name   = StringVar()        self.button_name.set("C")        self.side_option = StringVar()        self.side_option.set(LEFT)        self.fill_option   = StringVar()        self.fill_option.set(NONE)        self.expand_option = StringVar()        self.expand_option.set(YES)        self.anchor_option = StringVar()        self.anchor_option.set(CENTER)        # -------------- 상수 끝 ----------------        self.myParent = parent        self.myParent.geometry("640x400")        ### 최상위 틀은 myContainer1이라고 불리운다        self.myContainer1 = Frame(parent) ###        self.myContainer1.pack(expand=YES, fill=BOTH)        ### myContainer1 안에 수평적 (좌/우) 동선을 사용하겠다.        ### myContainer1 안에 control_frame과 demo_frame을 만든다.        # 제어 틀 - 기본적으로 데모 틀을 제외하면 이게 본체이다.        self.control_frame = Frame(self.myContainer1) ###        self.control_frame.pack(side=LEFT, expand=NO,  padx=10, pady=5, ipadx=5, ipady=5)        # control_frame 안에 헤더 라벨과         # 상단에 buttons_frame 그리고        # 하단에 demo_frame을 만든다        myMessage="This window shows the effects of the \nexpand, fill, and anchor packing options.\n"        Label(self.control_frame, text=myMessage, justify=LEFT).pack(side=TOP, anchor=W)        # 버튼 틀        self.buttons_frame = Frame(self.control_frame) ###        self.buttons_frame.pack(side=TOP, expand=NO, fill=Y, ipadx=5, ipady=5)        # 데모 틀        self.demo_frame = Frame(self.myContainer1) ###        self.demo_frame.pack(side=RIGHT, expand=YES, fill=BOTH)        ### 데모 틀 안에 top_frame과 bottom_frame을 만든다.        ### 이 틀들이 데모 틀이 된다.        # 상단 틀        self.top_frame = Frame(self.demo_frame)        self.top_frame.pack(side=TOP, expand=YES, fill=BOTH)  ###        # 하단 틀        self.bottom_frame = Frame(self.demo_frame,            borderwidth=5, 	relief=RIDGE,            height=50,            bg="cyan",            ) ###        self.bottom_frame.pack(side=TOP, fill=X)        ### 이제 top_frame 안에 left_frame과 right_frame이라는        ### 틀을 두 개 더 배치하겠다. top_frame 안에        ### 수평적 (좌/우) 동선을 사용하겠다.        # 좌측 틀        self.left_frame = Frame(self.top_frame,	background="red",            borderwidth=5, 	relief=RIDGE,            width=50,            ) ###        self.left_frame.pack(side=LEFT, expand=NO, fill=Y)        ### 우측 틀        self.right_frame = Frame(self.top_frame, background="tan",            borderwidth=5, 	relief=RIDGE,            width=250            )        self.right_frame.pack(side=RIGHT, expand=YES, fill=BOTH)        # 이제 해당 틀에 각각 버튼을 배치한다.        button_names = ["A", "B", "C"]        side_options = [LEFT, TOP, RIGHT, BOTTOM]        fill_options = [X, Y, BOTH, NONE]        expand_options = [YES, NO]        anchor_options = [NW, N, NE, E, SE, S, SW, W, CENTER]        self.buttonA = Button(self.bottom_frame, text="A")        self.buttonA.pack()        self.buttonB = Button(self.left_frame, text="B")        self.buttonB.pack()        self.buttonC = Button(self.right_frame, text="C")        self.buttonC.pack()        self.button_with_name = {"A":self.buttonA, "B":self.buttonB, "C":self.buttonC}        # 이제 the buttons_frame의 하부틀을 몇 개 만든다.        self.button_names_frame   = Frame(self.buttons_frame, borderwidth=5)        self.side_options_frame   = Frame(self.buttons_frame, borderwidth=5)        self.fill_options_frame   = Frame(self.buttons_frame, borderwidth=5)        self.expand_options_frame = Frame(self.buttons_frame, borderwidth=5)        self.anchor_options_frame = Frame(self.buttons_frame, borderwidth=5)        self.button_names_frame.pack(  side=LEFT, expand=YES, fill=Y, anchor=N)        self.side_options_frame.pack(  side=LEFT, expand=YES, anchor=N)        self.fill_options_frame.pack(  side=LEFT, expand=YES, anchor=N)        self.expand_options_frame.pack(side=LEFT, expand=YES, anchor=N)        self.anchor_options_frame.pack(side=LEFT, expand=YES, anchor=N)        Label(self.button_names_frame, text="\nButton").pack()        Label(self.side_options_frame, text="Side\nOption").pack()        Label(self.fill_options_frame, text="Fill\nOption").pack()        Label(self.expand_options_frame, text="Expand\nOption").pack()        Label(self.anchor_options_frame, text="Anchor\nOption").pack()        for option in button_names:            button = Radiobutton(self.button_names_frame, text=str(option), indicatoron=1,                value=option, command=self.button_refresh, variable=self.button_name)            button["width"] = button_width            button.pack(side=TOP)        for option in side_options:            button = Radiobutton(self.side_options_frame, text=str(option), indicatoron=0,                value=option, command=self.demo_update, variable=self.side_option)            button["width"] = button_width            button.pack(side=TOP)        for option in fill_options:            button = Radiobutton(self.fill_options_frame, text=str(option), indicatoron=0,                value=option, command=self.demo_update, variable=self.fill_option)            button["width"] = button_width            button.pack(side=TOP)        for option in expand_options:            button = Radiobutton(self.expand_options_frame, text=str(option), indicatoron=0,                value=option, command=self.demo_update, variable=self.expand_option)            button["width"] = button_width            button.pack(side=TOP)        for option in anchor_options:            button = Radiobutton(self.anchor_options_frame, text=str(option), indicatoron=0,                value=option, command=self.demo_update, variable=self.anchor_option)            button["width"] = button_width            button.pack(side=TOP)        self.cancelButtonFrame = Frame(self.button_names_frame)        self.cancelButtonFrame.pack(side=BOTTOM, expand=YES, anchor=SW)        self.cancelButton = Button(self.cancelButtonFrame,            text="Cancel", background="red",            width=button_width,            padx=button_padx,            pady=button_pady            )        self.cancelButton.pack(side=BOTTOM, anchor=S)        self.cancelButton.bind("<Button-1>", self.cancelButtonClick)        self.cancelButton.bind("<Return>", self.cancelButtonClick)        # 버튼에 초기 위치를 설정한다        self.demo_update()    def button_refresh(self):        button = self.button_with_name[self.button_name.get()]        properties = button.pack_info()        self.fill_option.set  (  properties["fill"] )        self.side_option.set  (  properties["side"] )        self.expand_option.set(  properties["expand"] )        self.anchor_option.set(  properties["anchor"] )    def demo_update(self):        button = self.button_with_name[self.button_name.get()]        button.pack(fill=self.fill_option.get()            , side=self.side_option.get()            , expand=self.expand_option.get()            , anchor=self.anchor_option.get()            )    def cancelButtonClick(self, event):        self.myParent.destroy()root = Tk()myapp = MyApp(root)root.mainloop()

한글판 johnsonj 2008.04.11

'파이썬 프로그래밍' 카테고리의 다른 글

tkinter 소개  (0) 2011.09.04
Tkinter GUI 프로그래밍  (0) 2011.04.24
TKINTER 요약  (0) 2011.04.24
UltraEdit Python 설정  (0) 2009.06.28
파이썬 데몬 만들기  (0) 2009.04.07

+ Recent posts