이벤트와 콜백

GUI 프로그래밍과 게임 프로그래밍에서 매우 중요한 개념 세 가지를 살펴본다.

  • 이벤트
  • 콜백 함수
  • 람다 함수

이벤트, 콜백, 람다 함수

이벤트와 이벤트 처리

구현된 애플리케이션에 사용된 위젯(스크린, 버튼, 입력창 등과 게임 캐릭터에 마우스 클릭, 버튼 누루기, 키 입력 등을 사용하여 영향을 미치는 사건을 이벤트(event)라 부르며, 그렇게 이벤트에 반응하도록 프로그램을 작성하는 것이 이벤트 처리(event handling)이다.

예를 들어, 객체 지향 프로그래밍: GUI 프로그래밍 소개 에서 소개한 '파이썬 퀴즈 게임' 애플리케이션에서 정답/오답 버튼을 누르면 지정된 소리를 내고, 퀴즈 수를 1씩 증가시켜야 한다. 이때, 정답/오답 버튼 누르기가 이벤트이고, 해당 이벤트가 발생하면 특정 기능이 수행되도록 프로그램을 작성하는 것이 이벤트 처리이다.

이벤트와 콜백 함수 연동

위젯 또튼 게임 캐릭터가 이벤트에 반응하는 기능을 가지려면 콜백(callback) 함수가 연동되어 있어야 한다. 콜백 함수는 특정 이벤트가 발생하면 자동으로 호출(callback)되어 실행되는 함수를 가리킨다. 즉, 연동된 이벤트가 먼저 발생한 후에 실행되며, 이벤트가 발생하지 않으면 전혀 실행되지 않는다.

예를 들어, '파이썬 퀴즈 게임'에서 정답/오답 버튼을 누를 때 실행되어 소리를 내고 정답/오답 수를 하나 키우는 기능을 수행하는 함수가 바로 콜백 함수이다.

  • play_correct_sound(): 정답 버튼이 눌렸을 때 실행
  • play_wrong_sound(): 오답 버튼이 눌렸을 때 실행
참고 사항
  • 사용하는 패키지, 모듈, 클래스 등에 따라 이벤트를 처리하는 방법이 다르다.
    • 거북이(turtle) 그래픽: 캐릭터와 이벤트 사이의 연동은 onkey() 메서드를 활용하여 콜백 함수 효과를 낸다.
    • 파이게임(pygame): 콜백 방식 보다는 무한 루프 상태에서 이벤트 발생 여부를 계속 감시하다가 알려진 이벤트 유형(type)이 발생하면 지정된 코드(프로세스)를 실행되도록 한다. 구현은 for 반복문과 if 조건문을 사용한다.
  • 콜백 방식 사용여부는 동기화/비동기화 등의 프로그램 실행 방식과도 연관되지만 이에 대한 이야기는 더 이상 하지 않는다.
  • 웹/모바일 프로그래밍 분야에서 가장 많이 사용되는 언어인 자바스크립트는 이벤트 처리를 위해 기본적으로 콜백 함수를 사용한다는 것을 기억해 두길 바란다.
  • 여기서는 tkinter 모듈의 위젯을 이용하여 이벤트 처리 방식을 설명한다. 언급한 대로 사용되는 패키지, 모듈, 클래스, 그리고 프로그래밍 언어마다 이벤트 처리 방식이 다르지만 이벤트 처리의 기본 개념은 동일하다.

위젯 이벤트와 콜백 함수를 연동하려면 command 인스턴스 속성이나 bind 인스턴스 메서드를 활용해야 한다.

command 인스턴스 속성 활용

아래 코드는 정답 버튼과 play_correct_sound 콜백 함수를 command 인스턴스 속성을 이용하여 연동하는 방식을 보여준다.

b1 = Button(frame3, text = "정답", width = 7, command = play_correct_sound)

아래 사항에 주의해야 한다.

  • 콜백 함수는 인자를 전혀 사용하지 않는 함수이어야 한다.
  • 콜백 함수로 지정할 때 함수 이름만 사용해야 하며, 괄호는 생략해야 한다. 괄호를 사용하면 위젯이 생성되는 과정에 함수 호출이 발생하기에 콜백 기능을 갖지 못한다.

bind 인스턴스 메서드 활용

command 인스턴스 속성에 콜백 함수를 지정하는 방식은 매우 편리하다. 하지만 다음 두 가지 한계를 갖는다.

  • 첫째, 모든 위젯이 command 속성을 활용한 콜백 함수 연동을 지원하지는 않는다. 예를 들어, Button에서는 지원되지만, Frame, Lable 등은 command 속성을 지원하지 않는다.
  • 둘째, command 속성으로 연동할 수 있는 이벤트가 버튼 클릭 등 매우 제한적이다. 따라서 예를 들어, 리턴(Return) 키를 누르는 이벤트와의 연동은 지원되지 않는다.

반면에 bind() 메서드를 활용한 방식은 command 속성 방식보다 훨씬 다양한 이벤트에 활용된다. 특정 위젯과 콜백 함수를 연동하는 방식은 다음과 같다.

widget.bind(event, handler, add='')
  • event: 이벤트 지정
  • handler: 이벤트 처리기(핸들러)로 사용될 콜백 함수 지정
    • 콜백 함수은 인자를 하나 받아야 한다. 그러면 실행될 때 발생한 이벤트가 자동으로 인자로 삽입된다.
  • add: 기존에 연결된 콜백 함수 함께 실행 여부 결정
    • add='': 기존 콜백 함수 연결 취소
    • add='+': 기존에 콜백 함수와 함께 연결. 하지만 어떤 콜백 함수가 선택될지는 아무도 모름.

예를 들어, 정답 버튼과 play_correct_sound 함수를 연동하는 방식은 다음과 같다.

b1 = Button(frame3, text = "정답", width = 7)
b1.bind("<Button-1>", play_correct_sound_1)

여기서 play_correct_sound_1() 함수는 인자를 하나 받도록 선언되어야 한다. 앞서 설명한 대로 발생한 이벤트가 자동으로 인자로 사용되기에 입력된 이벤트 객체를 활용할 수 있다. 하지만 정답 버튼의 경우 이벤트가 발생한 것만 중요하고 굳이 이벤트 객체를 활용할 필요가 없기에 다음과 같이 정의해도 된다.

def play_correct_sound_1(event):
    play_correct_sound()
이벤트 인자 활용 예제

아래 코드는 Frame 객체와 관련된 콜백 함수가 인자로 전달되는 이벤트 객체를 활용하는 하나의 방식을 보여준다.

  • frame.bind("<Button-1>", eventCallback): 프레임을 클릭 이벤트 처리
    • "<Button-1>": 마우스 왼쪽 버튼 클릭 이벤트
    • eventCallback: 이벤트 발생 후 실행되는 콜백 함수
  • eventCallback(event): 콜백 함수
    • dir(event): 발생한 이벤트 객체의 속성 및 메서드 정보
    • event.x, event.y: 마우스 클릭 지점의 좌표

In [1]:
from tkinter import *
app = Tk()
app.title("클릭 위치 확인")
app.geometry('300x100+200+100')

Label(app, text='하늘색 영역에서 마우스 클릭해보세요').pack()

def eventCallback(event):
    print(dir(event))
    print("\n클릭 지점 좌표:", event.x, event.y)

frame = Frame(app, bg='sky blue', width=250, height=70)
frame.bind("<Button-1>", eventCallback)
frame.pack()

app.mainloop()
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'char', 'delta', 'height', 'keycode', 'keysym', 'keysym_num', 'num', 'send_event', 'serial', 'state', 'time', 'type', 'widget', 'width', 'x', 'x_root', 'y', 'y_root']

