GUI 프로그래밍과 게임 프로그래밍에서 매우 중요한 개념 세 가지를 살펴본다.
구현된 애플리케이션에 사용된 위젯(스크린, 버튼, 입력창 등과 게임 캐릭터에 마우스 클릭, 버튼 누루기, 키 입력 등을 사용하여 영향을 미치는 사건을 이벤트(event)라 부르며, 그렇게 이벤트에 반응하도록 프로그램을 작성하는 것이 이벤트 처리(event handling)이다.
예를 들어, 객체 지향 프로그래밍: GUI 프로그래밍 소개 에서 소개한 '파이썬 퀴즈 게임' 애플리케이션에서 정답/오답 버튼을 누르면 지정된 소리를 내고, 퀴즈 수를 1씩 증가시켜야 한다. 이때, 정답/오답 버튼 누르기가 이벤트이고, 해당 이벤트가 발생하면 특정 기능이 수행되도록 프로그램을 작성하는 것이 이벤트 처리이다.
위젯 또튼 게임 캐릭터가 이벤트에 반응하는 기능을 가지려면 콜백(callback) 함수가 연동되어 있어야 한다. 콜백 함수는 특정 이벤트가 발생하면 자동으로 호출(callback)되어 실행되는 함수를 가리킨다. 즉, 연동된 이벤트가 먼저 발생한 후에 실행되며, 이벤트가 발생하지 않으면 전혀 실행되지 않는다.
예를 들어, '파이썬 퀴즈 게임'에서 정답/오답 버튼을 누를 때 실행되어 소리를 내고 정답/오답 수를 하나 키우는 기능을 수행하는 함수가 바로 콜백 함수이다.
play_correct_sound()
: 정답 버튼이 눌렸을 때 실행play_wrong_sound()
: 오답 버튼이 눌렸을 때 실행onkey()
메서드를 활용하여 콜백 함수 효과를 낸다.
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
: 마우스 클릭 지점의 좌표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()
다양한 이벤트 패턴에 대한 설명은 Events and callbacks – adding life to programs 을 참조하면 좋다. 하지만 위젯에 따라 사용 가능한 이벤트도 달라지기 때문에 인터넷 검색을 사용하여 필요한 경우에 대한 상세하며 친절한 사용법을 확인해야 한다.
파이썬에서 함수는 보통 아래 방식으로 정의된다.
def 함수이름(인자1, ..., 인자n):
함수본체
즉, 함수 이름을 먼저 선언하고 함수를 정의한다. 이렇게 하면, 함수 이름을 이용하여 필요한 기능을 재사용할 수 있으며, 함수가 어떻게 정의되었는지 굳이 몰라도 함수의 기본 기능만 알면 사용할 수 있다. 즉, 코드 추상화의 주요 도구 중의 하나로 사용된다.
하지만 경우에 따라 한 번만 사용할 간단한 함수를 이름 없이
정의하면 좋을 때가 있으며,
그럴 때 람다(lambda) 함수를 이용한다.
예를 들어, 이전 코드에서 사용한 eventCallback()
함수를
단순화 해서 아래와 같이 정의하자.
def eventCallback(event):
print("클릭 좌표:", event.x, event.y)
eventCallback()
함수와 동일한 기능을 수행하는 람다 함수는 다음과 같다.
lambda event: print("클릭 좌표:", event.x, event.y)
위 람다 함수를 이용하여 이전 코드를 다시 작성하면 다음과 같다.
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()
tkinter
모듈을 이용하여 보다 유용한 애플리케이션을 구현하는 과정을 살펴본다.
여기서 구현하는 애플리케이션은 실제로 작동하는
아래 모양의 계산기이다.
참조: 계산기 관련 코드는 Python Calculator – Create A Simple GUI Calculator Using Tkinter 의 내용을 참조하였다.
"계산기" GUI 애플리케이션의 구성 요소는 다음과 같다.
스크린은 tkinter
모듈에 포함된 Tk
클래스의 인스턴스로 구할 수 있다.
또한 버튼 입력 내용과 연산 실행 결과를 보여주는 창을 생성하려면 아래 도구를 사용한다.
StringVar
클래스: 문자열을 저장한 후 확인 및 업데이트 기능 제공Entry
클래스: 한 줄 문장을 입력받고 보여주는 제공textvariable
인스턴스 속성StringVar
클래스의 객체를 지정하면
창에 입력된 문자열과 StringVar
객체에 저장된 값이 연동됨.StringVar
의 set
, get
메서드를 이용하여 화면에 보여지는 내용 업데이트 가능.pack()
메서드 인자side
: 지정된 스크린에 추가할 때 위치 지정expand=YES
, fill=BOTH
: 스크린을 자유자재로 조정 가능하게 만듦.
구체적인 사용법은 직접 실험해보거나
tkinter 공식 문서 참조.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()
위 프로그램을 실행하면 지정된 타이틀과 입력 내용과 연산 실행 결과를 보여주는 입력창이 생성된 스크린이 아래와 같이 생성된다.
Frame
과 Button
클래스의 활용법을 이전에 살펴보았다.
여기서는 많은 프레임과 버튼을 생성해야 하기에
해당 클래스의 객체 생성과 추가하기(pack)를 지원하는
두 개의 함수를 선언한다.
frameInstance
: Frame
객체 생성 및 지정된 스크린에 생성된 프레임 추가하기(pack)buttonInstance
: Button
객체 생성 및 지정된 프레임에 생성된 버튼 추가하기(pack)코드의 나머지 부분은 실행 결과를 모두 삭제하는 클리어('C') 버튼을 추가하는 내용이다.
command
속성display
가 가리키는 StringVar()
객체에 담긴 문자열을 빈 문자열로 초기화.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('')
이제 나머지 버튼을 한꺼번에 생성하면서 동시에 이벤트 처리도 함께 다룬다. 이벤트 처리는 기본적으로 모두 동일하다. 다만 등호('=') 버튼의 이벤트 처리가 다른 버튼들과 다른 점에 주의해야 한다.
행별로 생성할 버튼과 순서는 다음과 같다.
위 버튼을 자동으로 생성하기 위해 2중 for
반복문을 사용한다.
for
반복문:Frame
객체 생성: 행에 포함되는 네 개의 버튼을 담는 프레임for
반복문각각의 버튼에 대한 콜백 함수는 다음과 같다.
display
창에 차례대로 입력됨.eval()
함수를 활용하여 문자열 내용을 해석함.eval()
함수는 문자열에 의미를 부여하여 실행하는 함수임.
의미가 부여될 수 없으면 오류 발생.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()
함수는 문자열로 주어진 표현식에 실제 의미를 부여한다.
eval("1+1")
eval("10*3/5 - 2")
문자열 내용이 의미 없는 표현식이면 구문 오류(SyntaxError)가 발생한다.
try:
eval("1-3*")
except SyntaxError:
print("제대로된 표현식이 아닙니다.")
그런데 command=calc
대신에 command=lamda: calc()
를 사용하였다.
이유는 lambda
함수로 정의하면 해당 람다 함수가
실제로 호출되어 실행될 때까지 함수 본체가 외부에 노출되지 않기 때문이다.
즉, 해당 람다 함수가 실행되기 전까지 파이썬 해석기가 람다 함수의 본체에
전혀 신경를 쓰지 않기 때문에 calc()
함수를 나중에 선언해도 된다.
반면에 command=calc
라고 선언하면 파이썬 해석기가
calc()
함수가 이미 선언되어 있는가를 확인한다.
그런데 위 코드에서는 함수가 버튼을 생성한 다음에 선언되어서
오류가 발생한다.
물론 아래 코드에서 처럼 calc()
함수를 먼저 선언하면 제대로 작동한다.
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()
tkinter 공식 문서 를 참조하여
계산기 애플리케이션을 생성하는 코드를 Calculator
라는 클래스에 모두 포함되도록 구현하라.
즉, 아래 명령문 하나로 계산기 애플리케이션이 실행되어야 한다.
Calculator().mainloop()
위 코드는 아래 코드와 동일하게 작동한다.
app = Calculator()
app.mainloop()
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(...)
등으로 사용하면 오류 발생함.
(함수 내부에서 선언된 지역변수는 외부에서 사용 못하기 때문임) 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()
frameInstance()
함수와 buttonInstance()
함수를 기본적으로
계산기 애플리케이션의 보조 함수로 사용하기에 클래스 내부로 포함시키는 것도 괜찮아 보인다.
다만, self
의 남용을 방지하기 위해 클래스 메서드로 선언하는 것이 좋아 보인다.
따라서 두 함수를 사용할 때마다 클래스 이름을 접두사로 덧붙혀야 한다.
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()