65.9K
CodeProject 正在变化。 阅读更多。
Home

文件资源管理库(.NET)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (26投票s)

2008年6月30日

CPOL

14分钟阅读

viewsIcon

139746

downloadIcon

2623

一个支持VS_VERSIONINFO版本资源的完整功能的.NET文件资源管理实现。

引言

有几篇关于从Windows PE可执行文件或DLL读取和写回资源的优秀文章。大多数文章专注于检索模块版本信息和修改版本信息,主要使用C++。有些文章详细介绍了光标、图标或对话框资源的相同操作。还有些文章功能有限,只能就地编辑结构。然而,目前还没有一个统一的托管.NET库可以检索和保存任何类型的资源,也没有一个库可以编辑或生成版本资源,更没有一个库对所有资源类型都具有一致的编程模型。

此实现是一个框架,它可以枚举资源并实现文件版本(VS_VERSIONINFO)、字符串(公司、版权和产品信息)、位图(RT_BITMAP)、图标(RT_GROUP_ICONRT_ICON)、对话框(RT_DIALOG)、菜单(RT_MENU)、光标(RT_GROUP_CURSORRT_CURSOR)、加速键(RT_ACCELERATOR)和SxS清单(RT_MANIFEST)资源的读写。随着时间的推移,该库通过这些资源类型得到了扩展,并且可以轻松地为所有剩余的资源类型进行补充。

背景

最初,我为dotNetInstaller Bootstrapper开源项目移植了Denis Zabavchik的C++ VerInfoLib中的版本资源实现。然后,它变得越来越大……

Using the Code

枚举资源

以下示例演示了按类型枚举资源。

string filename = Path.Combine(Environment.SystemDirectory, "atl.dll");
using (ResourceInfo vi = new ResourceInfo())
{
 vi.Load(filename);
 foreach (ResourceId type in vi.ResourceTypes)
 {
  foreach (Resource resource in vi.Resources[type])
  {
    Console.WriteLine("{0} - {1} ({2}) - {3} byte(s)",
        resource.TypeName, resource.Name, resource.Language, resource.Size);
  }
 }
}

从Windows Vista的atl.dll中,您通常会获得以下资源

MUI - 1 (1033) - 232 byte(s)
REGISTRY - 101 (1033) - 335 byte(s)
TYPELIB - 1 (1033) - 7132 byte(s)
RT_STRING - 1 (1033) - 72 byte(s)
RT_STRING - 7 (1033) - 38 byte(s)
RT_VERSION - 1 (1033) - 828 byte(s)

读取版本信息

您可以加载文件版本信息而无需枚举资源。

string filename = Path.Combine(Environment.SystemDirectory, "atl.dll");
VersionResource versionResource = new VersionResource();
versionResource.LoadFrom(filename);
Console.WriteLine("File version: {0}", versionResource.FileVersion);
StringFileInfo stringFileInfo = (StringFileInfo) versionResource["StringFileInfo"];
foreach (KeyValuePair<ResourceId, StringTableEntry> 
	versionStringTableEntry in stringFileInfo.Default.Strings)
{
 Console.WriteLine("{0} = {1}", versionStringTableEntry.Value.Key, 
			versionStringTableEntry.Value.StringValue);
}

写入版本信息

您可以将更新后的版本信息写回可执行文件。最简单的方法是加载现有的二进制资源,更新它,然后将其保存回。请注意,在内部,字符串资源存储时会额外添加一个空终止符。该库在这方面保持一致,在完成脏活累活后,始终存储带有两个空终止符的值,并仅在需要时将其附加。

string filename = Path.Combine(Environment.SystemDirectory, "atl.dll");
VersionResource versionResource = new VersionResource();
versionResource.LoadFrom(filename);
Console.WriteLine("File version: {0}", versionResource.FileVersion);
versionResource.FileVersion = "1.2.3.4";
StringFileInfo stringFileInfo = (StringFileInfo) versionResource["StringFileInfo"];
stringFileInfo["CompanyName"] = "My Company\0";
stringFileInfo["Weather"] = "Sunshine, beach weather.";
versionResource.SaveTo(filename);   

生成一个完整的版本资源头允许您将版本信息保存到没有版本信息的文件中。这更加复杂,因为您必须生成所有结构。ResourceLib使其变得容易,因为您不必担心结构大小或数据对齐。

