본문 바로가기

PyQt GUI

PyQt GUI (5) 어플리케이션 만들기 (1) : 100줄만에 계산기 만들기

728x90

https://www.youtube.com/watch?v=tnzLtWSAdg0

이번 포스팅에서는 지난 포스팅까지 소개한 내용을 바탕으로 간단한 계산기를 만들어 보도록 하겠습니다.

 

제목과 같이 약 100줄 정도의 코드만에 계산기를 만드는 것인데요, 이전 포스팅까지에서 공부한 내용이 얼마 없긴 하지만, 이 정도만 가지고도 계산기 정도는 만들어 낼 수 있습니다. 전체 코드는 아래와 같습니다. 

import sys
from PyQt5.QtWidgets import *

class Main(QDialog):
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        main_layout = QVBoxLayout()

        ### 각 위젯을 배치할 레이아웃을 미리 만들어 둠
        layout_operation = QHBoxLayout()
        layout_clear_equal = QHBoxLayout()
        layout_number = QGridLayout()
        layout_equation_solution = QFormLayout()

        ### 수식 입력과 답 출력을 위한 LineEdit 위젯 생성
        label_equation = QLabel("Equation: ")
        label_solution = QLabel("Solution: ")
        self.equation = QLineEdit("")
        self.solution = QLineEdit("")

        ### layout_equation_solution 레이아웃에 수식, 답 위젯을 추가
        layout_equation_solution.addRow(label_equation, self.equation)
        layout_equation_solution.addRow(label_solution, self.solution)

        ### 사칙연상 버튼 생성
        button_plus = QPushButton("+")
        button_minus = QPushButton("-")
        button_product = QPushButton("x")
        button_division = QPushButton("/")

        ### 사칙연산 버튼을 클릭했을 때, 각 사칙연산 부호가 수식창에 추가될 수 있도록 시그널 설정
        button_plus.clicked.connect(lambda state, operation = "+": self.button_operation_clicked(operation))
        button_minus.clicked.connect(lambda state, operation = "-": self.button_operation_clicked(operation))
        button_product.clicked.connect(lambda state, operation = "*": self.button_operation_clicked(operation))
        button_division.clicked.connect(lambda state, operation = "/": self.button_operation_clicked(operation))

        ### 사칙연산 버튼을 layout_operation 레이아웃에 추가
        layout_operation.addWidget(button_plus)
        layout_operation.addWidget(button_minus)
        layout_operation.addWidget(button_product)
        layout_operation.addWidget(button_division)

        ### =, clear, backspace 버튼 생성
        button_equal = QPushButton("=")
        button_clear = QPushButton("Clear")
        button_backspace = QPushButton("Backspace")

        ### =, clear, backspace 버튼 클릭 시 시그널 설정
        button_equal.clicked.connect(self.button_equal_clicked)
        button_clear.clicked.connect(self.button_clear_clicked)
        button_backspace.clicked.connect(self.button_backspace_clicked)

        ### =, clear, backspace 버튼을 layout_clear_equal 레이아웃에 추가
        layout_clear_equal.addWidget(button_clear)
        layout_clear_equal.addWidget(button_backspace)
        layout_clear_equal.addWidget(button_equal)

        ### 숫자 버튼 생성하고, layout_number 레이아웃에 추가
        ### 각 숫자 버튼을 클릭했을 때, 숫자가 수식창에 입력 될 수 있도록 시그널 설정
        number_button_dict = {}
        for number in range(0, 10):
            number_button_dict[number] = QPushButton(str(number))
            number_button_dict[number].clicked.connect(lambda state, num = number:
                                                       self.number_button_clicked(num))
            if number >0:
                x,y = divmod(number-1, 3)
                layout_number.addWidget(number_button_dict[number], x, y)
            elif number==0:
                layout_number.addWidget(number_button_dict[number], 3, 1)

        ### 소숫점 버튼과 00 버튼을 입력하고 시그널 설정
        button_dot = QPushButton(".")
        button_dot.clicked.connect(lambda state, num = ".": self.number_button_clicked(num))
        layout_number.addWidget(button_dot, 3, 2)

        button_double_zero = QPushButton("00")
        button_double_zero.clicked.connect(lambda state, num = "00": self.number_button_clicked(num))
        layout_number.addWidget(button_double_zero, 3, 0)

        ### 각 레이아웃을 main_layout 레이아웃에 추가
        main_layout.addLayout(layout_equation_solution)
        main_layout.addLayout(layout_operation)
        main_layout.addLayout(layout_clear_equal)
        main_layout.addLayout(layout_number)

        self.setLayout(main_layout)
        self.show()

    #################
    ### functions ###
    #################
    def number_button_clicked(self, num):
        equation = self.equation.text()
        equation += str(num)
        self.equation.setText(equation)

    def button_operation_clicked(self, operation):
        equation = self.equation.text()
        equation += operation
        self.equation.setText(equation)

    def button_equal_clicked(self):
        equation = self.equation.text()
        solution = eval(equation)
        self.solution.setText(str(solution))

    def button_clear_clicked(self):
        self.equation.setText("")
        self.solution.setText("")

    def button_backspace_clicked(self):
        equation = self.equation.text()
        equation = equation[:-1]
        self.equation.setText(equation)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = Main()
    sys.exit(app.exec_())

