EXE的程序图标描述及修改(非PE修改)

来源:互联网 发布:mac版农行支付宝 编辑:程序博客网 时间:2024/05/20 17:09

因为某些原因,我想将一个EXE中的程序图标换成另外一个EXE文件中的图标。

 

这里说的图标并不是指EXE中所有的图标。EXE文件中可能包含成百上千的图标资源,我的更换目标只是程序图标,就是在windows资源管理器中所看到的图标。

 

讲到图标,先讲讲这两天我在这方面的收获。

 

先说下ICO,图标格式文件。


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

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

 

ICONDIR是ICO文件头,ICONDIRENTRY是单个图像的描述。

 

一个ICONDIRENTRY是16个字节,一个ICONDIR是22个字节。

用十六进制编辑器打开一个标准的ICO文件,前面16个字节的数据就是ICONDIR。
一个文件只有一个ICONDIR,它描述了这个文件有几个图像或资源。
ICONDIRENTRY用于描述每个资源(图像)的属性和数据偏移,通过数据偏移就能找到实际的资源(图像)数据,至于这个数据最后要怎样显示出来,那是另外一个话题了。

 

ICONDIR的idCount属性描述了有几个资源,idEntries是第一个ICONDIRENTRY(资源属性)。如果idCount大于1,则剩下的ICONDIRENTRY,则紧跟在ICONDIR,16个字节之后。 

ICO文件中除了开头的ICONDIR文件头和紧跟其后的ICONDIRENTRY,剩下的就是资源的实际数据了。


之前一直以为ICO跟JPG,BMP一样,一个文件里面只一幅图像,而实际是一个ICO文件里可能有若干个文件。

还有,为什么很多开发工具为应用程序指定图标时,要选择ICO文件?原因跟ICO这个特性有关。

 

比如说,你在资源管理器里面可以以多种查看方式浏览文件,如缩略图、平铺、图标、列表、详细信息方式。

其中差别最大的时图标与列表方式,所展现的文件图标的大小差异很大,Windows在不同的查看方式下会寻找你程序中最适合的图标来显示。

 

当Windows需要显示程序的图标时,下面三个因素会影响Windows查找最适合显示的图标:
1.查看方式(缩略图、平铺、图标、列表、详细信息方式等)
2.当前显示器的显示颜色质量(256色,16位真彩,32真彩等)。
3.当前系统的语言(英文,简体中文,繁体中文等)

一般而言,你要为程序设计这几种类型的图标

16X16 256色,

32X32 256色,

48X48 256色,

16X16 16位真彩色,

32X32 16位真彩色,

48X48 16位真彩色,

Winddows 98/2000不支持16位真彩的图标,xp以上操作系统还支持32位真彩色的图标,但是个人认为16位真彩色与32位真彩色肉眼看不出差别,可以不设计此类图标。还有其它规格的图标,像24X24大小的,但是不常用。

 

这些知识帮我解决了心中一个疑惑:我之前用Delphi为程序指定图标时,经常是拿BMP图像去转成ICO用的,变成ICO里面只有一张图片,这样就不能适应资源管理器的多种查看方式,显示效果很差。


以前的做法是选些简单的图标,这样放大缩小后效果就不致于太糟。为了解决这个问题还专门去google过,看过有人拿VC的资源编辑器再建个资源文件给delphi用,还附有些副作用。

现在好了,知道原理后,以后为程序制作个“完整的”ICO文件就可以了。

photoshop不支持制作ICO,这里推荐个工具Axialis IconWorkshop,很好用。

 

讲了这么多,回到正题,怎样替换一个EXE中的程序图标呢?

从前从前,我写过篇通过解析PE格式来读出图标的文章,这种方式很麻烦,偶尔发现有人介绍了另外的方法,觉得难度要小不少。
可是在实际的编写代码的过程中,发现网上的文章的代码并不能直接使用,起码在我的电脑上不能。经过几番折腾,终于弄懂了怎么回事。

 

