Tag: ไพทอน

ทดสอบเขียนโปรแกรมไพทอน (Python)  บนเครื่องคิดเลข Casio fx-cg50 Prizm

ทดสอบเขียนโปรแกรมไพทอน (Python) บนเครื่องคิดเลข Casio fx-cg50 Prizm

ไพทอนบนเครื่องคิดเลข

ช่วงนี้ผมมีโอกาสทำงานใกล้ชิดกับภาคสนาม ทำให้มีโอกาสได้จับและใช้เครื่องคิดเลขมากกว่าปกติ ในเวลาที่ผ่านมาไม่ถึงเดือนผมได้ซื้อเครื่องคิดเลข Casio fx-CG50 Prizm เคสสีขาว ที่ซื้อมาเพราะทราบว่าถ้า update OS เป็นรุ่น 3.20 จะสามารถใช้ ไพทอน (Python) ได้ ก็ขอหมายเหตุสักนิดว่าเป็นไมโครไพทอน (Micropython) ที่ทางทีมงาน Micropython ได้พอร์ตออกมาให้มีขนาดเล็กเพื่อเอาไปรันในบอร์ด iOT ได้ หรือบอร์ดที่เป็นไมโครคอนโทรลเลอร์ทั้งหลาย เน้นขนาดเล็ก หน่วยความจำต่ำ กินไฟน้อย ต่ออินเทอร์เน็ตได้ในตัว ผมจะไม่มุ่งไปทางนี้หรอกครับ ในบทความนี้ แต่จะพูดถึงเครื่องคิดเลขคาสิโอ ที่นำเอาไมโครไพทอนมาลงเครื่องคิดเลขรุ่นนี้ เพราะว่าไมโครไพทอนกินหน่วยความจำต่ำ ก็เลยเหมาะสมที่จะเอามารันในเครื่องคิดเลขที่มีทรัพยากรต่ำอยู่แล้ว ให้เกิดประสิทธิภาพมากยิ่งขึ้นไป

เป็นที่ทราบกันว่าไพทอนในปัจจุบันมีความนิยมมากยิ่งขึ้น เนื่องจากเรียนรู้ได้ง่าย เขียนง่าย อ่านง่าย ทรงพลัง แต่สำหรับไมโครไพทอน เนื่องจากออกแบบให้มีขนาดเล็ก ดังนั้นไลบรารีต่างๆที่ใช้ได้ในไพทอนรุ่นใหญ่ จะสามารถนำมาใช้กับไมโครไพทอนได้ต้องมีการพอร์ตใหม่ อาจจะลดฟังก์ชั่นหรือลดสิ่งไม่จำเป็นออก ทำให้มีขนาดเล็กลง ดังนั้นผมทำใจในจุดนี้อยู่แล้ว ว่าไม่สามารถเอาไลบรารีรุ่นใหญ่มารันได้แน่นอนเช่น pyproj, mathplotlib

สำหรับเครื่องคิดเลขที่ใช้ในแวดวงวิศวกรรม โปรแกรมที่เขียนด้วยไมโครไพทอนที่มีไลบรารี math หรืออาจจะเสริมด้วยไลบรารีพวกเมตริก (matrix) น่าจะพอนำมาเขียนใช้งานกันได้

 การพัฒนาโปรแกรมด้วยชุดพัฒนาโปรแกรมภาษาซี (Software Development Kit)

นี่เป็นความข้องใจของผมในฐานะแฟนเครื่องคิดเลขคาสิโอ รุ่น fx-9860G ทางคาสิโอจัดทำ SDK ให้สามารถเขียนโปรแกรมด้วยภาษาซี ที่ผมเขียนโปรแกรมมาแจกใช้ในแวดวงงานสำรวจแล้วหลายโปรแกรม แต่รุ่นนี้กลับไม่ทำมาให้  (ที่จริงไม่ทำมาให้ตั้งแต่ fx-CG10/fx-CG20)ไม่ทราบว่าเพราะเหตุใด เครื่องคิดเลขรุ่นนี้ไม่มี SD Card แต่ทดแทนด้วยการใส่ Flash memory มา 16 MB  ซึ่งก็พอจะใส่โปรแกรมใช้งานได้มากโขอยู่ หรือคิดว่ามีไมโครไพทอน มาให้แล้วน่าจะตอบโจทย์ได้หมด แต่ผมก็ไม่คิดอย่างนั้น ยังมีโปรแกรมเมอร์ภาษาซีอีกพอสมควร และในสภาพแวดล้อมของเครื่องคิดเลขจริงๆ โปรแกรมที่เขียนด้วยภาษาซีจะเร็วกว่าไพทอนอยู่แล้ว แต่ไพทอนได้เปรียบในด้านความง่าย

 เครื่องมือพัฒนาโปรแกรมของชุมชน

ยังมีชุมชนของนักพัฒนาที่สร้าง SDK  ขึ้นมาใช้งานเอง มีประมาณ 2-3 กลุ่มแต่สุดท้ายดูเหมือนไม่มีความเคลื่อนไหวกันมาหลายปีแล้ว เครื่องมือที่มีชื่อเสียงมากที่สุดคือ PrizmSDK และอีกอันคือ mini-SDK ผมเองใช้ไลบรารี MyLib แต่เผอิญผู้พัฒนาได้ทำไว้สำหรับเครื่อง fx-9860G เท่านั้น ไม่เป็นไรขอมุ่งลองไพทอนบนเครื่องคิดเลขรุ่น fx-CG50 นี้ก่อน ถ้าพัฒนาโปรแกรมด้วยภาษาซี ผมก็ยังมุ่งไปที่เครื่องคิดเลข fx-9860G เหมือนเดิม 

