การพัฒนาซอฟต์แวร์ด้วย PyQt4 บน Raspberry Pi (Jessie) ตอนที่ 3

ตอนที่ 1 และ ตอนที่ 2 ก่อนหน้านี้เราได้ทราบเรื่องการสร้าง User Interface การใช้ Signal  และ Slot เพื่อใช้ประกอบการสร้าง Calculation Application ขึ้นมา แต่ยังขาดองค์ประกอบสำคัญไปอีกหนึ่งอย่างคือตัวที่ทำหน้าที่หาผลลัพธ์หลังจากผู้ใช้

ความต้องการของผมคือ Calculator  ไม่ได้รับค่าเหมือนเครื่องคิดเลขขนาดเล็กที่รับค่า operand (ตัวเลข) สลับกับ operator (เครื่องหมาย + - x /) แล้วตอบกลับด้วยผลลัพธ์ แต่ต้องสามารถรับค่าเป็น String เข้ามาแล้วทำการประมวลผลทีเดียวเลย เพื่อให้สามารถทำงานแบบที่ต้องการได้ เราต้องกล่าวถึงเรื่องนิพจน์ (expressions) แบบ infix และ postfix กันนิดหน่อย ครับ

เมื่อเราสร้างนิพจน์ทางคณิตศาสตร์ เช่น  9 + 3  มนุษย์จะสามารถตีความได้ง่ายมากคือเป็นการเพิ่มค่าให้ 9 ไปอีก 3 รูปแบบการเขียนนิพจน์แบบนี้เรียกว่า "infix" มีรูปแบบการเขียนสลับกันไปมาระหว่าง operand (หรือตัวเลขหรือตัวแปร) กับ operator (สัญญลักษณ์ทางคณิตศาสตร์) หากนิพจน์นี้ยาวหรือสลับขึ้นก็มีการจัดกลุ่มด้วยวงเล็บ เช่น (6+3) x (3+6)  รูปแบบ infix เป็นรูปแบบที่เราเรียนรู้กันในวิชาคณิตศาสตร์ตั้งแต่เด็กเราจึงตีความได้ง่าย แต่รูปแบบนี้ไม่เหมาะกับการทำให้คอมพิวเตอร์เข้าใจ  เช่น หากเรามีนิพจน์  9 + 3 x 10 / 2  ก็จะเริ่มเกิดความสับสนว่าจะเริ่มต้นจากตรงไหนดี 3 x 10 แล้ว หารด้วย 2 ตามด้วยบวกด้วย 9 หรือเริ่มต้นจาก 9 + 3 แล้วคูณด้วย 10 แล้วปิดท้ายด้วยการหารด้วย 2  (ที่ว่าสับสนนี้หมายถึงการเขียนชุดคำสั่งนะครับ คนเองไม่งงหรอก)

เพื่อลดความสับสนดังกล่าวเราจึงมีนิพจน์อีกแบบหนึ่งคือ "postfix" ซึ่งใช้การเขียนนิพจน์ที่ต้องนำเอา operator วางไว้หลัง operand สองตัวที่กระทำต่อกันเสมอ  รูปแบบนี้ทำให้การเขียนชุดคำสั่งง่ายเพราะมีรูปแบบที่แน่นอน (สำหรับคนแล้วอาจจะงงหน่อยหากยังไม่ชิน) ตัวอย่าง เช่น

InfixPostfix
9 + 39 3 +
9 + 3 x 10 / 23 10 2 x / 9 +
จากข้อมูลตรงนี้ภาระงานแรกที่เราต้องสร้างให้กับ Calculator Class คือ การแปลงนิพจน์ infix ที่มนุษย์เคยชินไปเป็นนิพจน์ postfix ที่เราประมวลผลได้ง่าย ซึ่ง algorithm ที่ผมเลือกใช้คือ Shunting-yard algorithm  ผมจะละไว้ท่านไปศึกษาเองครับ  มาดู Python script ของ Calculator Class

