首頁 > 軟體

「Python」console 程式

2021-03-01 12:00:14

前言

前一陣子用python寫了一支Windows的Console程式:因為需要為程式的輸出資訊加一點顏色而小小的'卡'了一陣子;上個月底終於把它解決了,特別記錄一下免得日後給忘了.

為Console輸出上色

這個Console上色的問題,因為不是主要的功能,所以一開始只解決了一半,就一直放著:輸出ANSI Terminal的控制代碼可以在git for windows的bash視窗上更改輸出資訊的顏色;但是一換到Windows CMD Console執行時這些ANSI Terminal的控制代碼就完全沒有效果,只是一五一十地印出來.

上個月底總算把程式的主要功能寫完測完,因此回頭解決它.但是,在Google上爬了許久(主要都是StackOverflow網站),大部是說需換掉Windows CMD讓它可以支援ANSI Terminal的控制代碼就可以了.可是這並不是我要的答案.其中也有試著下載一些其他的模組,大部分試不下去的原因都是因為在python for windows環境下並沒有termios這個模組可用.直到看到有人回覆說用colorama模組就可以解決,不過也試了幾次才順利把問題解決.

一開始我是直接像colorama 0.4.1的說明頁面上說的加了以下的code:

from colorama import initinit()

結果並不像說明頁面上說的這樣就可以了,而是剛好和我原本狀況相反:Windows CMD Console正常了,但是在git for windows的bash視窗中原本有上色的部分卻不見了.

還好之前為了暫時把Windows CMD Console的ANSI Terminal的控制代碼取消掉,找到了如何判別系統環境的程式碼:

if 'SHELL' not in os.environ:……

於是二者結合,娃哈哈…結果正確了.

if 'SHELL' not in os.environ:from colorama import init init()class bColors: ENDC = '33[0m' BOLD = '33[1m' UNDERLINE = '33[4m' Dk_BLACK = '33[30m' Dk_RED = '33[31m' Dk_GREEN = '33[32m' Dk_YELLOW = '33[33m' Dk_BLUE = '33[34m' Dk_VIOLET = '33[35m' Dk_BEIGE = '33[36m' Dk_WHITE = '33[37m' Li_RED = '33[91m' Li_GREEN = '33[92m' Li_YELLOW = '33[93m' Li_BLUE = '33[94m' Li_VIOLET = '33[95m'class colorMessage(bColors): def ok(self, msg): return self.Li_GREEN + msg + self.ENDC def err(self, msg): return self.Li_RED + msg + self.ENDC def warn(self, msg): return self.Dk_YELLOW + msg + self.ENDC def bold(self, msg): return self.BOLD + msg + self.ENDC def erPrint(self, msg, hmsg='Error: '): print(self.err(hmsg)+msg, file=sys.stderr) def wrnPrint(self, msg, hmsg='Warning: '): print(self.warn(hmsg)+msg, file=sys.stderr)cMsg = colorMessage()...def usage2(appName): print(cMsg.ok("nCommon Arguments:")) print(cMsg.bold(" -H")+", "+cMsg.bold("--full-help")) print(" full help messages.") print(cMsg.bold(" -h")+", "+cMsg.bold("--help")) print(" brief help messages.")...cMsg.erPrint(msg)...

說明:

首先(也是最重要的),前三行是:只有系統是Windows才需要載入colorama,並執行init().之所以會這樣子和colorama說明檔案不一致,我猜應該是我使用的環境的問題:我開發程式時用的是git for windows的bash環境,而正式要執行的環境是windows CMD console.問題應該是因為git for windows的系統底層用的是MSYS2所造成的,(其實要在windows環境下搞出一個和linux/unix完全相容的環境本來就有相當的難度).改天有空裝cent OS時,再來試一下是否真如colorama的說明頁面說的,只要import及init()就可以;也順便測試是否我的程式移到linux/unix也可以正常運作?

再來的class bColors:是定義不同顏色的ANSI Terminal控制代碼.

再來的class colorMessage(bColors):是定義一些基本的上色功能(前面加上色控制代碼,後面加回複句柄),以及二個列印到stderr的function.

再來的cMsg = colorMessage()則是產生一個colorMessage()的instance.

然後就可以使用定義好的功能了

補充修正

哈,真是狗屎運啊,沒想到之前的Google搜尋都是作白工!!??

今天在Google搜尋msys2 bash python for windows msvcrt getch結果找到了StackExchange SuperUser的這一遍,發現它說node,python,ipython,php,php5,psql等這幾支程式已知都需要Win32 Console才能正確執行,建議使用者在git for windows的bash裡下指令時,前面要多帶一個winpty.意即是在winpty模擬出來的pty(Pseudo tty)環境中執行這些程式.

例如:winpty /path/to/python.exe….