VersionResource versionResource = new VersionResource();
versionResource.FileVersion = "1.2.3.4";
versionResource.ProductVersion = "4.5.6.7";

StringFileInfo stringFileInfo = new StringFileInfo();
versionResource[stringFileInfo.Key] = stringFileInfo;
StringTable stringFileInfoStrings = new StringTable();
stringFileInfoStrings.LanguageID = 1033;
stringFileInfoStrings.CodePage = 1200;
stringFileInfo.Strings.Add(stringFileInfoStrings.Key, stringFileInfoStrings);
stringFileInfoStrings["ProductName"] = "ResourceLib";
stringFileInfoStrings["FileDescription"] = "File updated by ResourceLib";
stringFileInfoStrings["CompanyName"] = "Vestris Inc.";
stringFileInfoStrings["LegalCopyright"] = "All Rights Reserved";
stringFileInfoStrings["Comments"] = 
	"This file has a version resource updated by ResourceLib";
stringFileInfoStrings["ProductVersion"] = versionResource.ProductVersion;

VarFileInfo varFileInfo = new VarFileInfo();
versionResource[varFileInfo.Key] = varFileInfo;
VarTable varFileInfoTranslation = new VarTable("Translation");
varFileInfo.Vars.Add(varFileInfoTranslation.Key, varFileInfoTranslation);
varFileInfoTranslation[ResourceUtil.USENGLISHLANGID] = 1300;

versionResource.SaveTo(targetFilename);

一个名为VersionResourceTests.TestDeleteAndSaveVersionResource的单元测试实现了此行为,并可在ResourceLibUnitTests项目中找到。它将Windows系统目录中的atl.dll复制到临时文件夹,删除其版本资源,生成一个新的版本资源而不复制任何数据,并更新副本。

其他资源类型

您可以以类似的方式加载和保存其他资源类型。提供对所有资源访问的类是ResourceInfo,而每个资源类(例如MenuResource)可以直接用于从可执行文件LoadFromSaveTo到同一个或另一个可执行文件。

例如,以下代码从explorer.exe加载ID为204的英文MenuResource

MenuResource menuResource = new MenuResource();
menuResource.Name = new ResourceId(204);
menuResource.Language = ResourceUtil.USENGLISHLANGID;
menuResource.LoadFrom("explorer.exe"); 

实现

资源标识和类型

每个资源都有一个资源标识和一个资源类型,在ResourceId.cs中实现。资源标识可以是正整数或字符串。Windows API要求为这些参数中的每一个提供一个原始指针。如果值在1-65535之间,则为整数标识。否则,它指向一个字符串。这给托管代码带来了不必要的复杂性——用于操作资源的Win32函数的正确PInvoke参数是IntPtr,而不是Stringint