假设一下你想把1.exe中的程序图标换成2.exe中的图标。


首先,要更换一个程序中的图标要用到这几个API:

HANDLE BeginUpdateResource(
  LPCTSTR pFileName,             // pointer to executable file name
  BOOL bDeleteExistingResources  // deletion option
);

BOOL UpdateResource(
  HANDLE hUpdate, // update-file handle
  LPCTSTR lpType, // address of resource type to update
  LPCTSTR lpName, // address of resource name to update
  WORD wLanguage, // language identifier of resource
  LPVOID lpData,  // address of resource data
  DWORD cbData    // length of resource data, in bytes
);

BOOL EndUpdateResource(
  HANDLE hUpdate, // update-file handle
  BOOL fDiscard   // write flag
);


BeginUpdateResource,EndUpdateResource比较好说,关键是UpdateResource,

BOOL UpdateResource(
  HANDLE hUpdate,
// update-file handle,其实就是调用BeginUpdateResource的返回值
  LPCTSTR lpType,
// address of resource type to update,要更新的资源的类型,它其实是个字符器/整数双用型的参数,
//这里使用预定义的RT_ICON,其它类型参考MSDN。
  LPCTSTR lpName,
// address of resource name to update,要更新的资源的地址,也是字符器/整数双用型参数。
//当它的高字为零时,当整数使用;否则为字符串。大多数情况都是传整数参数,即图标编号。
  WORD wLanguage,
// language identifier of resource,要更新的资源的语言标识,也只是说Windows可以在不同语言平台为一个程序显示不同的图标。
//这里使用了MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED),这样的缺点就是只支持简体中文版操作系统了。
  LPVOID lpData, 
// address of resource data,指定要用什么数据去更新资源,这是个数据指针
  DWORD cbData   
// length of resource data, in bytes,所要更新的数据的大小
);


那这个函数就有两个问题待解决
1.如何获取资源的地址(图标编号)
2.如何获取要更新的数据及大小

 

第一个问题
因为程序里面也许会有好多的图标,只有其中几个才是程序图标,那怎么找到程序图标呢?
我的做法是这样的,因为程序图标总是从编号1开始,所以我先使用EnumResourceNames遍历一下里面的图标资源,目标是找到前10个图标,如果没有10个就返回实际数。

要使用两次EnumResourceNames,分别对1.exe和2.exe中的图标进行遍历。

使用EnumResourceNames要指定回调函数,在回调函数中可以得到本次遍历的图标编号。

遍历图标得到图标编号后,根据编号分别打开两个EXE中的图标资源,得到它们的大小,因为大家都是编号前10的图标,再对比一下它们彼此之前的大小,如果一样则进行替换。


那紧跟着就是第二个问题了,怎样根据编号获取到对应的数据呢?

 

HINSTANCE hInsSrc = LoadLibrary("文件名");
HRSRC hResInfo = ::FindResource(hInsSrc, MAKEINTRESOURCE(图标编号), RT_ICON);
HGLOBAL hGlobal = ::LoadResource(hInsSrc, hResInfo1);
DWORD dwSize = ::SizeofResource(hInsSrc, hResInfo1);
void* pData = ::LockResource(hGlobal2);//取得数据,系统自动释放资源。

 

这样,dwSize就是数据大小,pData就是数据指针了。

至此,更新资源最大的两个问题已经解决,但还有个很“奇怪”的问题,一开始调用UpdateResource,明明返回成功,但系统不会更换1.exe中的图标,反而在同目录下生成一个***.tmp的文件(***表示随机),把***.tmp改名后就是被修改后的1.exe了。最后发现,是因为我在UpdateResource之前对EXE进行遍历,获取资源操作时,LoadLibrary装载了文件。在UpdateResource之前就要关闭文件FreeLibrary才行。

但是UpdateResource又返回成功。。。。唉。

 

