深入了解图标,使用WIC对Windows Vista图标进行解码


2008年10月24日 编辑:Vista之家 - vista123.com 人气: 评论:0

Vista之家www.vista123.com):深入了解图标,使用 WIC 对 Windows Vista 图标进行解码

【本文引自《MSDN》杂志2008年第6期《使用 C++ 进行 Windows 开发:使用 WIC 对 Windows Vista 图标进行解码》】在 4 月份发行的《MSDN® 杂志》中,我介绍了 Windows® 图像处理组件 (WIC),向您展示了如何使用它对不同的图像格式进行编码和解码。本月,我将介绍如何通过编写自己的编解码器来扩展 WIC,并着重介绍开发适用于 Windows 图标格式的备用解码器的初始步骤。但首先,请允许我介绍一下编写编解码器的过程。

在大多数情况下,编写编解码器都非常简单。支持对特定图像格式进行编码的编解码器必须提供一个用于实现 IWICBitmapEncoder 接口的 COM 类。同样,支持对特定图像格式进行解码的编解码器还必须提供一个用于实现 IWICBitmapDecoder 接口的 COM 类。它们是同一类接口,供借助 WIC 处理图像的开发人员使用。实际上,编解码器的 COM 类在进程内加载,可以通过开发人员的应用程序代码直接访问。这样就提供了一种非常有效的抽象方法,使用者可实现高效的图像处理。

编码器还必须实现 IWICBitmapFrameEncode 接口,从而为图像的各个帧提供多种编码方法。即使图像格式并不支持多个帧,也需要使用 IWICBitmapFrameEncode 接口来表示图像像素。同样,解码器还必须实现 IWICBitmapFrameDecode 接口,从而为图像的各个帧提供多种解码方法。

编写好编解码器之后,使用者即可直接使用 CoCreateInstance 函数来创建它,但这样做并没有多大意义。要真正实现编解码器与 WIC 的集成,还需要执行另外几个步骤。尤其是,编码器和解码器必须在注册表中包含其他元数据,用以描述它们的功能。您还必须对包含编码器或解码器的所有 DLL 进行签名。借助元数据,WIC 可以在尝试为特定的图像格式查找合适的编码器,或为特定的图像文件查找合适的解码器时,确定编解码器是否提供了必要功能。由于使用者接收的特定实现可能在运行时确定,所以 WIC 仅允许已签名的编解码器参与发现和仲裁进程,以此尝试为使用者提供一定的保障。

了解一般概念后,现在让我们来看一个具体的示例。我将重点介绍如何实现适用于 Windows 图标格式的备用解码器。选用图标格式的原因有两个。首先,Windows Vista ® 引入了对图标格式的更新,这本身就非常值得注意;稍后我将深入介绍这些更改。其次,Windows Vista 附带的内置解码器实际上并不支持更新后的图标格式。

Windows 图标发展史

Windows 经历了多次蜕变,图标格式也同样有着自己的发展历史。在 16 位 Windows 操作系统刚刚问世时,仅支持设备相关位图 (DDB) 格式。这种位图指定了宽度和高度(以像素为单位),还指定了一个表,用于将像素值映射到特定设备调色板中的条目。设备不同,支持的分辨率和拥有的着色功能自然也不同,因此,要在不同设备之间移动这些位图并不容易。但由于 DDB 效率很高,所以至今仍然沿用它来执行某些操作。设备无关位图 (DIB) 格式的引入解决了 DDB 中存在的所有问题。这种位图本身附带颜色表,独立于任何特定设备。这样,位图的像素就会映射到位图的颜色表,此表使用红、绿、蓝 (RGB) 颜色值明确定义像素颜色。