[DllImport("kernel32.dll", EntryPoint = 
	"FindResourceExW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern IntPtr FindResourceEx
	(IntPtr hModule, IntPtr type, IntPtr name, UInt16 language);

为了从int创建IntPtr,我们使用new IntPtr(int),为了从string创建IntPtr,我们使用Marshal.PtrToStringUni(string)

头结构

每个资源结构都有一个类似的头,在ResourceTable.cs中实现。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct RESOURCE_HEADER
{
 public UInt16 wLength;
 public UInt16 wValueLength;
 public UInt16 wType;
}  

头后面通常是一个Unicode字符串(键)和一个数据结构数组,每个结构都有一个类似的资源头。

对齐

大多数资源结构都源自16位Windows,并对齐到16位WORD边界。32位Windows中的较新结构对齐到32位DWORD边界。例如,所有版本资源结构都对齐到DWORD,而图标资源对齐到WORD。其他资源类型可能对各种字段有特定的对齐要求。资源库使用数学来对齐指针(ResourceUtil.Align)到DWORD,并使用结构对齐(Pack = 2)来对齐结构到WORD。后者在可能的情况下更受欢迎,因为它会自动完成。

public static IntPtr Align(Int32 p)
{
    return new IntPtr((p + 3) & ~3);
}

public static IntPtr Align(IntPtr p)
{
    return Align(p.ToInt32());
} 
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct GRPICONDIR
{
    public UInt16 wReserved; 	// reserved
    public UInt16 wType; 		// type, 1 = icon, 2 = cursor
    public UInt16 wImageCount; 	// image count
} 

读取

由于头部的类型统一,您将在代码中发现读取结构化数据的模式相同。

这是StringTable的一个例子

public override IntPtr Read(IntPtr lpRes)
{
 IntPtr pChild = base.Read(lpRes);
 while (pChild.ToInt32() < (lpRes.ToInt32() + _header.wLength))
 {
  StringTableEntry res = new StringTableEntry(pChild);
  _strings.Add(res.Key, res);
  pChild = ResourceUtil.Align(pChild.ToInt32() + res.Header.wLength);
 }
 return new IntPtr(lpRes.ToInt32() + _header.wLength);
}

每个StringTableEntry是终端结构,没有子节点。

public void Read(IntPtr lpRes)
{
 _header = (Kernel32.RESOURCE_HEADER) Marshal.PtrToStructure
			( lpRes, typeof(Kernel32.RESOURCE_HEADER));
 IntPtr pKey = new IntPtr(lpRes.ToInt32() + Marshal.SizeOf(_header));
 _key = Marshal.PtrToStringUni(pKey);
 IntPtr pValue = ResourceUtil.Align(pKey.ToInt32() + (_key.Length + 1) * 2);
 _value = _header.wValueLength > 0 ? Marshal.PtrToStringUni
			(pValue, _header.wValueLength) : null;
}

写入

写入是读取的逆操作,但必须将头部更新为正确的长度。对结构进行对齐,并在写入后计算结构末尾与开头之间的差值会更容易。大小不包括任何填充。

public override void Write(BinaryWriter w)
{
 long headerPos = w.BaseStream.Position;
 base.Write(w);
 Dictionary<string, StringTable>.Enumerator stringsEnum = _strings.GetEnumerator();
 while (stringsEnum.MoveNext())
 {
  stringsEnum.Current.Value.Write(w);
 }
 ResourceUtil.WriteAt(w, w.BaseStream.Position - headerPos, headerPos);
 ResourceUtil.PadToDWORD(w);
}

二进制兼容性

确保库能够生成具有正确大小和正确对齐结构的二进制资源至关重要。每个顶级资源都可以从原始数据读取并写回原始数据。所有长度和大小在写入时都会重新计算,或者在加载和保存之间保留。一个有趣的复杂性存在于Side-by-Side清单资源中:重写相同的清单不一定必须保留RT_MANIFEST资源的大小,因为属性值中的新行会根据W3C规范第3.3.3节进行规范化。

一系列单元测试确保读取的数据与写入的数据相同。简单的开始是ResourceTests.TestReadWriteResourceBytes单元测试,它确保读取的数据与写入的数据相同。

[Test]
public void TestReadWriteResourceBytes()
{
    Uri uri = new Uri(Assembly.GetExecutingAssembly().CodeBase);
    string uriPath = Path.GetDirectoryName(HttpUtility.UrlDecode(uri.AbsolutePath));
    foreach (string filename in Directory.GetFiles(Path.Combine(uriPath, "Binaries")))
    {
        Console.WriteLine(filename);
        using (ResourceInfo ri = new ResourceInfo())
        {
            ri.Load(filename);
            foreach (Resource rc in ri)
            {
                Console.WriteLine("Resource: {0} - {1}", rc.TypeName, rc.Name);
                GenericResource genericResource = 
			new GenericResource(rc.Type, rc.Name, rc.Language);
                genericResource.LoadFrom(filename);
                byte[] data = rc.WriteAndGetBytes();
                ByteUtils.CompareBytes(genericResource.Data, data);
            }
        }
    }
} 

需要更全面的测试来确保即使资源内容发生变化,数据仍然是正确的:这通过从现有文件中加载资源,对数据进行深度复制(不包括任何结构字段(长度、元素数量等)),将副本写入字节向量并比较两者来实现。一个好的例子是VersionResourceTests.TestDeepCopyBytes

扩展到其他类型

将库扩展到其他类型意味着实现一个派生自Resource并实现ReadWrite函数的类。例如,对于RT_VERSION,我们将从以下骨架开始

public class VersionResource : Resource
{
    public VersionResource()
        : base(IntPtr.Zero,
            IntPtr.Zero,
            new ResourceId(Kernel32.ResourceTypes.RC_VERSION),
            null,
            ResourceUtil.NEUTRALLANGID,
            0)
    {

    }

    public VersionResource(IntPtr hModule, IntPtr hResource, 
	ResourceId type, ResourceId name, UInt16 language, int size)
        : base(hModule, hResource, type, name, language, size)
    {

    }

    internal override IntPtr Read(IntPtr hModule, IntPtr lpRes)
    {
        return lpRes;
    }

    internal override void Write(System.IO.BinaryWriter w)
    {
    }
}

新资源类型还必须添加到ResourceInfo.CreateResource中,以便在遇到此类资源时创建一个专门的实例。

switch (type.ResourceType)
{
    case Kernel32.ResourceTypes.RT_VERSION:
        return new VersionResource
		(hModule, hResourceGlobal, type, name, wIDLanguage, size);
}

版本

版本资源是最复杂的资源结构之一。它们的演变在Raymond Chen的“The Old New Thing”中得到了很好的描述。版本资源的顶部是一个VS_VERSION_INFO资源表头,后面跟着一个VS_FIXEDFILEINFO结构,该结构描绘了版本资源中静态部分,其中包含您在Windows资源管理器中看到的文件属性中的二进制文件版本信息。动态链接库的默认Windows固定文件信息如下所示:

public static VS_FIXEDFILEINFO GetWindowsDefault()
{
    VS_FIXEDFILEINFO fixedFileInfo = new VS_FIXEDFILEINFO();
    fixedFileInfo.dwSignature = Winver.VS_FFI_SIGNATURE;
    fixedFileInfo.dwStrucVersion = Winver.VS_FFI_STRUCVERSION;
    fixedFileInfo.dwFileFlagsMask = Winver.VS_FFI_FILEFLAGSMASK;
    fixedFileInfo.dwFileOS = (uint) Winver.FileOs.VOS__WINDOWS32;
    fixedFileInfo.dwFileSubtype = (uint) Winver.FileSubType.VFT2_UNKNOWN;
    fixedFileInfo.dwFileType = (uint) Winver.FileType.VFT_DLL;
    return fixedFileInfo;
}

之后是两个资源表,StringFileInfoVarFileInfo。它们包含可以为特定语言和代码页显示的,以及与特定语言和代码页组合无关的信息。这也是您在Windows资源管理器中看到的文件属性中的内容,但信息可能因操作系统的语言和区域以及登录用户的不同而有所不同。

图标

有了上述基础结构和对最复杂的所有资源(版本资源结构)的支持,就有可能将库扩展到另外二十几个已知资源类型之一。我们已经从图标开始。

将库扩展到支持图标意味着实现图标存储的数据结构并挂接ResourceInfo回调。当ResourceInfo遇到类型为14RT_GROUP_ICON)的资源时,它会创建一个IconDirectoryResource类型的对象。后者创建一个IconResource,它加载一个DeviceIndependentBitmap

  • IconDirectoryResource代表RT_GROUP_ICON,即图标资源的集合。
  • IconResource代表单个RT_ICON图标,包含一个或多个图像。
  • DeviceIndependentBitmap不是资源,而是嵌入在文件中的原始数据,其偏移量由图标资源定义,代表单个图标位图,格式为.bmp

为了将现有图标从.ico文件嵌入到可执行文件(.exe.dll)中,我们加载.ico文件并将其转换为IconDirectoryResource.ico文件中的结构与可执行文件中的图标结构相似。唯一的区别是可执行文件头存储图标ID,而.ico头包含图标数据的偏移量。有关实现细节,请参阅IconFileIconFileIcon类。IconDirectoryResource被写入目标文件,然后每个图标资源分别写入。请注意,当前实现会替换可执行文件中具有相同ID的图标,但如果您存储的图标图像数量少于之前的数量,它不会删除旧的图标——它可能应该这样做,因为这些图标会成为孤立的。

将库轻松扩展到图标支持,验证了我们最初的设计模型。

光标

继图标之后,下一个自然的扩展就是光标。光标结构与图标几乎相同,但有一些显著的区别。

  • CursorDirectoryResource代表RT_GROUP_CURSOR,即光标资源的集合。
  • CursorResource代表单个RT_CURSOR光标,包含一个光标图像。RT_CURSOR将单个光标图像的资源数据描述为一个两字节的热点x值,后跟一个两字节的热点y值,再后跟一个BITMAPINFOHEADER结构。光标的热点是Windows用于跟踪光标位置的点。
  • DeviceIndependentBitmap不是资源,而是嵌入文件中的原始数据,代表单个光标位图。这些数据包括图像的XOR位图和AND位图。这两个位图一起用于支持透明度。

.cur文件在wPlaneswBitsPerPixel字段中包含热点数据。当将DeviceIndependentBitmap转换为CursorResource时,这些数据会被复制到RT_CURSOR资源的顶部。

图标和光标之间的所有通用函数都实现了一个共享类DirectoryResource。差异则由两个派生类CursorDirectoryResourceIconDirectoryResource实现。

位图

位图资源是设备无关位图,按原样存储。大多数实现上的复杂性在于DeviceIndependentBitmap类,该类能够分离位图掩码和颜色信息。虽然技术上具有一定的挑战性,但这对于资源操作不是必需的,超出了本文的范围。当前实现中唯一期望的改进是能够将System.Drawing.Image实例分配给DeviceIndependentBitmap.Bitmap

对话框

有两种对话框资源格式。原始格式在Win32文档中作为DIALOGTEMPLATE结构的一部分进行描述。单个DIALOGTEMPLATE可以包含16位格式的DIALOGITEMTEMPLATE控件。在Windows NT 3.51中,Microsoft引入了DIALOGEXTEMPLATEDIALOGEXITEMTEMPLATE,这是一个32位格式。这种演变在MSDN的Raymond Chen的“The Old New Thing”中有详细解释。

读取对话框结构涉及决定对话框是标准格式还是扩展格式。扩展对话框以不同的0xFFFF头部开始。

internal override IntPtr Read(IntPtr hModule, IntPtr lpRes)
{
    switch ((uint)Marshal.ReadInt32(lpRes) >> 16)
    {
        case 0xFFFF:
            _dlgtemplate = new DialogExTemplate();
            break;
        default:
            _dlgtemplate = new DialogTemplate();
            break;
    }

    // dialog structure itself
    return _dlgtemplate.Read(lpRes);
}

后续结构比较直接,但增加了可变长度数组和结构内部不同对齐的难度。例如,字体名称在32位边界上对齐,但仅在存在时。DIALOGITEMTEMPLATEDIALOGEXITEMTEMPLATE控件在32位边界上对齐。

可选字段(例如,控件类ID)通常有三种识别方式:0x0000值表示该值不存在,0xFFFF表示附加了一个指定资源序数值的元素,否则为以NULL结尾的Unicode字符串。这在DialogTemplateUtil.ReadResourceIdDialogTemplateUtil.WriteResourceId函数对中得到了通用实现。

internal static IntPtr ReadResourceId(IntPtr lpRes, out ResourceId rc)
{
    rc = null;

    switch ((UInt16) Marshal.ReadInt16(lpRes))
    {
        case 0x0000: // no predefined resource
            lpRes = new IntPtr(lpRes.ToInt32() + 2);
            break;
        case 0xFFFF: 	// one additional element that specifies 
			// the ordinal value of the resource
            lpRes = new IntPtr(lpRes.ToInt32() + 2);
            rc = new ResourceId((UInt16)Marshal.ReadInt16(lpRes));
            lpRes = new IntPtr(lpRes.ToInt32() + 2);
            break;
        default: // null-terminated Unicode string that specifies the name of the resource
            rc = new ResourceId(Marshal.PtrToStringUni(lpRes));
            lpRes = new IntPtr(lpRes.ToInt32() + (rc.Name.Length + 1) * 2);
            break;
    }

    return lpRes;
}

internal static void WriteResourceId(BinaryWriter w, ResourceId rc)
{
    if (rc == null)
    {
        w.Write((UInt16) 0);
    }
    else if (rc.IsIntResource())
    {
        w.Write((UInt16) 0xFFFF);
        w.Write((UInt16) rc.Id);
    }
    else
    {
        ResourceUtil.PadToWORD(w);
        w.Write(Encoding.Unicode.GetBytes(rc.Name));
        w.Write((UInt16)0);
    }
}

库中的实现还尝试返回对话框及其控件的标准字符串表示形式,覆盖了ToString方法。

DialogResource: STRINGINPUT, RT_DIALOG
"STRINGINPUT" DIALOG 6, 18, 166, 96
STYLE WS_SYSMENU | WS_DLGFRAME | WS_BORDER | WS_CAPTION | 
WS_VISIBLE | WS_POPUP | DS_SETFONT | DS_SHELLFONT | DS_MODALFRAME
EXSTYLE WS_OVERLAPPED | WS_EX_LTRREADING | WS_EX_LTRREADING | WS_EX_LTRREADING
CAPTION "Set Options"
FONT 8, "MS Shell Dlg"
{
 Static "Prompt goes here" 301, 9, 12, 140, 8, WS_GROUP | WS_VISIBLE | WS_CHILD
 Edit "" 302, 18, 30, 101, 12, WS_TABSTOP | WS_BORDER | 
	WS_CAPTION | WS_VISIBLE | WS_CHILD | DS_MODALFRAME| ES_AUTOHSCROLL
 Button "OK" 1, 63, 55, 40, 14, WS_TABSTOP | WS_VISIBLE | 
	WS_CHILD | DS_ABSALIGN| BS_DEFPUSHBUTTON
 Button "Cancel" 2, 108, 55, 40, 14, WS_TABSTOP | WS_VISIBLE | WS_CHILD| BS_TEXT
} 

字符串表

与其他资源格式不同,其中资源标识符与*.rc*文件中列出的值相同,字符串资源被打包在中。每个字符串资源块包含十六个字符串。为了找到给定字符串的块ID,我们需要一些计算。

public static UInt16 GetBlockId(int stringId)
{
    return (UInt16)((stringId / 16) + 1);
}

读取字符串资源是一个循环,包含16个字符串,每个字符串前面都有其长度。由于每个RT_STRING资源块总是有16个字符串,因此缺失的字符串由长度为零标识。因此,无法添加空字符串。字符串ID源自块ID(资源Name)。

public UInt16 BlockId
{
    get
    {
        return (UInt16) Name.Id.ToInt32();
    }
    set
    {
        Name = new ResourceId(value);
    }
}

internal override IntPtr Read(IntPtr hModule, IntPtr lpRes)
{
    for (int i = 0; i < 16; i++)
    {
        UInt16 len = (UInt16)Marshal.ReadInt16(lpRes);
        if (len != 0)
        {
            UInt16 id = (UInt16) ((BlockId - 1) * 16 + i);
            IntPtr lpString = new IntPtr(lpRes.ToInt32() + 2);
            string s = Marshal.PtrToStringUni(lpString, len);
            _strings.Add(id, s);
        }
        lpRes = new IntPtr(lpRes.ToInt32() + 2 + (len * Marshal.SystemDefaultCharSize));
    }

    return lpRes;
}

例如,explorer.exe包含许多字符串表,包括这个。

STRINGTABLE
BEGIN
 300 Store letters, reports, notes, and other kinds of documents.
 301 Displays recently opened documents and folders.
 302 Store and play music and other audio files.
 303 Store pictures and other graphics files.
END

加速键

加速键是应用程序定义的按键组合,为用户提供执行任务的快捷方式。加速键的存储是ACCEL结构的简单列表,对齐在WORD边界上。RT_ACCELERATOR资源中的项目数量等于资源大小除以每个结构的大小。ACCEL包含标志的组合,这些标志可以组合Control、Alt和/或Shift键以及ASCII字符或特殊键。

特殊键列表讲述了微软合作伙伴关系和Windows演变的一些有趣故事,因为它包括诸如富士通/OASYS键盘的“左大拇指”键VK_OEM_FJ_TOUROKU)或NEC PC-9800键盘的“=”键VK_OEM_NEC_EQUAL)之类的键。

