본문 바로가기

카테고리 없음

PyQT6 연습

pyqt6 공부하고 있어 맨 먼저 첨부된 UI를 로딩하고 표시하는 코드를 보여주고 모든 라인에 주석을 달아줘

ChatGPT의 말:

좋습니다 👍 지금 첨부해주신 caculator.ui 파일은 Qt Designer에서 만든 UI 파일이에요. 이걸 PyQt6에서 불러와서 표시하려면 PyQt6.uic.loadUi 또는 uic.loadUiType을 활용할 수 있습니다. 여기서는 UI를 직접 로딩해서 보여주는 가장 기본적인 코드를 작성하고, 요청하신 대로 모든 라인에 주석을 달아드릴게요.

 
import sys # 시스템 관련 기능(명령행 인자, 종료 등)을 사용하기 위해 sys 모듈 임포트 from PyQt6 import QtWidgets, uic # PyQt6 위젯 모듈과 uic(디자이너 UI 로딩 모듈) 임포트 # QApplication 객체 생성 # 모든 PyQt 애플리케이션은 반드시 QApplication 객체가 있어야 GUI가 실행됨 app = QtWidgets.QApplication(sys.argv) # .ui 파일을 로딩해서 QWidget 객체 생성 # uic.loadUi() 함수는 Qt Designer에서 만든 .ui 파일을 읽어서 파이썬 객체로 변환함 window = uic.loadUi("caculator.ui") # 생성된 윈도우(메인 위젯)를 화면에 표시 window.show() # 이벤트 루프 실행 # app.exec()은 프로그램이 종료될 때까지 사용자 입력 이벤트를 처리함 sys.exit(app.exec())

코드 설명 정리

  1. sys 모듈 → 프로그램 실행 종료(sys.exit)와 실행 인자(sys.argv) 처리를 위해 필요.
  2. QtWidgets → PyQt의 기본 위젯(버튼, 윈도우 등)을 사용하기 위해 불러옴.
  3. uic 모듈 → .ui 파일을 직접 불러와 파이썬 객체로 바꿔주는 역할.
  4. QApplication → 모든 PyQt GUI 프로그램은 반드시 있어야 하며, 이벤트 루프를 관리.
  5. uic.loadUi("파일경로") → UI 파일을 로딩해서 PyQt 객체로 변환. 여기서는 window라는 변수에 담음.
  6. show() → 윈도우를 실제 화면에 표시.
  7. sys.exit(app.exec()) → 이벤트 루프 실행 후, 프로그램이 종료될 때 정리(clean exit)하도록 처리.

👉 질문: 이 UI 파일(caculator.ui)을 단순히 로딩해서 보여주는 것만 원하시나요, 아니면 버튼 클릭 같은 시그널-슬롯 연결까지 포함된 기본 예제도 같이 보여드릴까요?

나의 말:

버튼클릭해서 표시하는것 까지 하자 AC를 누르면 다 지워지고

ChatGPT의 말:

 
14s 동안 생각함

