摘要Chris Sells 讨论无类型清单资源和有类型资源它们是受 Microsoft NET 框架支持的两种资源他定义了这两种资源并介绍了如何在您自己的应用程序中使用它们下载 winformsexe 示例文件
假设要在应用程序中通过从文件加载位图来设置窗体的背景图像
public Form() { // Load a file from the file system thisBackgroundImage = new Bitmap(@C:\WINDOWS\Web\Wallpaper\Azuljpg);}
该代码的问题是并非所有 Microsoft Windows 的安装实例都有 Azuljpg即使是那些确实具有该文件的安装实例该文件可能也不在安装实例的相同位置即使您与应用程序一起交付该图片节省空间的用户也可能决定删除它这会导致您的应用程序出错确保图片或任何文件与代码在一起的唯一安全方式是将它作为资源嵌入并加载
清单资源
资源是在编译时添加到程序集中的例如如果您使用命令行编译器则可以使用 /resource 开关嵌入资源
C:\>cscexe myAppcs /resource:c:\windows\web\wallpaper\Azuljpg
/resource 开关将文件作为资源嵌入嵌入时使用文件名(没有路径)作为资源名称文件嵌入到程序集的清单 资源集中程序集的清单由一组作为程序集一部分的元数据组成该元数据的一部分是与每个嵌入资源关联的名称和数据执行 ildasm 时可以在清单部分看见程序集清单资源的列表如图 所示
C:\>ildasmexe myAppexe
图 ildasm 显示嵌入资源
可以像 ildasm 一样枚举清单资源的列表这需要使用 systemreflectionassembly 类的 getmanifestresourcenames 方法
using SystemReflection;// Get this types assemblyAssembly assem = thisGetType()Assembly;// Enumerate the assemblys manifest resourcesforeach( string resourceName in assemGetManifestResourceNames() ) { MessageBoxShow(resourceName);}
一旦通过枚举清单资源或硬编码一个您想要的清单资源而知道了清单资源的名称就可以通过 assembly 类的 getmanifestresourcestream 方法将该清单资源作为原始字节流进行加载如下所示
using SystemIO;public Form() { // Get this types assembly Assembly assem = thisGetType()Assembly; // Get the stream that holds the resource // NOTE: Make sure not to close this stream! // NOTE: Also be very careful to match the case // on the resource name itself Stream stream = assemGetManifestResourceStream(Azuljpg); // Load the bitmap from the stream thisBackgroundImage = new Bitmap(stream);}
因为资源可以像类型名称一样有沖突所以最好用资源自己的命名空间来嵌入资源该操作可以使用 /resource 开关的扩展格式来完成
C:\>csc myAppcs /resource:c:\\azuljpgResourcesAppAzuljpg
注意在要嵌入的文件名的逗号后面使用的备用资源名称备用资源名称允许您为资源任意地提供时间嵌套名称不管文件名是什么它是设置在程序集中的备用名称如图 所示
图 使用备用名称的嵌入资源
下面是使用备用名称的更新后的资源加载代码
public Form() { // Get this types assembly Assembly assem = thisGetType()Assembly; // Load a resource with an alternate name Stream stream = assemGetManifestResourceStream(ResourcesAppAzuljpg); // Load the bitmap from the stream thisBackgroundImage = new Bitmap(stream);}
为了更方便如果您的资源和加载资源的类碰巧使用了相同的命名空间则可以将类的类型作为可选的第一参数传递给 getmanifestresourcestream
namespace ResourcesApp { public class Form : Form { public Form() { // Get this types assembly Assembly assem = thisGetType()Assembly; // Load the resource using a namespace // Will load resource named ResourcesAppAzuljpg Stream stream = assemGetManifestResourceStream(thisGetType() Azuljpg); // Load the bitmap from the stream thisBackgroundImage = new Bitmap(stream); } }}
GetManifestResourceStream 将使用如下格式编写资源名称
<namespace><fileName>
在加载某些类型(比如 bitmap 类)时使用类型和文件名也是有用的这样可以通过提供构造函数避免由您自己打开流
namespace ResourcesApp {
public class Form : Form { public Form() {
// Get this types assembly Assembly assem = thisGetType()Assembly;
// Load the bitmap directly from the manifest resources thisBackgroundImage = new Bitmap(thisGetType() Azuljpg);
} }}
Visual Studio NET 中的清单资源
如果(大多数情况下)您使用 Visual Studio?NET 来开发和构建程序集则用命令行嵌入清单资源的方法不可能非常吸引您这种情况下您可以将资源添加到 Windows 窗体项目中该方法将把合适的命令行参数传递给编译器
要将资源添加到项目中请在 Solution Explorer 中右键单击项目然后选择 add New Item并选择您想作为资源嵌入的文件文件将复制到项目的目录中但仍然不会被嵌入要使文件作为资源嵌入请右键单击文件并选择 properties然后将 Build Action 从 content(默认)更改为 embedded Resource如图 所示
图 将文件的 Build Action 设置为 Embedded Resource
这种嵌入资源的方法会使 Visual Studio NET 为您创建一个备用资源名其组成类似这样
<defaultNamespace><folderName><fileName>
资源名称的默认命名空间部分是项目本身的默认命名空间它是通过 Solution Explorer>(右键单击)>properties>common Properties>general>default Namespace 来设置的由于这是在生成新类时新类得到的相同命名空间所以这就使通过使用类型和部分资源名称来加载资源变得很方便如果文件碰巧位于项目的子文件夹中就需要在文件夹名称中包括子文件夹并用点替换反斜槓例如一个名为 Azuljpg 的位图位于项目根下面的 foo\bar 文件夹中要加载它就需要这样做
// If this code called within the ResourcesAppForm class// and the file is \foo\bar\Azuljpg// will load ResourcesAppfoobarAzuljpgthisBackgroundImage = new Bitmap(thisGetType() foobarAzuljpg);
有类型资源
尽管文件有扩展名但清单资源是在没有类型信息的情况下嵌入的例如如果 Azuljpg 文件的名称实际上是 Azulquux这对于 bitmap 类来说是没有差别的因为这个类将通过查看数据本身来确定其类型(JPEGPNGGIF 等)这就需要由您来将每个资源的类型正确映射为加载该资源所需的对象的类型
但如果您愿意多走一步则可以用一个类型来标记资源 框架支持用于资源的一组扩展元数据其中包括两种格式的 MIME 类型信息一个是文本格式另一个是二进制格式这两种格式都有内置的读取器以便在运行时取得类型正确的资源
基于文本的格式是特定 框架的 XML 格式称为 ResX(resx 文件)不考虑其 XML 基础该格式不是专门为人工阅读而设计的(XML 格式很少是这样的)但是Visual Studio NET 仍然为 resx 文件提供了一个基本编辑器要将新的 resx 文件添加到 Visual Studio NET 项目中请从 project 菜单中选择 add New Item然后选择 assembly Resource File 模板如图 所示
图 将 resx 文件添加到项目中
到写本文时为止即使空的 resx 文件也是 行 XML而其中大多数是架构信息架构允许 resx 文件中有任意数目的项每项都有名称值注释类型和 MIME 类型图 显示了有两个项的 resx 文件即名为 mystring 的字符串和名为 myimage 的图像
图 设计器的数据视图中简单的 resx 文件
遗憾的是只有字符串项能够在 resx 编辑器的数据视图中实际进行编辑任何二进制数据都需要手动直接输入到 XML 中(而且只能是 base 编码)因此直接使用 resx 文件只对字符串资源有用(尽管间接使用会使 resx 文件对任何种类的数据都非常有用我们随后将讨论这一点)
来自 systemresources 命名空间的 resxresourcereader 类将分析 XML 文件并公开一组命名的有类型的值要取得具体的项需要查找它
using SystemCollections;using SystemResources;public Form() { using( ResXResourceReader reader = new ResXResourceReader(@Resourceresx) ) { foreach( DictionaryEntry entry in reader ) { if( entryKeyToString() == MyString ) { // Set form caption from string resource thisText = entryValueToString(); } } }}
使用 add New Item 对话框将 resx 文件添加到项目中会使该文件作为 Embedded Resource 添加进项目而编译项目时则会导致 resx 数据作为嵌套资源 嵌入(嵌套资源是分组到命名容器中的资源)容器的名称与作为资源添加的任何文件相同只是不使用 resx 扩展名使用 resource 扩展名假定一个项目的默认命名空间是 resourcesapp 而 resx 文件名为 resourcesresx则嵌套资源的容器名为 resourcesappresourcesresx如图 中的 ildasm 所示
图 嵌入的 resources 文件
resources 扩展名来自于在将 resx 文件作为资源嵌入之前 Visual Studio NET 处理该文件时所使用的工具工具名称是 resgenexe它用来将 resx XML 格式编译为二进制格式可以手动将 resx 文件编译成 resources 文件如下所示
C:\> resgenexe Resourceresx
在将 resx 文件编译成 resources 文件以后就可以使用 systemresources 命名空间中的 resourcereader 来枚举它
using( ResourceReader reader = new ResourceReader(@Resourceresources) ) { foreach( DictionaryEntry entry in reader ) { string s = stringFormat({} ({})= {} entryKey entryValueGetType() entryValue); MessageBoxShow(s); }}
除了类的名称和输入格式ResourceReader 类的使用方法与 resxresourcereader 相同包括都不能随机访问命名项
所以虽然您可以将 resx 文件转换成 resources 文件并使用 /resource 编译器命令行开关嵌入它但容易得多的方法是直接在项目中让 Visual Studio NET 接受被标记为 Embedded Resources 的 resx 文件然后将它编译进 resources 文件并嵌入它如图 图 和图 所示一旦将 resources 文件捆绑为资源访问 resources 文件中的资源就只需执行两个步骤的过程
// Load embedded resources fileusing( Stream stream = assemGetManifestResourceStream( thisGetType() Resourceresources) ) { // Find resource in resources file using( ResourceReader reader = new ResourceReader(stream) ) { foreach( DictionaryEntry entry in reader ) { if( entryKeyToString() == MyString ) { // Set form caption from string resource thisText = entryValueToString(); } } }}
因为 resourcereader 和 resxresourcereader 都需要该两步过程才能找到具体的资源因此 NET 框架提供了 resourcemanager 类该类公开了一个更简单的使用模型
资源管理器
ResourceManager 类也来自 SystemResources 命名空间该类包装了 ResourceReader用于在构造时枚举资源并使用其名称公开它们
public Form() { // Get this types assembly Assembly assem = thisGetType()Assembly; // Load the resources file into the ResourceManager // Assumes a file named Resourceresx as part of the project ResourceManager resman = new ResourceManager(ResourcesAppResource assem); // Set form caption from string resource thisText = (string)resmanGetObject(MyString); // The hard way thisText = resmanGetString(MyString); // The easy way}
用来查找 resources 文件的命名方式与命名任何其他种类的资源相同(注意追加到 resourceresources 文件中的项目默认命名空间的使用方法)只是 resources 扩展名是假定的并且不能包括在名称中为了更方便如果您碰巧将一个 resx 文件命名为类型名称则 resources 文件和程序集的名称将从类型确定
// Use the type to determine resource name and assemblyResourceManager resman = new ResourceManager(thisGetType());
一旦已经创建了资源管理器的实例就可以通过使用 getobject 方法并强制转换为合适的类型从而按名称找到嵌套资源如果使用 resx 文件来处理字符串资源则可以使用 getstring 方法该方法将执行到 systemstring 类型的强制转换
设计器资源
缺少用于 resx 文件的合适的编辑器使它们在使用除字符串资源以外的任何其他资源时非常困难您不仅必须通过手动编写代码才能在运行时输入数据而且无法在设计时看见资源的使用情况例如窗体的背景图像
幸运的是设计器再次在这里帮助了我们如果打开 Visual Studio NET Solution Explorer并选择 show All Files 按钮您将看见每个组件(无论它是窗体控件还是简单的组件)都有相应的 resx 文件这是为了让资源与组件的属性保持关联这种关联是在 Property Browser 中设置的例如如果设置窗体的 backgroundimage 属性那么不仅在设计器中窗体将显示背景图像而且窗体的 resx 文件将包含该图像的对应项同样如果在相同窗体上设置 PictureBox 控件的 image 属性则 resx 文件同样会增大以便包括该资源这两项都可以在图 中看到
图 组件的 resx 文件
每个组件的 resx 文件将作为 resources 文件进行编译和嵌入就像已经将您自己的 resx 文件添加到项目中一样这将使资源能够在运行时被组件使用除了组件的 resx 文件中的项之外设计器还会将代码添加到 initializecomponent 中以便加载组件的资源管理器并使用从资源获得的对象来填充组件的属性
namespace ResourcesApp { public class Form : Form { private void InitializeComponent() { ResourceManager resources = new ResourceManager(typeof(Form)); thispictureBoxImage = (SystemDrawingBitmap)resourcesGetObject(pictureBoxImage); thisBackgroundImage = (SystemDrawingBitmap)resourcesGetObject($thisBackgroundImage); } }}
注意 resourcemanager 对象是使用组件的类型来构造的该类型用来构造组件的 resources 资源名称还要注意设计器在命名资源时所使用的命名约定对于组件字段上的属性名称的格式是
<fieldName><propertyName>
对于组件本身的属性名称格式是
$this<propertyName>
如果您想添加供组件本身使用的自定义字符串属性您可以这样做但要确保与设计器生成的名称格式不同