ผมลองเขียนโปรแกรมทดสอบเล็กๆลองดูด้วยเครื่องมือ PrizmSDK ก็ได้ดังรูปข้างล่าง (โปรแกรมไม่มีอะไรมีแต่เมนู) เทียบกับโปรแกรม System Manager ที่มากับเครื่อง

คุณสมบัติของเครื่องคิดเลข

โดยรวมรวมแล้วเครื่องคิดเลขนั้นเหมาะสำหรับนักศึกษามาก เพราะมีฟังก์ชั่นคณิตศาสตร์ การเงิน สถิติ มีกราฟมากมายให้ใช้ แต่สำหรับผมแล้วไม่มีอะไรต้องใช้เลย ยกเว้นเรื่องโปรแกรมบนเครื่องคิดเลขอย่างเดียว ถ้าไม่มีสิ่งนี้ก็ใช้เป็นที่ทับกระดาษได้เลย เครื่องรุ่นนี้ใช้โปรเซสเซอร์ตระกูล SH4 ขนาดหน้าจอ 384 x 216 จอ LCD จำนวนสี 65000 สี มีความสว่างพอสมควรและปรับได้ ความกว้างหน้าจอแบบทะแยง 3.17 นิ้ว หน่วยความจำของเครื่อง 60 KB มี Flash memory ที่สามารถเขียนอ่านได้ 16  MB ซึ่งจะเป็นที่เอาไว้เก็บโปรแกรมหรือข้อมูล ใช้ถ่าน AAA 4 ก้อน  เท่าที่ผมเปิดเครื่องใช้บ้างในแต่ละวันมาประมาณสองสัปดาห์ พบว่าแบตเตอรี่ลดลงมานิดหนึ่ง อนาคตอาจจะหาถ่านชาร์จมาใช้ ตอนนี้ใส่อัลคาไลน์ไปก่อน

ประเดิมโปรแกรมด้วยไพทอน

จะลองโปรแกรมทั้งทีผมพยายามให้โปรแกรมมีขนาดซับซ้อนมานิดหนึ่ง  และเรียกใช้โมดูลด้วย คิดไปคิดมาก็เลยจะลองโปรแกรมแปลงค่าพิกัดระหว่างค่าพิกัดภูมิศาสตร์กับค่าพิกัดยูทีเอ็ม เนื่องจากไม่ค่อยมีเวลาเขียน เลยลองหาไลบรารีที่ท่านอื่นได้ทำไว้ ผมเคยเกริ่นไปแล้วข้างต้นว่าไลบรารีรุ่นใหญ่เช่น pyproj ไม่สามารถเอามาใช้ได้ ลองค้นดูพบว่ามีไลบรารีไพทอนเล็กๆ ชื่อ utm มีสัญญาอนุญาตเป็น MIT-License ผมเอาโค้ดมาดัดแปลงนิดหน่อยให้เหมาะสมกับเครื่องคิดเลข แล้วเขียนไปอยู่ในไฟล์ utm.py เพื่อให้สะดวกเวลาเรียกใช้

การเขียนโปรแกรมไพทอน ถ้าไปเขียนบนเครื่องคิดเลข จะชักช้าเสียเวลาครับ เนื่องจากไมโครไพทอน พอร์ตไลบรารีเช่น math แล้ว ดังนั้นถ้าโปรแกรมของเราไม่ได้ใช้อะไรพิศดารมาก ก็สามารถมาเขียนโค้ดบน PyCharm หรือ Idle ได้ ผมเลือก PyCharm เมื่อทดสอบโปรแกรมเสร็จสามารถ โอนโปรแกรมเข้าไปไว้ในเครื่องด้วยการต่อเครื่องคิดเลขด้วยสาย USB เข้ากับคอมพิวเตอร์ที่รันวินโดส์ตามผังด้านล่าง

เมื่อรันโปรแกรมได้ตามความต้องการแล้วจากนั้นก็เอาเครื่องคิดเลขมาต่อกับคอมพิวเตอร์ วินโดส์จะมองเห็นเป็นไดรว์ สามารถใช้ File Explorer ก๊อปปี้โปรแกรมจากคอมพิวเตอร์ไปยังเครื่องคิดเลขได้ ตัวโค้ดโปรแกรมไลบรารีดูได้ด้านล่างครับ


import math
K0 = 0.9996

E = 0.00669438
E2 = E * E
E3 = E2 * E
E_P2 = E / (1.0 - E)

SQRT_E = math.sqrt(1 - E)
_E = (1 - SQRT_E) / (1 + SQRT_E)
_E2 = _E * _E
_E3 = _E2 * _E
_E4 = _E3 * _E
_E5 = _E4 * _E

M1 = (1 - E / 4 - 3 * E2 / 64 - 5 * E3 / 256)
M2 = (3 * E / 8 + 3 * E2 / 32 + 45 * E3 / 1024)
M3 = (15 * E2 / 256 + 45 * E3 / 1024)
M4 = (35 * E3 / 3072)

P2 = (3. / 2 * _E - 27. / 32 * _E3 + 269. / 512 * _E5)
P3 = (21. / 16 * _E2 - 55. / 32 * _E4)
P4 = (151. / 96 * _E3 - 417. / 128 * _E5)
P5 = (1097. / 512 * _E4)

R = 6378137

ZONE_LETTERS = "CDEFGHJKLMNPQRSTUVWXX"

class OutOfRangeError(ValueError):
    pass