클릭 지점 좌표: 110 47

이벤트 패턴

다양한 이벤트 패턴에 대한 설명은 Events and callbacks – adding life to programs 을 참조하면 좋다. 하지만 위젯에 따라 사용 가능한 이벤트도 달라지기 때문에 인터넷 검색을 사용하여 필요한 경우에 대한 상세하며 친절한 사용법을 확인해야 한다.

콜백과 람다 함수

파이썬에서 함수는 보통 아래 방식으로 정의된다.

def 함수이름(인자1, ..., 인자n):
    함수본체

즉, 함수 이름을 먼저 선언하고 함수를 정의한다. 이렇게 하면, 함수 이름을 이용하여 필요한 기능을 재사용할 수 있으며, 함수가 어떻게 정의되었는지 굳이 몰라도 함수의 기본 기능만 알면 사용할 수 있다. 즉, 코드 추상화의 주요 도구 중의 하나로 사용된다.

하지만 경우에 따라 한 번만 사용할 간단한 함수를 이름 없이 정의하면 좋을 때가 있으며, 그럴 때 람다(lambda) 함수를 이용한다. 예를 들어, 이전 코드에서 사용한 eventCallback() 함수를 단순화 해서 아래와 같이 정의하자.

In [2]:
def eventCallback(event):
    print("클릭 좌표:", event.x, event.y)

