[Python - PyQt5] 계산기 만들기

haejun-kim·2020년 6월 20일
4
post-thumbnail

🔔 중간중간 코드 설명을 하는 포스트입니다.
예제 전체를 올리지 않으니 포스트 된 코드만 사용할 경우 프로그램이 제대로 동작하지 않습니다.

Python GUI 계산기 with PyQt5

저번 두개의 포스트에서는 CLI 환경에서의 계산기를 만들어보았다.
이번에는 GUI 환경에서의 계산기를 만들어보려고 한다. 사용하려는 라이브러리는 PyQt5이다.
먼저, PyQt5를 설치하는 방법은 간단하다.
터미널을 열고 아래의 코드를 입력해준다.

pip3 install pyqt5

( python version이 3.x인 경우 pip3 명령어로, 2.x인 경우에는 pip 명령어로 install 해주면 되겠다. )

화면 띄우기

PyQt5 를 설치했다면 사용하기 위해 import 해주자.

from PyQt5.QtWidgets import QMainWindow, QApplication

QMainWindow는 메인 메뉴 및 툴바, 그리고 하단에 상태바 등을 갖는 윈도우이다. 다이얼로그보다 더 많은 기능의 윈도우를 구현할 수 있다.

QApplication은 QApplication 객체의 exec_() 메서드를 호출하면 메인 메시지 루프를 만들며 GUI 이벤트를 핸들링하게 되는 GUI Application을 관리하는 클래스이다.

App Class 선언

class App(QMainWindow):
    def __init__(self): 
        super().__init__() 
        
        self.title = "계산기"
        self.setWindowTitle(self.title)

        self.left = 100
        self.top = 200
        self.width = 300
        self.height = 200
        self.setGeometry(self.left, self.top, self.width, self.height)
        self.show()

app = QApplication([])
calc = App()
app.exec_()
  • 클래스가 생성되면 가장 먼저 실행되는 초기화 영역. __init__() 함수가 호출 될 때 클래스에서 사용되는 변수 같은걸 초기화
  • 부모를 상속받아서 만들어진 자식클래스에서 부모 클래스의 생성자를 호출할 때 사용하는 메소드가 super()
  • 부모의 클래스에서 사용하던 어떤 변수들이 초기화 되지 않으면 부모 클래스가 정상적으로 동작하지 않기 때문에 대부분 상속받은 자식 클래스의 입장에서는 부모 클래스의 초기화 함수를 수행해야 한다.
  • super()메소드를 활용해서 부모클래스의 초기화 함수 부분을 먼저 수행하고, 자식 클래스에서 자신의 초기화 영역을 수행하는게 일반적이다.
  • Title을 설정해주고, 화면의 크기를 설정해준다. 그리고 exec_() 메서드를 호출하여 실행시켜준다.

실행결과

UI 구성하기

화면을 띄워봤으니 이제 그 화면을 구성해보자.
기본적으로 계산기는 정형화 된 양식이 있으니 그 양식을 참고하면 좋을 것 같다.

Button Class 만들기

굳이 만들 필요는 없다.
하지만 계산기를 보면 각 버튼마다 폰트, 색상 등이 다른 것을 알 수 있다. 그래서 모든 버튼마다 세밀한 부분까지 코딩해주기에는 너무 번거롭고 코딩 양도 많아질 것 같다. 연습용 프로그램이기 때문에 반복된 작업의 수고를 덜기 위해서 하나의 클래스를 만들어서 사용할 생각이다.
사실 이러한 점이 객체지향 언어에서 Class 를 사용하는 가장 큰 이유가 아닐까 싶다.

QToolButton를 사용

import QToolButton

QToolButton 을 사용해서 구현해보자.
QToolButton 공식 문서를 참고하면 어떤 특징이 있고 사용되는 메소드는 어떤것들이 있는지 확인할 수 있다.
Push Button 과는 다른 Flat 한 느낌의 Button 을 사용하고 싶기 때문에 해당 Button을 사용할 예정이다.

class Button(QToolButton):
    def __init__(self, text):
        super().__init__()
        buttonStyle = '''
        QToolButton:hover {border:1px solid #0078d7; background-color:#e5f1fb;}
        QToolButton:pressed {background-color:#a7c8e3}
        QToolButton {font-size:11pt; font-family:NaNum Gothic; border:1px solid #d6d7d8; background-color#f0f1f1}
        '''
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) #버튼의 사이즈 정책을 결정.
        self.setText(text)
        self.setStyleSheet(buttonStyle)
    
    def sizeHint(self):
        size = super(Button, self).sizeHint()
        size.setHeight(size.height() + 30)
        size.setWidth(max(size.width(), size.height()))
        return size

