누군가에게 학습용으로 계산기 프로그램을 제작하기로 하고 여러가지 내용을 다루다 보니 정리해 두는게 좋을 것 같아 포스팅한다.

 

인터페이스는 PyQt를 이용해 만들고 계산기는 후위표기법을 이용해 동작하게 끔 구성했다.

 

Postfix 알고리즘과 코드는 인터넷에서 가져온 것인데 출처를 다시 찾을 수 없어 퍼왔다는 것만 공지해 둔다.

가져온 코드로는 문자 하나만 계산이 가능한 형태여서 실제로는 이런저런 수정을 많이 한 상태다.

 

후위표기법을 위해서는 스택을 사용해야 하고 후위표기법과 스택에 대한 설명은 쉽게 찾을 수 있지만 그림이 있는 링크를 하나 첨부해 둔다.

 

velog.io/@inyong_pang/12%EA%B0%95-%EC%8A%A4%ED%83%9D%EC%9D%98-%EC%9D%91%EC%9A%A9-%EC%88%98%EC%8B%9D%EC%9D%98-%ED%9B%84%EC%9C%84-%ED%91%9C%EA%B8%B0%EB%B2%95Postfix-Notation

 

[12강] 스택의 응용 - 수식의 후위 표기법(Postfix Notation)

연산자가 피연산자들의 사이에 위치(A + B) \* (C + D)연산자가 피연산자들의 뒤에 위치AB + CD + \*중위 (A + B) \* C후위 A B + C \*여는 괄호는 스택에 push닫는 괄호를 만나면 여는 괄호가 나올 때까지 pop

velog.io

 

자료구조와 알고리즘 측면에서 계산기 프로그램은 좋은 학습용 프로그램 연습이라고 생각한다.

 

일단 화면을 구성하기 위해선 QtCreator를 설치해서 구성하는 것이 편리하다.

화면은 코드로 직접 작성하는 방법도 있고 확장자 .ui로 작성하는 방법 두가지가 있는데 편리성을 위해 여기선 별도의 ui파일을 구성하는 방법으로 작성한다.

 

화면이 하나라서 Qt for Python을 선택해 다이얼로그로 작성했다.
다이얼로그이름과 연결할 파이썬 소스코드이름을 정한다.

 

비어있는 화면이 나오면 사이즈를 조절한후 원하는 버튼과 텍스트박스 위젯을 배치한다.
원하는 위젯을 배치하고 개별 오브젝트 이름을 작성하면 화면디자인은 끝

 

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>pycalc_test</class>
 <widget class="QDialog" name="pycalc_test">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>250</width>
    <height>318</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>pycalc_test</string>
  </property>
  <property name="font" >
    <font>
    <pointsize>20</pointsize>
    </font>
  </property>

	(이하 생략)

 

QtCreator로 생성한 폼파일을 텍스트에디터로 열어보면 XML형태로 되어있어 필요하면 직접 코드를 추가하면 되는 편리함이 있다.

위의 코드에서는 폰트속성을 일괄적용하기위한 코드를 추가했다.

 

이제 본격적인 파이썬 코드를 작성해 보자

 

QtCreator로 디자인하면 자동으로 생성되는 코드가 만들어지는데 이 코드는 Qt 라이브러리 제작사의 공식 파이썬 버전인 PySide2 기반으로 생성된다.

 

Qt라이브러리는 원래 C/C++용 라이브러리로 자체적으로 만든 파이썬용 라이브러리가 없을 시절 Riverbank Computing이라는 회사에서 PyQt라는 이름으로 제작해 배포하기 시작해 정작 Qt for Python에 해당하는 라이브러리는 PySide라는 이름으로 나오게 되었고 현재는 PySide2로 업그레이드 버전으로 서비스되고 있다. 두 버전은 라이선스에서 LGPL을 지원하느냐 정도의 차이만 존재하고 문법적으로도 상당히 유사하다고 한다. 본문에서는 기존에 설치한 PyQt를 기반으로 코드를 작성한다. PyQt는 GPL와 상용 라이선스로 제공된다. 