下面贴代码了, 代码并不完善。vc6.0 + windows2003测试通过。欢迎指点我。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
static WORD g_wSrcIconCount1 = 0;
static WORD g_aIconID1[5] = {1, 2, 3, 4, 5};
static WORD g_wSrcIconCount2 = 0;
static WORD g_aIconID2[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};


BOOL CALLBACK EnumResNameProc1(HINSTANCE hModule, LPCTSTR lpszType, LPTSTR lpszName, LONG lParam)
{
 if(((DWORD)lpszName & 0xffff0000) == 0)
 {
  WORD wID = (WORD)lpszName;
  if(wID >= 1 && wID <= 5)
  {
   g_aIconID1[g_wSrcIconCount1] = wID;
   g_wSrcIconCount1 += 1;
  }
 }
 return TRUE;
}

BOOL CALLBACK EnumResNameProc2(HINSTANCE hModule, LPCTSTR lpszType, LPTSTR lpszName, LONG lParam)
{
 if(((DWORD)lpszName & 0xffff0000) == 0)
 {
  WORD wID = (WORD)lpszName;
  if(wID >= 1 && wID <= 10)
  {
   g_aIconID2[g_wSrcIconCount2] = wID;
   g_wSrcIconCount2 += 1;
  }
 }
 return TRUE;
}

int ReleaseExeIcon(char* szSrcFile, char* szDstFile)
{
 int nReplace = 0;
 HINSTANCE hInsSrc = LoadLibrary(szSrcFile);
 HINSTANCE hInsDst = LoadLibrary(szDstFile);
 if(hInsSrc == NULL || hInsDst == NULL)
 {
  if(hInsSrc != NULL) FreeLibrary(hInsSrc);
  if(hInsDst != NULL) FreeLibrary(hInsDst);
  return 0;
 }
 
 g_wSrcIconCount1 = 0;
 g_wSrcIconCount2 = 0;
 EnumResourceNames(hInsSrc, RT_ICON, EnumResNameProc1, 0);
 EnumResourceNames(hInsDst, RT_ICON, EnumResNameProc2, 0);
 
 for(int i = 0; i < g_wSrcIconCount1; i++)
 {
  HRSRC hResInfo1 = ::FindResource(hInsSrc, MAKEINTRESOURCE(g_aIconID1[i]), RT_ICON);
  HGLOBAL hGlobal1 = ::LoadResource(hInsSrc, hResInfo1);
  DWORD dwSize1 = ::SizeofResource(hInsSrc, hResInfo1);
  
  //在目标文件中找到合适大小的图标
  for(int j = 0; j < g_wSrcIconCount2; j++)
  {
   HRSRC hResInfo2 = ::FindResource(hInsDst, MAKEINTRESOURCE(g_aIconID2[j]), RT_ICON);
   HGLOBAL hGlobal2 = ::LoadResource(hInsDst, hResInfo2);
   DWORD dwSize2 = ::SizeofResource(hInsDst, hResInfo2);
   
   //如果大小一致才复制替换图标内容
   if(dwSize1 == dwSize2)
   {   
    //更新图标前必须关闭文件
    FreeLibrary(hInsSrc);
    //更新图标
    HANDLE hUpdate = ::BeginUpdateResource(szSrcFile, FALSE);
    void* pData = ::LockResource(hGlobal2);//取得数据,系统自动释放资源。
    BOOL bSuccess = ::UpdateResource(hUpdate, RT_ICON, MAKEINTRESOURCE(g_aIconID1[i]),
     MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED),
     pData, dwSize2);
    ::EndUpdateResource(hUpdate, FALSE);
    //更新完图标后自动打开文件
    hInsSrc = LoadLibrary(szSrcFile);
    /////
    if(bSuccess)
     nReplace += 1;
    break;
   }
  }
 }
 FreeLibrary(hInsSrc);
 FreeLibrary(hInsDst); 
 return nReplace;
}

原创,温校宏.2010/02/02