最近,好多人問(wèn)我如何通過(guò)寫個(gè)小程序,動(dòng)態(tài)替換可執(zhí)行文件的圖標(biāo)。這個(gè)問(wèn)題看起來(lái)雖小,但卻涉及到很多問(wèn)題。網(wǎng)上也只能找到一些零零散散的資料,卻沒(méi)有詳細(xì)的指導(dǎo)性文檔。所以我決定把這個(gè)問(wèn)題寫下來(lái),以方便大家查閱。
EXE文件圖標(biāo)的替換有很多方法,例如用一個(gè)EXE文件的圖標(biāo)替換另外一個(gè)EXE文件的圖標(biāo);用一個(gè)ICO文件內(nèi)的圖標(biāo)替換EXE文件的圖標(biāo)。這兩種情況替換的方法不太相同,下面會(huì)詳細(xì)討論。
EXE文件圖標(biāo)的替換更一般的情形,是PE(Portable Executable)文件圖標(biāo)的替換。只不過(guò)Windows操作系統(tǒng)只會(huì)顯示EXE文件的圖標(biāo)罷了。但DLL、OCX等PE文件也都可以包含圖標(biāo)資源。 下面我們從ICO文件格式說(shuō)起,一步步講解替換EXE文件圖標(biāo)的方法和原理。

.ico文件中圖標(biāo)的保存格式

  對(duì)于一個(gè)擴(kuò)展名是.ico的文件,大部分人會(huì)認(rèn)為一個(gè)ICO文件里面只能包含一個(gè)圖標(biāo)。但事實(shí)上,一個(gè)ICO文件里面可以包含很多圖標(biāo)。而且, 目前大部分ICO文件里面都包含有不同尺寸、不同色深的好幾個(gè)圖標(biāo)。我們以MSN安裝包里的msnmsn.ico為例,這個(gè)圖標(biāo)文件就包含了9個(gè)不同尺 寸、不同色深的圖標(biāo),如圖所示:
msnms.ico

  這樣做的目的,是為了保證不同的操作系統(tǒng)、不同的桌面色深,圖標(biāo)顯示均可達(dá)到最佳效果。操作系統(tǒng)會(huì)選擇并顯示一個(gè)最合適的圖標(biāo)。Windows XP支持32位色的圖標(biāo),Windows 2000最多只支持256色的圖標(biāo)。所以,如果我們開發(fā)的軟件若要同時(shí)支持Windows XP和2000,那么為了達(dá)到視覺(jué)上的最佳效果,每一個(gè)ICO文件應(yīng)至少包含兩個(gè)圖標(biāo),一個(gè)是32位色的,一個(gè)是256色的。 ICO文件頭部結(jié)構(gòu)定義如下:

1 typedef struct
2 {
3     WORD           idReserved;   // Reserved (must be 0)
4     WORD           idType;       // Resource Type (1 for icons)
5     WORD           idCount;      // How many images?
6     ICONDIRENTRY   idEntries[1]; // An entry for each image (idCount of 'em)
7 } ICONDIR, *LPICONDIR;

idCount表示該ICO文件包含圖標(biāo)的數(shù)量,所以理論上,一個(gè)ICO文件最多可以包含65535個(gè)圖標(biāo)。接下來(lái),是該文件所包含的每一個(gè)圖標(biāo)的描述。

01 typedef struct
02 {
03     BYTE        bWidth;          // Width, in pixels, of the image
04     BYTE        bHeight;         // Height, in pixels, of the image
05     BYTE        bColorCount;     // Number of colors in image (0 if >=8bpp)
06     BYTE        bReserved;       // Reserved ( must be 0)
07     WORD        wPlanes;         // Color Planes
08     WORD        wBitCount;       // Bits per pixel
09     DWORD       dwBytesInRes;    // How many bytes in this resource?
10     DWORD       dwImageOffset;   // Where in the file is this image?
11 } ICONDIRENTRY, *LPICONDIRENTRY;

ICONDIRENTRY中記錄了每一個(gè)圖標(biāo)的尺寸、色深、圖標(biāo)資源占用的字節(jié)數(shù)。dwImageOffset是一個(gè)文件偏移地址,指向圖標(biāo)資源數(shù)據(jù)起始位置。至于每一個(gè)圖標(biāo)資源內(nèi)部的具體格式,與本文關(guān)系不大,這里就不再詳細(xì)介紹了。