(상대적으로 파이썬 시장에서 점유율이 떨어지기때문인지 PySide2는 LGPL도 제공한다.)

 

PyQt는 Qt라이브러리를 래핑한 형태라서 Qt4, Qt5에 대응되는 PyQt4, PyQt5로 구분되어져있다.

 

코드에 대해 간략하게 소개하면

 

1. PyQt5를 임포트한 후에 폼파일을 읽어온다.

import sys
from PyQt5.QtWidgets import *
from PyQt5 import uic

form_class = uic.loadUiType("form.ui")[0]

2. 스택 클래스를 구성한다.

class Stack(list):
	def __init__(self):
		self.stack = []

	def push(self, data):
		self.stack.append(data)

	def pop(self):
		if self.is_empty():
			return -1
		return self.stack.pop()

	def length(self):
		return len(self.stack)

	def peek(self):
		return self.stack[-1]

	def is_empty(self):
		if len(self.stack) == 0:
			return True
		return False

3. 스택을 활용한 후위표기를 저장하고 연산하는 클래스를 구성한다.

class Calculator:
	def __init__(self, org_exp):
		self.org_exp = org_exp.replace(' ', '')
		self.postfix_exp = None

	def get_weight(self, oprt):
		if oprt == '*' or oprt == '/':
			return 8
		elif oprt == '+' or oprt == '-':
			return 7
		elif oprt == '(':
			return 9
		else:
			return -1

	def convert_to_postfix(self):
		exp_list = []
		oprt_stack = Stack()
		cnt = 0
		temp = ""

		for ch in self.org_exp:
			if ch.isdigit():
				temp = temp + ch
				#exp_list.append(ch)
				#print(exp_list)
			else:
				if ch == '(' or oprt_stack.is_empty():
					oprt_stack.push(ch)
					print("oprt_stack_push %c" % ch)
					exp_list.append(temp)
					print(exp_list)
					temp = ""
				elif ch == ')':
					op = oprt_stack.pop()
					while op != '(':
						exp_list.append(op)
						op = oprt_stack.pop()
				elif self.get_weight(ch) > \
                    self.get_weight(oprt_stack.peek()):
					oprt_stack.push(ch)
					print("oprt_stack_push %c" % ch)
					exp_list.append(temp)
					print(exp_list)
					temp = ""
				elif ch == '.':
					temp = temp + ch
					print("I'm dot")
				else:
					while oprt_stack and self.get_weight(ch) <=\
                          self.get_weight(oprt_stack.peek()):
						exp_list.append(oprt_stack.pop())
					oprt_stack.push(ch)
					print("oprt_stack_push %c" % ch)
					exp_list.append(temp)
					print(exp_list)
					temp = ""
		print("oprt_stack count %d" % oprt_stack.length())
		if temp != "":
			exp_list.append(temp)
		while oprt_stack.length() > 0:
			cnt = cnt + 1
			print("oprt_stack_pop count %d" % cnt)
			exp_list.append(oprt_stack.pop())
		#self.postfix_exp = ''.join(exp_list)
		self.postfix_exp = exp_list
		print(exp_list)
		print(self.postfix_exp)

	def get_postfix_exp(self):
		if not self.postfix_exp:
			self.convert_to_postfix()

		return self.postfix_exp

	def calc_two_oprd(self, oprd1, oprd2, oprt):
		if oprt == '+':
			print("run '+'")
			return oprd1 + oprd2
		elif oprt == '-':
			print("run '-'")
			return oprd1 - oprd2
		elif oprt == '*':
			print("run '*'")
			return oprd1 * oprd2
		elif oprt == '/':
			print("run '/'")
			return oprd1 // oprd2

	def calculate(self):
		oprd_stack = Stack()

		for ch in self.postfix_exp:
			if ch.isdigit():
				oprd_stack.push(int(ch))
			else:
				oprd2 = oprd_stack.pop()
				oprd1 = oprd_stack.pop()
				oprd_stack.push(self.calc_two_oprd(oprd1, oprd2, ch))
		return oprd_stack.pop()

4. Qt윈도우를 관리하는 클래스를 구성한다.