사실, 전체 라인수는 122라인으로 100라인이 넘습니다. 그러나 중간에 주석 부분을 삭제하고, 불필요하게 중복으로 짜여진 코드를 좀 더 최적화 한다면 100라인 안쪽으로 끊을 수 있습니다^^

위 코드를 실행하면, 위 그림과 같은 계산기가 실행됨을 확인할 수 있습니다. 버튼과 입출력창만 있는 것이 아니라 당연히 모든 계산이 정상적으로 수행됩니다. 

 

코드의 부분 부분이 어떻게 작성 돼 있는지 확인해 보도록 하겠습니다. 

    def init_ui(self):
        main_layout = QVBoxLayout()
        
        ### 이 부분에서 각종 위젯이 생성됩니다         
        ### 각 위젯은
        ### layout_equation_solution
        ### layout_operation 
        ### layout_clear_equal 
        ### layout_number
        ### 위 네 개의 레이아웃을 통해 배치되고,
        ### main_layout은 이 네 개의 레이아웃을 세로로 배치합니다
        
        main_layout.addLayout(layout_equation_solution)
        main_layout.addLayout(layout_operation)
        main_layout.addLayout(layout_clear_equal)
        main_layout.addLayout(layout_number)

        self.setLayout(main_layout)
        self.show()

이 프로그램의 메인 Dialog인 QDialog에 세팅된 레이아웃인 main_layout에 대한 설명입니다. 위 코드의 주석으로 설명한 바와 같이, QVBoxLayout()인 main_layout을 생성하고, 이 main_layout에 다양한 위젯을 배치합니다. 각 위젯들은 우선 분류에 맞게 서로 다른 네개의 레이아웃에 배치가 되고, 이렇게 생성된 네개의 레이아웃이 main_layout에 추가되게 되는 구조입니다. 

와 같은 배치입니다. 

 

그리고 각 레이아웃에는 LineEdit과 PushButton을 생성하고 배치하여 전체 프로그램을 구성합니다. 

 

(1) layout_equation_solution 레이아웃의 구성

 

이 레이아웃에는 수식을 입력하는 부분과 수식의 답을 출력하는 부분으로 나눠져 있습니다. 위 UI의 이미지에서도 볼 수 있듯, 이 레이아웃에서는 FormLayout을 활용하면 쉽게 위젯을 배치할 수 있습니다. 이 부분에 해당되는 코드는

        ### 각 위젯을 배치할 레이아웃을 미리 만들어 둠
        layout_equation_solution = QFormLayout()

        ### 수식 입력과 답 출력을 위한 LineEdit 위젯 생성
        label_equation = QLabel("Equation: ")
        label_solution = QLabel("Solution: ")
        self.equation = QLineEdit("")
        self.solution = QLineEdit("")

        ### layout_equation_solution 레이아웃에 수식, 답 위젯을 추가
        layout_equation_solution.addRow(label_equation, self.equation)
        layout_equation_solution.addRow(label_solution, self.solution)

