계산기 만들기
파이썬 학습의 기초를 다진 후, 이를 실제 프로그램으로 구현해 보는 것은 매우 중요합니다. 이번 실전 프로젝트에서는 콘솔 환경에서 동작하는 기본 계산기부터, PySide6와 Flet을 활용한 현대적인 GUI 데스크톱 계산기까지 단계별로 구현해 봅니다.
2. 1단계: 콘솔 기반 계산기 (input 함수 활용 – Non GUI)
가장 먼저 별도의 그래픽 인터페이스 없이 Python의 기본 input() 함수와 조건문을 활용하여 사칙연산을 수행하는 콘솔(Non-GUI) 계산기를 구현합니다.
핵심 소스코드
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
def divide(x, y):
if y == 0:
return "Error: Division by zero"
return x / y
print("선택하세요:")
print("1. 덧셈")
print("2. 뺄셈")
print("3. 곱셈")
print("4. 나눗셈")
choice = input("옵션을 선택하세요(1/2/3/4): ")
num1 = float(input("첫 번째 숫자를 입력하세요: "))
num2 = float(input("두 번째 숫자를 입력하세요: "))
if choice == '1':
print("결과:", add(num1, num2))
elif choice == '2':
print("결과:", subtract(num1, num2))
elif choice == '3':
print("결과:", multiply(num1, num2))
elif choice == '4':
print("결과:", divide(num1, num2))
else:
print("잘못된 입력입니다.")
3. 2단계: PySide6를 이용한 GUI 계산기
PySide6(Qt for Python)는 강력하고 전문적인 데스크톱 애플리케이션을 만들 수 있는 크로스 플랫폼 프레임워크입니다. 격자형 레이아웃(QGridLayout)과 스타일시트(QSS), 그리고 파이썬의 eval() 함수를 활용하여 간결하게 계산기를 구현합니다.
주요 특징 및 구조
-
QSS (Qt Style Sheets): CSS와 유사한 방식으로 버튼과 입력창(
QLineEdit)의 디자인, 색상, 테두리 둥글기 등을 일괄 제어합니다. -
QGridLayout: 5줄 4칸의 격자 구조를 생성하고,
columnSpan속성을 활용해 ‘0’ 버튼(2칸)과 ‘=’ 버튼(3칸)의 너비를 확장합니다. -
계산 로직:
button_click함수에서 연산자 기호(÷,x)를 파이썬 연산자(/,*)로 치환한 뒤eval()을 통해 수식을 계산합니다.
핵심 소스코드
import sys
import re
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QGridLayout,
QLineEdit, QPushButton, QVBoxLayout
)
from PySide6.QtCore import Qt, QSize
WINDOW_WIDTH, WINDOW_HEIGHT = 300, 450
TEXTFIELD_HEIGHT = 70
QSS = f"""
QWidget {{ background-color: #ECEFF1; }}
QLineEdit {{
background-color: white; color: black; border: 1px solid #B0BEC5;
font-size: 35px; padding-right: 15px; height: {TEXTFIELD_HEIGHT}px;
}}
QPushButton {{ height: 60px; font-size: 20px; border-radius: 10px; }}
QPushButton[style_type="num"] {{ background-color: white; color: black; }}
QPushButton[style_type="clear"] {{ background-color: #81D4FA; color: black; }}
QPushButton[style_type="equal"] {{ background-color: #FF7043; color: white; }}
"""
class CalculatorApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PySide6 계산기")
self.setFixedSize(QSize(WINDOW_WIDTH, WINDOW_HEIGHT))
central_widget = QWidget()
self.setCentralWidget(central_widget)
central_widget.setStyleSheet(QSS)
self.main_layout = QVBoxLayout(central_widget)
self.init_ui()
def init_ui(self):
self.result = QLineEdit("0")
self.result.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.result.setReadOnly(True)
self.main_layout.addWidget(self.result)
buttons = {
"7": "num", "8": "num", "9": "num", "÷": "num",
"4": "num", "5": "num", "6": "num", "x": "num",
"1": "num", "2": "num", "3": "num", "-": "num",
"0": "num", ".": "num", "+": "num",
"C": "clear", "=": "equal"
}
grid_layout = QGridLayout()
grid_layout.setHorizontalSpacing(10)
grid_layout.setVerticalSpacing(10)
widgets = {}
for text, style_type in buttons.items():
btn = QPushButton(text)
btn.setProperty("style_type", style_type)
btn.clicked.connect(lambda _, t=text: self.button_click(t))
widgets[text] = btn
grid_layout.addWidget(widgets["7"], 0, 0)
grid_layout.addWidget(widgets["8"], 0, 1)
grid_layout.addWidget(widgets["9"], 0, 2)
grid_layout.addWidget(widgets["÷"], 0, 3)
grid_layout.addWidget(widgets["4"], 1, 0)
grid_layout.addWidget(widgets["5"], 1, 1)
grid_layout.addWidget(widgets["6"], 1, 2)
grid_layout.addWidget(widgets["x"], 1, 3)
grid_layout.addWidget(widgets["1"], 2, 0)
grid_layout.addWidget(widgets["2"], 2, 1)
grid_layout.addWidget(widgets["3"], 2, 2)
grid_layout.addWidget(widgets["-"], 2, 3)
grid_layout.addWidget(widgets["0"], 3, 0, 1, 2)
grid_layout.addWidget(widgets["."], 3, 2)
grid_layout.addWidget(widgets["+"], 3, 3)
grid_layout.addWidget(widgets["C"], 4, 0)
grid_layout.addWidget(widgets["="], 4, 1, 1, 3)
self.main_layout.addLayout(grid_layout)
def button_click(self, text):
current = self.result.text()
if text == "C":
self.result.setText("0")
elif text == "=":
try:
expr = current.replace("÷", "/").replace("x", "*")
res = eval(expr)
if isinstance(res, (int, float)):
res = int(res) if res == int(res) else round(res, 8)
self.result.setText(str(res))
except Exception:
self.result.setText("Error")
else:
if current in ("0", "Error"):
self.result.setText(text)
else:
self.result.setText(current + text)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = CalculatorApp()
window.show()
sys.exit(app.exec())
4. 3단계: Flet을 이용한 GUI 계산기 (Flutter 기반)
Flet은 Google의 Flutter 프레임워크를 파이썬에서 사용할 수 있도록 지원하는 모던 프레임워크입니다. 웹, 데스크톱, 모바일을 아우르는 유려한 UI 디자인을 파이썬 코드만으로 손쉽게 구현할 수 있습니다.
주요 특징 및 구조
-
정밀한 크기 계산: 창의 크기 및 확장 버튼(
0버튼,=버튼)의 너비를 수학적으로 정확하게 산출하여 배치합니다. -
ft.Row / ft.Column: Flet의 수평·수직 레이아웃 컴포넌트를 조합하여 5×4 그리드 구조를 완성합니다.
-
단일 이벤트 핸들러: 모든 버튼이 하나의
button_click함수를 공유하며,e.control.text를 통해 입력값을 판별하고page.update()로 UI를 즉시 갱신합니다.
핵심 소스코드
import re
import flet as ft
BUTTON_WIDTH, BUTTON_HEIGHT = 60, 60
ROW_SPACING, COLUMN_SPACING = 10, 10
CONTAINER_PADDING = 15
GRID_COLS, GRID_ROWS = 4, 5
TEXTFIELD_HEIGHT = 70
DOUBLE_BUTTON_WIDTH = (2 * BUTTON_WIDTH) + ROW_SPACING
TRIPLE_BUTTON_WIDTH = (3 * BUTTON_WIDTH) + (2 * ROW_SPACING)
CALC_WIDTH = (BUTTON_WIDTH * GRID_COLS) + (ROW_SPACING * (GRID_COLS - 1)) + (CONTAINER_PADDING * 2)
CALC_HEIGHT = (TEXTFIELD_HEIGHT + 10) + (BUTTON_HEIGHT * GRID_ROWS) + (COLUMN_SPACING * GRID_ROWS) + (CONTAINER_PADDING * 2)
APP_BG_COLOR = ft.Colors.BLUE_GREY_50
BTN_STYLE_NUM = ft.ButtonStyle(
bgcolor={"default": ft.Colors.WHITE, "hovered": ft.Colors.BLUE_GREY_100},
color={"default": ft.Colors.BLACK},
shape=ft.RoundedRectangleBorder(radius=10),
)
BTN_STYLE_CLEAR = ft.ButtonStyle(
bgcolor={"default": ft.Colors.LIGHT_BLUE_200, "hovered": ft.Colors.LIGHT_BLUE_300},
color={"default": ft.Colors.BLACK},
shape=ft.RoundedRectangleBorder(radius=10),
)
BTN_STYLE_EQUAL = ft.ButtonStyle(
bgcolor={"default": ft.Colors.DEEP_ORANGE_400, "hovered": ft.Colors.DEEP_ORANGE_300},
color={"default": ft.Colors.WHITE},
shape=ft.RoundedRectangleBorder(radius=10),
)
def main(page: ft.Page):
page.title = "Flet 계산기"
page.window.width = CALC_WIDTH + 30
page.window.height = CALC_HEIGHT + 40
page.window.resizable = False
page.bgcolor = APP_BG_COLOR
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
result = ft.TextField(
value="0", text_align=ft.TextAlign.RIGHT, width=CALC_WIDTH - (CONTAINER_PADDING * 2),
read_only=True, text_size=35, height=TEXTFIELD_HEIGHT, bgcolor=ft.Colors.WHITE, color=ft.Colors.BLACK
)
def button_click(e):
current = result.value
text = e.control.text
if text == "C":
result.value = "0"
elif text == "=":
try:
expr = current.replace("÷", "/").replace("x", "*")
res = eval(expr)
result.value = str(int(res) if res == int(res) else round(res, 8))
except Exception:
result.value = "Error"
else:
if current in ("0", "Error"):
result.value = text
else:
result.value += text
page.update()
def create_btn(text, style, width=BUTTON_WIDTH):
return ft.ElevatedButton(text=text, on_click=button_click, style=style, width=width, height=BUTTON_HEIGHT, content=ft.Text(text, size=20))
row1 = ft.Row([create_btn("7", BTN_STYLE_NUM), create_btn("8", BTN_STYLE_NUM), create_btn("9", BTN_STYLE_NUM), create_btn("÷", BTN_STYLE_NUM)], alignment=ft.MainAxisAlignment.CENTER, spacing=ROW_SPACING)
row2 = ft.Row([create_btn("4", BTN_STYLE_NUM), create_btn("5", BTN_STYLE_NUM), create_btn("6", BTN_STYLE_NUM), create_btn("x", BTN_STYLE_NUM)], alignment=ft.MainAxisAlignment.CENTER, spacing=ROW_SPACING)
row3 = ft.Row([create_btn("1", BTN_STYLE_NUM), create_btn("2", BTN_STYLE_NUM), create_btn("3", BTN_STYLE_NUM), create_btn("-", BTN_STYLE_NUM)], alignment=ft.MainAxisAlignment.CENTER, spacing=ROW_SPACING)
row4 = ft.Row([create_btn("0", BTN_STYLE_NUM, width=DOUBLE_BUTTON_WIDTH), create_btn(".", BTN_STYLE_NUM), create_btn("+", BTN_STYLE_NUM)], alignment=ft.MainAxisAlignment.CENTER, spacing=ROW_SPACING)
row5 = ft.Row([create_btn("C", BTN_STYLE_CLEAR), create_btn("=", BTN_STYLE_EQUAL, width=TRIPLE_BUTTON_WIDTH)], alignment=ft.MainAxisAlignment.CENTER, spacing=ROW_SPACING)
page.add(
ft.Container(
content=ft.Column([
result, ft.Divider(height=10, color=ft.Colors.TRANSPARENT),
row1, ft.Divider(height=10, color=ft.Colors.TRANSPARENT),
row2, ft.Divider(height=10, color=ft.Colors.TRANSPARENT),
row3, ft.Divider(height=10, color=ft.Colors.TRANSPARENT),
row4, ft.Divider(height=10, color=ft.Colors.TRANSPARENT),
row5
], spacing=0),
padding=CONTAINER_PADDING, bgcolor=APP_BG_COLOR, width=CALC_WIDTH, height=CALC_HEIGHT
)
)
if __name__ == "__main__":
ft.app(target=main, view=ft.AppView.FLET_APP)