class CalcWindowClass(QMainWindow, form_class):
	def __init__(self):
		super().__init__()
		self.setupUi(self)

		self.pb_num0.clicked.connect(self.num_0_clicked)
		self.pb_num1.clicked.connect(self.num_1_clicked)
		self.pb_num2.clicked.connect(self.num_2_clicked)
		self.pb_num3.clicked.connect(self.num_3_clicked)
		self.pb_num4.clicked.connect(self.num_4_clicked)
		self.pb_num5.clicked.connect(self.num_5_clicked)
		self.pb_num6.clicked.connect(self.num_6_clicked)
		self.pb_num7.clicked.connect(self.num_7_clicked)
		self.pb_num8.clicked.connect(self.num_8_clicked)
		self.pb_num9.clicked.connect(self.num_9_clicked)

		self.pb_dot.clicked.connect(self.dot_clicked)
		self.pb_add.clicked.connect(self.add_clicked)
		self.pb_sub.clicked.connect(self.sub_clicked)
		self.pb_mul.clicked.connect(self.mul_clicked)
		self.pb_div.clicked.connect(self.div_clicked)
		self.pb_clear.clicked.connect(self.clear_clicked)
		self.pb_back.clicked.connect(self.back_clicked)

		self.pb_eq.clicked.connect(self.result_clicked)

		self.ret_string = ""

	def add_result(self, s):
		self.ret_string = self.ret_string + s

	def back_clicked(self):
		l = len(self.ret_string)
		self.ret_string = self.ret_string[:l-1]
		self.txtResult.setText(self.ret_string)

	def num_0_clicked(self):
		self.add_result("0")
		self.txtResult.setText(self.ret_string)

	def num_1_clicked(self):
		self.add_result("1")
		self.txtResult.setText(self.ret_string)

	def num_2_clicked(self):
		self.add_result("2")
		self.txtResult.setText(self.ret_string)

	def num_3_clicked(self):
		self.add_result("3")
		self.txtResult.setText(self.ret_string)

	def num_4_clicked(self):
		self.add_result("4")
		self.txtResult.setText(self.ret_string)

	def num_5_clicked(self):
		self.add_result("5")
		self.txtResult.setText(self.ret_string)

	def num_6_clicked(self):
		self.add_result("6")
		self.txtResult.setText(self.ret_string)

	def num_7_clicked(self):
		self.add_result("7")
		self.txtResult.setText(self.ret_string)

	def num_8_clicked(self):
		self.add_result("8")
		self.txtResult.setText(self.ret_string)

	def num_9_clicked(self):
		self.add_result("9")
		self.txtResult.setText(self.ret_string)

	def dot_clicked(self):
		self.add_result(".")
		self.txtResult.setText(self.ret_string)

	def add_clicked(self):
		self.add_result("+")
		self.txtResult.setText(self.ret_string)

	def sub_clicked(self):
		self.add_result("-")
		self.txtResult.setText(self.ret_string)

	def mul_clicked(self):
		self.add_result("*")
		self.txtResult.setText(self.ret_string)

	def div_clicked(self):
		self.add_result("/")
		self.txtResult.setText(self.ret_string)

	def clear_clicked(self):
		self.ret_string = ""
		self.txtResult.clear()

	def result_clicked(self):
		calc = Calculator(self.ret_string)
		calc.get_postfix_exp()
		result = calc.calculate()
		self.add_result("=")
		self.add_result(str(result))
		self.txtResult.setText(self.ret_string)
        

5. 윈도우를 불러와 실행하는 메인함수를 구성한다.

if __name__ == "__main__":
	app = QApplication(sys.argv)

	myWindow = CalcWindowClass()

	myWindow.show()

	app.exec_()

 

스택과 후위표기법을 위한 기본 코드는 인터넷에서 가져와 수정했는데 제때 메모를 해두지 않아서 다시 찾을 수 없어 코멘트만 남겨둔다.

 

우선순위 적용된 계산식

 

 

아직 최종본은 아니지만 현재까지 작성된 소스코드는 아래 링크에 있다.

 

github.com/godofslp/calculator_python_qt.git

Posted by 휘프노스
,