
Hello Everyone
이 전 페이지에서 PyQt 기초 예제를 연습해보았는데... 이번엔 MVC(Model-View-Controller) 디자인 패턴을 이용하여 계산기 GUI 앱을 구현해볼것이다.
우선 MVC 에 대해 간단히 설명해보자면...
- Model : 앱의 비즈니스 로직을 처리한다. 핵심 기능과 데이터를 포함한다. 계산기 앱에서는 입력 값과 계산을 처리한다.
- View : 앱의 GUI를 구현한다. 사용자가 상호작용 할 수 있는 모든 위젯을 포함하며, 사용자의 동작과 이벤트를 수신한다. 계산기 앱에서는 화면에 표시되는 창이 뷰다.
- Controller : 모델과 뷰를 연결하여 애플리케이션을 작동시킨다. 사용자의 이벤트나 요청은 컨트롤러로 전송되며, 컨트롤러는 모델을 작동시킨다. 모델이 올바른 형식으로 요청된 결과나 데이터를 제공하면 컨트롤러가 뷰로 전달한다. 계산기 앱에서 컨트롤러는 GUI에서 대상 수식을 받아 모델에게 계산을 요청하고 결과를 GUI에 반영한다.
- 사용자가 뷰(GUI)에서 동작이나 요청 수행
- 뷰는 사용자의 동작을 컨트롤러에게 알림
- 컨트롤러는 사용자의 요청을 받아 모델에 쿼리(query)하여 응답 얻음
- 모델은 컨트롤러의 쿼리를 처리하고 필요한 계산을 수행 한 뒤 결과 반환
- 컨트롤러는 모델의 응답을 받아 뷰를 업데이트
- 마지막으로 사용자는 뷰에서 요청된 결과를 확인
#pycalc.py
import sys
from PyQt6.QtWidgets import QMainWindow, QLineEdit, QPushButton, QGridLayout, QVBoxLayout, QWidget
from PyQt6.QtCore import Qt
WINDOW_SIZE = 235 # 창의 크기
DISPLAY_HEIGHT = 35 # 디스플레이 높이
BUTTON_SIZE = 40 # 버튼 크기
class PyCalcWindow(QMainWindow): # PyCalcWindow 클래스는 QMainWindow를 상속, 메인 화면 정의
def __init__(self):
super().__init__()
self.setWindowTitle("PyCalc") # 창 제목 설정
self.setFixedSize(WINDOW_SIZE, WINDOW_SIZE) #고정 크기 설정
self.generalLayout = QVBoxLayout() # 수직 레이아웃 생성
self._centralWidget = QWidget(self) # 중앙 위젯 생성
self.setCentralWidget(self._centralWidget) # 중앙 위젯 생성
self._centralWidget.setLayout(self.generalLayout) # 레이아웃 생성
self._createDisplay() # 디스플레이 생성
self._createButtons() # 버튼 생성
def _createDisplay(self):
# 디스클레이 생성 메소드
self.display = QLineEdit() #QLineEdit 위젯 생성
self.display.setFixedHeight(DISPLAY_HEIGHT) # 고정 높이 설정
self.display.setAlignment(Qt.AlignmentFlag.AlignRight) # 오른쪽 정렬 설정
self.display.setReadOnly(True) # 읽기 전용으로 설정
self.generalLayout.addWidget(self.display) # 레이아웃에 추가
def _createButtons(self):
# 버튼 생성 메소드
self.buttonMap = {} # 버튼을 저장할 딕셔너리
buttonsLayout = QGridLayout() # 그리드 레이아웃 생성
keyboard = [ # 키보드 버튼의 레이아웃
["7", "8", "9", "/", "C"],
["4", "5", "6", "*", "("],
["1", "2", "3", "-", ")"],
["0", "00", ".", "+", "="],
]
for row, keys in enumerate(keyboard): # 그리드에 버튼 추가
for col, key in enumerate(keys):
self.buttonMap[key] = QPushButton(key) # 버튼 생성
self.buttonMap[key].setFixedSize(BUTTON_SIZE, BUTTON_SIZE) # 버튼 크기 설정
buttonsLayout.addWidget(self.buttonMap[key], row, col) # 그리드에 버튼 추가
self.generalLayout.addLayout(buttonsLayout) # 버튼 레이아웃을 메인 레이아웃에 추가
def setDisplayText(self, text):
"""디스플레이 텍스트 설정 메소드"""
self.display.setText(text) # 텍스트 설정
self.display.setFocus() # 포커스 설정
def displayText(self):
"""디스플레이 텍스트 반환 메소드"""
return self.display.text() # 텍스트 반환
def clearDisplay(self):
"""디스플레이 지우기 메소드"""
self.setDisplayText("") # 텍스트 지우기
if __name__ == "__main__":
import sys
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv) # 애플리케이션 객체 생성
window = PyCalcWindow() # 메인 화면 객체 생성
window.show()
sys.exit(app.exec())