def to_latlon(easting, northing, zone_number, hemi):

    northern = (hemi == 'N')

    x = easting - 500000
    y = northing

    if not northern:
        y -= 10000000

    m = y / K0
    mu = m / (R * M1)

    p_rad = (mu +
             P2 * math.sin(2 * mu) +
             P3 * math.sin(4 * mu) +
             P4 * math.sin(6 * mu) +
             P5 * math.sin(8 * mu))

    p_sin = math.sin(p_rad)
    p_sin2 = p_sin * p_sin

    p_cos = math.cos(p_rad)

    p_tan = p_sin / p_cos
    p_tan2 = p_tan * p_tan
    p_tan4 = p_tan2 * p_tan2

    ep_sin = 1 - E * p_sin2
    ep_sin_sqrt = math.sqrt(1 - E * p_sin2)

    n = R / ep_sin_sqrt
    r = (1 - E) / ep_sin

    c = _E * p_cos**2
    c2 = c * c

    d = x / (n * K0)
    d2 = d * d
    d3 = d2 * d
    d4 = d3 * d
    d5 = d4 * d
    d6 = d5 * d

    latitude = (p_rad - (p_tan / r) *
                (d2 / 2 -
                 d4 / 24 * (5 + 3 * p_tan2 + 10 * c - 4 * c2 - 9 * E_P2)) +
                 d6 / 720 * (61 + 90 * p_tan2 + 298 * c + 45 * p_tan4 - 252 * E_P2 - 3 * c2))

    longitude = (d -
                 d3 / 6 * (1 + 2 * p_tan2 + c) +
                 d5 / 120 * (5 - 2 * c + 28 * p_tan2 - 3 * c2 + 8 * E_P2 + 24 * p_tan4)) / p_cos

    return (180/math.pi*(latitude),
            180/math.pi*(longitude) + zone_number_to_central_longitude(zone_number))


def from_latlon(latitude, longitude, force_zone_number=None):
    if not -80.0 <= latitude <= 84.0:
        raise OutOfRangeError('latitude out of range (must be between 80 deg S and 84 deg N)')
    if not -180.0 <= longitude <= 180.0: raise OutOfRangeError('longitude out of range (must be between 180 deg W and 180 deg E)') lat_rad = math.pi/180*(latitude) lat_sin = math.sin(lat_rad) lat_cos = math.cos(lat_rad) lat_tan = lat_sin / lat_cos lat_tan2 = lat_tan * lat_tan lat_tan4 = lat_tan2 * lat_tan2 if force_zone_number is None: zone_number = latlon_to_zone_number(latitude, longitude) else: zone_number = force_zone_number #zone_letter = latitude_to_zone_letter(latitude) if (latitude >= 0):
      hemi = 'N'
    else:
      hemi = 'S'

    lon_rad = math.pi/180*(longitude)
    central_lon = zone_number_to_central_longitude(zone_number)
    central_lon_rad = math.pi/180*(central_lon)

    n = R / math.sqrt(1 - E * lat_sin**2)
    c = E_P2 * lat_cos**2

    a = lat_cos * (lon_rad - central_lon_rad)
    a2 = a * a
    a3 = a2 * a
    a4 = a3 * a
    a5 = a4 * a
    a6 = a5 * a

    m = R * (M1 * lat_rad -
             M2 * math.sin(2 * lat_rad) +
             M3 * math.sin(4 * lat_rad) -
             M4 * math.sin(6 * lat_rad))

    easting = K0 * n * (a +
                        a3 / 6 * (1 - lat_tan2 + c) +
                        a5 / 120 * (5 - 18 * lat_tan2 + lat_tan4 + 72 * c - 58 * E_P2)) + 500000

    northing = K0 * (m + n * lat_tan * (a2 / 2 +
                                        a4 / 24 * (5 - lat_tan2 + 9 * c + 4 * c**2) +
                                        a6 / 720 * (61 - 58 * lat_tan2 + lat_tan4 + 600 * c - 330 * E_P2)))

    if latitude < 0:
        northing += 10000000

    return easting, northing, zone_number, hemi


def latitude_to_zone_letter(latitude):
    if -80 <= latitude <= 84: return ZONE_LETTERS[int(latitude + 80) >> 3]
    else:
        return None


def latlon_to_zone_number(latitude, longitude):
    if 56 <= latitude < 64 and 3 <= longitude < 12:
        return 32

    if 72 <= latitude <= 84 and longitude >= 0:
        if longitude <= 9:
            return 31
        elif longitude <= 21:
            return 33
        elif longitude <= 33:
            return 35
        elif longitude <= 42:
            return 37

    return int((longitude + 180) / 6) + 1


def zone_number_to_central_longitude(zone_number):
    return (zone_number - 1) * 6 - 180 + 3


โปรแกรมแปลงพิกัดภูมิศาสตร์ในภาคไพทอน

ผมเขียนไพทอนเป็นโมดูลอีกโมดูลเพื่อเรียกใช้ไลบรารี ตั้งชื่อว่า UTM2GEO.py โดยที่เขียนเมนูติดต่อการใช้งานง่ายๆ

from utm import *
   
def print_menu():
  print(5*'-',"MENU",5*'-')
  print('1: UTM to GEO')
  print('2: GEO to UTM')
  print('0: Exit')

def geo2utm(lon,lat):
  east,north,zn,hem=from_latlon(lat,lon)
  print("North={0:11.3f}".format(north))
  print("East={0:10.3f}".format(east))  
  print("UTM Zone No={0:0d}{1}".format(zn,hem))
    
def utm2geo(e,n,zoneno,hemi):
  lat,lon=to_latlon(e,n,zoneno,hemi)
  print("Latitude={0:11.7f}".format(lat))
  print("Longitude={0:10.7f}".format(lon))   
    
loop=True
while loop:   
  print_menu()
  choice=int(input('Selection[0-2]'))
  if (choice==0):
    loop=False
  elif (choice==1):
    loop=True
    y=float(input("Northing="))
    x=float(input("Easting="))
    zn=int(input("Zone No="))
    hem=input("Hemi (N/S)=")
    utm2geo(x,y,zn,hem)
  elif (choice==2):
    loop=True
    y=float(input("Latitude="))
    x=float(input("Longitude="))
    geo2utm(x,y)   

