1. 程式人生 > >利用Python和win32程式設計範例——按需定製一個按鍵精靈

利用Python和win32程式設計範例——按需定製一個按鍵精靈

本文假設各位看官對python是足夠熟悉的,但卻不太瞭解win32程式設計。

嘛。。其實我也沒學過win32程式設計,臉請各位看官隨意招呼。

需求:最近因為做課題,需要用面孔建模軟體FaceGen開啟大量的.fg檔案(幾千),並另存為圖片,以備後續操作。

分析:要想直接從檔案解析出面孔的圖片有一定的難度,原因在於.fg檔案的儲存格式很奇葩,300byte就能存下一張臉的全部資訊(基於PCA的面孔特徵抽取),必須模擬操作。這樣一來,事情也就變得簡單機械,無非把“載入.fg檔案並將其另存為圖片”,重複上千次。鑑於一系列原因,不是很想用按鍵精靈之類的東西。何況利用pywin32可以很方便的利用win32的一些介面,何必再去模擬操作。

Part 0: 開始之前?

首先,來這裡安裝一個Pywin32吧,Pywin32是一個Python庫,為python提供訪問Windows API的擴充套件,提供了齊全的windows常量、介面、執行緒以及COM機制等等。安裝後自帶一個pythonwin的IDE。或者也可以來這邊下載一個ActivePython,整合了pywin32和一些其他的庫以及一大堆的支援文件,他們的文件查起來是非常方便的。當然了,WIN32的一些相關函式在MSDN上也能直接找到。

其次,為了方面查詢目標視窗的控制代碼,可以下載一個微軟自家的Spy++,這玩意兒滿大街都是。有了它,還能很方便的檢視窗體的訊息。

控制代碼

是一個32位整數,在windows中標記物件用,類似一個dict中的key,詳情參看這篇文章

&

訊息是windows應用的重要部分,用來告訴窗體“發生了什麼”,比如給一個按鈕傳送BN_CLICKED這麼個訊息,按鈕就知道“哦,我被點了”,才能執行相應的下一步操作。本文將大量使用訊息機制。詳情參看這篇文章

Part 1: 查詢窗體控制代碼

貌似在win32程式設計的世界裡,包括視窗到文字框的所有控制元件就是窗體,所有的窗體都有獨立的控制代碼。要操作任意一個窗體,你都需要找到這個窗體的控制代碼,這裡,我們就可以用到FindWindow函式和FindWindowEx函式。在pywin32中,他們都屬於win32gui的模組。

  • FindWindow(lpClassName=None, lpWindowName=None):
    • 描述:自頂層視窗(也就是桌面)開始搜尋條件匹配的窗體,並返回這個窗體的控制代碼。不搜尋子視窗、不區分大小寫。找不到就返回0
    • 引數:
      • lpClassName:字元型,是窗體的類名,這個可以在Spy++裡找到。
      • lpWindowName:字元型,是視窗名,也就是標題欄上你能看見的那個標題。
    • 說明:這個函式我們僅能用來找主視窗。
  • FindWindowEx(hwndParent=0, hwndChildAfter=0, lpszClass=None, lpszWindow=None);
    • 描述:搜尋類名和窗體名匹配的窗體,並返回這個窗體的控制代碼。不區分大小寫,找不到就返回0。
    • 引數:
      • hwndParent:若不為0,則搜尋控制代碼為hwndParent窗體的子窗體。
      • hwndChildAfter:若不為0,則按照z-index的順序從hwndChildAfter向後開始搜尋子窗體,否則從第一個子窗體開始搜尋。
      • lpClassName:字元型,是窗體的類名,這個可以在Spy++裡找到。
      • lpWindowName:字元型,是視窗名,也就是標題欄上你能看見的那個標題。
    • 說明:找到了主視窗以後就靠它來定位子窗體啦。

有了這兩個函式,我們就可以寫出可以定義到任意一個窗體控制代碼的函式啦:

1 2 3 4 5 6 7 8 9 10 11 def find_idxSubHandle(pHandle,winClass,index=0): """         已知子視窗的窗體類名         尋找第index號個同類型的兄弟視窗         """ asserttype(index)==intandindex>=0 handle=win32gui.FindWindowEx(pHandle,0,winClass,None) whileindex>0: handle=win32gui.FindWindowEx(pHandle,handle,winClass,None) index-=1 returnhandle
1 2 3 4 5 6 7 8 9 10 11 12 def find_subHandle(pHandle,winClassList): """         遞迴尋找子視窗的控制代碼         pHandle是祖父視窗的控制代碼         winClassList是各個子視窗的class列表,父輩的list-index小於子輩         """ asserttype(winClassList)==list iflen(winClassList)==1: returnfind_idxSubHandle(pHandle,winClassList[0][0],winClassList[0][1]) else: pHandle=find_idxSubHandle(pHandle,winClassList[0][0],winClassList[0][1]) returnfind_subHandle(pHandle,winClassList[1:])