이렇게 뼈대를 만들었으니 비즈니스 로직 즉 계산기 애플리케이션에서는 수식을 계산할 모델이 필요하다. 또한 계산기 모델은 오류를 처리해야하므로 다음과 같은 전역 상수를 정의한다.
# pycalc.py
# ...
ERROR_MSG = "ERROR"
WINDOW_SIZE = 235
DISPLAY_HEIGHT = 35
BUTTON_SIZE = 40
# ...
import sys
from functools import partial
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QLineEdit, QPushButton, QGridLayout, QVBoxLayout, QWidget
ERROR_MSG = "ERROR"
WINDOW_SIZE = 235 # 창의 크기
DISPLAY_HEIGHT = 35 # 디스플레이 높이
BUTTON_SIZE = 40 # 버튼 크기
class PyCalcWindow(QMainWindow): # PyCalcWindow 클래스는 QMainWindow를 상속, 메인 화면 정의
def __init__(self):
super().__init__()
self.setWindowTitle("PyCalc") # 창 제목 설정
self.setFixedSize(WINDOW_SIZE, WINDOW_SIZE) #고정 크기 설정
self.generalLayout = QVBoxLayout() # 수직 레이아웃 생성
self._centralWidget = QWidget(self) # 중앙 위젯 생성
self.setCentralWidget(self._centralWidget) # 중앙 위젯 생성
self._centralWidget.setLayout(self.generalLayout) # 레이아웃 생성
self._createDisplay() # 디스플레이 생성
self._createButtons() # 버튼 생성
def _createDisplay(self):
# 디스클레이 생성 메소드
self.display = QLineEdit() #QLineEdit 위젯 생성
self.display.setFixedHeight(DISPLAY_HEIGHT) # 고정 높이 설정
self.display.setAlignment(Qt.AlignmentFlag.AlignRight) # 오른쪽 정렬 설정
self.display.setReadOnly(True) # 읽기 전용으로 설정
self.generalLayout.addWidget(self.display) # 레이아웃에 추가
def _createButtons(self):
# 버튼 생성 메소드
self.buttonMap = {} # 버튼을 저장할 딕셔너리
buttonsLayout = QGridLayout() # 그리드 레이아웃 생성
keyboard = [ # 키보드 버튼의 레이아웃
["7", "8", "9", "/", "C"],
["4", "5", "6", "*", "("],
["1", "2", "3", "-", ")"],
["0", "00", ".", "+", "="],
]
for row, keys in enumerate(keyboard): # 그리드에 버튼 추가
for col, key in enumerate(keys):
self.buttonMap[key] = QPushButton(key) # 버튼 생성
self.buttonMap[key].setFixedSize(BUTTON_SIZE, BUTTON_SIZE) # 버튼 크기 설정
buttonsLayout.addWidget(self.buttonMap[key], row, col) # 그리드에 버튼 추가
self.generalLayout.addLayout(buttonsLayout) # 버튼 레이아웃을 메인 레이아웃에 추가
def setDisplayText(self, text):
"""디스플레이 텍스트 설정 메소드"""
self.display.setText(text) # 텍스트 설정
self.display.setFocus() # 포커스 설정
def displayText(self):
"""디스플레이 텍스트 반환 메소드"""
return self.display.text() # 텍스트 반환
def clearDisplay(self):
"""디스플레이 지우기 메소드"""
self.setDisplayText("") # 텍스트 지우기
class PyCalc:
"""PyCalc의 컨트롤러 클래스."""
def __init__(self, model, view):
self._evaluate = model
self._view = view
self._connectSignalsAndSlots()
def _calculateResult(self):
"""결과 계산 메소드"""
result = self._evaluate(expression=self._view.displayText())
self._view.setDisplayText(result)
'_calculateResult()'메소드에서는 사용자가 계산기 디스플레이에 입력한 수식을 평가하기위해 '_evaluate()' 메소드를 활용한다. 그런 다음 결과를 디스플레이 텍스트에 업데이트하기위해 view에서 'setDisplayText()'가 호출된다.
def _buildExpression(self, subExpression):
"""수식 작성 메소드"""
if self._view.displayText() == ERROR_MSG:
self._view.clearDisplay()
expression = self._view.displayText() + subExpression
self._view.setDisplayText(expression)
def _connectSignalsAndSlots(self):
"""시그널 및 슬롯 연결 메소드"""
for keySymbol, button in self._view.buttonMap.items():
if keySymbol not in {"=", "C"}:
button.clicked.connect(
partial(self._buildExpression, keySymbol)
)
self._view.buttonMap["="].clicked.connect(self._calculateResult)
self._view.display.returnPressed.connect(self._calculateResult)
self._view.buttonMap["C"].clicked.connect(self._view.clearDisplay)
PyCalc 라는 클래스 내에서는 클래스의 초기화자가 정의되어있다. 이는 두 개의 인수, 즉 앱의 모델과 뷰를 가져온다. 이러한 인수는 적절한 인스턴스 속성에 저장된다. 그리고 '_connectSignalsAndSlots()' 메소드가 호출되어 신호와 슬롯 사이의 필요한 모든 연결이 설정된다.
def evaluateExpression(expression):
"""수식 평가 메소드"""
try:
result = str(eval(expression, {}, {}))
except Exception:
result = ERROR_MSG
return result
def main():
"""PyCalc's main function"""
pycalcApp = QApplication([])
pycalcWindow = PyCalcWindow()
pycalcWindow.show()
PyCalc(model=evaluateExpression, view=pycalcWindow)
sys.exit(pycalcApp.exec())
if __name__ == "__main__":
main()
import sys
from functools import partial
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QLineEdit, QPushButton, QGridLayout, QVBoxLayout, QWidget
ERROR_MSG = "ERROR"
WINDOW_SIZE = 235 # 창의 크기
DISPLAY_HEIGHT = 35 # 디스플레이 높이
BUTTON_SIZE = 40 # 버튼 크기
class PyCalcWindow(QMainWindow): # PyCalcWindow 클래스는 QMainWindow를 상속, 메인 화면 정의
def __init__(self):
super().__init__()
self.setWindowTitle("PyCalc") # 창 제목 설정
self.setFixedSize(WINDOW_SIZE, WINDOW_SIZE) #고정 크기 설정
self.generalLayout = QVBoxLayout() # 수직 레이아웃 생성
self._centralWidget = QWidget(self) # 중앙 위젯 생성
self.setCentralWidget(self._centralWidget) # 중앙 위젯 생성
self._centralWidget.setLayout(self.generalLayout) # 레이아웃 생성
self._createDisplay() # 디스플레이 생성
self._createButtons() # 버튼 생성
def _createDisplay(self):
# 디스클레이 생성 메소드
self.display = QLineEdit() #QLineEdit 위젯 생성
self.display.setFixedHeight(DISPLAY_HEIGHT) # 고정 높이 설정
self.display.setAlignment(Qt.AlignmentFlag.AlignRight) # 오른쪽 정렬 설정
self.display.setReadOnly(True) # 읽기 전용으로 설정
self.generalLayout.addWidget(self.display) # 레이아웃에 추가
def _createButtons(self):
# 버튼 생성 메소드
self.buttonMap = {} # 버튼을 저장할 딕셔너리
buttonsLayout = QGridLayout() # 그리드 레이아웃 생성
keyboard = [ # 키보드 버튼의 레이아웃
["7", "8", "9", "/", "C"],
["4", "5", "6", "*", "("],
["1", "2", "3", "-", ")"],
["0", "00", ".", "+", "="],
]
for row, keys in enumerate(keyboard): # 그리드에 버튼 추가
for col, key in enumerate(keys):
self.buttonMap[key] = QPushButton(key) # 버튼 생성
self.buttonMap[key].setFixedSize(BUTTON_SIZE, BUTTON_SIZE) # 버튼 크기 설정
buttonsLayout.addWidget(self.buttonMap[key], row, col) # 그리드에 버튼 추가
self.generalLayout.addLayout(buttonsLayout) # 버튼 레이아웃을 메인 레이아웃에 추가
def setDisplayText(self, text):
"""디스플레이 텍스트 설정 메소드"""
self.display.setText(text) # 텍스트 설정
self.display.setFocus() # 포커스 설정
def displayText(self):
"""디스플레이 텍스트 반환 메소드"""
return self.display.text() # 텍스트 반환
def clearDisplay(self):
"""디스플레이 지우기 메소드"""
self.setDisplayText("") # 텍스트 지우기
class PyCalc:
def __init__(self, model, view):
self._evaluate = model
self._view = view
self._connectSignalsAndSlots()
def _calculateResult(self):
"""결과 계산 메소드"""
result = self._evaluate(expression=self._view.displayText())
self._view.setDisplayText(result)
def _buildExpression(self, subExpression):
"""수식 작성 메소드"""
if self._view.displayText() == ERROR_MSG:
self._view.clearDisplay()
expression = self._view.displayText() + subExpression
self._view.setDisplayText(expression)
def _connectSignalsAndSlots(self):
"""시그널 및 슬롯 연결 메소드"""
for keySymbol, button in self._view.buttonMap.items():
if keySymbol not in {"=", "C"}:
button.clicked.connect(
partial(self._buildExpression, keySymbol)
)
self._view.buttonMap["="].clicked.connect(self._calculateResult)
self._view.display.returnPressed.connect(self._calculateResult)
self._view.buttonMap["C"].clicked.connect(self._view.clearDisplay)
def evaluateExpression(expression):
"""수식 평가 메소드"""
try:
result = str(eval(expression, {}, {}))
except Exception:
result = ERROR_MSG
return result
def main():
"""PyCalc's main function"""
pycalcApp = QApplication([])
pycalcWindow = PyCalcWindow()
pycalcWindow.show()
PyCalc(model=evaluateExpression, view=pycalcWindow)
sys.exit(pycalcApp.exec())
if __name__ == "__main__":
main()