วิธีก๊อปปี้โปรแกรม

จากนั้นผมก๊อปปี้สองไฟล์คือ utm.py  และ UTM2GEO.py ลงบนไดรว์เครื่องคิดเลขดังนี้

ผมเก็บไว้ที่ไดเรคทอรี \SAVE-F\PROGRAM เวลาจะถอดสาย USB เพื่อเลิกการเชื่อมต่อต้อง Safely removal โดยการคลิกเมาส์ขวา “Eject” ที่ File Explorer จากนั้นมาที่เครื่องคิดเลขจากให้กดคีย์ “EXE” และ “EXIT” ตามลำดับ ถ้าไม่ทำไฟล์อาจจะไม่ได้ซิงค์กันอาจจะหายหรือไม่สมบูรณ์ได้ ที่เครื่องคิดเลขกดคีย์ “MENU” เลือก “Python

แปลงพิกัดจากค่าพิกัดภูมิศาสตร์เป็นค่าพิกัดยูทีเอ็ม

จากรูปด้านบนกดคีย์ F1-Run จะเห็นหน้าจอขึ้นเมนูติดต่อมาง่ายๆ

เราจะเลือกแปลงพิกัดจากค่าพิกัดภูมิศาสตร์ไปเป็นค่าพิกัดยูทีเอ็มเลือกกดคีย์ “2” ที่เครื่องคิดเลขแล้วกดคีย์ “EXE” ป้อนค่าพิกัด Latitude = 39.95259668 Longitude  = -75.15132081 (ป้อนเป็นหน่วยดีกรี ในตอนนี้ยังไม่รับค่าแบบอื่น) จะได้ผลการคำนวณออกมา เนื่องจากในตอนนี้ไม่มีคำสั่งเบรคการแสดงผลเมื่อเขียนด้วยไพทอน (เอาละจะมาบ่นทีหลัง ว่าใส่ไพทอนมาแล้วทางคาสิโอไม่ให้เครื่องมืออะไรมาเลย) การจะดูผลลัพธ์ ผู้ใช้ต้องกดคีย์ “0” เพื่อออกจากโปรแกรมและใช้ลูกศรกดขึ้นไปทางด้านบนเพื่อไปดูผลลัพธ์

จะได้ค่า Northing = 4422506.896 Easting = 487074.371 อยู่ในโซน 18N

แปลงพิกัดจากค่าพิกัดยูทีเอ็มเป็นค่าพิกัดภูมิศาสตร์

ทำการรันโปรแกรมใหม่อีกครั้ง ที่เมนูเลือกกดเลข “1” ป้อนค่าพิกัด Northing = 2642783.110 Easting =232030.949 UTM Zone No = 46 Hemi = N

ดูค่าผลลัพธ์ได้ (กดคีย์ “0” ออกจากเมนูก่อนแล้วเลื่อนขึ้นไปดู)

สรุปการใช้งาน

ตอนแรกผมคาดหวังจากที่ทางคาสิโอเอาไมโครไพทอนมาลงเครื่องคิดเลขรุ่นนี้ ยังไงการเขียนโปรแกรมใช้งานยังไงๆผลลัพธ์ที่ออกมาก็ต้องระดับเทพ เพราะไพทอนมันทรงพลังด้วยตัวของมัน แต่เมื่อลองแล้วผิดหวังมาก จนบัดนี้คาสิโอ้ยังไม่ได้ออกคู่มือแสดงฟังก์ชั่นที่ไพทอนสามารถเรียกมาใช้ได้ มีฟังก์ชั่น input กับ print สองฟังก์ชั่นนี้เท่านั้น เพียงแค่ผมค้นหาฟังก์ชั่น clear screen หน้าจอยังทำไม่ได้ ฟังก์ชั่นที่ต้องการสนับสนุนได้แก่การเขียนเมนูที่เรียกใช้ด้วยคีย์ F1 ถึง F6 การปริ๊นท์แสดงสีต่างๆ การเรียกใช้ฟังก์ชั่นกราฟต่างๆหรือพล็อทกราฟ หรือใช้งานเมตริก เป็นต้น

เอาละตอนนี้ไพทอนที่ปรากฎบน OS รุ่น 3.20 เพิ่งออกมาเตือนตุลาคม 2018 (ขณะที่เขียนบทความนี้เดือนพฤศจิกายน 2018) คงต้องให้เวลาสักพักว่าจะเป็นอย่างไร บอกตามตรงว่าคงต้องเอาเครื่องคิดเลขรุ่นนี้มาทับกระดาษอีกสักพักใหญ่ๆ

คอมไพล์ Python Script เป็นไฟล์ Executable ด้วย PyInstaller

PyInstaller

คือเครื่องมือที่ช่วยการแปลงโปรแกรมที่เขียนด้วยไพทอนเป็น execute binary file  ที่สามารถนำไปรันได้โดยที่เครื่องคอมพิวเตอร์ปลายทางไม่ต้องติดตั้งไพทอน สำหรับ PyInstaller เป็น cross-platform สามารถใช้งานได้บนวินโดส์ แมค และลีนุกซ์ สนับสนุนไพทอนรุ่น 2.7 และ ไพทอน รุ่น 3.3 ถึง 3.6 จุดมุ่งหมายของ PyInstaller คือต้องการช่วยผู้ใช้ในการแปลงโปรแกรมไพทอน ที่ใช้โมดูลไลบรารีภายนอกเช่น Matplotlib, DJango, wxPython, PyQt เป็นต้น ให้สามารถทำได้ง่ายสะดวก

ติดตั้ง PyInstaller

ติดตั้งง่ายๆด้วยคำสั่ง pip ใน command prompt