class Calculator():
 def __init__(self):
  self.prec = {}
  self.prec["x"] = 3
  self.prec["/"] = 3
  self.prec["+"] = 2
  self.prec["-"] = 2
  self.prec["("] = 1
  self.operators = ['+','-','x','/']
  
 def calculate(self,input_str=None):
  if input_str is None :
   return "Error"
  postfix_str = self.infix2postfix(input_str)
  #print postfix_str
  if len(postfix_str) > 0 :
   return self.eval_postfix(postfix_str)
  else :
   return "Error" 

 def infix2postfix(self,infix_str):
  out_Queue = Queue()
  operator_Stack = Stack()
  tokens = infix_str.split()
  postfix_str=""
  for token in tokens :
   try :
    d = float(token)
    out_Queue.enqueue(d)
   except :
    if token == '(':
     operator_Stack.push(token)
    elif token == ')':
     tmp = operator_Stack.pop()
     while  tmp != '(' :   
      out_Queue.enqueue(tmp)
      tmp = operator_Stack.pop()
    else : # token is an operator  
     while (not operator_Stack.is_empty()) and \
                                        (self.prec[operator_Stack.peek()] >= self.prec[token]) :
      out_Queue.enqueue(operator_Stack.pop())
     operator_Stack.push(token)
   
  while not operator_Stack.is_empty() :
   out_Queue.enqueue(operator_Stack.pop())

  while not out_Queue.is_empty():
   postfix_str += str(out_Queue.dequeue())+" "

  return postfix_str

 def eval_postfix(self,postfix_str):
  operand_queue = Queue()
  operand_1=None
  operand_2=None
  tokens = postfix_str.split()
  #print tokens
  for token in tokens :
   try :
    d = float(token)
    operand_queue.enqueue(d)
   except:
    if not operand_queue.is_empty() :
     operand_1 = operand_queue.dequeue()
    else :
     operand_1 = None 
    if not operand_queue.is_empty() :
     operand_2 = operand_queue.dequeue() 
    else :
     operand_2 = None
    #print operand_1,operand_2
    if not (operand_1 is None or operand_2 is None) :     
     try:
      result = self.do_math(token,operand_1,operand_2)
     except ZeroDivisionError :
      return "Division Error"  
    else :
     result = 0 
    operand_queue.enqueue(result)
  return operand_queue.dequeue()

 def do_math(self,operator,operand1,operand2):     
  if operator == "x":
   return operand1 * operand2
  elif operator == "/":
    return operand1 / operand2
  elif operator == "+":
   return operand1 + operand2
  else:
   return operand1 - operand2

จาก script ข้างบนมีการอ้างอิงถึงอีกสอง Class คือ Stack และ Queue ซึ่งทั้งสองเป็น Abstract Data Type ที่ใช้ใน algorithm นี้ มีขึ้นมาก็เพื่ออำนวยความสะดวกในการจัดการกับข้อมูลครับ  ซึ่ง Python script แสดงไว้ข้างล่างนี้


class Stack():
 def __init__(self):
  self.items = []
  
 def push(self,val):
  self.items.append(val)
  
 def pop(self):
  return self.items.pop()
 
 def peek(self):
  return self.items[-1:][0]
  
 def is_empty(self):
  if self.items == [] :
   return True
  else :
   return False


class Queue:
 def __init__(self):
  self.items = []

 def is_empty(self):
  return self.items == []

 def enqueue(self,item):
  self.items.append(item)

 def dequeue(self):
  return self.items.pop(0)

 def front(self):
  return self.items[len(self.items)-1]

 def size(self):
  return len(self.items)


มาถึงตรงนี้เราก็มีทุกอย่างครบถ้วนแล้ว ก็มาดูผลการเรียกใช้งานบน Raspberry Pi กับ  source code







Complete source code


import sys
from PyQt4 import QtCore, QtGui, uic
Ui_MainWindow, QtBaseClass = uic.loadUiType("calculator_ui.ui")

class Stack():
 def __init__(self):
  self.items = []
  
 def push(self,val):
  self.items.append(val)
  
 def pop(self):
  return self.items.pop()
 
 def peek(self):
  return self.items[-1:][0]
  
 def is_empty(self):
  if self.items == [] :
   return True
  else :
   return False


class Queue:
 def __init__(self):
  self.items = []

 def is_empty(self):
  return self.items == []

 def enqueue(self,item):
  self.items.append(item)

 def dequeue(self):
  return self.items.pop(0)

 def front(self):
  return self.items[len(self.items)-1]

 def size(self):
  return len(self.items)