於是就順手試了一下,結果…前面真的都是作白工了:colorama真的只要二行就好,不需要多判斷.

不過這樣寫在windows CMD Console及加了winpty的bash環境下是正確的,但是直接使用bash執行卻沒有上色.所以呢,為了在直接使用bash執行時也可以輸出有上色的提示資訊我還是附加載入colorama的條件式.

if ('SHELL' not in os.environ) or not re.search(r'/dev/ptyd+', os.popen('tty').read()):from colorama import init init()

注一:由原本git bash視窗輸入指令:tty,指令輸出的tty名稱會是/dev/pty0,/dev/pty1…但如果是輸入winpty tty,則指令輸出的tty名稱會是/dev/cons0,/dev/cons1…

Console讀取按鍵

上面的'補充修正'還有一個更棒的效應:連我之前一直測試不出來的:使用msvcrt模組裡的getch()和kbhit()來直接讀取鍵盤的問題也一併解決了.

這個問題的狀況如下:python程式碼由msvcrt模組中匯入getch()和kbhit()二個函數,然後在無窮迴圈中getch()讀取按鍵輸入,然後執行對應的功能,一般遊戲程式都會需要這種功能(或者參看下面這一段子程式).結果,在windows CMD Console中執行都正常.但是,在git for windows的bash環境執行時只要執行到getch()就一直卡在裡面,幾乎是不論按什麼鍵都是一動也不動,無法跳出來.即使改用linux/unix的寫法sys.stdin.read(1)結果在git for windows的bash環境中就是失敗.

def readPSWD(prompt):sys.stdout.write(prompt) sys.stdout.flush() str = "" while True:# if not kbhit():# sleep(0.1)# continue c = getch() if c in (b'r', b'n'): return str elif c == b'b': if len(str) > 0: str = str[0:len(str)-1] sys.stdout.write('b b') sys.stdout.flush() continue str += c.decode() sys.stdout.write('*') sys.stdout.flush()

改用winpty /path/to/python.exe…來執行,上面的問題就都迎刃而解了.所以,以下的程式碼也都可以順利執行了.(先判斷是windows還是linux/unix,如果是linux/unix就定義替代的getch()和kbhit()二個函數)

try:from msvcrt import getch, kbhitexcept ImportError: import tty import termios def getch(): ... def kbhit(): ...

不過,執行時(git for windows的bash)python程式認為自己是在windows環境裡執行,使用的是msvcrt模組的功能而不是linux/unix的替代函數.

編譯python程式

python是一種直譯的手稿語言(scripting language),在程式的開發/除錯上相對容易.但是總不能叫程式的未端使用者也安裝個python和需要的模組套件吧?因此有了編譯python程式的想法.

其實要編譯python程式並不難,上網Google一查就有了:

Gordon McMillan's installer(cross-platform)

Thomas Heller's py2exe(Windows)

Anthony Tuininga's cx_Freeze(cross-platform)

Bob Ippolito's py2app(Mac)

我選用了第一個,也就是pyInstaller.雖然功能參數不少,第一次接觸有點嚇到.不過因為我的需求簡單,所以也很快就找到了對應的參數(它也很簡單).

pyinstaller -FmyApps.py

這樣就可以順利在子目錄dist中產生myApp.exe了.(當然還有其他的子錄目和很多檔案,但這些都是我們修正程式錯誤後,協助加快重新編譯用的,無需給末端使用者).

改變console程式執行時的字型大小

其實上一段只是為了鋪陳,這一段才是重點.編譯成執行檔給user後,第一回應是:字太小,可不可以改大一點?我一整個傻掉…

好的,沒問題,改給你.還好,從上網Google,到改完程式,重新編譯,15分鐘搞定.

先用把Windwos CMD Console視窗的字型名稱及字型大小調整好,再執行以下的程式,查出應該設定的數值.