PE文件中的圖標(biāo)保存格式

  PE文件中的圖標(biāo)保存格式與.ico文件中圖標(biāo)的保存格式略有不同。PE文件中,把ICONDIR和圖標(biāo)資源作為兩種資源類型分別保存,前者是 RT_GROUP_ICON類型,后者是RT_ICON類型。為了與.ico文件中圖標(biāo)的保存格式做以區(qū)分,我們把PE文件中的圖標(biāo)保存格式重新定義如 下:

01 // #pragmas are used here to insure that the structure's
02 // packing in memory matches the packing of the EXE or DLL.
03 #pragma pack( push )
04 #pragma pack( 2 )
05 typedef struct
06 {
07     WORD              idReserved;   // Reserved (must be 0)
08     WORD              idType;       // Resource type (1 for icons)
09     WORD              idount;       // How many images?
10     GRPICONDIRENTRY   idEntries[1]; // The entries for each image
11 } GRPICONDIR, *LPGRPICONDIR;
12  
13 typedef struct
14 {
15     BYTE    bWidth;               // Width, in pixels, of the image
16     BYTE    bHeight;              // Height, in pixels, of the image
17     BYTE    bColorCount;          // Number of colors in image (0 if >=8bpp)
18     BYTE    bReserved;            // Reserved
19     WORD    wPlanes;              // Color Planes
20     WORD    wBitCount;            // Bits per pixel
21     DWORD   dwBytesInRes;         // how many bytes in this resource?
22     WORD    nID;                  // the ID
23 } GRPICONDIRENTRY, *LPGRPICONDIRENTRY;
24 #pragma pack( pop )

  這里有一個(gè)區(qū)別,就是在.ico文件中,ICONDIRENTRY結(jié)構(gòu)最后一個(gè)成員dwImageOffset表示的是圖標(biāo)資源文件偏移地址。而PE文件中,GRPICONDIRENTRY結(jié)構(gòu)最后一個(gè)成員nID表示的是圖標(biāo)的索引ID。

Windows API

  Windows操作系統(tǒng)為我們提供了幾個(gè)API函數(shù),用來(lái)更新PE文件中資源的函數(shù)有:BeginUpdateResource, UpdateResource, EndUpdateResource。用來(lái)枚舉PE文件中資源的函數(shù) 有:EnumResourceTypes,EnumResourceNames,EnumResourceLanguages。具體的使用方法可以參見(jiàn) MSDN。

下面我們通過(guò)具體的例子,來(lái)驗(yàn)證上面的方案是否可行。

用一個(gè)EXE中的圖標(biāo)替換另外一個(gè)EXE文件的圖標(biāo)

在這個(gè)例子中,我們用Windows XP自帶的記事本的圖標(biāo)替換計(jì)算器的圖標(biāo)。

notepad記事本圖標(biāo)

calculator 計(jì)算器圖標(biāo)

下面代碼演示了如何替換32×32 32bits的圖標(biāo):

01 HMODULE hModule = ::LoadLibrary("notepad.exe");
02 HRSRC hResInfo = ::FindResource(hModule, MAKEINTRESOURCE(8), RT_ICON);
03 HGLOBAL hGlobal = ::LoadResource(hModule, hResInfo);
04 DWORD dwSize = ::SizeofResource(hModule, hResInfo);
05 void* pData = ::LockResource(hGlobal);
06  
07 HANDLE hUpdate = ::BeginUpdateResource("calc.exe", FALSE);
08 VERIFY(::UpdateResource(hUpdate, RT_ICON, MAKEINTRESOURCE(7),
09                         MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT),
10                         pData, dwSize));
11 VERIFY(::EndUpdateResource(hUpdate, FALSE));
12 VERIFY(::FreeLibrary(hModule));

  大家肯定有個(gè)疑問(wèn),上面代MAKEINTRESOURCE(8)和MAKEINTRESOURCE(7)是怎么來(lái)的呢?其實(shí)索引8和7分別是 notepad.exe和calc.exe中,32×32 32bits圖標(biāo)的索引。我們可以通過(guò)加載RT_GROUP_ICON資源,然后遍歷GRPICONDIRENTRY中每一個(gè)圖標(biāo)的大小、色深,找到這個(gè) 圖標(biāo)的索引。為了簡(jiǎn)便,這里直接寫死的索引號(hào),省略了這一動(dòng)態(tài)查找的過(guò)程。
