在图形用户界面(GUI)编程中,图标文件(.ico)是一种常见的资源文件格式,它允许存储一个或多个图像,通常用于应用程序的图标。尽管GDI+提供了丰富的图形处理功能,但在处理ICO文件时,它缺少一些关键功能,比如查看单个ICO文件中包含的不同版本的图标。本文将展示如何访问并利用这些不同版本的图像。
由于这个话题非常受欢迎,决定使用C#重新编写代码。C#版本与VB版本基本相同,但它提供了更多关于图标文件内部结构的细节。
通常,图标文件包含同一图像的多个版本。这些文件通常包含图像的较小和较大版本,这样就不必调整图像大小,从而避免了分辨率的损失。
Visual Studio中的新图标构造函数对多个图像版本的支持并不理想。虽然可以指定请求的图标的高度和宽度,但无法知道应该请求什么(如果不知道文件中有什么)。为了解决这个限制,必须直接查看ICO文件,并提取想要利用的位和字节。创建了一个图表来说明图标文件的内部布局。
由于空间原因,只在上述示例中放置了两个图标,但实际上可以有任意数量的图标。
图标头是访问整个文件的关键。这个6字节的块告诉正在访问的文件中有多少个图标,以及正在访问的文件的类型(0代表位图,1代表图标)。
读取图标头之后,将知道这个图标文件中有多少个图标。然后可以安全地读取那么多的图标条目块。图标条目块不包含图像信息,它们包含图像字节的偏移量和长度。
最简单的方法是创建一个FileStream,并将文件转储到一个字节数组中。然后可以将字节数组加载到Stream对象中。以下代码将演示如何操作。
Private Function readIcoFile(ByVal filename As String) As MemoryStream
' 打开文件
Dim icoBinaryFile As New FileStream(filename, FileMode.Open, FileAccess.Read)
' 创建字节数组并读取
Dim byteArray(icoBinaryFile.Length) As Byte
icoBinaryFile.Read(byteArray, 0, icoBinaryFile.Length)
icoBinaryFile.Close()
' 用字节数组加载流
icoStream = New MemoryStream(byteArray)
icoStream.Seek(0, SeekOrigin.Begin)
' 调试,这些值应该是相同的!
Console.WriteLine("Number of bytes: " & byteArray.Length)
Console.WriteLine("Length of Stream: " & icoStream.Length)
End Function
发现最好的方法是创建类。第一个类是图标头类,它只包含它读取的元素的定义以及一个新的过程,该过程将从流中读取信息。
Private Class iconHeader
Public Reserved As Short ' 总是0
Public Type As Short ' 0=Bitmap, 1=Icon
Public Count As Short ' 图标数量
' Sub: New
' 目的:从原始ICO文件中读取6字节的头部
' 注意:Short或Int16是2个字节
Public Sub New()
Dim icoFile As New BinaryReader(icoStream)
Reserved = icoFile.ReadInt16
Type = icoFile.ReadInt16
Count = icoFile.ReadInt16
End Sub
End Class
下一步几乎是一样的,但是处理的是图标条目数据。
Private Class iconEntry
Public Width As Byte ' 图像的宽度(像素)
Public Height As Byte ' 图像的高度(像素)
Public ColorCount As Byte ' 图像中的颜色数量(如果>=8bpp则为0)
Public Reserved As Byte ' 保留(必须为0)
Public Planes As Short ' 颜色平面
Public BitCount As Short ' 每像素位数
Public BytesInRes As Integer ' 这个资源有多少字节?
Public ImageOffset As Integer ' 文件中的这个图像在哪里?
' Sub: New
' 目的:从原始ICO文件中读取16字节的头部
' 注意:Byte是1个字节
' Short或Int16是2个字节
' Integer或Int32是4个字节
Public Sub New(ByVal Index As Integer)
Dim icoFile As New BinaryReader(icoStream)
Width = icoFile.ReadByte
Height = icoFile.ReadByte
ColorCount = icoFile.ReadByte
Reserved = icoFile.ReadByte
Planes = icoFile.ReadInt16
BitCount = icoFile.ReadInt16
BytesInRes = icoFile.ReadInt32
ImageOffset = icoFile.ReadInt32
End Sub
End Class
现在有了所有的偏移量、大小、颜色信息和图像的长度。
在这里实际上是使用从头部和条目中提取的信息来构建一个图标。通过创建另一个流并使用BinaryWriter来填充数据来实现这一点。下面是一个例子:
Private Function buildIcon(ByVal index As Integer) As Icon
Dim thisIcon As iconEntry = icons(index)
' 分配图标字节数组的空间
Dim icoByteArray(thisIcon.BytesInRes) As Byte
' 创建流
Dim newIcon As New MemoryStream
Dim writer As New BinaryWriter(newIcon)
' 这个文件中只有一个图标
Dim newCount As Short = 1
' 6字节+16字节是新的偏移量
Dim newOffset As Integer = 22
Console.WriteLine("Icon Index: " & index & ", Offset: " & thisIcon.ImageOffset)
' 写文件
With writer
.Write(icoHeader.Reserved)
.Write(icoHeader.Type)
.Write(newCount)
Console.WriteLine("Header written: " & newIcon.Position)
.Write(thisIcon.Width)
.Write(thisIcon.Height)
.Write(thisIcon.ColorCount)
.Write(thisIcon.Reserved)
.Write(thisIcon.Planes)
.Write(thisIcon.BitCount)
.Write(thisIcon.BytesInRes)
.Write(newOffset)
Console.WriteLine("Image Header written: " & newIcon.Position)
' 从流中读取图标
icoStream.Seek(thisIcon.ImageOffset, SeekOrigin.Begin)
icoStream.Read(icoByteArray, 0, thisIcon.BytesInRes)
' 写出去
.Write(icoByteArray)
.Flush()
Console.WriteLine("Image written: " & newIcon.Position)
End With
' 移动到开始
newIcon.Seek(0, SeekOrigin.Begin)
Dim thisImage As Icon = New Icon(newIcon, thisIcon.Width, thisIcon.Height)
writer.Close()
Return thisImage
End Function
注意,几乎只是写回了读取的数据的副本(做了一些改动)。所做的改动是:
' 这个文件中只有一个图标
Dim newCount As Short = 1
' 正在移动图像偏移量,使其紧接在头部之后
Dim newOffset As Integer = 22
Public thisIcon As multiIcon(filename)
' 这将创建类的一个新的实例。
PictureBox1.Image = thisIcon.image(ComboBox1.SelectedIndex).ToBitmap
' 这将加载一个图标到图片框中。
PictureBox1.Image = thisIcon.findIcon(multiIcon.iconCriteria.Largest).ToBitmap()
PictureBox1.Image = thisIcon.findIcon(multiIcon.iconCriteria.Smallest).ToBitmap()
' 这将加载特定版本的图标到图片框中。
Dim size As Size
For Each size In thisIcon.sizes
ComboBox1.Items.Add(size.ToString)
Next
' 这将列出当前可用的所有图标的大小。