계산기 본체 만들기 ( 버튼 생성 및 배치 )

버튼 클래스를 만들어놨으니 이 클래스를 사용해야겠다.
계산기에 필요한 숫자, 연산자 버튼을 생성하고 각 버튼들을 배치해보자.

연산기호 및 기능 버튼 생성 및 배치

  • 기본 세팅
self.display = QLineEdit("0") # 기본으로 0이 표시되게. 
self.display.setReadOnly(True) # 입력이 되지 않도록
self.display.setAlignment(Qt.AlignRight) # 우측으로 정렬
self.display.setStyleSheet("border:0px; font-size:20pt; font-family:Nanum Gothic; font-weight:bold; padding:10px")

gridLayout = QGridLayout()
gridLayout.setSizeConstraint(QLayout.SetFixedSize)
  • 레이아웃 지정
layout = QVBoxLayout()
layout.addWidget(self.display)
layout.addLayout(gridLayout)
self.setLayout(layout)
  • 연산기호 및 기능 버튼 생성
self.clearButton = self.createButton("CE", self.clear)
self.clearAllButton = self.createButton("C", self.clearAll)
self.backButton = self.createButton("Back", self.backDelete)
self.divButton = self.createButton("/", self.clickButtons)
self.multiplyButton = self.createButton("*", self.clickButtons)
self.minusButton = self.createButton("-", self.clickButtons)
self.plusButton = self.createButton("+", self.clickButtons)
self.equalButton = self.createButton("=", self.clickButtons)
self.dotButton = self.createButton(".", self.clickButtons)
self.reverseButton = self.createButton("R", self.reverse)
  • 생성한 버튼 배치
gridLayout.addWidget(self.clearButton, 0,0,1,1)
gridLayout.addWidget(self.clearAllButton, 0,1,1,1)
gridLayout.addWidget(self.backButton, 0,2,1,1)
gridLayout.addWidget(self.divButton, 0,3,1,1)
gridLayout.addWidget(self.multiplyButton, 1,3,1,1)
gridLayout.addWidget(self.minusButton, 2,3,1,1)
gridLayout.addWidget(self.plusButton, 3,3,1,1)
gridLayout.addWidget(self.equalButton, 4,3,1,1)
gridLayout.addWidget(self.dotButton, 4,2,1,1)
gridLayout.addWidget(self.reverseButton, 4,0,1,1)

gridLayout은 좌표를 지정해서 버튼을 쉽게 배치할 수 있는 레이아웃이다.
좌표가 네개가 있으면 앞의 두개 좌표는 row, col 좌표
뒤의 두개 좌표는 버튼 하나당 (1,1)한 칸을 차지하겠다는 의미이다.

이렇게 하면 숫자를 제외한 버튼의 배치가 끝난다.

실행결과

숫자 배치

먼저 0은 가장 아래에 배치하는게 일반적이기 때문에 고정시켜 배치한다.

gridLayout.addWidget(self.digitButtons[0], 4, 1, 1, 1)

나머지 숫자를 생성하고 배치하는 방법은 두가지가 있다.

  1. 1~9를 위의 방법처럼 하나하나 좌표를 입력해줘서 배치하는 방법
  2. 1~9까지의 반복된 작업을 반복문을 통해 표현하는 방법
  • 하나하나 좌표 입력하기
gridLayout.addWidget(self.digitButtons[1], 3, 0, 1, 1)
gridLayout.addWidget(self.digitButtons[2], 3, 1, 1, 1)
gridLayout.addWidget(self.digitButtons[3], 3, 2, 1, 1)
  • 반복문을 통해 표현
for i in range(1,10):
    row = int(((9 - i) / 3) + 1)
    col = ((i - 1 ) % 3)
    gridLayout.addWidget(self.digitButtons[i], row, col, 1, 1)

i 는 1~9까지의 정수
rowrow의 좌표가 1~3까지는 3 / 4~6까지는 2 / 7~9까지는 1 이라는 규칙을 확인할 수 있다.
위의 식에 대입해보면 1~3까지는 3.x 로 3으로 인식 / 4~6까지는 4.x로 인식 / 7~9는 1.x로 인식을 하기 때문에 이런식으로 표현이 가능하다.
colrow와 같이 대입해서 생각해보면 규칙에 부합한다고 볼 수 있다.

