=============================================================================================
声明【1】:这篇文章中的代码在写ICO文件时的方法并不够完善和准确。因此保存ICO文件时会有些小问题。目前看来,有两种方法,可能需要把微软的C++的范例源码借鉴并引入到C#项目中,另一种是把该范例改写为一个DLL,然后用PInvoke方式调用。总之,不管采用哪种解决方法,将会改变本文中的技术路线,因为代码修改量比较大,细节也非常繁琐,因此这不是一个短时间内可以完成的任务,暂且放置这样一个声明于此,以提醒借鉴者能注意(这个声明的原因是,一是对自己,本着求是和追求完美的态度,我不能容忍自己的技术日志有缺陷乃至关键性的逻辑错误;二是对他人,本着对潜在读者负责的态度,尽可能不对他人造成误解和负面影响)。我将在时间充裕的时候尽可能快的解决这个问题(有问题的代码暂时放置于此)。
------ hoodlum1980 于 2008年9月26日20:19:02。
============================================================================================
=============================================================================================
声明【2】:时隔两年后,我重新研究了一下ICO文件格式,并修改了代码。因此声明 1 作废。具体细节已补充到原文结尾处。
------ hoodlum1980 于 2010年8月23日
============================================================================================
最近发现我的自动关机程序的图标有点丑陋和粗糙,(是我自己制作的一个qq的老鼠头像),放在桌面上和其他xp图标一比不免相形见绌。于是突发奇想想把自动关机程序的图标换成系统的关机图标,于是我就想导出这个图标。这个图标的位置位于system32目录下的shell32.dll中,vs.net本身就可以直接打开一个模块的资源,但是我发现由于这个dll的图标太多,并且vs.net并没有提供一个方便的预览功能,所以一个个打开查看还是太慢了。于是想起这种图标导出工具很多,但是下载了几个下来一看却让我非常不爽和郁闷。因为这么简单的功能,我下了4~5个竟然没有一个免费版本,都是试用版并且注册要收费的。虽然它们安装以后能用,但是都提示你只能用10次以及经常弹出注册之类的提示,让我感到很生气。所以我干脆自己实现一个简单的。于是决定用开发一个自己用,并且把源码公开。其实很久前就有人给我看过这类工具,但是由于我觉得vs.net就能打开和导出了,再者也有人做了,所以我一直没有关注过,只是看了下相关API函数。
首先是一个应用程序截图:
把其他格式图转存为ICO格式:
我们要了解图标导出的关键API函数是:
HICON ExtractIconEx( LPCTSTR lpszFile, int nIconIndex, HICON FAR* phiconLarge, HICON FAR* phiconSmall, UINT nIcons );
这个函数的参数和用法我就不多说了,在sdk文档中写的很详细。总结一下,这个函数可以获取一组图标(填充句柄到一个句柄数组),可以获取模块资源中的图标个数。好了,在c#中我们还是要用到我们熟悉的P/Invoke将它在.Net中声明:
/// <summary>
/// 从某个索引开始的所有图标导出到一个数组,图标使用后必须调用destroyIcon!其他用法如下:
/// 获取图标数目:nIconIndex=-1,两个数组指针为NULL
/// 获取指定图标:nIconIndex为指定图标资源ID的负数,例如nIconIndex为-3时获取ID为3的图标!
/// </summary>
/// <param name="lpszFile">[in] Pointer to a null-terminated string specifying the name of an executable file, DLL, or icon file from which icons will be extracted. </param>
/// <param name="nIconIndex">[in] Specifies the zero-based index of the first icon to extract. </param>
/// <param name="phiconLarge">Pointer to an array of icon handles that receives handles to the large icons extracted from the file. If this parameter is NULL, no large icons are extracted from the file. </param>
/// <param name="phiconSmall"></param>
/// <param name="nIcons">[in] Specifies the number of icons to extract from the file. </param>
/// <returns>当获取数目时,返回包含的图标数目,其他情况返回一共导出了多少个图标</returns>
[DllImport("shell32.dll")]
public static extern uint ExtractIconEx(
string lpszFile,
int nIconIndex,
IntPtr[] phiconLarge, //[out]
IntPtr[] phiconSmall, //[out]
uint nIcons
);
这里再次强调一点:导出的图标是非托管资源,并且操作系统要求开发者必须自己调用destroyicon释放它们。所以实际上我们把它们到托管环境中创建一个克隆的对象,这样我们就能够马上释放掉它们。
核心功能介绍完,我们再看界面部分需要额外讲解的部分。我使用ListView显示图标,最初我把icons添加到listview的LargeImageList,但是这样以后的绘制图标被当作image,并且缩放到统一的尺寸,导致显示出黑色锯齿,失去了图标的精美设计外观。所以我考虑根本原因在于绘制时使用了DrawImage,而不是DrawIcon,导致外观异样。所以我决定改变ListView默认的绘制。所以我定义了IconListView类,从ListView继承。由于.net2.0增加泛型,所以我们可以很方便给我们自定义的iconlistview增加一个iconlist成员:
/// <summary>
/// 用大图标数组代替ImageList对象,以防止图标产生毛边!
/// </summary>
private List<Icon> m_IconList;
注意:改变默认绘制时必须设置ListView.OwnerDraw=true,否则系统忽略你的覆盖方法。
修改方法和修改ComboBox的绘制行为非常类似。(不同处在于在ListView中我没有发现可以复写的OnMeasureItem方法)
/// <summary>
/// 覆盖绘制Item方法!
/// DrawItem Event: Occurs when a ListView is drawn and the OwnerDraw property is set to true. ***
/// 注意e.Bounds已经刨除了Text区域,所以不要在这里试图绘制Item文本
/// </summary>
/// <param name="e"></param>
protected override void OnDrawItem(DrawListViewItemEventArgs e)
{
if (this.View == View.LargeIcon)
{
ListViewItem item = e.Item;
//如果没有图标,则采用默认行为绘制!
if (this.m_IconList == null)
return;
//绘制图标的横坐标位置!
int x_icon = e.Bounds.Left + e.Bounds.Width / 2 - 16;
//当item被选中时绘制背景
if (item.Selected)
{
this.m_Brush.Color = Color.FromKnownColor(KnownColor.Desktop);
e.Graphics.FillRectangle(this.m_Brush, e.Bounds); //图标背景32*32
}
//绘制图标
e.Graphics.DrawIcon(this.m_IconList[e.ItemIndex], x_icon, e.Bounds.Top);
}
else
base.OnDrawItem(e);
}
最后要注意的是,在OnDrawItem的参数e中的Bounds指的是什么,指的是Item在整个容器控件中的占据矩形(容器客户去坐标),所以它的Top会随着换行而递增。
同时系统还刨除了用于绘制Text的区域。所以在Bounds下方还有一块矩形区域是用于显示Item的文本,我们无法在这里绘制内容。
由于没有测量Item的方法,所以要改变Item的Bounds大小时比较费力(没有找到适合的属性或方法),这里我使用设置LargeImageList的ImageSize属性来影响Item的Bounds大小。
另外一点是绘制时我们必须还要注意item.selected属性对外观的影响,给用户提供足够明显的视觉反馈。
---------------------------------------------------------------------------------------------
在经过以上过程以后,正当我对自己所做的一切感到满意时,忽然发现一个非常严重的问题,.net类库中的image.save方法保存icon格式时,竟然是假的!因为当选择imageformat为icon时,从文件头的标志位可以看出实际保存成的格式是png格式!(只要吧后缀改成png即可打开)。而用icon.save(stream)的方法,又会丢失真彩色。(据我根据后来的努力分析,也许因为icon.save使用索引格式(16色或者256色)保存而非32bpp的格式,仅仅猜测)。
因此现在我们能正确显示出图标了,但是却无法保存为有效的ico文件!这个麻烦可就大了!所以我花了很多时间来分析ico文件格式,参考了很多网上资料,遗憾的是基本上没什么特别完整的资料。这里,我主要参考了微软的iconpro工具的源码,以及以下文档:
文档1:
Icons in Win32
文档2:(我发表完这篇日志才发现这个文档的内容和工作几乎和我做的几乎一样。。。!@#¥%……&)
Icon Browser: An Exercise in Resource Management
同时也用ultraedit来结合实际有效的ico文件的字节内容进行辅助分析。
终于对ico文件格式有了初步的认识,并且也保存成为有效的ico文件了。这里对ico文件格式不做具体说明,因为细节太多和繁杂,不是短小篇幅可以覆盖的。也不是这篇文章的重点。仅仅就简单说说比较重要的几点:
下图是一个16*16像素,32bpp的ICO文件(仅包含一个image)的字节数据:浅黄色背景的16个字节就是第一个Dir Entry。由于只有一个entry,所以它后面紧跟的就是第一个ImageData的BitmapInfoHeader结构(40 bytes)。文件中不包含RGBQUAD(色板)部分。BitmapInfoHeader后面直接就是XOR节(像素实际颜色,由于bpp=32,所以每个像素占据4个字节,注意在扫描行存储顺序是从图像最下方一行开始,向上方到顶部结束)。最后是AND Mask段(指示图标的透明部分)。
1.icon文件的格式是:
@ico File Header(可把前6个字节看作文件头)
reserved:(0x00,0x00)
iconType:(0x01,0x00)
imageCount:
@dir entry数组
entry[0], //16 bytes each entry
entry[1],
...
entry[imageCount-1]
dir entry可以看作是目录,每个条目中包含有指向Imagedata文件地址。
@Image Data数组
imageData[0]
BitmapInfoHeader //size=40,width=宽度,height=2*高度,colorCount,planes=1,bitCount=(bpp)。其他为0.
RGBQUAD数组 //自带palette
RGB[0] //4 bytes each color,RGB加一个保留位
RGB[1]
...
RGB[colorCount-1]
XOR bytes //像素数据,索引或者实际颜色数据
AND mask bytes //1bpp(和image一样大的1位深位图(只有黑白两色))?
imageData[1]
imageData[2]
...
imageData[imageCount-1]
image data是图像,有bitmap info header, RGBQUAD, XOR section, AND mask section组成。
2.
bitmap info header中,比较特殊的是height为图标高度的2倍(xor mask图高度+and mask图高度)。这里可以看到在逻辑上image的xor mask部分和and mask部分是上下排列的,因此宽度是image原始宽度,高度是2倍高度。如下图所示:在XOR部分显示出原始图像数据,可以理解为Photoshop中的一个普通RGB图层(layer),在AND mask部分可简单理解为在Photoshop中的应用在该图层上的蒙版(mask)概念。
在系统绘制icon时,先用AND Mask图像用与操作复制到屏幕上,可以从上图中看出,这时图标透明像素(mask中的白色部分)不受影响,而不透明部分都变成0(黑色),然后再用XOR mask图像用异或操作复制到屏幕,异或的特点是相同为0,相异为1,
0 ^ 0 =0
0 ^ 1 =1
1 ^ 1 =0
因此,XOR 图像的黑色部分将保留现有颜色,即实现了透明效果,而不透明部分和0异或,因此这部分XOR图像保持原来颜色被复制到屏幕。
RGBQUAD是色板,它的颜色个数由dir entry中的bColorCount指定,当为0时表示没有色板(例如bpp为32的真彩色图标),图标类似无压缩的BMP位图,每个像素的数据表示实际颜色。不为0时表示具有色板,这时图标实际是索引图像,像素数据是一个索引。色板中每个颜色4字节,分别是R,G,B,0.
XOR section:它是实际的像素数据。在32bpp的图标中就是类似位图的实际颜色数据。在索引格式时,是一个在色板中的颜色索引。像素的bpp(bits per pixel)由dir entry以及bitmap info header中的biBitCount指定。(这两者都包含同一bpp信息,是冗余的。)
AND mask section的bpp文档中说为1,如果为1则图标的该像素位置透明,为0表示不透明。
=====================================================================
以下补充:由 hoodlum1980 于 2010-8-23 16:36:22 提供。
=====================================================================
【关于声明1的补充】在本文开头,我曾经补充一个说明,因为那时由于对ICO文件格式研究的不够充分,所以写ICO文件的代码存在一定缺陷和错误,所以保存后的图标在VS中打开看是有问题的。两年后我再次重新认真研究了以下ICO文件格式。应该说由于没有比较详尽可靠的官方文档,所以要研究ICO文件格式是难度较大的,非常消耗精力。现在我终于修正了写ICO文件中存在的问题,并把注意点补充如下:
(1)ico entry 中的dwBytesInRes 指的是图片数据的大小。包括一个BitmapInfoHeader(40 bytes),Palatte(调色板,如果有,我们是真彩色图标,没有调色板,256色,16色的图片通常会有调色板。),位图数据(图标的XOR MASK部分,对于真彩色图标来说是24bpp,DWORD对齐),蒙版数据(图标的AND MASK部分,这里是1bpp,DWORD对齐)。写ico entry的代码如下:
这里的大小不能计算错误。(在声明1中存在的问题之一是这里的计算有误)。
代码_ico_entry
private static void WriteDirEntry(Bitmap bm, BinaryWriter bw)
{
//BitmapData bmData1 = bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); //每个像素占3个字节(24位)
int bpp1 = 24; //真彩色位图的bpp
int bpp2 = 1; //蒙版的bpp
int stride1 = (bm.Width * bpp1 + 31) / 32 * 4;
int stride2 = (bm.Width * bpp2 + 31) / 32 * 4;
byte bwidth = (byte)bm.Width;
byte bheight = (byte)bm.Height;
byte bcolorcount = (byte)0; //对于真彩图标32bpp来说,这个字段为0!
byte breserved = (byte)0;
Int16 wplanes = 1;
Int16 wbitcount = (Int16)bpp1; //8bit per pixel, 16 colors!
Int32 dwbytesInRes = 40 + (stride1 + stride2)*bm.Height; //40是BitmapInfoHeader的尺寸
Int32 dwImageOffset = 22; //???紧跟着就是第一个图像的入口!就是下一个字节的文件地址 6bytes header+ 16bytes entry
//解锁
//bm.UnlockBits(bmData1);
//写入
bw.Write(bwidth);
bw.Write(bheight);
bw.Write(bcolorcount);
bw.Write(breserved);
bw.Write(wplanes);
bw.Write(wbitcount);
bw.Write(dwbytesInRes);
bw.Write(dwImageOffset);
IconFileHelper.WriteIconImage(bm,bw);
}
(2)是AND MASK部分,即表示图标哪些部分是透明的。也是要已DWORD(32bits)对齐的。它的bpp是1,即每8个像素用一个字节来表示。因此它的扫描行宽度是: bm.Width * 1 + 31/32 * 4; 声明1中描述的问题之二,是对这部分数据我原来没有做DWORD对齐,所以也是有问题的。
有关写入AND MASK的代码如下:
代码_写入ANDMASK
//写入AND Mask , 1 bpp
stride = (1 * bm.Width + 31) / 32 * 4; //and mask 以DWORD对齐后的扫描行宽度
byte[] line = new byte[stride];
for (int j = bm.Height-1; j >=0; j--)
{
for (int i = 0; i < stride; i++)
line[i] = 0;
//处理当前扫描行
for (int i = 0 ; i < bm.Width; i++)
{
Color color = bm.GetPixel(i, j);
int colorsum = color.R + color.G + color.B;
if (colorsum == 0)
{
//在位图中是黑色说明该像素应该是透明的
line[i/8] |= (byte)(1<< (7 - (i & 0x07))); //i&7: 相当于i%8
}
}
bw.Write(line, 0, stride);
}
修正以上两个问题以后,导出的图标可以在IDE中打开,显示正常,可以正常使用了!
---------------------------------------------------------------------------------------------
最后是项目源代码(build by vs.net 2005 trial)的下载链接:(已经解决声明1中存在的问题)
http://files.cnblogs.com/hoodlum1980/IconExp.rar