前言
前一陣子用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 init
init()
結果並不像說明頁面上說的這樣就可以了, 而是剛好和我原本狀況相反: 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 = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
Dk_BLACK = '\033[30m'
Dk_RED = '\033[31m'
Dk_GREEN = '\033[32m'
Dk_YELLOW = '\033[33m'
Dk_BLUE = '\033[34m'
Dk_VIOLET = '\033[35m'
Dk_BEIGE = '\033[36m'
Dk_WHITE = '\033[37m'
Li_RED = '\033[91m'
Li_GREEN = '\033[92m'
Li_YELLOW = '\033[93m'
Li_BLUE = '\033[94m'
Li_VIOLET = '\033[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.
- 然後就可以使用定義好的功能了
補充修正2019/04/09
哈, 真是狗屎運啊, 沒想到之前的 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/pty\d+', 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 讀取按鍵2019/04/09
上面的 '補充修正' 還有一個更棒的效應: 連我之前一直測試不出來的: 使用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, kbhit
except ImportError:
import tty
import termios
def getch():
...
def kbhit():
...
不過, 執行時 (git for windows 的bash) python 程式認為自己是在 windows 環境裡執行, 使用的是msvcrt模組的功能而不是 linux/unix 的替代函數.
編譯 python 程式2019/04/13
python 是一種直譯的手稿語言 (scripting language), 在程式的開發/除錯上相對容易. 但是總不能叫程式的未端使用者也安裝個 python 和需要的模組套件吧? 因此有了編譯 python 程式的想法.
其實要編譯 python 程式並不難, 上網 Google 一查就有了: (參考這一篇 Compiling Python Code)
- 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 -F myApps.py
這樣就可以順利在子目錄 dist 中產生 myApp.exe 了. (當然還有其他的子錄目和很多檔案, 但這些都是我們修正程式錯誤後, 協助加快重新編譯用的, 無需給末端用戶).
改變 console 程式執行時的字型大小2019/04/13
其實上一段只是為了鋪陳, 這一段才是重點. 編譯成執行檔給 user 後, 第一回應是: 字太小, 可不可以改大一點? 我一整個儍掉… (PS. 心裡的惡魔說: 到底是公尛, 真是電腦小白到一個不行)
好的, 沒問題, 改給你. 還好, 從上網 Google, 到改完程式, 重新編譯, 15 分鐘搞定. (Good Luck! 因為查到 StackOverflow 的這一篇 )
先用把 Windwos CMD Console 視窗的字型名稱及字型大小調整好, 再執行以下的程式, 查出應該設定的數值.
import sys
from ctypes import POINTER, WinDLL, Structure, sizeof, byref
from ctypes.wintypes import BOOL, SHORT, WCHAR, UINT, ULONG, DWORD, HANDLE
LF_FACESIZE = 32
STD_OUTPUT_HANDLE = -11
class 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.GetLastError
get_last_error_func.argtypes = []
get_last_error_func.restype = DWORD
get_std_handle_func = kernel32_dll.GetStdHandle
get_std_handle_func.argtypes = [DWORD]
get_std_handle_func.restype = HANDLE
get_current_console_font_ex_func = kernel32_dll.GetCurrentConsoleFontEx
get_current_console_font_ex_func.argtypes = [HANDLE, BOOL, POINTER(CONSOLE_FONT_INFOEX)]
get_current_console_font_ex_func.restype = BOOL
set_current_console_font_ex_func = kernel32_dll.SetCurrentConsoleFontEx
set_current_console_font_ex_func.argtypes = [HANDLE, BOOL, POINTER(CONSOLE_FONT_INFOEX)]
set_current_console_font_ex_func.restype = BOOL
def 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()

Windows CMD Console 字型的設定輸出
再接著把你要的數值 (查到的數值) 填到以下的程式. (上一段的精簡版, 只有設定的功能)
import ctypes
LF_FACESIZE = 32
STD_OUTPUT_HANDLE = -11
class 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 = 16
font.dwFontSize.X = 11
font.dwFontSize.Y = 24
font.FontFamily = 54
font.FontWeight = 400
font.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 原本的設定.
錯誤結束時暫停一下2019/04/13
編譯成執行檔後, 附帶的多出了一個問題: 如果 User 直接在視窗環境滑鼠點二下執行, 然後直接出現錯誤 (可能他的環境少了個什麼東西而你沒考慮到…), 結果是執行視窗直接關掉, 什麼錯誤訊息也沒有看到. 怎麼辦啊?
原本以為這個問題會有點小棘手小障礙, 結果也是出奇的順利. (多謝 StackOverflow 這一篇)
以下是我修改後的程式.
import atexit
import sys, os
class 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 = exc
hooks = 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 colorama 模組下載及使用說明.
- StackExchange SuperUser 的 Q&A Can't use python in interactive mode on new msys-git terminal?
- Compiling Python Code
- Gordon McMillan's installer (cross-platform)
- Thomas Heller's py2exe (Windows)
- Anthony Tuininga's cx_Freeze (cross-platform)
- Bob Ippolito's py2app (Mac)
- StackOverflow 的 Q&A Python Programatically Change Console font size
- StackOverflow 的 Q&A How to find exit code or reason when atexit callback is called in Python?