좋아요! 첨부된 caculator.ui를 로드해서 표시하고, 숫자/연산 버튼 클릭 시 화면(라인에디트)에 표시, AC 버튼으로 전체 지우기까지 동작하는 최소 예제를 PyQt6로 작성했습니다.
요청하신 대로 모든 라인에 주석을 달았습니다. (UI 내부 위젯 이름: led(QLineEdit), btn_0~btn_9, btn_plus, btn_minus, btn_multiply(표시 X), btn_divide(/), btn_decimal(.), btn_equals(=), btn_ac(AC), btn_plus_minus(+/-), btn_percent(%) 가 확인되었습니다.)

 
import sys # 명령행 인자 처리와 정상 종료(sys.exit) 등을 위해 sys 모듈 임포트 from PyQt6 import QtWidgets, uic # PyQt6의 위젯과 .ui 로더(uic) 임포트 from PyQt6.QtWidgets import QMainWindow, QPushButton # 타입 힌트와 findChild 사용 편의를 위해 임포트 class CalculatorApp(QMainWindow): # 메인 윈도우로 사용할 클래스 선언(QMainWindow 상속) def __init__(self): # 객체 초기화 메서드 super().__init__() # 부모 클래스 초기화 uic.loadUi("caculator.ui", self) # Qt Designer에서 만든 .ui 파일을 현재 인스턴스(self)에 로드 self.display = self.findChild(QtWidgets.QLineEdit, "led") # UI 내 QLineEdit(이름: led) 참조 저장 self._connect_buttons() # 모든 버튼 시그널(클릭)과 슬롯(처리 함수) 연결 def _append_text(self, s: str): # 숫자/연산 기호를 표시창 끝에 붙이는 헬퍼 함수 current = self.display.text() # 현재 표시된 문자열 가져오기 self.display.setText(current + s) # 끝에 새 문자(s) 이어 붙여서 설정 def _clear_all(self): # AC 버튼: 전체 지우기 self.display.clear() # 표시창 텍스트를 빈 문자열로 설정(초기화) def _toggle_sign(self): # +/- 버튼: 부호 토글 txt = self.display.text() # 현재 문자열 if not txt: # 문자열이 비어 있으면 return # 아무 것도 하지 않음 try: # 숫자 변환을 시도 val = float(txt) # 실수로 변환 val = -val # 부호 반전 # 정수로 떨어지면 보기 좋게 정수 형태로, 아니면 그대로 문자열로 표시 self.display.setText(str(int(val)) if val.is_integer() else str(val)) # 결과 반영 except ValueError: # 숫자로 변환 불가(중간 수식 등) 시 if txt.startswith("-"): # 맨 앞에 -가 있으면 self.display.setText(txt[1:]) # - 제거 else: # -가 없으면 self.display.setText("-" + txt) # 맨 앞에 - 추가 def _percent(self): # % 버튼: 백분율(100으로 나누기) txt = self.display.text() # 현재 문자열 if not txt: # 비어 있으면 return # 아무 것도 하지 않음 try: # 숫자라면 val = float(txt) # 실수 변환 val = val / 100.0 # 100으로 나누어 백분율 처리 self.display.setText(str(int(val)) if val.is_integer() else str(val)) # 결과 표시 except ValueError: # 숫자가 아니면(수식 등) # 간단 처리: 표현식 끝에 /100을 추가하여 사용자 의도에 맞춤 self._append_text("/100") # 표현식 뒤에 /100 이어 붙임 def _evaluate(self): # = 버튼: 간단한 계산 실행 expr = self.display.text() # 현재 수식 문자열 if not expr: # 비어 있으면 return # 아무 것도 하지 않음 safe = expr.replace("X", "*") # UI에서 곱셈이 'X'로 표시되므로 파이썬 연산자 '*'로 치환 # 허용 문자만 남았는지 간단 검증(숫자, 괄호, 기본 연산자, 소수점, 공백) for ch in safe: # 문자열의 각 문자 검사 if ch not in "0123456789.+-*/()% " : # 허용 집합에 없으면 self.display.setText("Error") # 에러 표시 return # 계산 중단 try: # 계산 시도 result = eval(safe, {"__builtins__": {}}, {}) # 내장 제거한 환경에서 eval로 계산(간단용) # 정수로 떨어지면 정수 형태로, 아니면 실수 형태 그대로 표시 self.display.setText(str(int(result)) if isinstance(result, (int, float)) and float(result).is_integer() else str(result)) # 결과 반영 except Exception: # 계산 중 오류 발생 self.display.setText("Error") # 에러 표시 def _connect_buttons(self): # 버튼을 찾아서 클릭 시 동작 연결 # 숫자 버튼(0~9) 연결 for n in range(10): # 0부터 9까지 반복 btn = self.findChild(QPushButton, f"btn_{n}") # 예: btn_0, btn_1, ... if btn: # 버튼이 실제로 존재하면 btn.clicked.connect(lambda _, t=str(n): self._append_text(t)) # 클릭 시 해당 숫자 텍스트 추가 # 소수점 버튼(.) 연결 btn_decimal = self.findChild(QPushButton, "btn_decimal") # 이름으로 버튼 찾기 if btn_decimal: # 버튼 존재 시 btn_decimal.clicked.connect(lambda _: self._append_text(".")) # 클릭 시 '.' 추가 # 사칙연산 버튼(+,-,X,/) 연결 ops = { # 표시 텍스트 매핑(표시 문자 그대로 붙이고, X는 나중에 평가 시 *로 치환) "btn_plus": "+", # 더하기 "btn_minus": "-", # 빼기 "btn_multiply": "X", # 곱하기(표시용 X) "btn_divide": "/", # 나누기 } # 딕셔너리 종료 for name, sym in ops.items(): # 각 항목 순회 btn = self.findChild(QPushButton, name) # 버튼 찾기 if btn: # 버튼 존재 시 btn.clicked.connect(lambda _, t=sym: self._append_text(t)) # 클릭 시 해당 연산자 추가 # AC(전체 지우기) 버튼 연결 btn_ac = self.findChild(QPushButton, "btn_ac") # AC 버튼 찾기 if btn_ac: # 버튼 존재 시 btn_ac.clicked.connect(self._clear_all) # 클릭 시 표시창 초기화 # +/-(부호 토글) 버튼 연결 btn_pm = self.findChild(QPushButton, "btn_plus_minus") # +/- 버튼 찾기 if btn_pm: # 버튼 존재 시 btn_pm.clicked.connect(self._toggle_sign) # 클릭 시 부호 토글 # %(백분율) 버튼 연결 btn_percent = self.findChild(QPushButton, "btn_percent") # % 버튼 찾기 if btn_percent: # 버튼 존재 시 btn_percent.clicked.connect(self._percent) # 클릭 시 백분율 처리 # =(계산) 버튼 연결 btn_eq = self.findChild(QPushButton, "btn_equals") # = 버튼 찾기 if btn_eq: # 버튼 존재 시 btn_eq.clicked.connect(self._evaluate) # 클릭 시 수식 평가 # (선택) mode 버튼은 UI에 있으나, 여기서는 동작을 지정하지 않음 # 필요하면 아래처럼 연결 후 동작 추가 가능: # btn_mode = self.findChild(QPushButton, "btn_mode") # if btn_mode: # btn_mode.clicked.connect(lambda: print("mode clicked")) # 임시 동작 if __name__ == "__main__": # 스크립트 직접 실행 시에만 아래 코드 수행 app = QtWidgets.QApplication(sys.argv) # 모든 PyQt 애플리케이션의 진입점: QApplication 생성 win = CalculatorApp() # 메인 윈도우(계산기) 인스턴스 생성 win.show() # 윈도우 화면에 표시 sys.exit(app.exec()) # 이벤트 루프 진입(프로그램이 종료될 때까지 대기)

