2018年6月19日 星期二

python -- win32gui 特定程式窗口截圖(教學)



有天想說要用python來追蹤某視窗的畫面,並且使用圖像辨識opencv來辨識狀態,如果異狀則播放音效提醒。

上網google,首先找到這一篇:
https://hutdris.blogspot.com/2016/11/pythonopencv.html

該文作者最後仍未完成,因為功能也不是如想像的那般,可行是可行,不過侷限性太大了。作者也自行提出這個問題,想要找到解決方法。

「目前是抓取該程式的畫面相對位置,如果該程式縮到最小或被其他程式蓋過,還是會持續抓取該位置的畫面。 如果希望持續擷取該程式的畫面該怎麼作」


前提就到這裡了,本文將會「提供程式碼」與「編寫邏輯」,不過JN並非專科出身,可能會有講出錯的理解,希望能夠指正JN講錯的地方。




流程圖







關於如何對特定程式窗口截圖,首先要先瞭解到底截的對象是甚麼?

資料型態是寫程式的基本功,這個沒有紮實領會,寫程式就很容易碰壁。

JN透過搜尋另外一種關鍵字,找到了這篇問題。下方有人提供了大神解法與連結,也是本次JN主要參照的文章《Capturing Minimized Window》,運作流程也是照著這篇文章的流程。但是,這文章並不是用python作為程式語言,一開始雖然JN是放棄參照這篇大神邏輯,最後還是回過頭來仔細看這篇文章的概念。

本篇程式碼主要使用 win32gui 去完成,而 win32gui 剛好能找到那篇文章所使用的api,真的很巧。不過win32gui的document還是看得很頭痛,因為物件並不是用常見的python資料型態。

要如何將特定程式窗口截圖?首先要先抓區特定程式的「handle」,中國翻譯叫「句柄」,handle就是一個連結到程式窗口的路。在win32gui中,handle(PyHANDEL)通常是int型態,用int也能通大部分函式的PyHANDEL物件。

#以Name找出特定窗口的handle
win32gui.FindWindow('Name')
Ex:win32gui.FindWindow('未命名 - 小畫家')

用了 win32gui.FindWindow() 函式,就能找到我們目標窗口的handle,接下來要用此 handle 來找出其 handle device content (HDC)。HDC是窗口輸出的內容,各位想必開了瀏覽器來看這篇文章,你所看到的文章(畫面)都是瀏覽器的HDC,這個就是做特定程式窗口截圖的對象。

用電腦的時候,不一定只有開瀏覽器,可能還會打開檔案管理員。但是我們只看到瀏覽器的畫面,是因為瀏覽器的圖層比檔案管理員的還要更上方。HDC會輸出成圖層,變成圖層才會有上下之分,上方圖層會擋住下方的。如果我們直接抓取下方圖層的HDC,就不會被上方的圖層擋到的問題,也就能實踐本次的特定程式窗口截圖

透過handle,我們可以用 win32gui.GetWindowDC(handle) 來找出該程式窗口的HDC。接下來的工作就是要把HDC轉成常用的np.array型態,這樣就能儲存圖片與圖像辨識。HDC可以轉換成 bitmap 型態,再從 bitmap 又要轉換成 np.array。好像很複雜,你就當作是在 bitmap (畫布)輸出 HDC的內容,然後都是圖樣的東西,彼此(與np.array)互轉就不是難事了。

這邊,JN直接使用這篇範例的部分內容,主要就是將 bitmap 轉換成 np.array 。至於為什麼會這樣寫,JN並不是很瞭解,只是知道怎麼用而已。



'''
import win32con # 各種win32函式的參數
import win32gui
import win32ui
import numpy as np

hwnd = the handle of a window 
x = the x coordinate of the window
y = the y coordinate of the window
width = the width of the window
height = the height of the window
'''
# 創造輸出圖層
hwindc = win32gui.GetWindowDC(hwnd)
srcdc = win32ui.CreateDCFromHandle(hwindc)
memdc = srcdc.CreateCompatibleDC()
bmp = win32ui.CreateBitmap()
# 如果視窗最小化,則移到Z軸最下方
win32gui.SetWindowPos(hwnd, win32con.HWND_BOTTOM, x, y, width, height, win32con.SWP_NOACTIVATE)
# 複製目標圖層,貼上到 bmp
bmp.CreateCompatibleBitmap(srcdc, width, height)
memdc.SelectObject(bmp)
memdc.BitBlt((0 , 0), (width, height), srcdc, (0, 0), win32con.SRCCOPY)
# 將 bitmap 轉換成 np
signedIntsArray = bmp.GetBitmapBits(True)
img = np.fromstring(signedIntsArray, dtype='uint8')
img.shape = (height,width,4)
# 釋放device content
srcdc.DeleteDC()
memdc.DeleteDC()
win32gui.ReleaseDC(hwnd, hwindc)
win32gui.DeleteObject(bmp.GetHandle())


