上一篇把一些陳年問題解決了
就接著玩新的東西,該讓Windows API在Python上顯示圖片了
以前學Windows API時,原本的GDI只能讀BMP檔
但事隔多年,小畫家都支援多種格式了
微軟肯定有新的函式庫可以用
用Google找了一下,很快就找到了GDI+
GDI+支援jpg、png、gif、tiff等多種常用格式
那就是改使用它來提供顯示了
不過學習使用GDI+跟我想像中的不一樣
因為上一篇學到的東西,沒什麼派上用場的地方
而且要不是有Grok與ChatGPT等越來越好用的AI助陣
我真的寫不出來
因為微軟提供的C++範例把GDI+實作的函式庫包起來了
可以用Python呼叫的函式,沒有在範例裡顯示
算是藏在更深層的文件中
只靠我有限的時間與精力要爬到那些東西,不知道要花多久啊
還好有AI,那麼速速進入正題
先來看一下微軟官方給了範例碼好了,就這個「載入和顯示點陣圖」
裡面這邊寫的很簡單
//C++範例程式碼 Image image(L"Grapes.jpg"); graphics.DrawImage(&image, 60, 10);
就這樣兩行...
可是實際上Python要寫的至少是這些
graphics = c_void_p() GdipCreateFromHDC(hdc, byref(graphics)) image = POINTER(GpImage)() GdipLoadImageFromFile("Grapes.jpg", byref(image)) GdipDrawImageI(graphics, image, 60, 10)
更不用說還有些初始化的要求沒寫到
而且上面還是Grok與ChatGPT寫給我的
我用Google去找這些函數,也只找到參考文件,沒找到什麼範例程式碼
這次沒AI真是寫不出來了
廢話不多說,直接從頭到尾介紹在Python裡GDI+要運作需要哪些新東西與整個流程
再把可執行的整個範例程式碼放在後面
一些問題處理過程與心中碎碎念就跳過吧
首先我們要建立幾個需要的結構體(Structure)
2個屬於GDI+,1個是傳統的win32api用的資料
屬於GDI+的要用GdiplusStartupInput,還有GpImage
而屬於傳統Windows API的就是PAINTSTRUCT,使用變數常用ps
這東西對之前有碰Windows API畫圖的,就很熟了
那麼還要定義GDI+實際使用的函數,裡面的引數與回傳值
不過這邊就交給後面範例程式碼顯示了
接下來就是實際寫出顯示的東西了
首先要先初始化GDI+
#建立空token,屬性ulong gdiplus_token = c_ulong() #建立GDI+啟動的輸入結構體,版本1,其它不使用 startup_input = GdiplusStartupInput() startup_input.GdiplusVersion = 1 startup_input.DebugEventCallback = None startup_input.SuppressBackgroundThread = False startup_input.SuppressExternalCodecs = False #正式啟動GDI+函式庫 GdiplusStartup(byref(gdiplus_token), byref(startup_input), None)
補充一下,startup_input有簡潔的寫法,如下
startup_input = GdiplusStartupInput(1, None, False, False)
不過為了把東西講清楚,上面還是先用詳細一點的寫法
以上程式碼的位置就是要放在建立WNDCLASS的前面[註]
下面提供的範例就是在WNDCLASS建立之前初始化
因為在測試ChatGPT提供的程式版本時
那版本的錯誤寫法是把GDI+初始化放在showWindow之後
變成流程上是先在WM_PAINT裡的程式先載入圖檔與繪製影像
然後在之後才初始化,這樣的執行即使可能沒有任何錯誤碼,圖還是跑不出來
不過也因為這樣才更能理解GDI+怎麼運作的
初始化後的重點就是讀檔與顯示了,我先捨棄說明範例中可以隨視窗大小調整的作法
先用GdipDrawImageI來做簡易流程說明
Windows API的繪圖顯示都在WM_PAINT裡完成
實際執行,需要PS這個結構體外
還要使用BeginPaint跟EndPaint放程式的最前面跟最後面
這些基本雖然忘了,但一看到範例碼就都想起來了
稍稍有點懷念當年看不懂這東西的奮戰日子(苦笑)
回歸正題,精簡後程式碼如下,我逐行解釋
ps = PAINTSTRUCT() #建立PS結構 hdc = user32.BeginPaint(hwnd, byref(ps)) #開始畫圖 #準備載入圖片,從HDC元件建立圖片指標 graphics = c_void_p() GdipCreateFromHDC(hdc, byref(graphics)) #建立影像的指標,與圖檔路徑 image = POINTER(GpImage)() image_path = os.path.abspath("example.jpg") #啟用GDI+讀檔函式讀檔 GdipLoadImageFromFile(image_path, byref(image)) #將讀來的檔案指定給繪圖指標,並指定位置從(x=0, y=0)開始 GdipDrawImageI(graphics, image, 0, 0) # 顯示後清理繪圖指標與影像指標 GdipDisposeImage(image) GdipDeleteGraphics(graphics) user32.EndPaint(hwnd, byref(ps)) #結束畫圖
最後當整個程式結束後
要關掉有使用的GDI+物件,如下面程式碼
# 清理GDI+
GdiplusShutdown(gdiplus_token)
雖然有測過不加這段通常也不會怎樣
不過照規矩釋放資源比較不會有什麼奇怪的bug
這個記得要寫啊
流程上就這樣而已,沒有很複雜
只是如前面所提,東西原本是包起來的
沒有靠AI的話,我還真的難以搞定
誰知道像DrawImage其實裡面包著其實是什麼GdipDrawImageI(位置指定用整數)
GdipDrawImageRect(位置指定用浮點數,可以指定圖片大小)
或GdipDrawImageRectI(位置指定用整數,同樣可以指定圖片大小)等等
C++可以單用DrawImage就搞定,Python不行啊
在放上完整可執行的程式碼前
再稍稍再解說一下一些上面說明沒提到的GDI+函數
GdipGetImageWidth、GdipGetImageHeight與GdipDrawImageRect
因為使用GdipDrawImageI只會照原始圖片大小繪製在視窗上
如果要能依視窗大小縮放圖片,就不能用這個
而是需要GdipGetImageWidth與GdipGetImageHeight取得圖片的高與寬
然後與視窗大小做運算得到等比例縮放後
再靠著GdipDrawImageRect繪製在視窗上
詳細的程式碼長這樣子:
from ctypes import WINFUNCTYPE, WinDLL, Structure, c_long, c_void_p, c_ulong from ctypes import POINTER, byref, c_float from ctypes.wintypes import HINSTANCE, HWND, UINT, WPARAM, LPARAM, MSG, RECT from ctypes.wintypes import BOOL, INT, BYTE, LPCWSTR, HICON, HANDLE, HBRUSH, HDC import os # 定義必要的Windows API結構和常數 WNDPROC = WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM) user32 = WinDLL('user32') kernel32 = WinDLL('kernel32') gdiplus = WinDLL('gdiplus') user32.DefWindowProcW.argtypes = [HWND, UINT, WPARAM, LPARAM] # GDI+ 啟動結構 class GdiplusStartupInput(Structure): _fields_ = [ ("GdiplusVersion", UINT), ("DebugEventCallback", c_void_p), #註1 ("SuppressBackgroundThread", BOOL), ("SuppressExternalCodecs", BOOL), ] #註1:因為實際通常指定為空,所以就不使用實際結構節省程式碼(就當我偷懶了) # 視窗結構 class WNDCLASS(Structure): _fields_ = [ ("style", UINT), ("lpfnWndProc", WNDPROC), ("cbClsExtra", INT), ("cbWndExtra", INT), ("hInstance", HINSTANCE), ("hIcon", HICON), ("hCursor", HANDLE), ("hbrBackground", HBRUSH), ("lpszMenuName", LPCWSTR), ("lpszClassName", LPCWSTR), ] # 繪畫結構 class PAINTSTRUCT(Structure): _fields_ = [ ("hdc", HDC), ("fErase", BOOL), ("rcPaint", RECT), ("fRestore", BOOL), ("fIncUpdate", BOOL), ("rgbReserved", BYTE * 32), ] # GDI+ 圖像結構 (簡化版) class GpImage(Structure): pass # 定義GDI+函數的引數與回傳值 gdiplus.GdiplusStartup.argtypes = [POINTER(c_ulong), POINTER(GdiplusStartupInput), c_void_p] gdiplus.GdiplusStartup.restype = UINT gdiplus.GdipCreateFromHDC.argtypes = [HDC, POINTER(c_void_p)] gdiplus.GdipCreateFromHDC.restype = UINT gdiplus.GdipLoadImageFromFile.argtypes = [LPCWSTR, POINTER(POINTER(GpImage))] gdiplus.GdipLoadImageFromFile.restype = UINT gdiplus.GdipGetImageWidth.argtypes = [POINTER(GpImage), POINTER(UINT)] gdiplus.GdipGetImageWidth.restype = UINT gdiplus.GdipGetImageHeight.argtypes = [POINTER(GpImage), POINTER(UINT)] gdiplus.GdipGetImageHeight.restype = UINT gdiplus.GdipDrawImageRect.argtypes = [c_void_p, POINTER(GpImage), c_float, c_float, c_float, c_float] gdiplus.GdipDrawImageRect.restype = UINT gdiplus.GdipDisposeImage.argtypes = [POINTER(GpImage)] gdiplus.GdipDisposeImage.restype = UINT gdiplus.GdipDeleteGraphics.argtypes = [c_void_p] gdiplus.GdipDeleteGraphics.restype = UINT gdiplus.GdiplusShutdown.argtypes = [c_ulong] gdiplus.GdiplusShutdown.restype = None # 視窗過程函數 def wnd_proc(hwnd, msg, wparam, lparam): if msg == 0x000F: # WM_PAINT ps = PAINTSTRUCT() #建立PS結構 hdc = user32.BeginPaint(hwnd, byref(ps)) #開始畫圖 # 取得視窗大小 rect = RECT() user32.GetClientRect(hwnd, byref(rect)) window_width = rect.right - rect.left window_height = rect.bottom - rect.top #準備載入圖片,從HDC元件建立圖片指標 graphics = c_void_p() gdiplus.GdipCreateFromHDC(hdc, byref(graphics)) #建立影像的指標,與圖檔路徑 image = POINTER(GpImage)() image_path = os.path.abspath("example.jpg") #啟用GDI+讀檔函式讀檔 gdiplus.GdipLoadImageFromFile(image_path, byref(image)) # 取得圖片的寬與高 img_width = UINT() img_height = UINT() gdiplus.GdipGetImageWidth(image, byref(img_width)) gdiplus.GdipGetImageHeight(image, byref(img_height)) # 計算縮放比例 scale_x = window_width / img_width.value scale_y = window_height / img_height.value scale = min(scale_x, scale_y) scaled_width = img_width.value * scale scaled_height = img_height.value * scale # 讓圖片居中顯示 x = (window_width - scaled_width) / 2 y = (window_height - scaled_height) / 2 gdiplus.GdipDrawImageRect(graphics, image, x, y, scaled_width, scaled_height) # 顯示後清理繪圖指標與影像指標 gdiplus.GdipDisposeImage(image) gdiplus.GdipDeleteGraphics(graphics) user32.EndPaint(hwnd, byref(ps)) #結束畫圖 elif msg == 0x0010: # WM_CLOSE user32.PostQuitMessage(0) return user32.DefWindowProcW(hwnd, msg, wparam, lparam) def main(): # 初始化GDI+ gdiplus_token = c_ulong() """ 這邊是詳細的寫法 startup_input = GdiplusStartupInput() #建立啟動結構 startup_input.GdiplusVersion = 1 #版本指定1 startup_input.DebugEventCallback = None #DebugEvent不使用 startup_input.SuppressBackgroundThread = False #不使用 startup_input.SuppressExternalCodecs = False #不使用 """ #實務可以用這個簡易寫法 startup_input = GdiplusStartupInput(1, None, False, False) #正式啟動GDI+函式庫 gdiplus.GdiplusStartup(byref(gdiplus_token), byref(startup_input), None) # 設置視窗類別 h_instance = kernel32.GetModuleHandleW(None) class_name = "SampleWindowClass" wc = WNDCLASS() wc.lpfnWndProc = WNDPROC(wnd_proc) wc.hInstance = h_instance wc.lpszClassName = class_name wc.hCursor = user32.LoadCursorW(None, LPCWSTR(32512)) # IDC_ARROW wc.hbrBackground = c_void_p(5 + 1) # COLOR_WINDOW + 1,直接使用COLOR_WINDOW的值(5)加1 user32.RegisterClassW(byref(wc)) # 創建視窗 hwnd = user32.CreateWindowExW( 0, class_name, "用GDI+顯示圖片範例", 0x00CF0000, # WS_OVERLAPPEDWINDOW 0x8000, # CW_USEDEFAULT 0x8000, # CW_USEDEFAULT 800, 600, None, None, h_instance, None ) if not hwnd: return user32.ShowWindow(hwnd, 1) # SW_SHOWNORMAL user32.UpdateWindow(hwnd) # 消息循環 msg = MSG() while user32.GetMessageW(byref(msg), None, 0, 0): user32.TranslateMessage(byref(msg)) user32.DispatchMessageW(byref(msg)) # 清理GDI+ gdiplus.GdiplusShutdown(gdiplus_token) if __name__ == "__main__": main()
不過,在主視窗直接用WM_PAINT的話
主視窗就只有顯示圖片而已了
要拿來寫遊戲或許可以直接這麼做
但要在上面加上其它元件只會變成這樣
是的,這程式寫法的圖就只是背景圖
要規畫放置的大小,也不是不行,但就沒什麼彈性
所以實際上要與其它GUI元件併存的做法
還是得用子視窗的方式處理會比較好擺放與縮放
考量到這篇的篇幅也不小了,再寫一篇好了
下一篇,再來弄個看圖程式範例
視窗上面有按鈕可以用點選目錄,然後讀取裡面圖檔顯示在右邊
左邊則顯示圖片的檔名,實際的大小與一些其它資訊
用這範例做為這次這個系列最終的成果
註:2025/08/06更新
原本這邊是寫「以上程式碼的位置要放在執行showWindow之前,在建立WNDCLASS的前或後都行」
不過後續有遇到其它狀況還是有問題,一步一步釐清才確認了一件事
更正確的寫法應該是要在CreateWindowExW之前
因為如果在WM_CREATE使用GDI+的元件,那就會在CreateWindowExW的時候就會執行GDI+
在這更之前就必須進行初始化
之前ChatGPT的範例,是那時GDI+是寫在WM_PAINT裡,所以執行showWindow時才會啟動
這樣初始化可以放在showWindow前就好了
那麼最好的作法,還是「GDI+初始化都寫在WNDCLASS註冊前」
確保絕對不會有事
沒有留言:
張貼留言