어떻게 동작하나요?

  • UI 로딩: uic.loadUi("caculator.ui", self) 로 Qt Designer UI를 그대로 씁니다.
  • 버튼 클릭 입력: 숫자/연산 버튼을 누르면 led에 문자를 추가합니다.
  • AC: btn_ac → self._clear_all()로 표시창 전체 지우기.
  • +/-: btn_plus_minus → self._toggle_sign()로 부호 토글.
  • %: btn_percent → self._percent()로 백분율(100으로 나누기).
  • =: btn_equals → self._evaluate()로 간단 계산 수행(X→* 치환 후 제한된 eval 사용).

참고: 곱셈 기호는 UI에서 X로 표시되어 있어서, 내부 계산 전에 *로 바꿔서 평가합니다.

필요하시면 다음을 더 확장해 드릴 수 있어요:

  • 안전한 수식 파서(eval 완전 제거, 토큰 기반 파싱)
  • 키보드 입력 지원(0~9, Enter, Backspace 등)
  • 실수/연속 연산 UX 개선(연산 중복 입력 방지, 자동 클리어 등)

나의 말:

키보드 입력만 추가해줘

ChatGPT의 말:

 
6s 동안 생각함

좋아요! 기존 예제에 키보드 입력만 깔끔하게 추가한 버전을 드릴게요.
아래 코드는 숫자(0~9), 소수점(.), 연산자(+,-,*,/, x/X), Enter/Return(=), Esc(AC), Backspace(한 글자 삭제), 괄호((, )) 및 % 입력을 처리합니다.
표시 일관성을 위해 *를 눌렀을 때는 화면에는 X로 보이도록 했고(내부 계산 시 X→*로 치환), 나머지는 화면에 입력한 그대로 표시됩니다. 요청하신 대로 모든 라인에 주석을 달았습니다.

 
import sys # 명령행 인자 처리와 정상 종료를 위해 sys 모듈 임포트 from PyQt6 import QtWidgets, uic # PyQt6 위젯과 .ui 로더(uic) 임포트 from PyQt6.QtWidgets import QMainWindow, QPushButton # QMainWindow, QPushButton 타입 사용을 위해 임포트 from PyQt6.QtCore import Qt # 키보드 이벤트에서 Qt.Key 열거형을 사용하기 위해 임포트 class CalculatorApp(QMainWindow): # 메인 윈도우 역할을 할 계산기 클래스 정의(QMainWindow 상속) def __init__(self): # 생성자 메서드 super().__init__() # 부모 클래스 초기화 uic.loadUi("caculator.ui", self) # Qt Designer에서 만든 .ui 파일을 현재 인스턴스(self)에 로드 self.display = self.findChild(QtWidgets.QLineEdit, "led") # QLineEdit(이름: led)을 찾아 표시창으로 사용 self._connect_buttons() # UI 버튼들의 클릭 시그널을 처리 함수에 연결 def _append_text(self, s: str): # 표시창 끝에 문자열을 추가하는 헬퍼 함수 current = self.display.text() # 현재 표시된 문자열을 읽음 self.display.setText(current + s) # 끝에 새 문자열 s를 이어 붙여 설정 def _clear_all(self): # AC(전체 지우기) 기능 self.display.clear() # 표시창 텍스트를 빈 문자열로 초기화 def _backspace(self): # 한 글자 지우기(Backspace 키 대응) self.display.backspace() # QLineEdit의 backspace()로 마지막 글자 삭제 def _toggle_sign(self): # +/- 부호 토글(키보드로는 F9에 매핑, 선택 기능) txt = self.display.text() # 현재 표시 문자열 if not txt: # 비어 있으면 return # 아무 것도 하지 않음 try: # 숫자 변환 가능한 경우 val = float(txt) # 실수로 변환 val = -val # 부호 반전 # 정수로 나누어떨어지면 정수로, 아니면 실수 문자열로 표시 self.display.setText(str(int(val)) if val.is_integer() else str(val)) # 결과 반영 except ValueError: # 수식 등 숫자 변환이 안 되는 경우 if txt.startswith("-"): # 맨 앞이 - 이면 self.display.setText(txt[1:]) # - 제거 else: # 아니면 self.display.setText("-" + txt) # 맨 앞에 - 추가 def _percent(self): # % 처리(100으로 나누기 또는 표현식에 /100 추가) txt = self.display.text() # 현재 문자열 if not txt: # 비어 있으면 return # 아무 것도 하지 않음 try: # 숫자라면 val = float(txt) # 실수 변환 val = val / 100.0 # 100으로 나눔 self.display.setText(str(int(val)) if val.is_integer() else str(val)) # 보기 좋게 표시 except ValueError: # 숫자가 아니면(수식 등) self._append_text("/100") # 간단히 표현식 끝에 /100을 붙여 의도를 반영 def _evaluate(self): # = 계산 실행 expr = self.display.text() # 현재 수식 문자열 if not expr: # 비어 있으면 return # 아무 것도 하지 않음 safe = expr.replace("X", "*") # 화면 표시용 'X'를 파이썬 연산자 '*'로 치환 # 허용 문자만 포함되는지 안전 체크(숫자, ., + - * / ( ) % 공백) for ch in safe: # 각 문자 반복 if ch not in "0123456789.+-*/()% " : # 허용되지 않은 문자가 나오면 self.display.setText("Error") # 에러 표시 return # 계산 중단 try: # 계산 시도 result = eval(safe, {"__builtins__": {}}, {}) # 내장 제거한 제한 환경에서 eval 수행 # 정수로 떨어지면 정수로, 아니면 실수 그대로 문자열 변환 if isinstance(result, (int, float)) and float(result).is_integer(): # 결과가 정수값이면 self.display.setText(str(int(result))) # 정수 형태로 표시 else: # 아니면 self.display.setText(str(result)) # 문자열로 표시 except Exception: # 계산 중 예외 발생 시 self.display.setText("Error") # 에러 표시 def _connect_buttons(self): # UI 버튼들과 기능 연결 for n in range(10): # 숫자 0~9 버튼 연결 btn = self.findChild(QPushButton, f"btn_{n}") # 예: btn_0, btn_1, ... if btn: # 버튼이 존재하면 btn.clicked.connect(lambda _, t=str(n): self._append_text(t)) # 클릭 시 해당 숫자 추가 btn_decimal = self.findChild(QPushButton, "btn_decimal") # 소수점 버튼 찾기 if btn_decimal: # 존재하면 btn_decimal.clicked.connect(lambda _: self._append_text(".")) # '.' 추가 ops = { # 사칙연산 버튼 매핑 "btn_plus": "+", # 더하기 "btn_minus": "-", # 빼기 "btn_multiply": "X", # 곱하기(표시용 X) "btn_divide": "/", # 나누기 } # 딕셔너리 종료 for name, sym in ops.items(): # 각 연산 버튼 처리 btn = self.findChild(QPushButton, name) # 버튼 찾기 if btn: # 존재하면 btn.clicked.connect(lambda _, t=sym: self._append_text(t)) # 클릭 시 연산자 추가 btn_ac = self.findChild(QPushButton, "btn_ac") # AC 버튼 찾기 if btn_ac: # 존재하면 btn_ac.clicked.connect(self._clear_all) # 클릭 시 전체 지우기 btn_pm = self.findChild(QPushButton, "btn_plus_minus") # +/- 버튼 찾기 if btn_pm: # 존재하면 btn_pm.clicked.connect(self._toggle_sign) # 클릭 시 부호 토글 btn_percent = self.findChild(QPushButton, "btn_percent") # % 버튼 찾기 if btn_percent: # 존재하면 btn_percent.clicked.connect(self._percent) # 클릭 시 % 처리 btn_eq = self.findChild(QPushButton, "btn_equals") # = 버튼 찾기 if btn_eq: # 존재하면 btn_eq.clicked.connect(self._evaluate) # 클릭 시 계산 실행 # ------------------------------ # 여기서부터 키보드 입력 처리 추가 # ------------------------------ def keyPressEvent(self, event): # 키가 눌렸을 때 호출되는 이벤트 핸들러 오버라이드 key = event.key() # 눌린 키 코드를 가져옴 # Enter / Return → 계산 실행 if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): # 엔터 키 두 종류 처리 self._evaluate() # 계산 실행 return # 추가 처리 없이 종료 # Esc → AC (전체 지우기) if key == Qt.Key.Key_Escape: # ESC 키 self._clear_all() # 전체 지우기 return # 종료 # Backspace → 한 글자 삭제 if key == Qt.Key.Key_Backspace: # 백스페이스 키 self._backspace() # 한 글자 지우기 return # 종료 # F9 → +/- (선택 기능: 부호 토글) if key == Qt.Key.Key_F9: # F9 키 self._toggle_sign() # 부호 토글 return # 종료 # 숫자 키(상단 숫자열) 처리: 0~9 if Qt.Key.Key_0 <= key <= Qt.Key.Key_9: # 숫자 키 범위 검사 digit = chr(key) # 키 코드를 문자로 변환('0'~'9') self._append_text(digit) # 해당 숫자 추가 return # 종료 # 넘패드 숫자 처리: NumPad 0~9 numpad_map = { # 넘패드 키와 문자 매핑 Qt.Key.Key_NumPad0: "0", # 넘패드 0 Qt.Key.Key_NumPad1: "1", # 넘패드 1 Qt.Key.Key_NumPad2: "2", # 넘패드 2 Qt.Key.Key_NumPad3: "3", # 넘패드 3 Qt.Key.Key_NumPad4: "4", # 넘패드 4 Qt.Key.Key_NumPad5: "5", # 넘패드 5 Qt.Key.Key_NumPad6: "6", # 넘패드 6 Qt.Key.Key_NumPad7: "7", # 넘패드 7 Qt.Key.Key_NumPad8: "8", # 넘패드 8 Qt.Key.Key_NumPad9: "9", # 넘패드 9 } # 딕셔너리 종료 if key in numpad_map: # 눌린 키가 넘패드 숫자이면 self._append_text(numpad_map[key]) # 해당 숫자 추가 return # 종료 # 연산자 키 처리: +, -, /, *, x, X, %, ., (, ) # 화면 표시는 기존 UI와 맞추기 위해 '*'입력 시 'X'로 보이게 처리하고, # 계산 시에는 _evaluate()에서 'X'를 '*'로 바꿔서 평가함. if key in (Qt.Key.Key_Plus, Qt.Key.Key_Minus, Qt.Key.Key_Slash): # +, -, / self._append_text(event.text()) # 눌린 기호 그대로 추가 return # 종료 if key in (Qt.Key.Key_Asterisk,): # * (상단 숫자열 또는 넘패드 별표) self._append_text("X") # 화면에는 X로 표시(일관성 유지) return # 종료 if key in (Qt.Key.Key_X,): # x 또는 X(문자 x 입력 시) # 대소문자 구분 없이 X로 표시되도록 처리 self._append_text("X") # 화면에 X 추가 return # 종료 if key in (Qt.Key.Key_Percent,): # % 키 # 간단하게 현재 입력이 숫자면 100으로 나누고, 아니면 /100 추가 self._percent() # % 처리 호출 return # 종료 if key in (Qt.Key.Key_Period, Qt.Key.Key_Comma): # . 또는 , 키(소수점으로 통일) self._append_text(".") # 소수점 추가 return # 종료 if key == Qt.Key.Key_ParenLeft: # 왼쪽 괄호 '(' self._append_text("(") # '(' 추가 return # 종료 if key == Qt.Key.Key_ParenRight: # 오른쪽 괄호 ')' self._append_text(")") # ')' 추가 return # 종료 # 그 외 입력은 기본 동작(부모 처리)에 맡김 super().keyPressEvent(event) # 필요 시 기본 처리 호출 if __name__ == "__main__": # 스크립트가 직접 실행될 때만 동작 app = QtWidgets.QApplication(sys.argv) # QApplication 인스턴스 생성(모든 PyQt 앱에 필수) win = CalculatorApp() # 계산기 메인 윈도우 인스턴스 생성 win.show() # 윈도우 화면에 표시 sys.exit(app.exec()) # 이벤트 루프 실행 후 종료 코드 반환