如此,我們透過 win32gui 的函式搜尋找到「特定窗口的handle」,然後透過 「handle 找出 HDC」, 再把 「HDC」 轉到 「bitmap」 ,然後換成「 np.array」 。到了np.array型態,用cv2.imshow()就能秀出圖片。主要功能到這裡就完成了。

接下來要修飾主要功能

主要功能只是抓取特定窗口,但是在「最小化」的時候,卻只會抓到 icon。如果窗口不解除最小化,就沒有辦法抓到完整畫面。

依照JN主要參考的文法,它的做法是將圖層透明化、取消最大小化的動畫,最後再把窗口秀出來。因為透明化的窗口圖層不會被看見,就能有「偷偷地截取」的感覺,取消動畫是讓作動加速,而窗口秀出來是避免讓截圖只抓到icon。

到這裡還沒結束,如果我們在程式運作中,又想點開特定程式窗口,如果此時沒有將該圖層透明化給還原,那麼就會發現用滑鼠再怎麼點icon都沒有畫面。但是透明度一還原,就會發現其實程式圖層在上方,擋到其他程式窗口。按照主要參考文章,它的作法會將窗口最小化後,再把圖層透明度還原。但是,依照這樣的概念運作,就會發現工作列實在閃閃發亮很熱鬧,並沒有偷偷地截圖。

所以JN這裡採取將窗口圖層移動到最下方,再把透明度還原,而沒有把窗口最小化。


'''
如果要用 win32gui.SetLayeredWindowAttributes()
hwnd (int) 要變成 WindowLong 的型態
'''
# 將 hwnd 的型態變成 WindowLong
s = win32gui.GetWindowLong(hwnd,win32con.GWL_EXSTYLE)
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, s|win32con.WS_EX_LAYERED)

# 將窗口最大小化動畫給取消
win32gui.SystemParametersInfo(win32con.SPI_SETANIMATION, 0)

# 如果沒有把 hwnd 的型態轉換,代入此函式會報錯
# 第一個 0 是 顏色,第二個 0 是 不透明度
win32gui.SetLayeredWindowAttributes(hwnd, 0, 0, win32con.LWA_ALPHA)

# 讓指定窗口顯示出來,最後一項參數( win32con.SW_...... )可以改變顯示方式 
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) 

# 將圖層移到最下層
# 最後一項參數,JN使用過 SWP_NOMOVE 及 SWP_NOSIZE 都會導致圖層無法移到最下層
win32gui.SetWindowPos(hwnd, win32con.HWND_BOTTOM, x, y, width, height, win32con.SWP_NOACTIVATE)

'''
此部分程式碼,「不包含」還原圖層透明度。
'''




透過這樣的方式抓取特定窗口截圖,就能解決前提連結的問題,不僅不會被別的窗口蓋過,就連最小化的窗口也能抓取。此外,也改進主要文章的缺陷,JN的方式不會讓工作列不停閃爍。

這也是JN第一篇發在GitHub的程式碼,當了初心者那麼久了,總算有東西值得發布了。



2018.06.20 刪除了冗碼,並以win10校正,避免截圖抓到四周的透明部分。


後記

程式碼發布是JN第一次嘗試,看到別人的網站爾偶會有制式的表格,JN很好奇是怎麼樣產生的。如果有甚麼方法能夠快速產生展示程式碼的表格,也請拜託在下方留言教學。

現在寫程式能寫出以前沒有能力完成的,我想這就是經驗的累積吧。慢慢累積各種方法與技巧,融會貫通就又開啟了一扇門˙,而且是以前認為沒辦法寫出來的東西。

這篇文章也是讓JN第一次接觸win32系列,win32不只存在於python,在C#還是其他的程式語言都有。我想win32系列就是windows的架構,每天從螢幕看到的、鍵盤操作的,都能用win32系列來完成,而且能做到的更強大,但是,卻也更源頭的物件。這次還碰到二進位,之前在寫都沒有注意到位元,也花了一些時間學習如何處理。

最後還有一些詞彙釐清。會用「窗口」是因為這是中國用語,而台灣常講「視窗」,然而用窗口是比較容易找到教學。此次撰寫,也讓JN分清楚「window」、「handle」、「device content」、「layer」的差異,不同的詞都有不同呈現的方式,要瞭解自己操作的是哪一種,才不會混淆或作不出來。