eventCallback() 함수와 동일한 기능을 수행하는 람다 함수는 다음과 같다.

In [3]:
lambda event: print("클릭 좌표:", event.x, event.y)
Out[3]:
<function __main__.<lambda>(event)>

위 람다 함수를 이용하여 이전 코드를 다시 작성하면 다음과 같다.

In [4]:
from tkinter import *
app = Tk()
app.title("클릭 위치 확인")
app.geometry('300x100+200+100')

Label(app, text='하늘색 영역에서 마우스 클릭해보세요').pack()

frame = Frame(app, bg='sky blue', width=250, height=70)
frame.bind("<Button-1>", lambda event: print("클릭 좌표:", event.x, event.y))
frame.pack()

app.mainloop()
클릭 좌표: 76 25

GUI 애플리케이션 예제: 계산기

tkinter 모듈을 이용하여 보다 유용한 애플리케이션을 구현하는 과정을 살펴본다. 여기서 구현하는 애플리케이션은 실제로 작동하는 아래 모양의 계산기이다.

참조: 계산기 관련 코드는 Python Calculator – Create A Simple GUI Calculator Using Tkinter 의 내용을 참조하였다.

계산기 GUI 애플리케이션 구성 요소 확인

"계산기" GUI 애플리케이션의 구성 요소는 다음과 같다.

  1. 입력 내용과 연산 실행 결과 창
    • 마우스로 입력되는 내용을 표시
    • '=' 버튼을 누르면 연산결과 표시
  2. 'C' 버튼: 초기화 버튼
  3. 숫자/연산자 입력 버튼 및 연산 실행 버튼

구성 요소 1: 입력 내용과 연산 실행 결과 창

스크린은 tkinter 모듈에 포함된 Tk 클래스의 인스턴스로 구할 수 있다. 또한 버튼 입력 내용과 연산 실행 결과를 보여주는 창을 생성하려면 아래 도구를 사용한다.

  • StringVar 클래스: 문자열을 저장한 후 확인 및 업데이트 기능 제공
  • Entry 클래스: 한 줄 문장을 입력받고 보여주는 제공
    • textvariable 인스턴스 속성
      • StringVar 클래스의 객체를 지정하면 창에 입력된 문자열과 StringVar 객체에 저장된 값이 연동됨.
      • StringVarset, get 메서드를 이용하여 화면에 보여지는 내용 업데이트 가능.
    • 나머지 인스턴스 속성은 창의 모양과 출력 방식과 관련됨. 자세한 설명은 인터넷 검색 또는 tkinter 공식 문서 참조.
    • pack() 메서드 인자
      • side: 지정된 스크린에 추가할 때 위치 지정
      • expand=YES, fill=BOTH: 스크린을 자유자재로 조정 가능하게 만듦. 구체적인 사용법은 직접 실험해보거나 tkinter 공식 문서 참조.
In [5]:
from tkinter import *

calculator = Tk()
calculator.title("계산기")

# 버튼 입력 내용 및 연산 결과 표시 창
display = StringVar()
displayFrame = Entry(calculator, relief=FLAT, textvariable=display, 
                     justify='right', bd=30, bg="sky blue")
displayFrame.pack(side=TOP, expand=YES, fill=BOTH)

calculator.mainloop()

위 프로그램을 실행하면 지정된 타이틀과 입력 내용과 연산 실행 결과를 보여주는 입력창이 생성된 스크린이 아래와 같이 생성된다.

구성 요소 2: 클리어 버튼

FrameButton 클래스의 활용법을 이전에 살펴보았다. 여기서는 많은 프레임과 버튼을 생성해야 하기에 해당 클래스의 객체 생성과 추가하기(pack)를 지원하는 두 개의 함수를 선언한다.

  • frameInstance: Frame 객체 생성 및 지정된 스크린에 생성된 프레임 추가하기(pack)
  • buttonInstance: Button 객체 생성 및 지정된 프레임에 생성된 버튼 추가하기(pack)