사용 팁

  • Enter/Return: 계산 실행
  • Esc: AC(모두 지우기)
  • Backspace: 마지막 글자 삭제
  • 숫자/연산자: 즉시 입력( * 입력 시 화면에는 X 로 보임 )
  • F9: +/- 부호 토글(선택 기능)

다음으로 원하시면 키보드 단축키(예: Ctrl+C/ Ctrl+V), 포커스 자동 이동, 잘못된 연산 중복 방지(예: ++ 금지) 같은 UX 개선도 덧붙여 드릴게요.

나의 말:

다음은 ui를 load하지 말고 caculator.ui디자인데로 코드에서 만드는 버전으로 하자. 버튼 배치는 이중for문으로 하고 style이나 칼라 특별한 모양 필요없어 최대한 코드가 단순해 지도록 만들어 보자.

ChatGPT의 말:

네 👍 이번에는 .ui 파일을 불러오지 않고 caculator.ui에 있는 계산기 UI 구조를 코드에서 직접 작성해 보겠습니다.
버튼 배치는 이중 for문으로 간단히 처리하고, 스타일/색상은 전혀 꾸미지 않고 최소한의 구조만 넣었습니다.

아래 예제는 PyQt6에서 바로 실행 가능한 최소 계산기 코드입니다.

 
import sys from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QLineEdit, QPushButton, QGridLayout, QVBoxLayout ) from PyQt6.QtCore import Qt class CalculatorApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Calculator") # 메인 위젯과 레이아웃 central = QWidget() self.setCentralWidget(central) vbox = QVBoxLayout(central) # 표시창 self.display = QLineEdit() self.display.setReadOnly(True) self.display.setAlignment(Qt.AlignmentFlag.AlignRight) vbox.addWidget(self.display) # 버튼 레이아웃 (Grid) grid = QGridLayout() vbox.addLayout(grid) # 버튼 레이블 배열 (UI 구조 재현) buttons = [ ["AC", "+/-", "%", "/"], ["7", "8", "9", "X"], ["4", "5", "6", "-"], ["1", "2", "3", "+"], ["0", ".", "="], ] # 버튼 생성 및 배치 for row, row_vals in enumerate(buttons): for col, text in enumerate(row_vals): btn = QPushButton(text) grid.addWidget(btn, row, col) # 각 버튼 기능 연결 if text == "AC": btn.clicked.connect(self._clear_all) elif text == "=": btn.clicked.connect(self._evaluate) elif text == "+/-": btn.clicked.connect(self._toggle_sign) elif text == "%": btn.clicked.connect(self._percent) else: btn.clicked.connect(lambda _, t=text: self._append_text(t)) def _append_text(self, s: str): self.display.setText(self.display.text() + s) def _clear_all(self): self.display.clear() def _toggle_sign(self): txt = self.display.text() if not txt: return try: val = float(txt) val = -val self.display.setText(str(int(val)) if val.is_integer() else str(val)) except ValueError: if txt.startswith("-"): self.display.setText(txt[1:]) else: self.display.setText("-" + txt) def _percent(self): txt = self.display.text() if not txt: return try: val = float(txt) / 100 self.display.setText(str(int(val)) if val.is_integer() else str(val)) except ValueError: self._append_text("/100") def _evaluate(self): expr = self.display.text().replace("X", "*") try: result = eval(expr, {"__builtins__": {}}, {}) if isinstance(result, (int, float)) and float(result).is_integer(): self.display.setText(str(int(result))) else: self.display.setText(str(result)) except Exception: self.display.setText("Error") def keyPressEvent(self, event): key = event.key() if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): self._evaluate() elif key == Qt.Key.Key_Escape: self._clear_all() elif key == Qt.Key.Key_Backspace: self.display.backspace() elif Qt.Key.Key_0 <= key <= Qt.Key.Key_9: self._append_text(chr(key)) elif key == Qt.Key.Key_Plus: self._append_text("+") elif key == Qt.Key.Key_Minus: self._append_text("-") elif key == Qt.Key.Key_Slash: self._append_text("/") elif key in (Qt.Key.Key_Asterisk, Qt.Key.Key_X): self._append_text("X") elif key == Qt.Key.Key_Period: self._append_text(".") elif key == Qt.Key.Key_Percent: self._percent() else: super().keyPressEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) win = CalculatorApp() win.show() sys.exit(app.exec())