class Calculator():
 def __init__(self):
  self.prec = {}
  self.prec["x"] = 3
  self.prec["/"] = 3
  self.prec["+"] = 2
  self.prec["-"] = 2
  self.prec["("] = 1
  self.operators = ['+','-','x','/']
  
 def calculate(self,input_str=None):
  if input_str is None :
   return "Error"
  postfix_str = self.infix2postfix(input_str)
  #print postfix_str
  if len(postfix_str) > 0 :
   return self.eval_postfix(postfix_str)
  else :
   return "Error" 

 def infix2postfix(self,infix_str):
  out_Queue = Queue()
  operator_Stack = Stack()
  tokens = infix_str.split()
  postfix_str=""
  for token in tokens :
   try :
    d = float(token)
    out_Queue.enqueue(d)
   except :
    if token == '(':
     operator_Stack.push(token)
    elif token == ')':
     tmp = operator_Stack.pop()
     while  tmp != '(' :   
      out_Queue.enqueue(tmp)
      tmp = operator_Stack.pop()
    else : # token is an operator  
     while (not operator_Stack.is_empty()) and (self.prec[operator_Stack.peek()] >= self.prec[token]) :
      out_Queue.enqueue(operator_Stack.pop())
     operator_Stack.push(token)
   
  while not operator_Stack.is_empty() :
   out_Queue.enqueue(operator_Stack.pop())

  while not out_Queue.is_empty():
   postfix_str += str(out_Queue.dequeue())+" "

  return postfix_str

 def eval_postfix(self,postfix_str):
  operand_queue = Queue()
  operand_1=None
  operand_2=None
  tokens = postfix_str.split()
  for token in tokens :
   try :
    d = float(token)
    operand_queue.enqueue(d)
   except:
    if not operand_queue.is_empty() :
     operand_1 = operand_queue.dequeue()
    else :
     operand_1 = None 
    if not operand_queue.is_empty() :
     operand_2 = operand_queue.dequeue() 
    else :
     operand_2 = None
    if not (operand_1 is None or operand_2 is None) :     
     try:
      result = self.do_math(token,operand_1,operand_2)
     except ZeroDivisionError :
      return "Division Error"  
    else :
     result = 0 
    operand_queue.enqueue(result)
  return operand_queue.dequeue()

 def do_math(self,operator,operand1,operand2):     
  if operator == "x":
   return operand1 * operand2
  elif operator == "/":
    return operand1 / operand2
  elif operator == "+":
   return operand1 + operand2
  else:
   return operand1 - operand2
 
  
class MyApp(QtGui.QMainWindow, Ui_MainWindow):
 #on_click = pyqtSignal(str)
 
 def __init__(self):
  QtGui.QMainWindow.__init__(self)
  Ui_MainWindow.__init__(self)
  self.setupUi(self)
  self.init_ui()
  self.calculator = Calculator()

 def init_ui(self):
  self.num_btn_1.clicked.connect(self.append_text)
  self.num_btn_2.clicked.connect(self.append_text)
  self.num_btn_3.clicked.connect(self.append_text)
  self.num_btn_4.clicked.connect(self.append_text)
  self.num_btn_5.clicked.connect(self.append_text)
  self.num_btn_6.clicked.connect(self.append_text)
  self.num_btn_7.clicked.connect(self.append_text)
  self.num_btn_8.clicked.connect(self.append_text)
  self.num_btn_9.clicked.connect(self.append_text)
  self.num_btn_0.clicked.connect(self.append_text)
  self.left_paren_btn.clicked.connect(self.append_text)
  self.right_paren_btn.clicked.connect(self.append_text)
  self.plus_btn.clicked.connect(self.append_text)
  self.minus_btn.clicked.connect(self.append_text)
  self.mult_btn.clicked.connect(self.append_text)
  self.div_btn.clicked.connect(self.append_text)  
  self.dot_btn.clicked.connect(self.append_text)
  self.bsp_btn.clicked.connect(self.backspace)
  self.clear_btn.clicked.connect(self.clear_text)
  self.exe_btn.clicked.connect(self.calculate)

 
 def append_text(self):
  sender = self.sender()
  val = sender.text()
  if val in ['/','+','-','(',')','x'] :
   val = " "+val+" "
  disp = self.disp_text.text()
  disp += val
  self.disp_text.clear()
  self.disp_text.setText(disp)
  
 def backspace(self):
  self.disp_text.backspace() 
  
 def clear_text(self):
  self.disp_text.clear()  
  
 def calculate(self):
  # need to change to pythonic string
  txt = str(self.disp_text.text())  
  if len(txt) > 0 :
   result = self.calculator.calculate(txt)
   self.disp_text.clear()
   self.disp_text.setText(str(result))
   
   
  
if __name__ == "__main__":

    app = QtGui.QApplication(sys.argv)
    window = MyApp()
    window.show()
    sys.exit(app.exec_())


ปล. โปรแกรมนี้เป็นโปรแกรมตัวอย่างที่นำมาประกอบการเขียนบทความ ยังไม่ใช่โปรแกรมที่สมบูรณ์แบบท่านสามารถนำ source code ไปพัฒนาและปรับปรุงได้อีกมากมาย ครับ ท่านที่สนใจสามารถดาวน์โหลดได้ที่นี่ 

Previous
Next Post »