터미널에서 애플리케이션을 실행하면 계산기가 뜰것이다. PyQt를 추가적으로 수정하고싶다면 Qt Designer이 있다. 이는 드래그 앤 드롭 인터페이스를 사용하여 GUI를 설계하고 구축 할 수 있다. 이번 프로젝트를 통해 익힌 내용
- Python과 PyQt를 사용하여 그래픽 사용자 인터페이스를 구축하는 방법
- 사용자의 이벤트를 앱의 논리에 연결하는 방법
- 적절한 프로젝트 레이아웃을 사용하여 PyQt 애플리케이션을 조직하는 방법
- PyQt를 사용하여 실제 GUI 애플리케이션을 만드는 방법
import sys
from functools import partial
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QLineEdit, QPushButton, QGridLayout, QVBoxLayout, QWidget
ERROR_MSG = "ERROR"
WINDOW_SIZE = 235 # 창의 크기
DISPLAY_HEIGHT = 35 # 디스플레이 높이
BUTTON_SIZE = 40 # 버튼 크기
class PyCalcWindow(QMainWindow): # PyCalcWindow 클래스는 QMainWindow를 상속, 메인 화면 정의
def init(self):
super().init()
self.setWindowTitle("PyCalc") # 창 제목 설정
self.setFixedSize(WINDOW_SIZE, WINDOW_SIZE) #고정 크기 설정
self.generalLayout = QVBoxLayout() # 수직 레이아웃 생성
self._centralWidget = QWidget(self) # 중앙 위젯 생성
self.setCentralWidget(self._centralWidget) # 중앙 위젯 생성
self._centralWidget.setLayout(self.generalLayout) # 레이아웃 생성
self._createDisplay() # 디스플레이 생성
self._createButtons() # 버튼 생성
def _createDisplay(self):
# 디스클레이 생성 메소드
self.display = QLineEdit() #QLineEdit 위젯 생성
self.display.setFixedHeight(DISPLAY_HEIGHT) # 고정 높이 설정
self.display.setAlignment(Qt.AlignmentFlag.AlignRight) # 오른쪽 정렬 설정
self.display.setReadOnly(True) # 읽기 전용으로 설정
self.generalLayout.addWidget(self.display) # 레이아웃에 추가
def _createButtons(self):
# 버튼 생성 메소드
self.buttonMap = {} # 버튼을 저장할 딕셔너리
buttonsLayout = QGridLayout() # 그리드 레이아웃 생성
keyboard = [ # 키보드 버튼의 레이아웃
["7", "8", "9", "/", "C"],
["4", "5", "6", "*", "("],
["1", "2", "3", "-", ")"],
["0", "00", ".", "+", "="],
]
for row, keys in enumerate(keyboard): # 그리드에 버튼 추가
for col, key in enumerate(keys):
self.buttonMap[key] = QPushButton(key) # 버튼 생성
self.buttonMap[key].setFixedSize(BUTTON_SIZE, BUTTON_SIZE) # 버튼 크기 설정
buttonsLayout.addWidget(self.buttonMap[key], row, col) # 그리드에 버튼 추가
self.generalLayout.addLayout(buttonsLayout) # 버튼 레이아웃을 메인 레이아웃에 추가
def setDisplayText(self, text):
"""디스플레이 텍스트 설정 메소드"""
self.display.setText(text) # 텍스트 설정
self.display.setFocus() # 포커스 설정
def displayText(self):
"""디스플레이 텍스트 반환 메소드"""
return self.display.text() # 텍스트 반환
def clearDisplay(self):
"""디스플레이 지우기 메소드"""
self.setDisplayText("") # 텍스트 지우기
class PyCalc:
"""PyCalc의 컨트롤러 클래스."""
def __init__(self, model, view):
self._evaluate = model
self._view = view
self._connectSignalsAndSlots()
def _calculateResult(self):
"""결과 계산 메소드"""
result = self._evaluate(expression=self._view.displayText())
self._view.setDisplayText(result)
def _buildExpression(self, subExpression):
"""수식 작성 메소드"""
if self._view.displayText() == ERROR_MSG:
self._view.clearDisplay()
expression = self._view.displayText() + subExpression
self._view.setDisplayText(expression)
def _connectSignalsAndSlots(self):
"""시그널 및 슬롯 연결 메소드"""
for keySymbol, button in self._view.buttonMap.items():
if keySymbol not in {"=", "C"}:
button.clicked.connect(
partial(self._buildExpression, keySymbol)
)
self._view.buttonMap["="].clicked.connect(self._calculateResult)
self._view.display.returnPressed.connect(self._calculateResult)
self._view.buttonMap["C"].clicked.connect(self._view.clearDisplay)
def evaluateExpression(expression):
"""수식 평가 메소드"""
try:
result = str(eval(expression, {}, {}))
except Exception:
result = ERROR_MSG
return result
def main():
"""PyCalc's main function"""
pycalcApp = QApplication([])
pycalcWindow = PyCalcWindow()
pycalcWindow.show()
PyCalc(model=evaluateExpression, view=pycalcWindow)
sys.exit(pycalcApp.exec())
if name == "main":
main()