这是Windows Vista explorer.exe中ID为251的加速键表示例

251 ACCELERATORS
BEGIN
 VK_F4, 305, ALT
 VK_TAB, 41008, VIRTKEY, NOINVERT
 VK_TAB, 41008, VIRTKEY, NOINVERT, SHIFT
 VK_TAB, 41008, VIRTKEY, NOINVERT, CONTROL
 VK_TAB, 41008, VIRTKEY, NOINVERT, SHIFT, CONTROL
 VK_F5, 41061, VIRTKEY, NOINVERT
 VK_F6, 41008, VIRTKEY, NOINVERT
 VK_RETURN, 413, VIRTKEY, NOINVERT, ALT
 Z, 416, VIRTKEY, NOINVERT, CONTROL
 VK_F3, 41093, VIRTKEY, NOINVERT
 M, 419, VIRTKEY, NOINVERT, ALT
END

菜单

菜单资源有两种:经典的16位菜单和32位扩展菜单。后者是在Windows 95中引入的,并一直沿用到Windows Vista。这种演变在MSDN的Raymond Chen的“The Old New Thing”中得到了详细解释。与对话框类似,我们实现了一个MenuResource,它包含MenuTemplateMenuExTemplate格式的菜单。每个菜单包含一个MenuTemplateItemCollectionMenuExTemplateItemCollection的菜单项。对于16位经典菜单,后者可以是MenuTemplateItemCommandMenuTemplateItemPopup,而对于32位扩展菜单,则可以是MenuExTemplateItemCommandMenuExTemplateItemPopup