当然,图标格式可以使用 DIB 格式,但存在一个问题。DIB 已经使用 Alpha 通道来表示透明度。当时的硬件也不允许对每个像素添加一个额外的字节,但 Alpha 通道却需要对每个像素使用一个额外的字节。这就需要改用其他解决方案。DIB 将在色位图之后附加一个额外的位图,此位图是一个有效的位掩码,用于标识色位图中的透明像素和不透明像素。通过使用 BitBlt 函数传输位块,可以轻松地使用布尔运算将位掩码中的位与目标设备表面相结合;而且此方法效率很高,这一点尤为重要。

图标格式本身包含多个 DIB 结构,每个结构都有与其相关的位掩码,可获得不同大小。这样,单个图标即可针对不同方案提供图像。例如,如果在桌面上显示图标,将使用 32×32 像素 DIB。如果在窗口的标题栏中显示图标,将使用 16×16 像素 DIB。此外,图标格式也需要符合不同的显示功能,因此还支持带有不同图标尺寸的颜色表的其他 DIB。

最后,图标不再使用颜色表,而是直接针对每像素存储 24 位 (bpp),其中红、绿、蓝色值各 8 位。这样可以显示色彩鲜明的图像,但由于仍然使用位掩码来确定透明度,所以边缘经常非常粗糙。幸运的是,图标位图通常比较小,因此这实际上并不是什么大问题。

在开发 Windows XP 期间,Microsoft 希望引入质量更好的较大图标,但指定透明度时所用的位掩码方法却无法达到此目的。此时需要一个 Alpha 通道,使每个像素都能够达到任何透明度级别,从而实现阴影等效果。在 Windows XP 中,图标开始使用 32bpp,其中容纳了 Alpha 通道,通道中的每个像素都可以指定自己所需的透明度级别。为实现兼容,这些图标仍然包含位掩码,但是如果旧版应用程序没有使用 Alpha 通道,而是误用了位掩码,则将导致图标的轮廓明显参差不齐,并且没有阴影效果和图标设计人员希望实现的其他视觉效果。不过,针对 Windows XP 正确编写的应用程序则可包括精美的 32bpp 图标。Windows XP 也已经开始进一步提升较大图标(高达 48×48 像素),从而增强视觉效果。

在开发 Windows Vista 期间,Microsoft 意识到已无法延续在 Windows XP 中制定的发展趋势。虽然 DIB 格式在运行时效率很高,但随着位图越来越大,它们占用的磁盘和内存空间也越来越大。计算能力的提高意味着可以使用功能更加强大的图像格式,因此 Microsoft 选择了 PNG。Windows Vista 中的图标可以嵌入 PNG 图像而不是 DIB 图像,而 PNG 图像现已广泛应用于 Windows 中,它允许图标使用精美的 32bpp 提供大小为 256×256 像素的位图,而空间需求只是 DIB 的一小部分。图 1 显示了 Windows Vista 中的部分图标。

Windows Vista 在图标中使用这些高质量的 PNG 图像,使用户产生了以下错觉:图标是矢量图像,可以瞬间从 16×16 像素直接放大到 256×256 像素。若要查看整个过程,请打开 Windows 资源管理器,按住 Ctrl 键并滚动鼠标滚轮。此效果令人印象深刻。当然,Windows Vista 的功能不仅限于此,它还可以创建带有文件夹内容预览效果的文件夹图标,但这已经超出了图标格式本身的范围,而是特定于 Windows 资源管理器的一项功能。

Windows 图标格式

图 2 中描述了图标格式的结构。第一个 WORD(两个字节)始终为 0,第二个 WORD 始终为 1;可将后者视为资源类型。图标和光标的格式凑巧非常相似。区分两者的方法是检查第二个 WORD,如果始终为 1,则是图标,如果始终为 2,则是光标。第三个 WORD 指示图标中包含的图像数。

紧跟这三个 WORD 值之后的是一系列用于描述图像的结构。此子结构中真正有用的值只有图像大小和图像偏移值。其余的值只起提示作用,并且不能保证始终正确填充,因此并不可靠。在大多数情况下,忽略这些值即可;通过检查 DIB 或 PNG 图像位本身,即可了解所需的任何与图像有关的信息。图像大小指示图像占用的字节数。图像偏移指示文件的开始部分与图像的起始位之间的偏移量。