코드의 나머지 부분은 실행 결과를 모두 삭제하는 클리어('C') 버튼을 추가하는 내용이다.

  • command 속성
    • 클리어('C') 버튼을 누르기 이벤트 처리를 위한 콜백 함수 지정
    • 콜백 함수: display 가 가리키는 StringVar() 객체에 담긴 문자열을 빈 문자열로 초기화.
In [6]:
from tkinter import *

calculator = Tk()
calculator.title("계산기")

# 버튼 입력 내용 및 연산 결과 표시 창
display = StringVar()
displayFrame = Entry(calculator, relief=FLAT, textvariable=display, justify='right', bd=30, bg="sky blue")
displayFrame.pack(side=TOP, expand=YES, fill=BOTH)

# Frame 객체 생성 및 지정된 스크린에 생성된 프레임 추가
def frameInstance(window, side):
    frame = Frame(window, borderwidth=4, bd=4, bg="sky blue")
    frame.pack(side=side, expand=YES, fill=BOTH)
    return frame

# Button 객체 생성 및 지정된 프레임에 생성된 버튼 추가
def buttonInstance(frame, side, text, command=None):
    button = Button(frame, text=text, command=command)
    button.pack(side=side, expand=YES, fill=BOTH)
    return button

# 클리어 버튼 추가 및 이벤트 처리
# 먼저 프레임을 생성한 후 그곳에 클리어 버튼만 추가
# 콜백 함수: C 버튼을 누르면 입력 내용과 연산 실행 결과 전부 삭제
clearButtonFrame = frameInstance(calculator, TOP)
buttonInstance(clearButtonFrame, LEFT, "C", command=lambda: display.set(''))


calculator.mainloop()

위 프로그램을 실행하면 'C'로 표기된 클리어 버튼이 생성된다. 'C' 버튼을 눌러도 아무런 변화가 발생하지 않는다. 클리어할 내용이 아직 없기 때문이다.

콜백과 람다 함수 (추가 설명)

람다 함수를 이용하면 함수에 특정 인자를 지정한 상태로 콜백 기능을 사용할 수 있다. 예를 들어, 위 코드에서 클리어 버튼 누리기 이벤트와 연동된 콜백 함수는 다음과 같이 람다 함수로 정의되었다.

lambda: display.set('')

위 함수 자체는 인자를 전혀 사용하지 않는 함수인데, lambda와 콜론 사이에 매개 변수가 사용되지 않았다는 사실에서 확인할 수 있다.

여기서 display.set('')StringVar() 에 속한 set() 메서드를 빈 무자열 인자와 함께 호출하는 것을 가리킨다. 하지만 command=display.set('') 형식으로 사용하면 안된다. 이유는 그렇게 하면 display.set('') 이 버튼을 생성할 때 바로 시행되기 때문이다. 즉, 콜백 함수로서의 기능을 수행하지 못한다.

이럴 때 lambda(람다) 기호를 위와 같이 사용하면 인자 없는 함수로 선언되어 버튼 클릭 이벤트가 발생하면 바로 호출이 된다. 실제로 위 함수를 아래와 같이 정의할 수 있다.

def c_buttonCallback():
    display.set('')

구성 요소 3: 숫자/연산자 입력 버튼 및 연산 실행 버튼

이제 나머지 버튼을 한꺼번에 생성하면서 동시에 이벤트 처리도 함께 다룬다. 이벤트 처리는 기본적으로 모두 동일하다. 다만 등호('=') 버튼의 이벤트 처리가 다른 버튼들과 다른 점에 주의해야 한다.

행별로 생성할 버튼과 순서는 다음과 같다.

  • '7', '8', '9' 숫자 버튼과 나눗셈 연산자 '/' 버튼
  • '4', '5', '6' 숫자 버튼과 곱셈 연산자 '*' 버튼
  • '1', '2', '3' 숫자 버튼과 뺄셈 연산자 '-' 버튼
  • '0' 숫자 버튼, 소숫점 기호 '.' 버튼, 등호 기호 '=' 버튼, 덧셈 연산자 '+' 버튼

위 버튼을 자동으로 생성하기 위해 2중 for 반복문을 사용한다.

  • 바깥쪽 for 반복문:
    • Frame 객체 생성: 행에 포함되는 네 개의 버튼을 담는 프레임
  • 안쪽 for 반복문
    • 하나의 행에 포함된 네 개의 문자에 해당하는 버튼 생성 및 이벤트 처리