這樣在後續的呼叫中,我們就能使用我們定義的finde_subHandle來方便地找到某個特定的Edit窗體控制元件,比如要定位000A848開啟對話方塊的這個Edit控制元件,

圖1

直接這樣呼叫即可:

1 handle=find_subHandle(self.Mhandle,[("ComboBoxEx32",1),("ComboBox",0),("Edit",0)])

另外,python中找回來的控制代碼都是十進位制整型,Spy++裡顯示的都是十六進位制整型,這個要注意下,除錯的時候用十六進位制輸出控制代碼,如下:

1 print"%x"%(handle)

Part 2:選單操作

有了控制代碼,我們就可以操作FaceGen了!嗯,要先開啟檔案,File→Open,然後再File→Save Image(很悲劇,Save Image沒有快捷鍵,所以不得不進行選單操作)。現在我們有了FindWindow和FindWindowEx,要怎麼操作選單呢?

哦,抱歉,靠他倆還做不到。

視窗的選單就像視窗的標題欄一樣,是視窗自身的一部分,不是其他窗體控制元件,也就沒有辦法用FindWindow和FindWindowEx返回控制代碼。所以要對選單進行操作的話,我們需要新的函式,也就是GetMenu,GetSubMenu和GetMenuItemID,它們也都屬於win32gui模組。結合下圖來說:

FacegenMenu

  • GetMenu(hwnd)
    • 描述:獲取視窗的選單控制代碼。
    • 引數:
      • hwnd:整型,需要獲取選單的視窗的控制代碼。
    • 說明:獲取的是插圖中黃色的部分。
  • GetSubMenu(hMenu, nPos)
    • 描述:獲取選單的下拉選單或者子選單。
    • 引數:
      • hMenu:整型,選單的控制代碼,從GetMenu獲得。
      • nPos:整型,下拉選單或子選單的的索引,從0算起。
    • 說明:這個可以獲取插圖中藍色的部分;如描述所述,這個不僅可以獲取本例中的下拉選單,還可以獲取子選單。
  • GetMenuItemID(hMenu, nPos)
    • 描述:獲取選單中特定專案的識別符號。
    • 引數:
      • hMenu:整型,包含所需選單項的選單控制代碼,從GetSubMenu獲得。
      • nPos:整型,選單項的索引,從0算起。
    • 說明:這個獲取的就是紅色區域中的專案啦,注意,分隔符是被編入索引的,所以Open的索引是2而非1,而Exit的索引是9而非6。

找到這個選單項的識別符號,我們就可以通過訊息機制告訴應用程式:我們要執行這個選單項的命令!這需要我們要給應用程式發個訊息,讓它執行所需選單項的命令。假設之前獲取的Open的識別符號是open_ID,那麼只需要這樣:

1 win32gui.PostMessage(self.Mhandle,win32con.WM_COMMAND,open_ID,0)

就會有一個開啟檔案的對話框出現啦。

解釋一下:

  • PostMessage(hWnd, Msg, wParam, lParam)
    • 描述:在訊息佇列中加入為指定的窗體加入一條訊息,並馬上返回,不等待執行緒對訊息的處理。
    • 引數:
      • hWnd:整型,接收訊息的窗體控制代碼
      • wParam:整型,訊息的wParam引數
      • lParam:整型,訊息的lParam引數
    • 說明:簡單說,就是給指定程式發一個訊息,這些訊息都用整型編好號,作為windows的常量可以查詢的。在這裡,我們用的就是win32con這個庫裡定義的WM_COMMAND這個訊息,具體的wParam和lParam是根據訊息的不同而不同的。具體請根據MSDN查閱。

關於wParam的low word和high word:

圖2

查閱MSDN的訊息時,會發現有的wParam定義了low word和high word,這是什麼呢?wParam的定義是32位整型,high word就是他的31至16位,low word是它的15至0位,如圖。有時,一個訊息只需要不超過兩個引數,那wParam就可以當一個引數用。萬一引數多了,wParam就給拆成了兩個int16來使用。這種時候在python裡記得用16進位制把整形表示出來就比較清爽啦。

更新一下我們定義的類,把選單新增進去:

1 2 3 4 5 6 classFaceGenWindow(object): def __init__(self,fgFilePath=None): self.Mhandle=win32gui.FindWindow("FaceGenMainWinClass",None) self.menu=win32gui.GetMenu(self.Mhandle) self.menu=win32gui.GetSubMenu(self.menu,0) print"FaceGen initialization compeleted"

然後定義一個選單操作的方法:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def menu_command(self,command): """         選單操作         返回彈出的開啟或儲存的對話方塊的控制代碼 dig_handle         返回確定按鈕的控制代碼 confBTN_handle         """ command_dict={# [目錄的編號, 開啟的視窗名] "open":[2,u"開啟"], "save_to_image":[5,u"另存為"], } cmd_ID=win32gui.GetMenuItemID(self.menu,command_dict[command][0]) win32gui.PostMessage(self.Mhandle,win32con.WM_COMMAND,cmd_ID,0) foriinrange(10): ifwin32gui.FindWindow(None,command_dict[command][1]): break else: win32api.Sleep(200) dig_handle=win32gui.FindWindow(None,command_dict[command][1]) confBTN_handle=win32gui.FindWindowEx(dig_handle,0,"Button",None) returndig_handle,confBTN_handle

在這裡順便返回了彈出來的對話方塊的控制代碼和確定按鈕的控制代碼,後面操作會用到。

Part 3:控制元件操作A

通過選單的目錄操作,我們打開了開啟檔案對話方塊。為了簡單起見,我們可以直接在檔名處填入要開啟檔案的絕對路徑。怎麼填呢?

首先還是定位到文字框控制元件

1 handle=find_subHandle(Mhandle,[("ComboBoxEx32",0),("ComboBox",0),("Edit",0)])

find_subHandle()是在Part 1中定義的函式,可以按照列表的資訊查詢Mhandle的子窗體。列表中的元組提供窗體的類名和排位號(z-index)。列表索引編號較小的為父窗體。

接著我們依然是利用win32的訊息機制,給這個文字框控制元件送去一個訊息:

1 win32api.SendMessage(handle,win32con.WM_SETTEXT,0,os.path.abspath(fgFilePath).encode('gbk'))

在這裡,我們用了SendMessage而不是PostMessage,其區別就在於我們可以通過SendMessage取得訊息的返回資訊。因為對於我們要設定文字框資訊的WM_SETTEXT資訊來說,設定成功將返回True。

  • SendMessage(hWnd, Msg, wParam, lParam)
    • 描述:在訊息佇列中加入為指定的窗體加入一條訊息,直到窗體處理完資訊才返回。
    • 引數:
      • hWnd:整型,接收訊息的窗體控制代碼
      • wParam:整型,訊息的wParam引數
      • lParam:整型,訊息的lParam引數
    • 說明:wParam和IParam根據具體的訊息不同而有不同的定義,詳情參閱Part 2.
  • WM_SETTEXT 訊息
    • 描述:設定窗體的文字
    • 引數:
      • wParam:未使用
      • lParam:一個指標,指向以null結尾的字串。窗體文字將被設定為該字串。
    • 返回值:
      • 如果成功設定,則返回1(MSDN原文是返回True)
    • 說明:
      • 上面的定義是直接從MSDN上翻譯過來的,在Python的語境裡面沒有指標,你只需要把變數名作為lParam傳入就好了。
      • 另外,請注意編碼,包含中文請用gbk編碼,否則亂碼

再利用一個WM_COMMAND訊息來點選確定按鈕:

1 win32api.SendMessage(Mhandle,win32con.WM_COMMAND,1,confirmBTN_handle)
  • WM_COMMAND 訊息
    • 描述:當用戶選擇了選單(或按鈕等控制元件的)命令,或控制元件傳送通知到父視窗,或加速鍵擊(accelerator keystroke is translated)時傳送。
    • 引數:根據情景不同而不同,在這裡屬於使用者命令,引數配置如下
      • wParam:HIWORD為0(未使用),LOWORD為控制元件的ID
      • lParam:0(未使用)
    • 返回值:如果窗體處理了訊息,應返回0

至此,利用以上的win32API,便可完成開啟fg檔案的操作:

1 2 3 4 5 6 7 def open_fg(self,fgFilePath): """開啟fg檔案""" Mhandle,confirmBTN_handle =self.menu_command('open') handle=find_subHandle(Mhandle,[("ComboBoxEx32",0),("ComboBox",0),("Edit",0)]) ifwin32api.SendMessage(handle,win32con.WM_SETTEXT,0,os.path.abspath(fgFilePath).encode('gbk'))==1: returnwin32api.SendMessage(Mhandle,win32con.WM_COMMAND,1,confirmBTN_handle) raise Exception("File opening path set failed")

順便,如果要獲取目標文字框的內容呢,可以使用WM_GETTEXT,如下:

  • WM_GETTEXT訊息:
    • 描述:將窗體的文字內容複製到指定的buffer物件中
    • 引數:
      • wParam:要複製字元的最大長度,包括截尾的空位元組
      • lParam:用來儲存字串的buffer的指標
    • 返回值:返回複製字元的數量,不包括截尾的空位元組

利用win32gui.PyMakeBuffer(len, addr)可以造一個buffer物件,類似python3中的bytearray,lParam的返回值。而利用WM_GETTEXTLENGTH可以獲取不含截尾空位元組的文字長度的長度,可以用來設定Buffer的長度。完整的示例如下:

1 2 3 4 buf_size=win32gui.SendMessage(hwnd,win32con.WM_GETTEXTLENGTH,0,0)+1# 要加上截尾的位元組 str_buffer=win32gui.PyMakeBuffer(buf_size)# 生成buffer物件 win32api.SendMessage(hwnd,win32con.WM_GETTEXT,buf_size,str_buffer)# 獲取buffer str=str(str_buffer[:-1])# 轉為字串

Part 4:控制元件操作B

至於另存為圖片,情況要稍微複雜一點,因為另存為圖片的預設選項是BMP,特別不巧,我使用的FaceGen版本儲存為BMP有BUG,不能成功儲存,所以我們除了定位儲存檔案的路徑以外,還需要對檔案型別的下拉組合框(ComboBox進)行操作:

我們假設我們找到了組合框的控制代碼為CB_handle,我們可以用CB_SETCURSEL訊息來更改當前的選項:

  • CB_SETCURSEL 訊息
    • 描述:
    • 引數:
      • wParam:以0起始的待選選項的索引;如果該值為-1,將從組合框列表中刪除當前選項,並使當前選項為空
      • lParam:未使用。
    • 返回值:
      • 更改選擇成功將返回所設定選項的索引號。

只要給組合框發一個CB_SETCURSEL訊息,你就會發現下拉列表的選項已經改變了。

這時點儲存,你就會發現,這儲存的跟之前的一樣啊!根本沒有變!

問題在哪裡?

我們用滑鼠或者鍵盤操作一下,是沒有問題的,一旦更儲存型別,儲存窗口裡的預覽也會隨之變化。所以,除了CB_SETCURSEL以外,一定還缺了點兒什麼。

呼叫Spy++的訊息機制檢視手動操作,我們的下拉組合框除了渲染和點選,好像沒有什麼特別值得注意的。

那再看看父窗體呢?好像有點兒不太一樣的東西:

  • CBN_SELENDOK 通知(notification code)
    • 描述:當用戶選擇了有效的列表項時傳送,提示父窗體處理使用者的選擇。父窗體通過WM_COMMAND訊息接收這個通知。
    • 引數:(作為WM_COMMAND的引數)
      • wParam:LOWORD為組合框的ID. HIWORD為CBN_SELENDOK的值。
      • lParam:組合框的控制代碼。
  • CBN_SELCHANGE 通知(notification code)
    • 描述:當用戶更改了列表項的選擇時傳送,不論使用者是通過滑鼠選擇或是通過方向鍵選擇都會發送此通知。父窗體通過WM_COMMAND訊息接收這個通知。
    • 引數:(作為WM_COMMAND的引數)
      • wParam:LOWORD為組合框的ID. HIWORD為CBN_SELCHANGE的值。
      • lParam:組合框的控制代碼。
  • 說明:他們是WM_COMMAND訊息wParam的high word(wParam的16-31位,詳情參見Part 2)的常數之一,在Python中可以用位移操作將其移動到高位上(a<<16),再用加法加上低位的內容。

繼續查MSDN的資料,我們發現,對於一個有效的選擇,一定會發送這兩個通知,傳送完CBN_SELENDOK以後馬上傳送CBN_SELCHANGE。而且,使用CB_SETCURSEL訊息時,CBN_SELCHANGE通知是不會被送達的!

問題就在這裡,加上這兩個訊息之後,就能正常操作下拉選單了。

1 2 3 4 5 ifwin32api.SendMessage(CB_handle,win32con.CB_SETCURSEL,format_dict[format],0)==format_dict[format]: win32api.SendMessage(PCB_handle,win32con.WM_COMMAND,win32con.CBN_SELENDOK&lt;&lt;16+0,CB_handle)# 控制元件的ID是0,所以低位直接加0 win32api.SendMessage(PCB_handle,win32con.WM_COMMAND,win32con.CBN_SELCHANGE&lt;&lt;16+0,CB_handle) else: raise Exception("Change saving type failed")