입니다. 간단한 코드이니 특별한 설명은 필요가 없을 것 같습니다. labe_equation과 label_solution은 굳이 생성할 필요가 없고 addRow("Equation : ", self.equation)과 같이 addRow를 하는 부분에서 바로 "Equation :" 처럼 입력해 주어도 되지만, 나중을 위해서 QLabel을 이용하여 위젯을 생성하고 이 위젯을 FormLayout의 라벨 위젯으로 설정하였습니다. 수식의 입력과 답의 출력에는 QLineEdit을 사용하였는데, QLabel를 사용해도 되지만 QLineEdit이 여러모로 좋습니다. 

 

(2) layout_operation 레이아웃의 구성

 

이 레이아웃은 사칙연산 버튼으로 구성 돼 있습니다. 사칙연산 버튼은 이 버튼을 클릭 했을 때, 수식 입력창의 마지막 부분에 각 사칙연산의 기호가 추가적으로 입력이 돼야 합니다. 버튼을 만들고 배치하는 것은 쉬운일이니 버튼을 클릭했을 때, 이벤트를 제대로 설정해 주는 것이 중요합니다. 이 부분에 해당되는 코드는 아래와 같습니다. 

        layout_operation = QHBoxLayout()
    
        ### 사칙연상 버튼 생성
        button_plus = QPushButton("+")
        button_minus = QPushButton("-")
        button_product = QPushButton("x")
        button_division = QPushButton("/")
        
        ### 생성된 버튼을 layout_operatoin에 추가
        layout_operation.addWidget(button_plus)
        layout_operation.addWidget(button_minus)
        layout_operation.addWidget(button_product)
        layout_operation.addWidget(button_division)

        ### 사칙연산 버튼을 클릭했을 때, 각 사칙연산 부호가 수식창에 추가될 수 있도록 시그널 설정
        button_plus.clicked.connect(lambda state, operation = "+": self.button_operation_clicked(operation))
        button_minus.clicked.connect(lambda state, operation = "-": self.button_operation_clicked(operation))
        button_product.clicked.connect(lambda state, operation = "*": self.button_operation_clicked(operation))
        button_division.clicked.connect(lambda state, operation = "/": self.button_operation_clicked(operation))
        
        
    def button_operation_clicked(self, operation):
        equation = self.equation.text()
        equation += operation
        self.equation.setText(equation)

핵심적인 부분은

button_plus.clicked.connect(lambda state, operation = "+": self.button_operation_clicked(operation))

입니다. 버튼이 클릭되면, 람다함수로 정의된 함수가 실행이 되는 것인데요, button_operation_clicked() 메서드가 실행이 되는데, operation 값을 "+"로 지정하게 됩니다. button_operation_clicked() 메서드는 self.equation LineEdit에 출력돼 있는 text를 받아와서, 이 text의 마지막 부분에 "+"를 추가한 다음에 self.equation에 다시 출력하는 일을 합니다. 이렇게 하면 우리가 원하는 + 버튼의 기능이 간단하게 구현 됩니다. + 뿐아니라 -, * / 가 모두 이와 같이 작동하는 것이기 때문에 각 연산별로 메서드를 따로 만든것이 아니라 operation을 입력 인자로 받는 하나의 메서드를 만들고, 이를 람다함수를 사용하여 버튼의 이벤트에 연결한 것 입니다. 이러한 문법은 비단 이번 어플리케이션 뿐 아니라 GUI 프로그래밍의 많은 부분에서 사용되는 것이니 꼭 이해하고 익혀 두는 것이 필요합니다. 

 

(3) layout_clear_equal 레이아웃의 구성

 

수식 입력창의 모든 수식을 지우는 Clear 버튼, 마지막으로 입력된 문자를 지우는 Backspace버튼, 그리고 수식을 계산하여 답을 구하는 = 버튼은 위 (1), (2)를 이해했다면 직관적으로 이해할 수 있는 버튼과 기능입니다. 코드는 아래와 같습니다. 

        layout_clear_equal = QHBoxLayout()
        
        ### =, clear, backspace 버튼 생성
        button_equal = QPushButton("=")
        button_clear = QPushButton("Clear")
        button_backspace = QPushButton("Backspace")
        
        ### =, clear, backspace 버튼 클릭 시 시그널 설정
        button_equal.clicked.connect(self.button_equal_clicked)
        button_clear.clicked.connect(self.button_clear_clicked)
        button_backspace.clicked.connect(self.button_backspace_clicked)
        
    def button_equal_clicked(self):
        equation = self.equation.text()
        solution = eval(equation)
        self.solution.setText(str(solution))

    def button_clear_clicked(self):
        self.equation.setText("")
        self.solution.setText("")

    def button_backspace_clicked(self):
        equation = self.equation.text()
        equation = equation[:-1]
        self.equation.setText(equation)