각각의 버튼에 대한 콜백 함수는 다음과 같다.

  • 등호 기호 제외 버튼
    • 해당 버튼의 문자가 display 창에 차례대로 입력됨.
  • 등호 기호 '=' 버튼
    • 입력된 내용을 실행하여 다시 display 참에 보여줌
    • eval() 함수를 활용하여 문자열 내용을 해석함.
      • eval() 함수는 문자열에 의미를 부여하여 실행하는 함수임. 의미가 부여될 수 없으면 오류 발생.
    • 잘못 입력된 내용은 오류 처리.
In [7]:
from tkinter import *

calculator = Tk()
calculator.title("계산기")

# 버튼 입력 내용 및 연산 결과 표시 창
display = StringVar()
displayFrame = Entry(calculator, relief=FLAT, textvariable=display, justify='right', bd=30, bg="sky blue")
displayFrame.pack(side=TOP, expand=YES, fill=BOTH)

# Frame 객체 생성 및 지정된 스크린에 생성된 프레임 추가
def frameInstance(window, side):
    frame = Frame(window, borderwidth=4, bd=4, bg="sky blue")
    frame.pack(side=side, expand=YES, fill=BOTH)
    return frame

# Button 객체 생성 및 지정된 프레임에 생성된 버튼 추가
def buttonInstance(frame, side, text, command=None):
    button = Button(frame, text=text, command=command)
    button.pack(side=side, expand=YES, fill=BOTH)
    return button

# 클리어 버튼 추가 및 이벤트 처리
# 먼저 프레임을 생성한 후 그곳에 클리어 버튼만 추가
# 콜백 함수: C 버튼을 누르면 입력 내용과 연산 실행 결과 전부 삭제
clearButtonFrame = frameInstance(calculator, TOP)
buttonInstance(clearButtonFrame, LEFT, "C", command=lambda: display.set(''))

# 숫자/연산자 입력 버튼 및 연산 실행 버튼 생성 및 이벤트 처리
# 이중 for 반복문 사용
# 1. 행별로 네 개의 문자 버튼 추가
for symbols in ("789/", "456*", "123-", "0.=+"):
    # 네 개의 버튼을 담을 프레임 생성
    lineFrame = frameInstance(calculator, TOP)
    # 생성된 프레임에 네 개의 버튼 추가 및 이벤트 처리
    for aSymbol in symbols:
        # 등호 기호(=)는 특별한 기능 수행
        # 기하 버트는 입력 기능 수행
        if aSymbol != '=':
            buttonInstance(lineFrame, LEFT, aSymbol, lambda char=aSymbol: display.set(display.get()+char))
        else:
            # 등호 기호(=) 버튼을 누르면 입력 내용을 실행하고 그 결과를 display에 보여줌
            buttonInstance(lineFrame, LEFT, aSymbol, lambda: calc())

# 등호 기호 (=) 버튼 콜백 함수
# 입력된 내용을 실행하여 다시 display 참에 보여줌
# eval() 함수를 활용하여 문자열 내용을 해석함.
# 잘못 입력된 내용은 오류 처리.
def calc():
        try:
            display.set(eval(display.get()))
        except:
            display.set("ERROR")

                
calculator.mainloop()

위 프로그램을 실행하면 아래 모양의 계산기가 실행되며, 실제로 사용할 수 있다.

콜백과 람다 함수 (추가 설명)

위 코드에서 추가된 버튼과 연동된 콜백 함수는 다음과 같다.

등호 기호 제외 버튼의 콜백 함수

등호 기호 제외 버튼의 콜백 함수는 해당 버튼의 문자가 display, 즉, 맨위에 위치한 Entry 창에 차례대로 입력되어야 한다. 아래 람다 함수가 해당 기능을 수행한다.

lambda char=aSymbol: display.set(display.get()+char)

위 함수를 보면 lambda 기호 다음에 char=aSymbol 이라는 인자가 사용되는 것처럼 보인다. 반면에 command 속성은 인자가 사용되지 않는 함수를 사용해야 한다. 다행히도 위 람다 함수는 아래와 같이 정의된 함수와 동일하다.

def char_buttonCallback(char=aSymbol):
    display.set(display.get()+char)

즉, 원래 하나의 인자가 필요한 함수이지만 기본값이 지정되어 있어서 인자 없이 호출이 가능한 함수가 된다. 실제로 위 람다 함수가 호출되면 char_buttonCallback() 함수가 인자 없이 호출되는 것과 동일하다. 그리고 char 매개변수에 이미 aSymbol 인자가 할당되어 호출되기에 아래와 같이 함수가 호출되는 것과 동일한 기능을 수행한다. 즉, display 창에 이미 입력된 문자열에 새로 누른 버튼의 문자를 추가한다.