import sysfrom ctypes import POINTER, WinDLL, Structure, sizeof, byreffrom ctypes.wintypes import BOOL, SHORT, WCHAR, UINT, ULONG, DWORD, HANDLELF_FACESIZE = 32STD_OUTPUT_HANDLE = -11class COORD(Structure):_fields_ = [ ("X", SHORT), ("Y", SHORT), ]class CONSOLE_FONT_INFOEX(Structure): _fields_ = [ ("cbSize", ULONG), ("nFont", DWORD), ("dwFontSize", COORD), ("FontFamily", UINT), ("FontWeight", UINT), ("FaceName", WCHAR * LF_FACESIZE) ]kernel32_dll = WinDLL("kernel32.dll")get_last_error_func = kernel32_dll.GetLastErrorget_last_error_func.argtypes = []get_last_error_func.restype = DWORDget_std_handle_func = kernel32_dll.GetStdHandleget_std_handle_func.argtypes = [DWORD]get_std_handle_func.restype = HANDLEget_current_console_font_ex_func = kernel32_dll.GetCurrentConsoleFontExget_current_console_font_ex_func.argtypes = [HANDLE, BOOL, POINTER(CONSOLE_FONT_INFOEX)]get_current_console_font_ex_func.restype = BOOLset_current_console_font_ex_func = kernel32_dll.SetCurrentConsoleFontExset_current_console_font_ex_func.argtypes = [HANDLE, BOOL, POINTER(CONSOLE_FONT_INFOEX)]set_current_console_font_ex_func.restype = BOOLdef main(): # Get stdout handle stdout = get_std_handle_func(STD_OUTPUT_HANDLE) if not stdout: print("{:s} error: {:d}".format(get_std_handle_func.__name__, get_last_error_func())) return # Get current font characteristics font = CONSOLE_FONT_INFOEX() font.cbSize = sizeof(CONSOLE_FONT_INFOEX) res = get_current_console_font_ex_func(stdout, False, byref(font)) if not res: print("{:s} error: {:d}".format(get_current_console_font_ex_func.__name__, get_last_error_func())) return # Display font information print("Console information for {:}".format(font)) for field_name, _ in font._fields_: field_data = getattr(font, field_name) if field_name == "dwFontSize": print(" {:s}: {{X: {:d}, Y: {:d}}}".format(field_name, field_data.X, field_data.Y)) else: print(" {:s}: {:}".format(field_name, field_data)) while 1: try: height = int(input("nEnter font height (invalid to exit): ")) except: break # Alter font height font.dwFontSize.X = 10 # Changing X has no effect (at least on my machine) font.dwFontSize.Y = height # Apply changes res = set_current_console_font_ex_func(stdout, False, byref(font)) if not res: print("{:s} error: {:d}".format(set_current_console_font_ex_func.__name__, get_last_error_func())) return print("OMG! The window changed :)") # Get current font characteristics again and display font size res = get_current_console_font_ex_func(stdout, False, byref(font)) if not res: print("{:s} error: {:d}".format(get_current_console_font_ex_func.__name__, get_last_error_func())) return print("nNew sizes X: {:d}, Y: {:d}".format(font.dwFontSize.X, font.dwFontSize.Y))if __name__ == "__main__": print("Python {:s} on {:s}n".format(sys.version, sys.platform)) main()

再接著把你要的數值(查到的數值)填到以下的程式.(上一段的精簡版,只有設定的功能)

import ctypesLF_FACESIZE = 32STD_OUTPUT_HANDLE = -11class COORD(ctypes.Structure):_fields_ = [("X", ctypes.c_short), ("Y", ctypes.c_short)]class CONSOLE_FONT_INFOEX(ctypes.Structure): _fields_ = [("cbSize", ctypes.c_ulong), ("nFont", ctypes.c_ulong), ("dwFontSize", COORD), ("FontFamily", ctypes.c_uint), ("FontWeight", ctypes.c_uint), ("FaceName", ctypes.c_wchar * LF_FACESIZE)]font = CONSOLE_FONT_INFOEX()font.cbSize = ctypes.sizeof(CONSOLE_FONT_INFOEX)font.nFont = 16font.dwFontSize.X = 11font.dwFontSize.Y = 24font.FontFamily = 54font.FontWeight = 400font.FaceName = "Consolas"handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)ctypes.windll.kernel32.SetCurrentConsoleFontEx( handle, ctypes.c_long(False), ctypes.pointer(font))

接著就是修改主程式:在windows環境下才修改視窗設定.(假設上一段程式儲存成set_font.py)

if 'SHELL' not in os.environ:import set_font

搞定:無論User的Windows CMD Console怎麼設定,我的程式一定會以固定字型Consolas,固定大小的字型開始.程式結束後,也不會影響到User原本的設定.

錯誤結束時暫停一下

編譯成執行檔後,附帶的多出了一個問題:如果User直接在視窗環境滑鼠點二下執行,然後直接出現錯誤(可能他的環境少了個什麼東西而你沒考慮到…),結果是執行視窗直接關掉,什麼錯誤資訊也沒有看到.怎麼辦啊?

原本以為這個問題會有點小棘手小障礙,結果也是出奇的順利.

以下是我修改後的程式.

import atexitimport sys, osclass ExitHooks(object):def __init__(self): self.exit_code = None self.exception = None def hook(self): self._orig_exit = sys.exit sys.exit = self.exit sys.excepthook = self.exc_handler def exit(self, code=0): self.exit_code = code self._orig_exit(code) def exc_handler(self, exc_type, exc, *args): self.exception = exchooks = ExitHooks()hooks.hook()def goodbye(): if not (hooks.exit_code is None and hooks.exception is None): os.system('pause')# input("nPress Enter key to exit.")atexit.register(goodbye)

#python#


IT145.com E-mail:sddin#qq.com