基本解码器

现在,我们来了解一下如何开发适用于图标的基本 WIC 解码器。大多数情况下,这只是如何适当实现 IWICBitmapDecoder 接口的问题。不过,您需要了解一下有关下列八个最重要的接口方法的一些信息:QueryCapability、Initialize、GetFrame、GetContainerFormat、GetDecoderInfo、CreateComponentInfo、GetFrameCount 和 GetFrame。

QueryCapability 方法用于检查给定流并确定它是否能够解码图像。如果能,则指示它能解码图像中的所有帧 (WIC­BitmapDecoderCapabilityCanDecodeAllImages),还是只能解码其中的部分帧 (WICBitmap-DecoderCapabilityCanDecodeSomeImages)。此外,还可以指定其他功能标志。切记在返回前还原流位置。您可以通过 QueryCapability 读取我在上一部分中介绍的图标格式的基础结构,从而确保该结构是解码器预期的结构。您可能还希望枚举图标中的图像,以确保可以进行加载。

Initialize 方法提供可供解码器读取的流。根据所提供的选项,它可能会立即加载所有帧 (WICDecodeMetadataCacheOnLoad),也可能仅在使用 GetFrame 方法特别要求 (WICDecode­MetadataCacheOnDemand) 时进行加载。如果属于后者,您需要一直持有流,直到所有帧加载完毕。

以下是我执行的一些操作,可能会对您有所帮助:我发现,可以根据图标中每个图像的图像大小和偏移创建子流,然后将这些子流传递到 IWICBitmapFrameDecode 接口实现中,这很有用。幸运的是,借助我在 2008 年 4 月一期的专栏中介绍的 WIC 图像工厂对象,可以非常轻松地完成此操作。图 3 显示了我借助 WIC 流对象创建子流时使用的 CreateSubStream 帮助程序函数。

创建子流 

HRESULT CreateSubStream(IWICImagingFactory* factory,
IStream* stream,
ULONGLONG offset,
ULONGLONG maxSize,
IStream** subStream)
{
ASSERT(0 != factory);
ASSERT(0 != stream);
ASSERT(0 != subStream);

*subStream = 0;

CComPtr<IWICStream> wicStream;
HR(factory->CreateStream(&wicStream));

ULARGE_INTEGER quadOffset = { 0 };
quadOffset.QuadPart = offset;
ULARGE_INTEGER quadMaxSize = { 0 };
quadMaxSize.QuadPart = maxSize;

HR(wicStream->InitializeFromIStreamRegion(stream,
quadOffset,
quadMaxSize));

return wicStream.QueryInterface(subStream);
}


GetContainerFormat 方法指示解码器表示的图像格式。在这种特例中,我们可以只返回内置图标容器格式标识符 (GUID_ContainerFormatIco)。

GetDecoderInfo 方法可返回用于描述解码器的 IWICBitmapDecoderInfo 接口。该接口并不特别复杂,但却相当大,实现起来肯定会很繁琐。值得庆幸的是,它实际上只用于抽象存储在注册表中的编解码器元数据。WIC API 提供了一个库存实现,可供您使用。同样,此实现由图像工厂对象提供。图 4 中提供了一个典型的 GetDecoderInfo 实现。您只需要将解码器的 CLSID 传递到 CreateComponentInfo 方法,该方法将创建一个对象,并使用存储在注册表中的解码器元数据进行填充。

 实现 GetDecoderInfo

HRESULT Kerr::IconDecoder::GetDecoderInfo(__out IWICBitmapDecoderInfo** decoderInfo)
{
if (0 == m_imagingFactory)
{
return E_UNEXPECTED;
}

if (0 == decoderInfo)
{
return E_POINTER;
}

*decoderInfo = 0;

CComPtr<IWICComponentInfo> componentInfo;

HR(m_imagingFactory->CreateComponentInfo(__uuidof(IconDecoder),
&componentInfo));

return componentInfo.QueryInterface(decoderInfo);
}