display.set(display.get()+aSymbol)

이렇게 람다 함수를 정의할 때도 기본 인자를 지정하는 방식을 활용할 수 있다.

등호 기호 버튼의 콜백 함수

등호 기호 '=' 버튼을 누르면 그때까지 입력한 문자열을 수식으로 간주하여 실행한 결과를 다시 display에 보여주어야 한다. 이를 위해 따로 선언된 calc() 함수를 활용한다. calc() 함수 정의에 사용된 eval() 함수는 문자열로 주어진 표현식에 실제 의미를 부여한다.

In [8]:
eval("1+1")
Out[8]:
2
In [9]:
eval("10*3/5 - 2")
Out[9]:
4.0

문자열 내용이 의미 없는 표현식이면 구문 오류(SyntaxError)가 발생한다.

In [10]:
try:
    eval("1-3*")
except SyntaxError:
    print("제대로된 표현식이 아닙니다.")
제대로된 표현식이 아닙니다.

그런데 command=calc 대신에 command=lamda: calc()를 사용하였다. 이유는 lambda 함수로 정의하면 해당 람다 함수가 실제로 호출되어 실행될 때까지 함수 본체가 외부에 노출되지 않기 때문이다. 즉, 해당 람다 함수가 실행되기 전까지 파이썬 해석기가 람다 함수의 본체에 전혀 신경를 쓰지 않기 때문에 calc() 함수를 나중에 선언해도 된다.

반면에 command=calc 라고 선언하면 파이썬 해석기가 calc() 함수가 이미 선언되어 있는가를 확인한다. 그런데 위 코드에서는 함수가 버튼을 생성한 다음에 선언되어서 오류가 발생한다. 물론 아래 코드에서 처럼 calc() 함수를 먼저 선언하면 제대로 작동한다.

In [11]:
from tkinter import *

calculator = Tk()
calculator.title("계산기")

display = StringVar()
displayFrame = Entry(calculator, relief=FLAT, textvariable=display, justify='right', bd=10, bg="sky blue")
displayFrame.pack(side=TOP, expand=YES, fill=BOTH)

def frameInstance(window, side):
    frame = Frame(window, borderwidth=4, bd=4, bg="sky blue")
    frame.pack(side=side, expand=YES, fill=BOTH)
    return frame

def buttonInstance(frame, side, text, command=None):
    button = Button(frame, text=text, command=command)
    button.pack(side=side, expand=YES, fill=BOTH)
    return button

clearButtonFrame = frameInstance(calculator, TOP)
buttonInstance(clearButtonFrame, LEFT, "C", lambda: display.set(''))

def calc():
        try:
            display.set(eval(display.get()))
        except:
            display.set("ERROR")


for symbols in ("789/", "456*", "123-", "0.=+"):
    lineFrame = frameInstance(calculator, TOP)
    for aSymbol in symbols:
        if aSymbol != '=':
            buttonInstance(lineFrame, LEFT, aSymbol, lambda char=aSymbol: display.set(display.get()+char))
        else:
            buttonInstance(lineFrame, LEFT, aSymbol, calc)
            
                
calculator.mainloop()

연습문제

  1. tkinter 공식 문서 를 참조하여 계산기 애플리케이션을 생성하는 코드를 Calculator 라는 클래스에 모두 포함되도록 구현하라. 즉, 아래 명령문 하나로 계산기 애플리케이션이 실행되어야 한다.

    Calculator().mainloop()
    

    위 코드는 아래 코드와 동일하게 작동한다.

    app = Calculator()
     app.mainloop()
    

모범답안 A