pip install pyinstaller

ใช้งาน PyInstaller

การใช้งานสามารถใช้งานผ่าน command line ได้ แต่สำหรับโปรแกรมที่เรียกใช้โมดูลไลบรารีข้างนอกและต้องขนข้อมูล (data) ที่โมดูลไลบรารีนั้นๆต้องการใช้  ผมแนะนำให้ใช้ไฟล์สคริปท์ (Spec file) มาช่วยจะดีกว่า ปรับแต่งได้มากกว่า ตัว spec file จริงๆก็คือไฟล์สคริปท์ของไพทอนนั่นเอง กรณีที่ต้องใช้ Spec file อีกกรณีหนึ่งคือต้องการขนรันไทม์ไลบรารีเช่น .dll หรือ .so ไปแบบแมนวล กรณีที่ผมเจอคือผมใช้ PySide2 ที่รุ่นทางการจริงๆยังไม่ออกมา แต่ hook file ก็มีมาให้แล้วพร้อมกับ PyInstaller รุ่นใหม่ 3.3 แต่ผมใช้งานแล้วยังไม่สำเร็จ ดังนั้นจึงต้องใช้ Spec file นี้เป็นตัวช่วยในการขนรันไทม์ไลบรารีไป ส่วนเรื่อง hook file คืออะไรค่อยว่าอีกที

กรณีศึกษาด้วย Surveyor Pocket Tools บนวินโดส์

โปรแกรม Surveyor Pocket Tools พัฒนาด้วยไพทอน ปัจจุบันใช้ไพทอน รุ่น 3.6 ใช้โมดูลไลบรารีข้างนอกคือ openpyxl, pyproj, geographiclib, gmplot, simplekml, pyshp และที่ขาดไม่ได้คือ PySide2 ซึ่งสำหรับ openpyxl และ pyproj จะมีการขนข้อมูลไปด้วย ส่วน PySide2 ผมจะขนไฟล์ dll  ที่ต้องการด้วยมือล้วนๆ

Spec file ของ Surveyor Pocket Tools

มาดูไฟล์สคริปนี้ ผมตั้งชื่อว่า “setup.spec”

# -*- mode: python -*-
# -*- mode: python -*-
import sys
import PySide2
import os
block_cipher = None

dirname = os.path.dirname(PySide2.__file__)
plugins_path = os.path.join(dirname, 'plugins', '')

pyside2_plugins = [(plugins_path + 'iconengines/*', 'plugins/iconengines/'),
                  (plugins_path + 'imageformats/*', 'plugins/imageformats/'),
		  (plugins_path + 'platforms/*', 'plugins/platforms/'),
		  (plugins_path + 'printsupport/*', 'plugins/printsupport/'),
		  (plugins_path + 'sqldrivers/*', 'plugins/sqldrivers/')]

added_files = [('markers/*', 'markers/'),
               ('geoids/*', 'geoids/'),
	       ('database/*', 'database/'),
	       ('example data/*', 'example data/'),
	       ('qt.conf', ''), 
	       ('*.xml', '')]

a = Analysis(['main.py'],
             pathex=['D:\\sourcecodes\\python\\surveyor pocket tools'],
             binaries=None,
             datas=added_files + pyside2_plugins,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=True,
          name='surveyor pocket tools',
		  icon='Land Surveying-64.ico',
          debug=False,
          strip=False,
          upx=False,
          console=False )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               name='setup')

ลองมาดูโค้ดกัน เริ่มจาก import PySide2 เข้ามาเพื่อจะตรวจสอบว่า PySide2 ที่เราใช้งานเป็น 32 บิตหรือ 64 บิต เพื่อจะได้ขน .dll ไปถูกรุ่น จากนั้นเก็บไดเรคทอรีของ PySide2 เข้าเก็บใน dirname ผ่านฟังก์ชัน os.path.dirname() ที่นี้เราทราบว่าในไดเรคทอรีของ PySide2 จะมีไดเรคทอรีย่อยชื่อ “plugins” อยู่ ทำการเก็บไดเรคทอรีนี้ด้วยฟังก์ชั่น os.path.join() ไปเก็บไว้ในตัวแปร plugins_path

# -*- mode: python -*-
import sys
import PySide2
import os
block_cipher = None

dirname = os.path.dirname(PySide2.__file__)
plugins_path = os.path.join(dirname, 'plugins', '')

ต่อไปคือตัวแปร pyside2_plugins จะเป็นลิสต์เก็บ tuple โดยสมาชิกตัวแรกจะเก็บชื่อไฟล์ไดเรคทอรีต้นทาง ใช้เครื่องหมาย * เพราะต้องการทุกๆไฟล์ในไดเรคทอรีนี้ สมาชิกตัวที่สอง จะเก็บชื่อไดเรคทอรีปลายทางที่ต้องการไฟล์เหล่านี้ไปอยู่


pyside2_plugins = [(plugins_path + 'iconengines/*', 'plugins/iconengines/'),
                   (plugins_path + 'imageformats/*', 'plugins/imageformats/'),
		   (plugins_path + 'platforms/*', 'plugins/platforms/'),
		   (plugins_path + 'printsupport/*', 'plugins/printsupport/'),
		   (plugins_path + 'sqldrivers/*', 'plugins/sqldrivers/')]

ลองมาดูว่าไดเรคทอรี “plugins” ผมไฮไลท์ไว้เฉพาะไดเรคทอรีที่โปรแกรม Surveyor Pocket Tools ต้องการ

ต่อไปจะขนไฟล์ที่โปรแกรม Surveyor Pocket Tools ต้องการใช้ ให้ใส่ไว้ที่ตัวแปร added_files โครงสร้างเป็น tuple เหมือนกัน และขนไฟล์ชื่อ qt.conf ที่ PySide2 ต้องการไปด้วย

