图标文件格式研究
作者:小雨哥
常看到有人说,图标文件就是普通的位图文件。我不知道为什么这样说。其实图
标文件确实有点象位图文件,但它还是有自身的表达结构的。图标文件的开头就
是一个奇怪的格式,定义如下:
tagIconDir = record
idReserved:WORD;
idType:WORD;
idCount:WORD;
idEntries:array[0..0] of tagIconDirEntry;
end;
其中的 idReserved 是保留域,目前始终为 0 ,idType 不象位图文件那样定义为
文件类型,而是定义为资源类型,是图标的话,它是 $0001 ,是光标的话,它是
$0002 ,由此可见,在定义这个类型时,MS 完全是做为资源类文件而确定的,估
计当时留下开头一个保留域的原因,也是参考位图文件格式的定义方法而保留的,
只是不知道为什么后来一直没有给这个保留域正式确定名字。 idCount 表示的是
这个文件里包含了几个图标,最早的时候,它一直是个$0002 ,也就是我们常见
的一个16X16和一个32X32两张位图的图标,现在有些图标,比如在 XP 中,已经
高达 8 个位图了。接下来是一个 idEntries 的数组结构。这个结构的大小,不是始
终为 1 的一个数组,它需要根据图标数目 ( idCount ) 来确定真实的数组大小。
为了加深理解,让我们来看一个 Windows98 安装到 Windows 目录下的一个图标:
WinUpd.ico 的开头 22byte 的情况:
00 00 01 00 06 00 20 20 10 00 00 00 00 00 E8 02 00 00 66 00 00 00
这里红色部分就是保留域 idReserved,绿色部分是资源类型 idType,紫色的是指
出这个文件包含有的图标数目 idCount,这里可以看到是一个 $6 ,表示它包含有
6 个图标。后面紧接着开始的是第一个 idEntries 数组(因为有 6 个图标,所以总
共应该有 6 个这样的 idEntries 数组)。
下面就让我们看看 idEntries 数组是怎么定义的:
tagIconDirEntry = record
bWidth:BYTE;// 图标图片的显示宽度
bHeight:BYTE;// 图标图片的显示高度
bColorCount:BYTE;// 图标图片的颜色数
bReserved:BYTE;// 保留域总是 0
wPlanes:WORD;// 图标图片的位面数
wBitCount:WORD;// 图标图片的颜色深度
dwBytesInRes:DWORD;// 图标图片占用的数据量
dwImageOffset:DWORD; // 图标图片的开始位置
end;
这个结构是很固定的 16Byte 数据,其各自的含义上面已经标出来了。由于同一
个文件中的每一个图标都有一个这样的结构,所以它实际上指的是单个图标的具
体信息。
不知道为什么,Microsoft 从来没有正式文档对上面的结构定义做过声明,John _
Hornick 在 95 年为 VC 开发者写的唯一的一个描述,成了目前所有对图标感兴趣
的开发者的圣经。因为从我的观点看来,上面结构中的一些定义一直保持着它最
初设计者的最原始的思想,Borland 公司在自己的 Win32 开发环境中跳出 MS 的
约束,自己定义了一个可以和 Canvas 共存的图标类 -- TIconImage ,从而注
定了 Borland 公司将使用自己的方式解释图标。
在后面我们会看到,tagIconDirEntry 一直不能被 MS 的核心 API 吸收为正式成
员,除了其中的 dwBytesInRes 和 dwImageOffset 2 个成员以外, 其他成员基本
没有被使用,而这 2 个成员也是作为了 MS 文件读写 API 的用途。因此,正如MS
自己所说的那样,图标文件是 Shell 的成员,只在外壳存在的时候才有效。 (之一)
正如我们上面讲到的那样,图标是 Shell 的成员,Shell 在读文件时,是根据扩展
名来确定怎么解释这个文件的。当遇到 ico 文件时,它会读该文件的第二个 word
字节,以确定资源类型是否是 $0001 或 $0002 ,得到确定以后,进一步读取第
三个 word 字节,以便为 idEntries 分配足够的内存:
idEntries 的内存分配总量=tagIconDir.idCount * SizeOf(tagIconDirEntry)
由于 tagIconDirEntry 始终是 16Byte ,所以分配的内存数,只与 idCount 有关。
以此为基础,我们可以读到连续的多个 tagIconDirEntry 内容,从而确定每个图标
图片的开始位置( dwImageOffset )和包含的信息总量( dwBytesInRes )。
得到了图标图片的开始位置,就可以读取这个图片的内容了。图标图片,实际上
就是位图格式的图片。继续用上面例子中的 WinUpd.ico 图标为例,从第 7 个 Byte
开始,是第一个 idEntries 数组:20 20 10 00 00 00 00 00 E8 02 00 00 66 00 00 00
在这个数组中,我们可以找到 dwBytesInRes=$000002E8 和 dwImageOffset=
$00000066 。也就是说,从文件开头算起的第 $66 字节开始,到 $34E 的内容,
是第一个图标图片的全部内容。这个内容就是一个标准的位图格式,为了与正式
的位图格式有所区别,我们称它为 tagIconImage (注意别和 Borland 的混淆):
tagIconImage = record
icHeader:TBitmapInfoHeader;
icColors:array[0..0]of TRGBQuad;
icXOR:array[0..0]of BYTE;
icAND:array[0..0]of BYTE;
end;
从上面的结构我们会发现 icXOR 和 icAND 2 个成员。普通的位图信息里是没有这
2 个成员的。它们代表了什么?
没错,猜都可以猜到,这是 2 个位图像素信息。大家知道,图标在被显示时,是
利用遮罩方法将 2 副位图在同一个位置显示才产生任意轮廓的,先使用 XOR 位
图抠出需要显示的区域,然后再在抠出的区域中显示出需要显示的图形。由于这
个缘故,图标的位图格式中的位图信息头 ( TBitmapInfoHeader ) 是 2 个位图共用
的。它与普通位图头信息最大的不同是 TBitmapInfoHeader.biHeight 成员,显然
它是 2 副位图高度的总和。由于我不打算在这里细说位图格式,有关位图的知识
请参见《位图文件格式研究》。在下面的一篇里,我将利用上面介绍的知识,直
接按这个 ICON 的格式规范,利用一幅真彩位图,组装出一个自己的图标。 (之二)
基于对 Windows 绘图 API 的研究,我们可以得到一个基本的事实,那就是最底层
的绘图操作 DrawDIB中。DrawDibDraw 是 Windows 所有位图绘图最直接的调用
函数,它本身只需要位图信息头(TBitmapInfoHeader)。深入区分调色板模式和
真彩模式以后,我们可以认识到,Windows 只要从位图信息头中获取信息就足够
了,它借以解释在其后出现的数据应该如何处理。如果是调色板模式,其后的数
据,包含有调色板和像素点颜色索引,如果是真彩色,其后的数据直接就是像素
点的 RGB 颜色值。
知道了这个情况,我们可以简单地把上面提到的图标图形结构(tagIconImage)
理解为位图信息(tagBITMAPINFO)就对了。这 2 个结构,最初都是对调色板位
图进行的数据描述,到了真彩色年代时,MS 直接把调色板占用的位置也挪做像素
描述了。这样,一个基于真彩色的位图描述,就变得异常简单了,我们根本不需
要真的去画一幅图,而只需要对关键数据进行程序填充就可以让 Windows 工作
得很好。
下面的代码,直接按 Icon 格式的要求,把一个只要尺寸不大于 255 x 255 像素
的任意真彩位图,封装成标准图标格式的真彩图标(真实的位图宽高尺寸保持不
变,所以可以做出最大 255 x 255 的真彩图标来)。代码分成 2 个函数,把取得
的位图文件,首先送入函数 CheckBMP(const MS:TStream):Boolean 进行位图格式
检查,这里主要检查位图的宽、高尺寸和有无压缩,检查通过的话,顺便对图标
格式需要的基本数据进行填充。这个格式,我简化为 IconHand 数组。然后使用
函数 CoalitionICO(var MS:TMemoryStream):Boolean 进行正式的位图填充。
这个生成真彩色图标的函数不使用常见的绘图做法,它的代码如下:
function CoalitionICO(var MS:TMemoryStream): Boolean;
var
M1,M2:TMemoryStream;
Size:Longint;
FValue:DWord;
begin
M1:=TMemoryStream.Create;
M2:=TMemoryStream.Create;
Size:=MS.Size-14;
MS.Position:=14;
try
M1.SetSize(Size);
MS.Read(M1.Memory^,Size);
M2.SetSize(Size-40);
FillChar(M2.Memory^,Size-40,0);
M2.Position:=0;
FValue:=IconHand[7]*2;
M1.Seek(8,soFromBeginning);
M1.Write(FValue,4);
FValue:=M2.Size*2;
M1.Seek(20,soFromBeginning);
M1.Write(FValue,4);
M1.Position:=0;
MS.SetSize(0);
MS.Write(IconHand,22);
MS.Write(M1.Memory^,M1.Size);
MS.Write(M2.Memory^,M2.Size);
Result:=True;
finally
FreeAndNil(M1);
FreeAndNil(M2);
end;
end;
完整的源代码,请参见附件,里面同时附有编译完成的 exe 文件和几个演
示用的图片。本程序产生的真彩图标,对任何 Win32 开发工具都兼容。
=================================================
补充一个另外的做法是:
uses CommCtrl;
procedure BitmapToIcon(Bitmap:TBitmap;IconWidth,IconHeight:Integer;IconFileName:string='');
// 使用时,最好先把位图处理到合适的尺寸后进行转换
var
ImgListHandle,
IconHandle:THandle;
n: Integer;
Icon:TIcon;
begin
if (Bitmap.Width<>IconWidth) or (Bitmap.Height<>IconHeight) then
begin
// 这里添加一些缩放原始图形到合适尺寸的代码...(未完成)
end;
ImgListHandle:= ImageList_Create(IconWidth,IconHeight,ILC_COLOR32,1,1);
try
n := ImageList_Add(ImgListHandle,Bitmap.Handle,0);
IconHandle:= ImageList_GetIcon(ImgListHandle,n,ILD_NORMAL);
if (IconHandle<>0) and (IconFileName<>'') then
begin
// 这里输出为标准的图标文件保存
if lowercase(Copy(IconFileName,Length(IconFileName)-3,4))<>'.ico' then
IconFileName:=IconFileName+'.ico';
Icon:=TIcon.Create;
Icon.Handle:=IconHandle;
Icon.SaveToFile(IconFileName);
Icon.Free;
end;
finally
ImageList_Destroy(ImgListHandle);
end;
end;
使用这个函数导出图标,一定要为Delphi打上我下文提供的图标修正补丁。
(之三)