Programming/Projects

[Python] 계산기 만들기 - tkinter

Brian Go 2021. 9. 30. 18:59

 

 

 

가벼운 사이드 프로젝트를 하나 해볼까 싶어

뭘 할까 고민하다가 계산기를 한번 만들어보면 좋을 것 같아서

바로 ㄱㄱ

 

기획을 해보면

1. 디스플레이 창에는 입력 불가, 두 개의 디스플레이. 하나는 입력값, 하나는 지금까지 입력한 식.

2. 연속으로 기호 입력 금지.

3. 기호 이후에 = 도 금지.

 

그러면 일단 처음 해야 할 것은 엔트리 두 개 만들기와 버튼 만들기.

드가자 !

 

import tkinter as tk
import tkinter.messagebox as msgbox

root = tk.Tk()
root.resizable(False, False)
root.title('계산기')

#화면을 계속 업데이트하기
root.mainloop()

 

우선 이렇게 위젯을 만들어준다.

엔트리를 만들 차례. 그런데 우리는 입력하는대로 족족 값이 업데이트 되기를 원한다.

이럴 때 쓰는게 바로 tk.Stringvar() 객체.

이걸 만들어주고, 하나의 변수를 정해서 tk.stringvar().set(변수) 꼴로 해주면

우리가 원하는대로 엔트리에 자동으로 값을 업데이트 할 수 있다.

 

값을 설정할 때마다 set 해주면 된다.

display_val = 0
sub_val = ''

str_value = tk.StringVar()
str_value.set(str(display_val))

entry= tk.Entry(root, state='readonly', textvariable=str_value, justify='right')
entry.grid(row=0, column=0, columnspan=4, ipadx=80, ipady=30)

sub_str_value = tk.StringVar()
sub_str_value.set(sub_val)

sub_entry = tk.Entry(root, state='readonly', textvariable=sub_str_value, justify='right')
sub_entry.grid(row=1,column=2, columnspan=2, ipadx=10, ipady=10)

 

이렇게 엔트리를 만들어준다.

값을 나타내는 엔트리는 크고 0,0에서 네 칸, 식을 나타낼 엔트리는 2,1에서 두 칸을 잡아먹도록 설정했다.

 

 

 

렇게 두 개의 엔트리가 입력 불가능한 상태로 잘 나타나는 것을 볼 수 있다.

 

이제 버튼을 만들어보자.

반복작업은 귀찮으니, 반복문으로 만들 수는 없을까?? 하다가 생각난 방법.

대충 버튼이 숫자 0~9, +-*/=에 클리어까지 16개만 있으면 되고

1은 (0,2), 2는 (1,2) ...

5는 (0,3) .. 식이니까

row를 2부터 시작해서 네 개마다 하나씩 늘리고, column은 그냥 하면 되겠다.

그럼 num 리스트를 만들고 버튼에 적용해보자.

 

num = ([1,2,3,4], [5,6,7,8], [9,'0','+','-'], ['/', '*', '=', 'C'])
for i, items in enumerate(num, start=2):
    for j, item in enumerate(items):
        btn = tk.Button(root, text=item, width=10, height=5,\ (줄바꾸기 위해 enter)
 command= lambda x=item: click(x))
        btn.grid(column=j, row=i)

 

일단 command에 함수도 지정을 해봤다. click이란 함수인데, 모든 버튼은 다른 item을 가지고 있기에

command=에 '인자를 가지는 함수'를 넣어야 한다. 그러나 그냥 command=click(item)이라고 하면

command는 작동하지 않는다.

 

그래서 lambda를 이용하여 넣어줬다. 람다는 임시 함수를 만들어주는데, lambda x: function 꼴로 쓴다.

그래서 x에 item값을 넣어주고 임시 함수를 실행시키는 것.

이제 click함수만 지정해주면 끝날 것 같다.

 

사실 여기서 처음에 계산을 일일히 함수에 넣어서 만들었으나

곱하기나 나누기를 더하기빼기보다 먼저 계산하지 않는 현상이 일어났다.

그래서 고민을 하다가, 그냥 문자열 형식으로 값을 보존한다음 eval이라는 것을 이용해서

문자열 그대로 계산하는 방법을 찾아냈다.

 

여기서 고려해야 할 점은 1. 버튼을 누르면 그에 따른 값이 디스플레이에 올라가기

2. 기호를 눌렀을 때 이전까지 입력한 값이 sub entry 에 올라가기

3. 기호를 연속으로 두 번 누르거나 기호 다음에 = 를 누를 시 오류 메세지를 띄우기.

 

그러기 위해서 일단 위에서 설정한 display_val 과 sub_val이 필요하고,

이전에 누른 기호의 값을 알고 있어야 하기 때문에 operation이라는 기호 모음 튜플과 oper_val이란 변수를 만들겠다.

display_val = 0
oper_val = ''
sub_val = ''
operation = ('+', '-', '/', '*')

def click(value):
    global oper_val, sub_val, display_val
    pass

 

 

우선 이렇게 두자.

이제 누른 버튼값이 숫자형인지, 문자열인지에 따라서 조건을 나눠주면 되는데, 예외처리를 이용해보자.

문자열은 정수형으로 전환이 안되니까 이를 이용해서 나눠보면 될 것 같다.

숫자 버튼을 누르면 입력값 창은 0이 되어야 하는데 숫자를 누를 때마다 초기화할 순 없으니

기호를 누를 때 입력값을 0으로 만들어주고,

숫자 키를 누르면 그걸 업데이트 하는 식으로 해야겠다.

 

 