특징

  • .ui 파일 없이 전부 코드에서 UI 구성.
  • 버튼은 2차원 배열 + 이중 for문으로 배치.
  • 최소한의 기능: 숫자/연산 입력, AC, =, %, 부호 토글, 키보드 입력 지원.
  • 스타일(색상, 크기 조절 등) 전혀 없음 → 최대한 단순 코드.

👉 혹시 이 버전을 추가로 세로 크기 고정된 버튼 크기까지 지정해드릴까요, 아니면 지금처럼 Qt 기본 크기 조절되는 모양을 원하시나요?

나의 말:

keyPressEvent를 list나 dict으로 간결하게 정리해줘 더 좋은 방법이 있으면 제한해도 되고

ChatGPT의 말:

좋습니다 👍
지금 keyPressEvent는 if/elif가 길어져서 가독성이 떨어집니다.
이 부분을 dict 매핑 방식으로 정리하면 훨씬 간결하고 유지보수가 쉬워집니다.

아래는 dict와 list를 섞어서 깔끔하게 정리한 버전입니다.

 
import sys from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QLineEdit, QPushButton, QGridLayout, QVBoxLayout ) from PyQt6.QtCore import Qt class CalculatorApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Calculator") # 메인 위젯과 레이아웃 central = QWidget() self.setCentralWidget(central) vbox = QVBoxLayout(central) # 표시창 self.display = QLineEdit() self.display.setReadOnly(True) self.display.setAlignment(Qt.AlignmentFlag.AlignRight) vbox.addWidget(self.display) # 버튼 레이아웃 grid = QGridLayout() vbox.addLayout(grid) # 버튼 배열 buttons = [ ["AC", "+/-", "%", "/"], ["7", "8", "9", "X"], ["4", "5", "6", "-"], ["1", "2", "3", "+"], ["0", ".", "="], ] # 버튼 생성 및 연결 for row, row_vals in enumerate(buttons): for col, text in enumerate(row_vals): btn = QPushButton(text) grid.addWidget(btn, row, col) if text == "AC": btn.clicked.connect(self._clear_all) elif text == "=": btn.clicked.connect(self._evaluate) elif text == "+/-": btn.clicked.connect(self._toggle_sign) elif text == "%": btn.clicked.connect(self._percent) else: btn.clicked.connect(lambda _, t=text: self._append_text(t)) # 키 매핑: 단순 입력 키 → 문자 self.key_map = { Qt.Key.Key_Plus: "+", Qt.Key.Key_Minus: "-", Qt.Key.Key_Slash: "/", Qt.Key.Key_Asterisk: "X", # 화면에는 X로 표시 Qt.Key.Key_X: "X", Qt.Key.Key_Period: ".", } # 특수 키 → 함수 매핑 self.func_map = { Qt.Key.Key_Return: self._evaluate, Qt.Key.Key_Enter: self._evaluate, Qt.Key.Key_Escape: self._clear_all, Qt.Key.Key_Backspace: self.display.backspace, Qt.Key.Key_Percent: self._percent, } def _append_text(self, s: str): self.display.setText(self.display.text() + s) def _clear_all(self): self.display.clear() def _toggle_sign(self): txt = self.display.text() if not txt: return try: val = float(txt) val = -val self.display.setText(str(int(val)) if val.is_integer() else str(val)) except ValueError: if txt.startswith("-"): self.display.setText(txt[1:]) else: self.display.setText("-" + txt) def _percent(self): txt = self.display.text() if not txt: return try: val = float(txt) / 100 self.display.setText(str(int(val)) if val.is_integer() else str(val)) except ValueError: self._append_text("/100") def _evaluate(self): expr = self.display.text().replace("X", "*") try: result = eval(expr, {"__builtins__": {}}, {}) if isinstance(result, (int, float)) and float(result).is_integer(): self.display.setText(str(int(result))) else: self.display.setText(str(result)) except Exception: self.display.setText("Error") # ------------------------- # 간결하게 정리한 키보드 입력 # ------------------------- def keyPressEvent(self, event): key = event.key() # 숫자 키 (상단 숫자열) if Qt.Key.Key_0 <= key <= Qt.Key.Key_9: self._append_text(chr(key)) return # 넘패드 숫자 numpad_keys = { Qt.Key.Key_NumPad0: "0", Qt.Key.Key_NumPad1: "1", Qt.Key.Key_NumPad2: "2", Qt.Key.Key_NumPad3: "3", Qt.Key.Key_NumPad4: "4", Qt.Key.Key_NumPad5: "5", Qt.Key.Key_NumPad6: "6", Qt.Key.Key_NumPad7: "7", Qt.Key.Key_NumPad8: "8", Qt.Key.Key_NumPad9: "9", } if key in numpad_keys: self._append_text(numpad_keys[key]) return # 단순 문자 입력 매핑 if key in self.key_map: self._append_text(self.key_map[key]) return # 특수 기능 매핑 if key in self.func_map: self.func_map[key]() return # 기본 처리 super().keyPressEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) win = CalculatorApp() win.show() sys.exit(app.exec())

