導言
繼上篇《用Qt寫軟件系列二:QIECookieViewer》之后,有一段時間沒有更新博客了。這次要寫的是一個簡單的系統工具,需求來自一個內部項目。功能其實很簡單,就是查看當前當前系統中運行的進程信息以及系統中已安裝軟件信息。說出來也就這么兩句話,然而做起來的時候,問題卻層出不窮。另外,一直想研究一下Qt中的樣式表(Style Sheet)的使用,就這這個機會實踐了一下,也算收獲頗多。
這一篇主要講該工具的底層實現。前面也說過,這個小工具總共有有兩個功能:查看進程信息和已安裝軟件信息。因此我們分成兩個部分看。首先說明,我的開發環境為Visual Studio 2010旗艦版,Qt庫版本為Qt 5.2.1 (OpenGL)。操作系統為Windows 7 64bits英文版。
查看進程信息
(1)32位系統
對於32位操作系統,情況很簡單,直接調用Process32First()和Process32Next()這一對函數進行遍歷就可以得到當前正在運行的程序列表。將PROCESSENTRY32結構體變量作為這兩對函數的參數傳遞進去,該結構體中的字段即為相應進程的基本信息。當然,這個結構體中的信息還是過於簡單,如果要獲得對應進程所占用的內存情況,還得調用其他Windows API來獲取,如:GetProcessMemoryInfo(),VirtualQueryEx().這兩個函數獲取的內存信息則比較詳盡。
問題來了,如果我們需要列出一個進程對應的可執行文件所在的路徑該怎么辦呢?上面也提到,PROCESSENTRY32結構體中的信息過於簡單,只包含了對應進程的進程名稱。網上有解決方案說,可以用GetModuleFileNameEx()來獲取。於是立馬在程序中調用,又發現了一個問題:該函數對於32位進程而言正常工作無疑,但是當查詢的進程是64位的時候,這個函數直接返回0了,查詢失敗。顯然,該函數對32位進程和64位進程的運行結果不一樣。后來在MSDN在線幫助文檔GetModuleFileNameEx()該頁面下有評論說:To clarify a bit, this function DOES work to find the module names that are the same "bitness" as the running application. 意思是說,當前進程如果編譯為32位,那么在該進程中調用GetModuleFileNameEx()只能查詢32位的進程,對於64位進程是一樣的道理。而我的VS2010編譯的目標機器類型也是32位,這也就難怪了。
(2)兼容64位系統
將目標機器類型改為64位的顯然又無法查詢到32位進程的信息。那么只好另尋他法了。就在該評論頁面,另外一個人有提到一個可行的解決方案:
使用GetProcessImageFileName()來提取信息。然而,該評論也指出:這個函數返回的進程文件路徑是windows內核格式的(形如:\Device\HarddiskVolume1\***)。要轉換成我們熟悉的格式還得做些額外的工作。不過轉換原理也簡單:從驅動盤符A到盤符Z逐個掃描對比,將形如\Device\HarddiskVolume1\的前綴替換為C:,D:……。轉換代碼如下:
1 bool RetrieveHelper::NormalizeNTPath(wchar_t* pszPath, size_t nMax) 2 { 3 wchar_t* pszSlash = wcschr(&pszPath[1], '\\'); 4 if (pszSlash) pszSlash = wcschr(pszSlash+1, '\\'); 5 if (!pszSlash) 6 return false; 7 wchar_t suffix[MAX_PATH]; 8 wcscpy_s(suffix, MAX_PATH, pszSlash); 9 *pszSlash = 0; 10 11 wchar_t szNTPath[MAX_PATH]; 12 wchar_t szDrive[MAX_PATH] = TEXT("A:"); 13 // We'll need to query the NT device names for the drives to find a match with pszPath 14 for (wchar_t cDrive = 'A'; cDrive < 'Z'; ++cDrive) 15 { 16 szDrive[0] = cDrive; 17 szNTPath[0] = 0; 18 if (0 != QueryDosDevice(szDrive, szNTPath, MAX_PATH) && 0 == _wcsicmp(szNTPath, pszPath)) 19 { 20 // Match 21 wcscat_s(szDrive, MAX_PATH, suffix); 22 wcscpy_s(pszPath, nMax, szDrive); 23 return true; 24 } 25 } 26 return false; 27 }
如此轉換之后,結果令人滿意:
已安裝軟件信息
(1)一般情況
什么是一般情況?在32位程序和系統仍然大行其道的今天,要是拋棄32位程序,完全擁抱64位是不現實的。所以我們的程序必須要能處理32位系統的情況,這是最基本的要求。那么,在32位系統環境下,如何來提取系統已經安裝的程序的信息呢?不知道360安全衛士、金山衛士等軟件是怎么做的,反正我最自然的想法就是去讀注冊表。每一款軟件安裝后都會在注冊表里留下記錄,除非是綠色免安裝軟件。那么,要讀注冊表的話要去哪個位置讀呢?網上的很多資料都指向了一個固定的位置:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall (1)
在注冊表編輯器regedit中打開這個路徑,果然可以看到一些軟件的注冊信息,如下:
等等,稍微一掃描我就覺得有什么不對勁了:我每天用的QQ去哪了?沒道理這么大一款軟件不使用注冊表啊?想來難道又是64位系統的緣故?於是又在網上查到了一些關鍵信息,一個路徑:
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall (2)
路徑存在着這么一個結點名:Wow6432Node。顯然,我們就得了解下這個Wow64是個什么東西了。
(2)Wow64是什么?
咋一看Wow這個縮寫,讓我莫名的熟悉,難道是這個Wow(World of Warcraft)?顯然和魔獸世界沒有什么聯系。一查資料才知道,原來是32bits Windows On Windows 64bits的縮寫。照這字面意思,就是微軟在64為系統上模擬了一個32位程序運行的環境,這也解釋了,為什么我的電腦上會有兩個這樣的文件夾:
這篇博客講的很詳細,對於是什么、為什么、怎么樣都有詳細敘述。后面還有一些參考文獻,也是相關的解釋。我就不再賣弄什么是Wow了。
(3)兼容64位系統
好了,那么我們既然知道了Wow是個什么東西,就去上面的路徑(2)瞧個清楚啊。一路打開, QQ的注冊信息赫然在列:
照這么一分析,就明白了:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall這個路徑下放的是64位程序的注冊信息,而HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall放的則是32位程序的信息。為什么這么命名,仔細想想就會發現這樣命名其實是自然而然的。上面提到的博客也有說明。
好嘛,余下的事情就很明朗了:判斷是否為64位系統,如果是,則讀取上述兩個位置的信息。至於讀取出來的信息完整不完整,我也不確定,畢竟又沒有一個統一的標准,不過相信也八九不離十了。關於怎么判斷系統的位數,網上流傳着一段代碼,這里經過了修改也一並貼上:
1 typedef BOOL (WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL); 2 BOOL bIsWow64 = false; // assume 32 bit 3 4 // can't call IsWow64Process on x32, so first look up the entry point in kernel32 5 LPFN_ISWOW64PROCESS fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle(TEXT("kernel32")),"IsWow64Process"); 6 // if we have an entry point for IsWow64Process, we can call it 7 if (NULL != fnIsWow64Process) 8 { 9 if (!fnIsWow64Process(GetCurrentProcess(), &bIsWow64)) 10 { 11 bIsWow64 = FALSE; 12 } 13 } 14 return bIsWow64;
界面設計
這個工具的功能較為簡單,只需要兩個表格來顯示底層獲取的信息即可。考慮使用QTableView來做數據視圖,QStandardItemModel做數據存儲,Qt MVC框架的好處自不必贅述。整體使用垂直布局管理器來進行部件管理。另外,我們還想實現上下文菜單,那么還得去繼承QTableView重寫虛函數contextMenuEvent()達到目的。最終的界面看下面。
界面截圖及代碼
典型的Windows 7默認主題,看起來普通平凡,沒有一絲個性。下一篇《用Qt寫軟件系列二:一個簡單的系統工具之界面美化》將對該界面進行個性化定制。