如您所料,GetFrameCount 方法可返回图像中正在解码的帧数。

最后是 GetFrame 方法,此方法可返回代表图像中特定帧的 IWIC­Bitmap­FrameDecode 接口。

至此,我已经介绍完了如何实现 IWICBitmapDecoder 接口。实现此接口后,您就能够使用 CoCreateInstance 创建解码器实例了,但只需要再做一点工作,您就可以将编解码器完全集成到由 WIC 提供的发现和仲裁框架中,而这样一来,应用程序将无需事先了解编解码器,即可自动利用它。

应用程序可通过多种方法使用特定计算机上的可用编解码器。对于初学者,应用程序可以书面枚举所有的编码器和解码器,从中找出要使用的一项,或者为用户提供选择首选编解码器的选项。图 5 提供了如何枚举编码器的示例。请注意,生成的 IWICBitmapEncoderInfo 接口提供了大量有关每个编码器功能的有用信息。

枚举编码器

HRESULT EnumerateEncoders()
{
CComPtr<IWICImagingFactory> imagingFactory;
HR(imagingFactory.CoCreateInstance(CLSID_WICImagingFactory));

CComPtr<IEnumUnknown> enumerator;

HR(imagingFactory->CreateComponentEnumerator(WICEncoder,
WICComponentEnumerateDefault, &enumerator));

CComPtr<IUnknown> unknown;

while (S_OK == enumerator->Next(1,
&unknown,
0)) // ignored
{
CComQIPtr<IWICBitmapEncoderInfo> info(unknown);
unknown.Release();

if (info)
{
CLSID clsid = { 0 };
HR(info->GetCLSID(&clsid));

UINT count = 0;

HR(info->GetFriendlyName(0, // no buffer length
0, // no buffer
&count));

CString friendlyName;

HR(info->GetFriendlyName(count,
friendlyName.GetBufferSetLength(count),
&count));

friendlyName.ReleaseBuffer(count - 1);

TRACE(L"%s\n", friendlyName);
}
}

return S_OK;
}

或者,应用程序可能要依赖 WIC 来自动选择合适的编解码器。正如我在上期专栏中所述,图像工厂提供了 CreateDecoder 方法,您可以使用此方法为特定图像格式创建解码器而不必考虑其实现。CreateDecoderFromStream 方法更加强大,它可以检查流并将检查结果与计算机上可用解码器中的注册信息进行比较,自动为您选择合适的解码器。

未来计划

本月内容到此结束。尽管 Windows 图像处理组件不完全支持 Windows Vista 中新的高分辨率图标格式,但您可以使用自己的编解码器来扩展 WIC,从而对其进行处理。综上所述,WIC 可能不太适合解码图标图像,因为并没有一种明确的方法区分具有不同分辨率的不同图像帧,但是练习编写解码器有助于了解 WIC 的设计原理。在大多数情况下,LoadImage 函数仍然是加载图标图像的最佳方法。

请将您的问题和意见发送至 mmwincpp@microsoft.com。

Kenny Kerr 是一位专门从事 Windows 软件开发的软件专家。他热衷于撰写有关编程和软件设计的文章,并向开发人员讲授与此有关的知识。您可通过 weblogs.asp.net/kennykerr 与 Kenny 联系。

Vista之家www.vista123.com),爱上网,爱上Vista123.com

      

热门文章

软媒旗下软件: 魔方 | 旗鱼浏览器(极速核心) | 闪游浏览器 | 软媒时间 | 酷点桌面 | Win7优化大师 | Win8优化大师 | Vista优化大师 | Windows一键还原 | 软媒手机APP应用

软媒旗下网站 IT之家 | 辣品 | IT圈 | 6655网址之家 | Win10之家 | iPhone之家 | Win8之家 | Win7之家 | Vista之家