首页  编辑  

图标文件格式研究

Tags: /超级猛料/Format.格式,单位/File.文件格式/   Date Created:

图标文件格式研究

作者:小雨哥

常看到有人说,图标文件就是普通的位图文件。我不知道为什么这样说。其实图

标文件确实有点象位图文件,但它还是有自身的表达结构的。图标文件的开头就

是一个奇怪的格式,定义如下:

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打上我下文提供的图标修正补丁。

       (之三)

204-BmpToIco[1].rar (63.5KB)
293-Iconrev[1].rar (136.6KB)
img_18664.bmp (547.9KB)