이번 기회에 위의 표현 방식을 배웠는데, 반복된 작업에서 규칙을 찾아 코드로 표현하는 연습은 정말 많이 필요한 것 같다. 너무 어렵다 😩

이제 출력해보자

실행 결과

계산하기

UI 구성은 다 했으니 이제 계산에 관련된 코딩을 해야한다.
저번 포스트에서 구성했던 코드를 이용해서 프로그램 할 예정이다.
CLI 계산기 구현

각 버튼 기능 구현

계산 식을 구현하기 전에 버튼들의 기능부터 구현을 해보자.

  • clear 함수
def clear(self):
    if self.waitingForOperand:
        return
    self.display.setText("0")
    self.input_temporary = ""
    self.waitingForOperand = True
  • clearAll 함수
def clearAll(self):
    self.display.setText("0")
    self.input_temporary = ""
    self.input_history = ""
    self.waitingForOperand = True
  • BackSpace 함수
def backDelete(self):
    if self.waitingForOperand:
        return
    text = self.display.text()[:-1]
    self.input_temporary = text
    if not text: # text 가 없는 경우 ( ex. 9하나 입력하고 백 눌렀을 땐 남은 텍스트가 없으니 이런 경우에 해당 )
        text = "0"
        self.input_temporary = ""
        self.waitingForOperand = True
    self.display.setText(text)
  • reverse 함수
def reverse(self): # 음수 양수를 변환
    text = self.display.text()
    value = float(text)
    if value > 0.0:
        text = "-" + text
    elif value < 0.0:
        text = text[1:]
    self.display.setText(text)
    self.input_temporary = text
  • ClickButton 함수
def clickButtons(self):
    clickedButton = self.sender()
    digitValue = clickedButton.text()
    self.processKeyValue(digitValue)
  • 각 버튼 기능 구현 함수
def processKeyValue(self, digitValue):
    if digitValue == "=":
        if self.calculator():
            self.waitingForOperand = True
    elif digitValue == "+" or digitValue == "-" or digitValue == "*" or digitValue == "/":
        if self.waitingForOperand: # 연산자 입력 상태에서 대기중인 경우에 연산자가 또 입력되면
            self.replaceLastOperator(digitValue)
        else:
            self.inputHistory(digitValue)
            self.calculator()
        self.waitingForOperand = True
    elif digitValue == ".":
        if self.waitingForOperand: # 연산자를 입력 한 상태라는 뜻
            self.display.setText("0")
        if "." not in self.display.text():
            self.display.setText(self.display.text() + ".")
            self.inputHistory(str("."))
        self.waitingForOperand = False # 연산자 대기 상태로 바꾼다.
    else: # 숫자인 상황
        keyvalue = ord(digitValue)
        if keyvalue >= 48 and keyvalue <= 57:
            if self.display.text() == "0" and digitValue == "0.0":
                return
            if self.waitingForOperand: # 연산자를 입력한 후 숫자를 누르면 기존 display 는 사라지고 새로 입력한 숫자가 display 돼야 함.
                self.display.clear()
                self.waitingForOperand = False # 숫자가 연속으로 입력이 가능해짐 
            self.display.setText(self.display.text() + digitValue)
            self.inputHistory(str(digitValue))
  • 키보드 이벤트 함수
def keyPressEvent(self, e):
    if e.key() == Qt.Key_Backspace:
        self.backDelete()
    elif e.key() == Qt.Key_Enter:
        self.processKeyValue("=")
    elif e.key() >= 47 and e.key() <= 57:
        self.processKeyValue(chr(e.key()))
    elif e.key() == 42 or e.key() == 43 or e.key() == 45 or e.key() == 46:
        self.processKeyValue(chr(e.key()))

입력 된 값 계산

이제 모든 기능이 구현 됐으니, 입력 된 값을 계산만 하면 된다.
계산에 관련된 코드는 저번 포스트에 작성한 코드를 그대로 사용했다.

실행결과
6 * 15 - 30 / 3 + 40

( 사용자의 입력값 순서대로 연산하는 계산기라 사칙 연산 순서와는 다른 결과값이 나온다. )

완성해보니 아직도 완벽한 계산기는 아닌 것 같다. 약간 미흡한 부분이 있는 것 같다.

🏆 마무리

계산기라고 하면 쉬운 프로그램이라고 생각되기 쉽상인데, 아직 내 수준엔 굉장히 어려운 프로그램인 것 같다. 😞
개인적인 사정으로 약 2주정도 공부를 못했는데, 다시 하려니 기억이 잘 안난다.
다시 시작하면서 텐션을 끌어올려야겠다.

0개의 댓글