added_files = [('markers/*', 'markers/'),
               ('geoids/*', 'geoids/'),
	       ('database/*', 'database/'),
	       ('example data/*', 'example data/'),
	       ('qt.conf', ''), 
	       ('*.xml', '')]

มาดูไดเรคทอรีที่โปรแกรมต้องการดังนี้

ต่อไปมาดูโค้ดส่วนที่สำคัญมาก ‘main.py’ คือไฟล์สคริปท์หลักของโปรแกรม Surveyor Pocket Tools ต่อไปคือ pathex เป็นไดเรคทอรีของไฟล์ไพทอนสคริปท์ และ datas ที่ผมจัดการรวม added_files และ pyside2_plugins เข้าด้วยกัน สุดท้าย hookspath คือไดเรคทอรีที่เก็บไฟล์ hook ไว้ สำหรับไฟล์ hook นี้ PyInstaller จะอ่านสคริปท์นี้ทีละไฟล์มาตัดสินใจว่าจะขนข้อมูลไดเรคทอรีไหนไป ผมเลือกใช้ดีฟอลท์ครับคือปล่อยว่าง

a = Analysis(['main.py'],
             pathex=['D:\\sourcecodes\\python\\surveyor pocket tools'],
             binaries=None,
             datas=added_files + pyside2_plugins,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)

สำหรับไดเรอทอรี hooks ที่เป็นดีฟอลท์มากับ PyInstaller ผมใช้ไฟล์เพียงสองไฟล์เท่านั้น ตามที่ไฮไลท์ไว้

ใช้ PyInstaller คอมไพล์ไฟล์ setup.spec

ผมใช้ Minoconda เมื่อจะคอมไพล์ก็เรียก command prompt มาดังนี้ ใช้คำสั่ง cd เข้ามาที่พาทของสคริปท์ของไพทอน ใช้คำสั่ง dir ดูไฟล์ setup.spec

ต่อไปทำการคอมไพล์ ด้วยคำสั่ง

pyinstaller setup.spec

ผลลัพธ์ของ PyInstaller

เมื่อคอมไพล์เสร็จแล้ว ไม่มี error จะได้ไดเรคทอรีมาสองคือ “build” และ “dist” เมื่อเข้าไปดูใน “dist” จะเห็นไดเรคทอรีย่อยช “setup” ชื่อไดเรคทอรีนี้ PyInstaller จะสร้างตามชื่อหน้าของไฟล์ setup.spec เมื่อเข้าดูที่ไดเรคทอรี “setup” จะเห็นไฟล์ต่างๆที่โปรแกรมต้องการ

ผมลองดับเบิ้ลคลิกไฟล์ “surveyor pocket tools.exe” ก็สามารถเปิดมาและทำงานได้ตามปกติ ลองดูชื่อไดเรคทอรีจะเห็นสองไดเรคทอรี ที่ได้จากไฟล์ hooks คือ openpyxl และ pyproj ลองเข้าไปดูในไดเรคทอรี จะเห็นข้อมูลที่ pyproj ขนไปใช้ หมายเหตุว่าข้อมูลนี้ pyproj จะนำไปเป็นฐานข้อมูลในการแปลงพิกัดตาม datum และ projection

ทำไฟล์ Setup ด้วย Inno Setup

จากนั้นผมจะ copy ไดเรคทอรีที่อยู่ใน “setup” ไปไว้อีกที่หนึ่ง พื้นที่นี้สำหรับใช้ Inno Setup มาทำไฟล์ติดตั้ง ลองดูไดเรคทอรี

ในไดเรคทอรีนี้ผมจะมีไฟล์ “surveyorpockettools64.iss” เป็นไฟล์สคริปท์ของ Inno Setup เพื่อสร้างไฟล์ติดตั้ง setup สำหรับวินโดส์ 64 บิต

#define MyAppName "Surveyor Pocket Tools"
#define MyAppEXE "Surveyor Pocket Tools.exe"
#define MyShortAppName "SurveyorPocketTools"
#define MyMainRoot "Survey Suite"
#define Developer "Prajuab Riabroy"
#define Version "0.98"
#define Build "573"