MenuTemplate是一个简单的头部和一个菜单项集合。弹出菜单(一级更深)通过菜单项头部的mtOption字段中的MF_POPUP标志来标识,而集合中的最后一个项通过MF_END标志来标识。每个项和每个菜单字符串都按WORD边界对齐。

MenuExTemplate更复杂。MENUEXTEMPLATE头部有一个奇怪的偏移量结构:第一个项从偏移量头部值本身开始。因此,读取和写入菜单的实现可能看起来很奇怪。

internal override IntPtr Read(IntPtr lpRes)
{
    _header = (User32.MENUEXTEMPLATE) Marshal.PtrToStructure(
        lpRes, typeof(User32.MENUEXTEMPLATE));

    IntPtr lpMenuItem = new IntPtr(lpRes.ToInt32()
        + _header.wOffset // offset from offset field
        + 4 // offset of the offset field
        );

    return _menuItems.Read(lpMenuItem);
}

internal override void Write(System.IO.BinaryWriter w)
{
    long head = w.BaseStream.Position;
    // write header
    w.Write(_header.wVersion);
    w.Write(_header.wOffset);
    w.Write(_header.dwHelpId);
    // pad to match the offset value
    ResourceUtil.Pad(w, (UInt16) (_header.wOffset - 4));
    // seek to the beginning of the menu item per offset value
    // this may be behind, ie. the help id structure is part of the first popup menu
    w.BaseStream.Seek(head + _header.wOffset + 4, System.IO.SeekOrigin.Begin);
    // write menu items
    _menuItems.Write(w);
}