還有一個(gè)疑問(wèn)應(yīng)該就MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT)了。PE文件中,每一個(gè)資源都至少對(duì)應(yīng)一種語(yǔ)言。因?yàn)槲业牟僮飨到y(tǒng)是英文的,所以記事本和計(jì)算器中的圖標(biāo)資源語(yǔ)言也是英 文的。對(duì)于簡(jiǎn)體中文Windows XP操作系統(tǒng)所自帶的記事本和計(jì)算器,這個(gè)值應(yīng)該是MAKELANGID(LANG_CHINESE, SUBLANG_SYS_DEFAULT)。
那么我們?cè)趺床拍苤酪粋€(gè)PE文件中,圖標(biāo)資源的語(yǔ)言是什么呢?我們可以通過(guò)資源枚舉API,枚舉所有圖標(biāo)、語(yǔ)言??梢詤⒖忌厦嫣岬竭^(guò)的那幾個(gè)API 函數(shù),并查閱MSDN獲取這些函數(shù)的幫助文檔。 我們用記事本32×32 32bits圖標(biāo)替換計(jì)算器同樣尺寸、色深的圖標(biāo)后,效果如下,在Titles顯示方式下,圖標(biāo)大小是48×48的,圖標(biāo)沒(méi)有被改變:

notepad 48x48
在Icons顯示方式下,圖標(biāo)大小是32×32的,圖標(biāo)被我們改變了:
notepad 32x32

用一個(gè)ICO文件中的圖標(biāo)替換另外一個(gè)EXE文件的圖標(biāo)

  用ICO文件中的圖標(biāo)替換EXE文件圖標(biāo)稍微有點(diǎn)麻煩,我們必須借助數(shù)據(jù)結(jié)構(gòu)ICONDIR和ICONDIRENTRY來(lái)完成。我們使用msnms.ico中的32×32 32bits圖標(biāo)替換計(jì)算器中同樣大小色深的圖標(biāo):

01 DWORD dwSize = sizeof(ICONDIRENTRY);
02  
03 HANDLE hFile = ::CreateFile("msnms.ico", GENERIC_READ, FILE_SHARE_READ,
04                             NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
05 ::SetFilePointer(hFile, sizeof(ICONDIR) + dwSize * 6, NULL, FILE_BEGIN);
06  
07 DWORD dwRead = 0;
08 ICONDIRENTRY Entry;
09 VERIFY(::ReadFile(hFile, &Entry, dwSize, &dwRead, NULL));
10  
11 ::SetFilePointer(hFile, Entry.dwImageOffset, NULL, FILE_BEGIN);
12  
13 void* pData = new char[Entry.dwImageOffset];
14 VERIFY(::ReadFile(hFile, pData, Entry.dwBytesInRes, &dwRead, NULL));
15  
16 HANDLE hUpdate = ::BeginUpdateResource("calc.exe", FALSE);
17 VERIFY(::UpdateResource(hUpdate, RT_ICON, MAKEINTRESOURCE(7),
18                         MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT),
19                         pData, Entry.dwBytesInRes));
20 VERIFY(::EndUpdateResource(hUpdate, FALSE));
21  
22 delete[] pData;
23 pData = NULL;
24  
25 VERIFY(::CloseHandle(hFile));

  上面代碼中sizeof(ICONDIR) + dwSize * 6的意思是定位到第8個(gè)標(biāo)結(jié)構(gòu)體ICONDIRENTRY的位置,這個(gè)圖標(biāo)是32×32 32bits的。我們可以通過(guò)遍歷每一個(gè)ICONDIRENTRY來(lái)判斷,到底哪個(gè)圖標(biāo)是這個(gè)尺寸的。這里我們?yōu)榱撕?jiǎn)便,把這部分代碼省略了。 定位到第8個(gè)圖標(biāo)結(jié)構(gòu)體ICONDIRENTRY的位置后,Entry.dwImageOffset的值就是第8個(gè)圖標(biāo)資源的文件偏移地 址,Entry.dwBytesInRes的值是第8個(gè)圖標(biāo)圖標(biāo)資源的大小。然后我們將文件指針定位到Entry.dwImageOffset,并讀取 Entry.dwBytesInRes大小的數(shù)據(jù)到指針pData指向的內(nèi)存當(dāng)中。 最后,是替換文件圖標(biāo)資源的代碼,這部分代碼跟上一個(gè)例子是相同的。

  本文拋磚引玉,介紹了EXE文件圖標(biāo)的替換,但完全可以推廣到所有PE文件圖標(biāo)的替換(包括EXE、DLL等),也可推廣到所有PE文件資源的替換(包括圖標(biāo)、圖片、文字資源、對(duì)話框模板、菜單等)??晒┫嚓P(guān)人員參考。