[Setup]
AppName={#MyAppName}
AppVerName={#MyAppName} V{#Version}
DefaultDirName={pf}\{#MyMainRoot}\{#MyAppName}
DefaultGroupName={#MyMainRoot}\{#MyAppName}
UseSetupLdr=yes
UninstallDisplayIcon={app}\{#MyAppEXE}
VersionInfoProductName={#MyAppName}
VersionInfoCompany=priabroy
VersionInfoCopyright=Copyright 2000-2017 by {#Developer}
VersionInfoDescription={#MyAppName}
VersionInfoProductVersion={#Version}
VersionInfoVersion={#Version}
OutputDir=Setup
OutputBaseFilename={#MyShortAppName}V{#Version}Build{#Build}Setup64
;OutputDir=TraverseProV250Setup64
; "ArchitecturesAllowed=x64" specifies that Setup cannot run on
; anything but x64.
ArchitecturesAllowed=x64
; "ArchitecturesInstallIn64BitMode=x64" requests that the install be
; done in "64-bit mode" on x64, meaning it should use the native
; 64-bit Program Files directory and the 64-bit view of the registry.
;ArchitecturesInstallIn64BitMode=x64
ArchitecturesInstallIn64BitMode=x64
AppPublisher={#Developer}
AppPublisherURL=https://www.surveyorpockettools.org
AppVersion={#Version}.{#Build}
LicenseFile = eula.txt
ChangesEnvironment=yes
SolidCompression=yes
Compression=lzma2/ultra64
LZMAUseSeparateProcess=yes
LZMADictionarySize=1048576
LZMANumFastBytes=273

[Files]
Source: "{#MyAppName}.exe"; DestDir: "{app}"
Source: "base_library.zip"; DestDir: "{app}" ;
Source: "plugins\*"; DestDir: "{app}\plugins\"; Flags: ignoreversion recursesubdirs
Source: "database\*"; DestDir: "{userappdata}\{#MyAppName}\database\";
Source: "geoids\*"; DestDir: "{userappdata}\{#MyAppName}\geoids\";
Source: "markers\*"; DestDir: "{userappdata}\{#MyAppName}\markers\";
Source: "example data\*"; DestDir:"{userappdata}\{#MyAppName}\example data\";
Source: "pyproj\data\*"; DestDir: "{app}\pyproj\data\";
Source: "openpyxl\*"; DestDir: "{app}\openpyxl\";
;Source: "requests\*"; DestDir: "{app}\requests\";
Source: "*.html"; DestDir: "{userappdata}\{#MyAppName}\";
Source: "*.dll"; DestDir: "{app}"
Source: "*.pyd"; DestDir: "{app}"
Source: "*.xml"; DestDir: "{userappdata}\{#MyAppName}\";
Source: "qt.conf"; DestDir: "{app}"

[Icons]
;create icon at start menu group
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExe}"
;create icon at desktop
Name: "{commondesktop}\{#MyAppName}"; FileName:"{app}\{#MyAppExe}"

[Registry]
; Start "Software\My Company\My Program" keys under HKEY_CURRENT_USER
; and HKEY_LOCAL_MACHINE. The flags tell it to always delete the
; "My Program" keys upon uninstall, and delete the "My Company" keys
; if there is nothing left in them.
Root: HKCU; Subkey: "Software\{#MyMainRoot}"; Flags: uninsdeletekeyifempty
Root: HKCU; Subkey: "Software\{#MyMainRoot}\{#MyAppName}"; Flags: uninsdeletekey
Root: HKLM; Subkey: "Software\{#MyMainRoot}"; Flags: uninsdeletekeyifempty
Root: HKLM; Subkey: "Software\{#MyMainRoot}\{#MyAppName}"; Flags: uninsdeletekey
Root: HKLM; Subkey: "Software\{#MyMainRoot}\{#MyAppName}\Settings"; ValueType: string; ValueName: "InstalledPath"; ValueData: "{app}"
Root: HKLM; Subkey: "Software\{#MyMainRoot}\{#MyAppName}\Settings"; ValueType: string; ValueName: "DevelopedBy"; ValueData: "{#Developer}"
Root: HKLM; Subkey: "Software\{#MyMainRoot}\{#MyAppName}\Settings"; ValueType: string; ValueName: "ApplicationName"; ValueData: "{#MyAppName}"
;Root: HKCU; Subkey: "Environment"; ValueType:string; ValueName:"PROJ_LIB"; ValueData:"{userappdata}\{#MyAppName}\geoidgrids\" ; Flags: preservestringtype ;

ถ้าเป็นไฟล์สำหรับ Surveyor Pocket Tools รุ่น 32 บิตเพียงใส่คอมเมนต์หน้า ;ArchitecturesInstallIn64BitMode=x64 ก็พอ สำหรับรายละเอียดสคริปท์ของ Inno Setup ผมจะไม่กล่าวถึงรายละเอียดในที่นี้ผู้อ่านที่สนใจสามารถศึกษาได้ครับ จากนั้นก็ใช้ Inno Setup ทำการ build ก็จะได้ไฟล์ Exe เดี่ยวๆ ที่สามารถ zip ไปให้ผู้ใช้ได้ download ต่อไป พบกันตอนหน้าครับ

แนะนำการย้ายโค้ดจาก PyQt5 เป็น PySide2

ย้ายโค้ด XSection Plot

ในขณะนี้ทำงานอยู่ที่บังคลาเทศ โครงการก่อสร้างรถไฟฟ้าที่กรุงธากา มีโอกาสกลับมาพัก ก็พอมีเวลาว่างพยายามย้ายโค้ดของโปรแกรม XSection Plot จากของเดิมที่พัฒนาด้วย PyQt5 ที่ยังติดเรื่องลิขสิทธิ์บางส่วน โดยย้ายมาใช้ PySide2 ที่เปิดกว้างกว่า ความจริงทั้งคู่ใช้เครื่องยนต์ (Engine) เดียวกันคือ Qt5 platform ดังนั้นเมื่อย้ายโค้ดสำเร็จแล้วเวลารันก็หน้าตาเหมือนกันเป๊ะดังรูปด้านล่างที่คอมไพล์ด้วย PySide2

จัดการปลั๊กอิน PySide2

การย้ายโค๊ดใช้เวลาไม่นานนัก ใช้เวลาประมาณ 2 ชั่วโมง เนื่องจากผมเคยย้ายโค้ด Surveyor Pocket Tools ทำให้รู้แนวทางลัดพอสมควร อันดับแรกขอย้อนกลับหน่อย เนื่องจาก PySide2 จะมองหาโฟลเดอร์ลิ๊งค์ไลบรารีของตัวเองชื่อ “plugins” ถ้าไม่เจอจะ error แล้วหยุดทันที ดังนั้นก่อนอื่นควรจะแทรกโค๊ดนี้เข้าไปก่อน เริ่มตั้งแต่ import PySide2 ตามด้วยตรวจสอบว่า PySide2 อยู่ที่โฟลเดอร์ไหนจัดเก็บเข้าตัวแปร dirname จากนั้นค้นหาพาทของ “plugins” ที่อยู่ใต้โฟลเดอร์ dirname ด้วยคำสั่ง os.path.join()

import os
import sys
import PySide2
dirname = os.path.dirname(PySide2.__file__)
plugin_path = os.path.join(dirname, 'plugins', '')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
print('plugin_path = ', plugin_path)

ที่เครื่องคอมพิวเตอร์ผม จะปริ๊นท์พาทของ “plugins” ดังนี้

plugin_path =  C:\Miniconda3\envs\py36_64\lib\site-packages\PySide2\plugins\

เพราะว่าผมใช้ Miniconda เป็นตัวจัดการระบบ environment ของ python ผมสร้าง envs ชื่อ “py36_64” เป็น python รุ่น 3.6 แบบ 64 บิต และ PySide2 ก็จะถูกติดตั้งมาอยู่ภายใต้โฟลเดอร์นี้ อีก envs หนึ่งที่สร้างไว้ชื่อ “py36_32” เป็น python รุ่น 3.6 แบบ 32 บิต เมื่อรันโปรแกรมแล้วจะปริ๊นท์พาทมาดังนี้

plugin_path =  C:\Miniconda3\envs\py36_32\lib\site-packages\PySide2\plugins\

วิธีการสร้าง environment สำหรับไพทอนก็กลับไปดูโพสต์เก่าของผมได้ครับ

เปลี่ยนคำ PyQt5 เป็น PySide2

ยังอยู่ในส่วน import ที่โค๊ดเดิมของโปรแกรมผมเรียกใช้ไลบรารีของ PyQt5 ดังนี้

from PyQt5.QtGui import QPixmap, QIcon, QKeySequence, QFont, QIntValidator, QCursor
from PyQt5.QtCore import Qt, pyqtSlot, QSettings, QFileInfo, QSize, QFile
from PyQt5.QtWidgets import QUndoStack, QSplashScreen, QApplication, QMainWindow, QTabWidget, QAction, QStatusBar,\
     QMenu, QWidget, QSizePolicy, QLineEdit, QFileDialog, QMessageBox, QDesktopWidget

เปลี่ยนเป็น

from PySide2.QtGui import QPixmap, QIcon, QKeySequence, QFont, QIntValidator, QCursor
from PySide2.QtCore import Qt, Slot, QSettings, QFileInfo, QSize, QFile
from PySide2.QtWidgets import QUndoStack, QSplashScreen, QApplication, QMainWindow, QTabWidget, QAction, QStatusBar,\
     QMenu, QWidget, QSizePolicy, QLineEdit, QFileDialog, QMessageBox, QDesktopWidget

ส่วนใหญ่เกือบ 99.99% ที่เหมือนกัน ยกเว้น Signal & Slot

Signal and Slot

มีข้อแตกต่างกันเล็กน้อย เช่นเดิมใน PyQt5 เรียกใช้ pyqtSlot, pyqtSignal ให้เปลี่ยนเป็น Slot, Signal ใน PySide2 ครับ
นอกจากส่วน import แล้ว ในโค๊ดเดิมที่ประกาศคลาส โค้ดเดิมผมเรียกใช้ Signal and Slot ดังนี้

class OverlapSection(QObject):
    '''Horizontal & Vertical overlapped.'''
    overlapped = pyqtSignal(str)

    def __init__(self):
            QObject.__init__(self)

    def emitOverlapSignal(self, message):
        self.overlapped.emit(message)

เปลี่ยนใหม่เป็น

class OverlapSection(QObject):
    '''Horizontal & Vertical overlapped.'''
    overlapped = Signal(str)

    def __init__(self):
            QObject.__init__(self)

    def emitOverlapSignal(self, message):
        self.overlapped.emit(message)

ติดตั้ง PySide2 จากไฟล์ wheel

ก็ขอแนะอีกนิดว่า PySide2 เวลาติดตั้งให้ใช้แพ๊คเกจแบบ wheel ที่ทางทีมงานได้ทำไว้ดีกว่าครับ ติดตั้งง่ายไม่งอแง ดาวน์โหลดได้ตามลิ๊งค์นี้ จะสังเกตเห็นชื่อไฟล์ประมาณนี้

PySide2-5.6-cp35-cp35m-win32.whl 	 
PySide2-5.6-cp35-cp35m-win_amd64.whl 	 
PySide2-5.6-cp36-cp36m-win32.whl 
PySide2-5.6-cp36-cp36m-win_amd64.whl

จะเห็นว่า PySide2 สนับสนุนทั้งไพทอน 3.5 และ 3.6 และในตอนนี้ Qt5 รุ่น  5.6 สำหรับคำสั่งที่ติดตั้งก็ง่ายๆใช้ pip ตามด้วยชื่อไฟล์ wheel

pip install PySide2-5.6-cp36-cp36m-win_amd64.whl

สรุปแล้วการย้ายโค้ดง่ายๆไม่ลำบากกินแรง แต่ไปกินแรงเข็นครกอีกทีคือตอนสร้างไบนารีไฟล์ด้วย Pyinstaller ความจริง Pyinstaller ถ้าเข้าใจแล้วปรับใช้ได้ไม่ยาก แต่สำหรับมือใหม่บอกตรงๆว่า ถ้าโปรแกรมที่พัฒนาเรียกใช้ไลบรารีมากหลายอันแล้ว เป็นนรกลูกย่อมๆครับ ถ้าไลบรารีตัวไหนมีคนเขียนไฟล์ hook ให้ก็ง่ายหน่อย แต่ถ้าไม่มีต้องออกแรงกันพอสมควร สำหรับ PySide2 ผมจัดการแบบ manual ครับ รู้ว่าตอนโปรแกรมรันมันต้องการอะไร ตอนใช้ Pyinstaller ผมก็จัดการ copy ไฟล์ไปตามต้องการ ถ้ามีโอกาสจะมาเขียนเรื่องการใช้ Pyinstaller อีกสักตอน พบกันตอนหน้าครับ