每个项目集合都有一个前缀,其中包含上下文帮助ID。弹出项和集合中的最后一项通过为此目的保留的特殊字段来标识,而不是像16位结构那样使用标志。每个项和每个菜单字符串都按DWORD边界对齐。

菜单分隔符是一个有趣的特殊情况。有两种方法可以识别分隔符:菜单标志或类型字段包含MFT_SEPARATOR选项,或者类型和菜单字符串分别为零和NULL

字体

该库对字体提供有限的支持。

字体资源与其他类型的资源不同,因为它们通常不会添加到特定应用程序的资源中。字体资源会添加到重命名为.fon文件的.exe文件中。这些文件是库而不是应用程序。

FontDirectoryResource由一个或多个FontDirectoryEntry实例组成。每个实例包含一个描述字体的FONTDIRENTRY结构。请注意,这是唯一一个不按偶数边界对齐的结构。

Side-by-Side清单

自Windows XP以来,Windows为Side-by-Side清单保留了一种新的资源类型RT_MANIFEST,类型为24。清单名称可以是以下值之一,最佳描述在这篇MSDN文章中。

  • CREATEPROCESS_MANIFEST_RESOURCE_ID
  • ISOLATIONAWARE_MANIFEST_RESOURCE_ID
  • ISOLATIONAWARE_NOSTATICIMPORT_MANIFEST_RESOURCE_ID

读取和写入清单资源是完全直接的。数据加载到XmlDocument中。该库避免使用XmlDocument,除非用于由调用者进行更改——这确保了读写之间的二进制兼容性。在加载和重新保存XmlDocument时,属性值中的新行会根据W3C规范第3.3.3节进行规范化。

源代码

本文的最新版本和源代码始终可以在CodePlex上找到,网址为http://resourcelib.codeplex.com/

链接

本文结合、实现、移植或废弃了以下出版物中的部分或全部功能

历史

  • 2008-06-30:初始版本
  • 2008-09-28:增加了对图标的支持
  • 2009-02-19:迁移到CodePlex,发布1.1版本
  • 2009-09-15:增加了对光标、位图、对话框、字符串表、加速键、菜单和SxS清单的支持
© . All rights reserved.