最近,好多人问我如何通过写个小程序,动态替换可执行文件的图标。这个问题看起来虽小,但却涉及到很多问题。网上也只能找到一些零零散散的资料,却没有详细的指导性文档。所以我决定把这个问题写下来,以方便大家查阅。
EXE文件图标的替换有很多方法,例如用一个EXE文件的图标替换另外一个EXE文件的图标;用一个ICO文件内的图标替换EXE文件的图标。这两种情况替换的方法不太相同,下面会详细讨论。
EXE文件图标的替换更一般的情形,是PE(PortableExecutable)文件图标的替换。只不过Windows操作系统只会显示EXE文件的图标罢了。但DLL、OCX等PE文件也都可以包含图标资源。下面我们从ICO文件格式说起,一步步讲解替换EXE文件图标的方法和原理。
.ico文件中图标的保存格式
对于一个扩展名是.ico的文件,大部分人会认为一个ICO文件里面只能包含一个图标。但事实上,一个ICO文件里面可以包含很多图标。而且,目前大部分ICO文件里面都包含有不同尺寸、不同色深的好几个图标。我们以MSN安装包里的msnmsn.ico为例,这个图标文件就包含了9个不同尺寸、不同色深的图标,如图所示:
这样做的目的,是为了保证不同的操作系统、不同的桌面色深,图标显示均可达到最佳效果。操作系统会选择并显示一个最合适的图标。WindowsXP支持32位色的图标,Windows 2000最多只支持256色的图标。所以,如果我们开发的软件若要同时支持WindowsXP和2000,那么为了达到视觉上的最佳效果,每一个ICO文件应至少包含两个图标,一个是32位色的,一个是256色的。ICO文件头部结构定义如下:
6 | ICONDIRENTRY idEntries[1]; |
idCount表示该ICO文件包含图标的数量,所以理论上,一个ICO文件最多可以包含65535个图标。接下来,是该文件所包含的每一个图标的描述。
11 | } ICONDIRENTRY, *LPICONDIRENTRY; |
ICONDIRENTRY中记录了每一个图标的尺寸、色深、图标资源占用的字节数。dwImageOffset是一个文件偏移地址,指向图标资源数据起始位置。至于每一个图标资源内部的具体格式,与本文关系不大,这里就不再详细介绍了。
PE文件中的图标保存格式
PE文件中的图标保存格式与.ico文件中图标的保存格式略有不同。PE文件中,把ICONDIR和图标资源作为两种资源类型分别保存,前者是RT_GROUP_ICON类型,后者是RT_ICON类型。为了与.ico文件中图标的保存格式做以区分,我们把PE文件中的图标保存格式重新定义如下:
10 | GRPICONDIRENTRY idEntries[1]; |
11 | } GRPICONDIR, *LPGRPICONDIR; |
23 | } GRPICONDIRENTRY, *LPGRPICONDIRENTRY; |
这里有一个区别,就是在.ico文件中,ICONDIRENTRY结构最后一个成员dwImageOffset表示的是图标资源文件偏移地址。而PE文件中,GRPICONDIRENTRY结构最后一个成员nID表示的是图标的索引ID。
Windows API
Windows操作系统为我们提供了几个API函数,用来更新PE文件中资源的函数有:BeginUpdateResource,UpdateResource,EndUpdateResource。用来枚举PE文件中资源的函数有:EnumResourceTypes,EnumResourceNames,EnumResourceLanguages。具体的使用方法可以参见MSDN。
下面我们通过具体的例子,来验证上面的方案是否可行。
用一个EXE中的图标替换另外一个EXE文件的图标
在这个例子中,我们用Windows XP自带的记事本的图标替换计算器的图标。
记事本图标
计算器图标
下面代码演示了如何替换32×32 32bits的图标:
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); |
07 | HANDLE hUpdate = ::BeginUpdateResource( "calc.exe" , FALSE); |
08 | VERIFY(::UpdateResource(hUpdate, RT_ICON, MAKEINTRESOURCE(7), |
09 | MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT), |
11 | VERIFY(::EndUpdateResource(hUpdate, FALSE)); |
12 | VERIFY(::FreeLibrary(hModule)); |
大家肯定有个疑问,上面代MAKEINTRESOURCE(8)和MAKEINTRESOURCE(7)是怎么来的呢?其实索引8和7分别是notepad.exe和calc.exe中,32×3232bits图标的索引。我们可以通过加载RT_GROUP_ICON资源,然后遍历GRPICONDIRENTRY中每一个图标的大小、色深,找到这个图标的索引。为了简便,这里直接写死的索引号,省略了这一动态查找的过程。
还有一个疑问应该就MAKELANGID(LANG_ENGLISH,SUBLANG_DEFAULT)了。PE文件中,每一个资源都至少对应一种语言。因为我的操作系统是英文的,所以记事本和计算器中的图标资源语言也是英文的。对于简体中文Windows XP操作系统所自带的记事本和计算器,这个值应该是MAKELANGID(LANG_CHINESE,SUBLANG_SYS_DEFAULT)。
那么我们怎么才能知道一个PE文件中,图标资源的语言是什么呢?我们可以通过资源枚举API,枚举所有图标、语言。可以参考上面提到过的那几个API函数,并查阅MSDN获取这些函数的帮助文档。 我们用记事本32×3232bits图标替换计算器同样尺寸、色深的图标后,效果如下,在Titles显示方式下,图标大小是48×48的,图标没有被改变:
在Icons显示方式下,图标大小是32×32的,图标被我们改变了:
用一个ICO文件中的图标替换另外一个EXE文件的图标
用ICO文件中的图标替换EXE文件图标稍微有点麻烦,我们必须借助数据结构ICONDIR和ICONDIRENTRY来完成。我们使用msnms.ico中的32×32 32bits图标替换计算器中同样大小色深的图标:
01 | DWORD dwSize = sizeof (ICONDIRENTRY); |
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); |
09 | VERIFY(::ReadFile(hFile, &Entry, dwSize, &dwRead, NULL)); |
11 | ::SetFilePointer(hFile, Entry.dwImageOffset, NULL, FILE_BEGIN); |
13 | void * pData = new char [Entry.dwImageOffset]; |
14 | VERIFY(::ReadFile(hFile, pData, Entry.dwBytesInRes, &dwRead, NULL)); |
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)); |
25 | VERIFY(::CloseHandle(hFile)); |
上面代码中sizeof(ICONDIR) + dwSize *6的意思是定位到第8个标结构体ICONDIRENTRY的位置,这个图标是32×3232bits的。我们可以通过遍历每一个ICONDIRENTRY来判断,到底哪个图标是这个尺寸的。这里我们为了简便,把这部分代码省略了。定位到第8个图标结构体ICONDIRENTRY的位置后,Entry.dwImageOffset的值就是第8个图标资源的文件偏移地址,Entry.dwBytesInRes的值是第8个图标图标资源的大小。然后我们将文件指针定位到Entry.dwImageOffset,并读取Entry.dwBytesInRes大小的数据到指针pData指向的内存当中。 最后,是替换文件图标资源的代码,这部分代码跟上一个例子是相同的。
本文抛砖引玉,介绍了EXE文件图标的替换,但完全可以推广到所有PE文件图标的替换(包括EXE、DLL等),也可推广到所有PE文件资源的替换(包括图标、图片、文字资源、对话框模板、菜单等)。可供相关人员参考。