개선 포인트

  • key_map: 문자 그대로 추가할 키 (+, -, /, *, ., x)
  • func_map: 특정 동작 실행할 키 (Enter → 계산, Esc → AC, Backspace → 삭제, % → 퍼센트)
  • 숫자 키/넘패드는 따로 블록으로 묶어 간단히 처리

👉 지금처럼 dict 기반으로 정리하면 새로운 키 매핑 추가가 매우 간단합니다.

원하시면, 이 키 매핑까지도 한 개의 dict로 합쳐서 "type":"append"/"func" 구조로 더 정리된 버전도 만들어 드릴까요?

나의 말:

전체 프로그램을 더 정리하거나 효율적으로 할수 있으면 해줘

ChatGPT의 말:

 
6s 동안 생각함

좋아요! 전체 코드를 **데이터 주도형(버튼/키 매핑 딕셔너리)**으로 정리하고,
키 입력은 keyPressEvent 대신 QShortcut로 매핑해서 더 간결하고 유지보수 쉽게 만들었습니다.
또한 eval 대신 **AST 기반의 안전한 계산기(+, -, *, /, %, 괄호, 실수/정수 지원)**를 넣어 보안과 안정성을 높였어요.

 
import sys import ast import operator as op from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QLineEdit, QPushButton, QGridLayout, QVBoxLayout ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QShortcut, QKeySequence # --- 안전한 수식 평가기: + - * / % 괄호, 음수/양수 부호, 실수/정수 지원 --- _ALLOWED_OPS = { ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.Mod: op.mod, ast.UAdd: op.pos, ast.USub: op.neg, } def safe_eval(expr: str) -> float: """ 안전한 계산만 허용하는 간단 파서. 지원: 숫자(int/float), 괄호, + - * / %, 단항부호. """ def _eval(node): if isinstance(node, ast.Expression): return _eval(node.body) if isinstance(node, ast.BinOp): left = _eval(node.left) right = _eval(node.right) fn = _ALLOWED_OPS.get(type(node.op)) if fn is None: raise ValueError("Unsupported operator") return fn(left, right) if isinstance(node, ast.UnaryOp): fn = _ALLOWED_OPS.get(type(node.op)) if fn is None: raise ValueError("Unsupported unary operator") return fn(_eval(node.operand)) if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): return node.value # 파이썬 3.8 이하 호환이 필요하면 ast.Num 처리 추가 raise ValueError("Unsupported expression") tree = ast.parse(expr, mode="eval") return _eval(tree) class CalculatorApp(QMainWindow): # 버튼 구성: 2차원 배열(이중 for로 배치) BUTTON_LAYOUT = [ ["AC", "+/-", "%", "/"], ["7", "8", "9", "X"], ["4", "5", "6", "-"], ["1", "2", "3", "+"], ["0", ".", "="], ] def __init__(self): super().__init__() self.setWindowTitle("Calculator") # ----- UI 구성 ----- central = QWidget() self.setCentralWidget(central) vbox = QVBoxLayout(central) self.display = QLineEdit() self.display.setReadOnly(True) self.display.setAlignment(Qt.AlignmentFlag.AlignRight) vbox.addWidget(self.display) grid = QGridLayout() vbox.addLayout(grid) # 버튼 생성/이벤트 연결 for r, row in enumerate(self.BUTTON_LAYOUT): for c, text in enumerate(row): btn = QPushButton(text) grid.addWidget(btn, r, c) btn.clicked.connect(self._make_button_handler(text)) # ----- 키보드 단축키(QShortcut) 매핑 ----- # 문자를 입력하는 키 append_keys = { "0": "0", "1": "1", "2": "2", "3": "3", "4": "4", "5": "5", "6": "6", "7": "7", "8": "8", "9": "9", ".": ".", "+": "+", "-": "-", "/": "/", "*": "X", "x": "X", "X": "X", "(": "(", ")": ")", } for key, literal in append_keys.items(): QShortcut(QKeySequence(key), self, activated=lambda lit=literal: self.append(lit)) # 기능 키 QShortcut(QKeySequence(Qt.Key.Key_Return), self, activated=self.evaluate) QShortcut(QKeySequence(Qt.Key.Key_Enter), self, activated=self.evaluate) QShortcut(QKeySequence(Qt.Key.Key_Escape), self, activated=self.clear_all) QShortcut(QKeySequence(Qt.Key.Key_Backspace), self, activated=self.backspace) QShortcut(QKeySequence("%"), self, activated=self.percent) # +/- 토글(F9은 관례상 부호 토글로 많이 씀) QShortcut(QKeySequence(Qt.Key.Key_F9), self, activated=self.toggle_sign) # ---------- 버튼 핸들러 생성기 ---------- def _make_button_handler(self, text: str): # 버튼 텍스트에 따라 적절한 콜백 반환 special = { "AC": self.clear_all, "+/-": self.toggle_sign, "%": self.percent, "=": self.evaluate, } if text in special: return special[text] # 나머지는 그대로 append (곱셈은 X로 표기) return lambda t=text: self.append(t) # ---------- 편의 메서드 ---------- def set_text(self, s: str): self.display.setText(s) def text(self) -> str: return self.display.text() def append(self, s: str): self.set_text(self.text() + s) def clear_all(self): self.set_text("") def backspace(self): self.display.backspace() def toggle_sign(self): t = self.text().strip() if not t: return # 숫자 전체를 부호 반전: 숫자면 float 변환 후 반전, 아니면 앞에 - 토글 try: v = float(t) v = -v self.set_text(str(int(v)) if v.is_integer() else str(v)) except ValueError: self.set_text(t[1:] if t.startswith("-") else "-" + t) def percent(self): t = self.text().strip() if not t: return try: v = float(t) / 100.0 self.set_text(str(int(v)) if v.is_integer() else str(v)) except ValueError: # 수식이면 간단히 /100 이어붙이기 self.append("/100") def evaluate(self): t = self.text().strip() if not t: return # 화면 표기용 'X' → 계산용 '*' expr = t.replace("X", "*") try: result = safe_eval(expr) self.set_text(str(int(result)) if float(result).is_integer() else str(result)) except Exception: self.set_text("Error") if __name__ == "__main__": app = QApplication(sys.argv) win = CalculatorApp() win.show() sys.exit(app.exec())

무엇이 개선되었나요?

  • QShortcut 기반 키 입력: keyPressEvent 분기문 제거 → 키 매핑이 선언적이고 짧음.
  • 딕셔너리/람다로 버튼·키 매핑: 버튼/키 추가·변경이 매우 쉬움.
  • 안전한 AST 계산기: eval 제거로 보안·안정성 향상(지원 연산: +, -, *, /, %, 괄호, 단항부호).
  • 데이터 주도 UI: BUTTON_LAYOUT 한 곳만 바꾸면 레이아웃/버튼 자동 반영.
  • 간결한 헬퍼 메서드: set_text, text, append로 중복 로직 최소화.

원하시면:

  • Enter 입력 시 결과 표시 후 다음 입력이 시작되면 자동 초기화(일반 계산기 UX),
  • 연산자 연속 입력 방지(예: ++ 자동 정정),
  • 키보드 단축키 커스터마이즈(예: Ctrl+C/V, Ctrl+L=Clear)
    같은 UX 향상도 깔끔하게 붙여드릴게요.