각 버튼에 연결 돼 있는 메서드만 이해하면 되는데요, button_equal_clicked()에서는 수식을 계산하여 숫자로 바꾸어주는 python의 기본 모듈인 eval()를 사용하고 있습니다. 그 밖에 특별히 어려운 부분은 없습니다. 

 

(4) layout_number 레이아웃의 구성

 

계산기의 중요한 구성 요소중 하나인 숫자버튼을 포함하는 layout_number 레이아웃입니다. 이 레이아웃은 숫자 버튼을 격자에 맞게 배치하고 있는데요, 따라서 QGridLayout을 이용하면 4 x 3 의 버튼을 쉽게 배치할 수 있습니다. 이 부분에 해당하는 코드는 아래와 같습니다. 

        layout_equation_solution = QFormLayout()
        
        ### 숫자 버튼 생성하고, layout_number 레이아웃에 추가
        ### 각 숫자 버튼을 클릭했을 때, 숫자가 수식창에 입력 될 수 있도록 시그널 설정
        number_button_dict = {}
        for number in range(0, 10):
            number_button_dict[number] = QPushButton(str(number))
            number_button_dict[number].clicked.connect(lambda state, num = number:
                                                       self.number_button_clicked(num))
            if number >0:
                x,y = divmod(number-1, 3)
                layout_number.addWidget(number_button_dict[number], x, y)
            elif number==0:
                layout_number.addWidget(number_button_dict[number], 3, 1)

        ### 소숫점 버튼과 00 버튼을 입력하고 시그널 설정
        button_dot = QPushButton(".")
        button_dot.clicked.connect(lambda state, num = ".": self.number_button_clicked(num))
        layout_number.addWidget(button_dot, 3, 2)

        button_double_zero = QPushButton("00")
        button_double_zero.clicked.connect(lambda state, num = "00": self.number_button_clicked(num))
        layout_number.addWidget(button_double_zero, 3, 0)
        
    def number_button_clicked(self, num):
        equation = self.equation.text()
        equation += str(num)
        self.equation.setText(equation)

0~10 버튼을 하나 하나 만들수는 없으니 for 문을 이용하여 0 ~ 10의 버튼을 생성하였습니다. 그 다음에는 순서대로 하나씩 3 x 4 구조의 그리드에 추가하였습니다. 각 버튼의 위치를 (x,y)로 나타낸다면, divmod를 이용하여 그 위치를 쉽게 구할 수 있습니다. 숫자 0은 1~9 숫자에 비해서 조금 특별한 위상을 갖고 있으니, 0번은 따로 처리를 해 두었습니다. 

 

소숫점 버튼(.)과 00버튼 역시 따로 만들어서 추가할 수 있습니다. 각 버튼을 클릭했을 때는 사칙연산 버튼때와 마찬가지로 self.equation 의 맨 마지막 부분에 각 버튼에 해당하는 text를 추가하고 다시 self.equation에 출력하면 됩니다. number_button_clicked 메서드를 만들어 활용하였습니다. 

 

사실, number_button_clicked 메서드와 button_operation_clicked 메서드는 하는 일이 같기 때문에 둘 중에 하나만 사용해도 되지만, 이해의 편의를 위해서 서도 다른 매서드를 두개 만들었습니다. 

 

PyQt를 이용하여 간단하게 계산기를 만들어 보았습니다. 계산기는 간단한 어플리케이션이라서 지금까지 배웠던, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QPushButton, clicked.connect 정도만 알아도 100줄안에 만들 수 있는 어플리케이션이었습니다. 하지만, 계산기만 만들 수 있다면 원리적으로는 PyQt를 이용한 Python GUI 프로그래밍의 절반을 깨우쳤다고도 볼 수 있습니다. 

728x90