Calculator 클래스를 다음과 같이 정의할 수 있다.

  • frameInstance() 함수와 buttonInstance() 함수는 클래스 밖에서 선언하는 것이 좋다. 이유는 생성된 위젯을 추가할 왼도우 창 또는 프레임을 독립적으로 지정할 있기 때문이다.

  • Calculator 클래스

    • Frame 클래스 상속
      • 초기 설정 단계에서 master=None 으로 설정하면 자동으로 Tk()master로 설정함.
      • 즉, 기본 스크린을 자동으로 생성함.
    • 초기 설정 __init__() 메서드
      • master=None: 앞서 설명함.
      • super().__init__(master): Tk() 객체, 즉, 기본 스크린 초기 설정 실행.
      • self.pack(expand=YES, fill=BOTH): 추가하기(pack) 옵션
      • self.master.title("계산기"): 기본 스크린 타이틀 설정
    • 기본 스크린과 위젯 생성은 모두 초기 설정 과정에서 해결한다.
      • 주의: 이전 코드에서 사용된 calculator 란 계산기 객체 이름을 모두 self 로 변경해야 함. 즉, self 생성될 계산기 객체를 가리킴.
    • calc() 메서드
      • entryWidget 매개변수 추가: 어느 디스플레이에 있는 표현식의 의미를 확인할 것인가를 지정해야 함.
      • display__init__() 함수 내부에서 지역변수로 선언되었음. 따라서 이전 처럼 display.set(...) 등으로 사용하면 오류 발생함. (함수 내부에서 선언된 지역변수는 외부에서 사용 못하기 때문임)
In [12]:
from tkinter import * 

def frameInstance(window, side):
    frame = Frame(window, borderwidth=4, bd=4, bg="sky blue")
    frame.pack(side=side, expand=YES, fill=BOTH)
    return frame

def buttonInstance(frame, side, text, command=None):
    button = Button(frame, text=text, command=command)
    button.pack(side=side, expand=YES, fill=BOTH)
    return button

class Calculator(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.pack(expand=YES, fill=BOTH)
        self.master.title("계산기")
        
        display = StringVar()
        displayFrame = Entry(self, relief=FLAT, textvariable=display, justify='right', bd=10, bg="sky blue")
        displayFrame.pack(side=TOP, expand=YES, fill=BOTH)
        
        clearButtonFrame = frameInstance(self, TOP)
        buttonInstance(clearButtonFrame, LEFT, "C", lambda: display.set(''))

        for symbols in ("789/", "456*", "123-", "0.=+"):
            lineFrame = frameInstance(self, TOP)
            for aSymbol in symbols:
                if aSymbol != '=':
                    buttonInstance(lineFrame, LEFT, aSymbol, lambda char=aSymbol: display.set(display.get()+char))
                else:
                    buttonInstance(lineFrame, LEFT, aSymbol, lambda: self.calc(display))

    def calc(self, entryWidget):
        try:
            entryWidget.set(eval(entryWidget.get()))
        except:
            entryWidget.set("ERROR")

Calculator().mainloop()

모범답안 B

frameInstance() 함수와 buttonInstance() 함수를 기본적으로 계산기 애플리케이션의 보조 함수로 사용하기에 클래스 내부로 포함시키는 것도 괜찮아 보인다. 다만, self의 남용을 방지하기 위해 클래스 메서드로 선언하는 것이 좋아 보인다. 따라서 두 함수를 사용할 때마다 클래스 이름을 접두사로 덧붙혀야 한다.

In [13]:
from tkinter import * 

class Calculator(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.pack(expand=YES, fill=BOTH)
        self.master.title("계산기")
        
        display = StringVar()
        displayFrame = Entry(self, relief=FLAT, textvariable=display, justify='right', bd=10, bg="sky blue")
        displayFrame.pack(side=TOP, expand=YES, fill=BOTH)
        
        clearButtonFrame = Calculator.frameInstance(self, TOP)
        Calculator.buttonInstance(clearButtonFrame, LEFT, "C", lambda: display.set(''))

        for symbols in ("789/", "456*", "123-", "0.=+"):
            lineFrame = Calculator.frameInstance(self, TOP)
            for aSymbol in symbols:
                if aSymbol != '=':
                    Calculator.buttonInstance(lineFrame, LEFT, aSymbol, lambda char=aSymbol: display.set(display.get()+char))
                else:
                    Calculator.buttonInstance(lineFrame, LEFT, aSymbol, lambda: self.calc(display))
    
    @staticmethod
    def frameInstance(window, side):
        frame = Frame(window, borderwidth=4, bd=4, bg="sky blue")
        frame.pack(side=side, expand=YES, fill=BOTH)
        return frame

    @staticmethod
    def buttonInstance(frame, side, text, command=None):
        button = Button(frame, text=text, command=command)
        button.pack(side=side, expand=YES, fill=BOTH)
        return button

    def calc(self, entryWidget):
        try:
            entryWidget.set(eval(entryWidget.get()))
        except:
            entryWidget.set("ERROR")

Calculator().mainloop()