文件版本信息的存在使得應用程序正確的安裝文件變得簡單,並且使安裝程序能夠分析文件的當前安裝狀態。通常,版本信息應該包括文件的版本號、文件功能描述以及文件作者等多項重要內容。
在應用層面上,程序員可以通過保存在應用程序文件或動態鏈接庫文件中的版本信息判斷一個文件是否應該被安裝,並確定當前安裝文件的沖突。在文件有了版本信息這個屬性後,我們編寫的程序就能夠實現以下功能:
1. 避免在新版本的組件上安裝舊版本的相同組件;
2. 在多語言系統環境中,操作系統根據文件版本信息裡提供的語言信息在啟動程序時決定使用的正確語言;
3. 可以防止在不同的路徑下安裝多個文件的拷貝;
4. 應用程序在運行時,便能判斷文件的版本是否正確;
5. 在應用程序的關於對話框中顯示可執行文件的版本號;
6. 在線升級程序可以判斷一個文件是否因為版本過舊,從而進行必要的文件升級。
因為Windows系統在設計上的先天缺陷,其使用的FSO並不支持文件的版本信息功能。為了能對文件的版本信息進行管理,微軟在Platform SDK(平台軟件開發工具包)中通過API(可編程接口)的方式為軟件開發人員提供了管理版本信息的方法。這些API是GetFileVersionInfo、GetFileVersionInfo、VerFindFile、VerInstallFile和VerQueryValue,它們被存放在Windows系統目錄下一個被命名為VERSION.DLL的動態鏈接庫文件中。
在Windows系統中任何可以包含Windows資源的文件都可以包含版本信息,比如動態鏈接庫文件、可執行文件、字體文件等。版本信息被包裝成一個VERSIONINFO結構的資源,通過編譯器打包進這些文件中。
本文試圖通過分別講解這些API的使用方法,向讀者介紹在Delphi中通過編程的方法,怎樣實現在運行時動態獲取程序文件版本信息的功能。
一、幾個重點版本信息函數的使用方法
在Windows操作系統中采用語言標識符來區別不同的自然語言,語言標識符是一個32位的無符號整數,一個數值唯一對應一種自然語言。因此,除了前面列出的五個函數外,在我們編寫程序的時候還會經常使用到VerLanguageName函數,通過調用這個函數,我們的程序便能夠自由的在用整數表示的語言標識符和用字符串表示的語言名稱之間進行轉換。
以下便是用於處理版本信息函數的完整列表。須要注意的是:1. 在Windows NT 3.51或更早版本的操作系統中,版本信息函數不能操作16位的Windows文件像(File Image);2. 在Windows 95/98/Me/NT4/2000操作系統中,這些函數同時可以操作16位和32位兩種文件像;3. 在Windows XP操作系統中,這些函數同時可以操作16位、32位和64位共三種文件像。
[插入表格1]
在這篇文章中,我們將著重介紹GetFileVersionInfo、GetFileVersionInfoSize、VerLanguageName和VerQueryValue這四個函數的使用方法。在實際應用時,其余的兩個函數很少使用,因此不作為重點內容進行介紹。
1.1. GetFileVersionInfo函數
GetFileVersionInfo函數被用來獲取包含在指定文件中的版本信息。其Delphi函數聲明如下:
function GetFileVersionInfo(
lptstrFilename: PChar; // 文件名
dwHandle: DWORD; // 忽略
dwLen: DWORD; // 緩沖區大小
lpData: Pointer // 版本信息緩沖區
): BOOL; stdcall;
參數說明:
lptstrFilename,一個以NULL結束字符串,它指定了期望從中獲取版本詳細的文件名。如果文件名不包含完整路徑,函數將使用LoadLibrary函數的默認搜索次序進行搜索。在Windows 95/98/Me操作系統中路徑名不能超過126個字符。
dwHandle,這個參數沒有使用,將被忽略。
dwLen,請先調用GetFileVersionInfoSize函數確定文件版本信息的字節數大小。dwLen必須等於或大於這個值。如果lpDate指向的緩沖區空間不夠,函數將根據實際大小裁減出文件的版本信息。
lpData,指向一個用於保存函數調用後返回的文件版本信息的緩沖區。
如果函數調用成功,它將返回True;否則返回False。可通過GetLastError函數得到擴展的錯誤信息。
在調用GetFileVersionInfo函數前必須先調用GetFileVersionFileSize。為了從文件版本信息中獲取有用信息,必須使用VerQueryValue函數。
1.2. GetFileVersionInfoSize函數
GetFileVersionInfoSize函數被用來判斷操作系統是否能夠從指定文件中獲取版本信息。如果存在版本信息,便返回以字節為單位的這些信息所占用空間的大小。其Delphi函數聲明如下:
function GetFileVersionInfoSize(
lptstrFilename: PChar; // 文件名
var lpdwHandle: DWORD // set to zero
): DWORD; stdcall;
參數說明:
lptstrFilename,一個以NULL結束字符串,它指明期望從哪個文件中獲取版本信息的文件名。
lpdwHandle,一個指向將被函數設置為0的變量的指針。
如果函數調用成功,它將返回文件版本信息的字節大小;否則返回0,可通過GetLastError函數得到擴展的錯誤信息。
在調用GetFileVersionInfo函數前應先調用GetFileVersionInfoSize函數。GetFileVersionInfoSize函數的返回值確定了GetFileVersionInfo函數所使用的版本信息緩沖區的大小。
1.3. VerLanguageName函數
VerLanguagename函數被用來獲取與指定的二進制微軟語言標示相關聯的語言描述字符串。其Delphi函數聲明如下:
function VerLanguageName(
wLang: DWORD; // 微軟語言標識符
szLang: PChar; // 語言描述緩沖區
nSize: DWORD // 緩沖區大小
): DWORD; stdcall;
參數說明:
wLang,語言標識符,是一個二進制數字。指定二進制語言標識符。如果向得到完整的語言標識符列表,請參見語言標識符部分的內容。舉個例子,與語言標識符0x040A相關聯的描述字符串就是“卡斯蒂利亞西班牙語”。如果是一個未知的標識符,那麼szLang參數就會指向一個缺省字符串--“Language Neutral”。
szLang,這個參數指向一個緩沖區。這個緩沖區用於存儲由wLang參數所確定的、用來描述語言的、以NULL結尾的字符串。
nSize 指定緩沖區的大小,單位是字符數量。
函數將返回存儲在緩沖區中字符串的以字符為單位的大小。返回值不包含結束NULL字符。如果描述字符串小於或等於緩沖區的大小,那麼整個描述字符串將保存在這個緩沖區中;否則,緩沖區中將之保留描述字符串的前面大小等於緩沖區大小的部分。
如果發生錯誤,返回值將等於0。未知的語言標識符不會產生錯誤。
通常,安裝程序通過這個函數來翻譯從VarQuery函數返回的語言標識符。當出現語言沖突的時候,這個得到的文本字符串便可以用在一個向用戶詢問怎樣處理的對話框中,提示用戶進行處理。
1.4. VerQueryValue函數
VerQueryValue函數被用來從指定的版本信息資源中獲取指定版本信息。最常用的獲取版本信息的邏輯流程是:先調用GetFileVersionInfoSize函數,緊接著再調用GetFileVersionInfo函數,最後再調用VerQueryValue函數。其Delphi函數聲明如下:
function VerQueryValue(
pBlock: Pointer; // 存放版本資源的緩沖區
lpSubBlock: PChar; // 期望獲取的值
var lplpBuffer: Pointer; // 指向存放版本值緩沖區的指針
var puLen: UINT // 版本信息長度
): BOOL; stdcall;
參數說明:
pBlock,一個指向用於存儲版本信息資源的緩沖區的指針,這個版本信息資源是從GetFileVersionInfo函數返回的。
lpSubBlock,指向一個零結尾的字符串,指定到底獲得哪個版本信息值。這個字符串必須由被反斜線符號(\)分開的名字組成如下格式之一:
→“\”,指定根區域。函數將返回一個指向VS_FIXEDFIELDFILEINFO結構的版本信息資源。
→“\VarFileInfo\Translation”,指定一個保存在可變類型變量信息的結構中的轉換陣列。函數返回一個指向語言和代碼頁標識符數組的指針。應用程序可以使用這些標識符來訪問存儲在版本信息資源中的特定語言字符串表結構。
→“\StringFileInfo\lang-codepage\string-name”,指定存儲在特定語言字符串表中結構的值。其中,lang-codepage的書寫格式是:用雙字(DWORD)表示的、保存在資源中的轉換陣列的語言與代碼頁標識符對,並且需要書寫成十六進制形式的字符串;string-name必須是在後面注釋中預定義的字符串之一。函數根據指定的語言與代碼頁,返回一個與之相關的字符串。
lplpBuffer,一個指向用於保存指向被請求的版本信息緩沖區的變量的指針。簡單的說,就是一個指向指針的指針。
puLen,指向一個保存版本信息長度的緩沖區。
如果指定的版本信息結構存在並且有效,函數將返回一個非0值。如果長度緩沖區的地址等於0,指定的版本信息名稱將無效。
並且,在指定的名稱不存在或指定的資源無效時,函數的返回值將等於0。
以下列表是預定義的版本信息統一字符編碼標准字符串:
Comments、InternalName、ProductName、CompanyName、LegalCopyright、ProductVersion、FileDescription、LegalTrademarks、PrivateBuild、FileVersion、OriginalFilename、SpecialBuild
二、一個關於版本信息函數使用的實例
下面開始,我們給出一個實例。通過這個實例,大家可以學習到在我們自己編寫的應用程序中怎麼使用這些函數來達到獲取文件版本信息的目的。
2.1. 建立實例窗體樣板
首先,我們需要建立兩個Delphi窗體,它們分別是程序主窗體和關於對話框窗體。用於這個程序只作演示用途,所以沒有必要在主窗體中實現過多的功能。事實上,在我們這個實例中,主窗體上只有“關於”和“退出”兩個菜單項。
稍微復雜一點的要算是關於對話框窗體了。在這個窗體中,將包含“產品名稱”、“產品版本號”、“文件說明”、“產品合法商標”、“運行文件版本號”、“公司名稱”、“合法商標”、“產品內部名稱”、“原文件名”等幾個重要且基本的版本信息。當然,正如你所看到的,在這個窗體中還包含了操作系統信息版本和系統內存信息。但由於篇幅的關系,兩條信息的獲取方法在本文中不會加以說明。我們會在下一次的學習中討論這兩個話題。關於對話框的布局如下圖,大家可以對比這個圖片自己用Delphi制作一個對話框處出來。
[插入圖片1]
2.2. 填寫應用程序的運行文件版本信息
在Delphi的集成編輯器中按下Shift+Ctrl+F11鍵,在彈出的對話框中按照下表的內容,在相應鍵值的未知輸入內容:
[插入表格2]
2.3. 實現獲取版本信息功能的代碼
下面的代碼是這個關於對話框創建時的事件響應代碼片斷。大家把它們拷貝+粘貼到窗體的OnCreate事件中就可以了。
const
InfoNum = 9;
InfoStr: array[1..InfoNum] of string = (
'ProductName',
'ProductVersion',
'FileDescription',
'LegalCopyright',
'FileVersion',
'CompanyName',
'LegalTradeMarks',
'InternalName',
'OriginalFileName'
);
var
S: string;
BufSize, Len: DWORD;
Buf: PChar;
Value: PChar;
begin
S := Application.ExeName;
BufSize := GetFileVersionInfoSize(PChar(S), BufSize);
if BufSize > 0 then begin
Buf := AllocMem(BufSize);
GetFileVersionInfo(PChar(S), 0, BufSize, Buf);
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[1]), Pointer(Value), Len) then
ProductName.Caption := Value;
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[2]), Pointer(Value), Len) then
ProductVersion.Caption := '產品版本: ' + Value;
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[3]), Pointer(Value), Len) then
FileDescription.Caption := '文件說明: ' + Value;
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[4]), Pointer(Value), Len) then
LegalCopyright.Caption := '合法版權: ' + Value;
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[5]), Pointer(Value), Len) then
FileVersion.Caption := '文件版本: ' + Value;
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[6]), Pointer(Value), Len) then
CompanyName.Caption := '公司名稱: ' + Value;
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[7]), Pointer(Value), Len) then
LegalTrademarks.Caption := '合法商標: ' + Value;
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[8]), Pointer(Value), Len) then
InternalName.Caption := '內部名稱: ' + Value;
if VerQueryValue(Buf, PChar('StringFileInfo\080403A8\' + InfoStr[9]), Pointer(Value), Len) then
OriginalFilename.Caption := '原文件名: ' + Value;
FreeMem(Buf, BufSize);
// OperatingSystem.Caption := GetOSVerInfo;
// SystemMemory.Caption := GetMemStat;
end
else begin
Application.MessageBox('獲取產品信息時遇到致命錯誤,請嘗試重新啟動軟件。'+ #13 + '若仍未能解決問題,請聯系產品服務人員。','錯誤',MB_OK + MB_ICONSTOP);
Application.Terminate;
end;
在這段代碼中,首先通過調用GetFileVersionInfoSize()函數獲得在內存中存儲版本信息資源所需要的最小內存空間大小。如果函數的返回值小於等於0,則函數調用失敗,繼而程序轉入錯誤處理部分。在這部分的錯誤處理代碼中,程序會提示用戶程序運行實現錯誤,並要求用戶重新啟動軟件。
如果函數調用成功,程序將通過AllocMem()函數向操作系統申請一塊內容空間,用於保存從文件中獲得的版本信息資源。
此後,利用GetFileVersionInfo()函數從文件中得到版本信息。但這些信息對我們來說是不可讀的,因為它們都是一些二進制的編碼。在隨後的程序中,通過一連串對VerQueryValue()函數的調用,我們得到了文件的版本信息,並把它們顯示在這個關於對話框窗體中正確的位置上。
在使用完這些信息後,我們一定要記住用FreeMem()函數釋放前面用AllocMem()函數分配的內存,否則就會出現內存洩漏。
// OperatingSystem.Caption := GetOSVerInfo;
和
// SystemMemory.Caption := GetMemStat;
這兩行代碼調用的兩個函數是我自己編制的,分別用於獲取操作系統版本信息和內存信息。前面已經說過,由於篇幅的關系,我們在這裡暫時跳過這兩行代碼,不去管它。反正程序已經可以正常的運行了。
三、總結
通過本文中對文件版本信息處理方法的簡單介紹,相信讀者已經對這種技術有了一個初步的認識。這種實用且非常重要的技術是不是很簡單呢?答案當然是肯定的!正如上面所講述的內容,其實就是對那六個函數的交替調用。在這裡唯一要提醒讀者的是,在使用這些函數的時候,一定要注意調用次序,不能搞錯,否則很有可能怎麼也不能正常訪問這些函數(函數返回值始終是調用失敗)。
在第二節中的哪個實例的源代碼,我們在Windows 2000簡體中文專業版和Delphi 6.0企業版的編程環境下調試通過,所有代碼均能正常運行。