def click(value):
    global oper_val, sub_val, display_val
    try:
        str_value.set(str(display_val)) #값 업데이트
        value = int(value) #여기서 문자열이라면 except로 빠질 것
        display_val = display_val * 10 + value #십의 자리 단위로 밀리면서 입력
        str_value.set(str(display_val))

    except:
       
        if value == '=': #=버튼을 누르면
            
            sub_val = sub_val + str(display_val) #전체 식을 나타내기 위해 기존 값 문자열+방금 입력한 값 문자열
            display_val = eval(sub_val) #디스플레이 값=전체 식 문자열을 계산
            str_value.set(str(display_val)) #디스플레이 값 업데이트
            sub_str_value.set(sub_val)
            display_val = 0
            sub_val = ''
        if value =='C': #C버튼을 누르면
            display_val = 0 #디스플레이 초기화
            sub_val = '' # 전체 식 초기화
            oper_val= '' #이전에 누른 기호값 초기화
            str_value.set(str(display_val))
            sub_str_value.set(sub_val)
 
        elif oper_val in operation: #기호를 연속으로 입력할 시
             msgbox.showerror('error', 'Cannot insert operation continuesly. Please insert Numbers.')
        
        else:
            sub_val = sub_val+str(display_val)+str(value) #기존 식+디스플레이 창+ 기호
            display_val = 0
            oper_val = value #누른 기호값 저장
            str_value.set(str(display_val))
            sub_str_value.set(sub_val)

 

이제 이전에 누른 기호값을 저장은 했는데, 초기화를 어디서 시켜줘야 될까?

연속으로 기호를 못 누르도록 해놓은 값이니까 숫자를 입력하면 oper_val 변수를 초기화해주면 되지 않을까?

그러니까 try 부분의 끝에 초기화를 넣어주자.

 

try:
        str_value.set(str(display_val))
        value = int(value) #여기서 문자열이라면 except로 빠질 것
        display_val = display_val * 10 + value
        str_value.set(str(display_val))
        oper_val = '' #기호 기록 초기화

 

마지막으로 기호 뒤에 =을 누르면 에러 메세지가 나오게만 해주면 된다.

그러면 if value == '=' 부분에다

 

if oper_val in operation:
                msgbox.showerror('Error', 'Operation in last. Please enter some numbers in front.')
                return

 

이 한 줄을 넣어주면 될 것 같다. 그러면 메세지가 뜨고 함수가 멈춰서 변경점이 없을 것.

 

그래서 최종 코드는

 

import tkinter as tk
import tkinter.messagebox as msgbox
from typing import SupportsBytes

root = tk.Tk()
root.resizable(False, False)
root.title('계산기')


display_val = 0
oper_val = ''
sub_val = ''

str_value = tk.StringVar()
str_value.set(str(display_val))

entry= tk.Entry(root, state='readonly', textvariable=str_value, justify='right')
entry.grid(row=0, column=0, columnspan=4, ipadx=80, ipady=30)

sub_str_value = tk.StringVar()
sub_str_value.set(sub_val)

sub_entry = tk.Entry(root, state='readonly', textvariable=sub_str_value, justify='right')
sub_entry.grid(row=1,column=2, columnspan=2, ipadx=10, ipady=10)

num = ([1,2,3,4], [5,6,7,8], [9,'0','+','-'], ['/', '*', '=', 'C'])
operation = ('+', '-', '/', '*')

    
def click(value):
    global display_val, oper_btn, sub_val, oper_val

    try:
        str_value.set(str(display_val))
        value = int(value) #여기서 문자열이라면 except로 빠질 것
        display_val = display_val * 10 + value
        str_value.set(str(display_val))
        oper_val = ''

    except:
       
        if value == '=':
            if oper_val in operation:
                print(1)
                msgbox.showerror('Error', 'Operation in last. Please enter some numbers first.')
                return
            else:
                print(sub_val)
                sub_val = sub_val + str(display_val)
                print(sub_val)
                display_val = eval(sub_val)
                print(sub_val)
                str_value.set(str(display_val))
                sub_str_value.set(sub_val)
                display_val = 0
                sub_val = ''

        elif value =='C':
            display_val = 0
            sub_val = ''
            oper_val= ''
            str_value.set(str(display_val))
            sub_str_value.set(sub_val)
        
        elif oper_val in operation:
             msgbox.showerror('error', 'Cannot insert operation continuesly. Please insert Numbers.')
        
        else:
            sub_val = sub_val+str(display_val)+str(value)
            display_val = 0
            oper_val = value
            str_value.set(str(display_val))
            sub_str_value.set(sub_val)

for i, items in enumerate(num, start=2):
    for j, item in enumerate(items):
        btn = tk.Button(root, text=item, width=10, height=5, command= lambda cmd=item: click(cmd))
        btn.grid(column=j, row=i)

root.mainloop()

이렇게 약 80줄이 나온다.

작동시켜보면 잘 된다 !

 

 

사실 eval함수는 쓰기 좋은 함수는 아니다.

문자열을 계산하는게 아니라 '실행'하기 때문.

그래서 저기다 명령어를 넣으면 실행이 된다. 그 과정에서 컴퓨터의 중요한 정보가 노출될 수도 있고,

파일이 손상될 수도 있다.

하지만 계산기는 타자로 입력 불가능하고 입력값도 제한적이기 때문에

써도 된다고 판단했다.

 


 

이번 프로젝트에서 배운 것:

1. lambda 를 이용하여 한 줄로 함수를 편하게 만들 수 있다.

2. tkinter에서 stringvar로 값을 업데이트 해줄 수 있다.

3. eval 안 쓰고 하려면 진짜 힘들다. 개인적으로는 곱셈과 나눗셈에 우선순위를 주는 방법을

찾아낼 수가 없었고, 코드 자체도 훨씬 길게 나왔다.

4. 코딩을 하면 시력이 떨어진다 ㅋ