l 前言
l 了解资源文件
l 创建资源文件
l 在程序中使用资源文件
l 资源文件的命名和部署
l 参考
前言:
在学习如何使用.NET资源文件以及如何开发World-Ready程序之前,我们先通过一个例子来看看为什么要使用资源文件,以及使用它的好处。
假设要在程序中根据当前的Culutre来设置Form的Title和Logo:
private void Form1_Load(object sender, System.EventArgs e) {
CultureInfo ci = new CultureInfo(Thread.CurrentThread.CurrentUICulture.ToString());
switch (ci.ToString().ToLower()) {
case "zh-cn": // 中文版本
this.Text=FormTitle_ZH_CN;
imgLogo.Image = new Bitmap(Application.StartupPath + "/Logo_ZH_CN.jpg");
break;
case "en-us": // 英文版本
this.Text=FormTitle_EN_US;
imgLogo.Image = new Bitmap(Application.StartupPath + "/Logo_EN_US.jpg");
break;
default: // 默认版本
this.Text=FormTitle_Neutral;
imgLogo.Image = new Bitmap(Application.StartupPath + "/Logo_Neutral.jpg");
break;
}
}
这段代码有两个问题:
首先,Logo文件是暴露给用户的,而且是以普通文件的格式存储的,这导致其他程序或是用户很容易修改这些文件;节省硬盘空间的用户还可能会选择删除它,这些都可能会导致应用程序出错。确保图片或任何其他文件和代码在一起的唯一的安全方式是将它作为资源文件嵌入在程序集中并加载。
其次,这是一个World-Ready程序,如果需要新加入一个新的Culture,你可能不得不更改你的源代码,加入新的case,然后重新编译来适应新的Culture的需要,这对一个World-Ready程序来说是不现实的。开发World-Ready程序很重要的一点就是要保证程序的逻辑界面和资源界面的隔离。任何时候加入一个新的Culture资源,我们都不应该重新编译源程序,相反,我们只需要把新的资源文件准备好,然后发布给用户并部署在合适的目录下就可以了。应用程序应该能够根据不同的Culture来自动寻找合适的资源。
本文的目的就是通过实例来帮助读者了解什么是Resources,以及如何使用Resources来消除上面所提到的两个问题。
全文分为四部分:
第一部分是一些和资源相关的概念。
第二部分是一个实例程序(ResourceGenerator),用来说明如何创建资源文件。
第三部分是另外一个实例程序(WorldAPP),用来说明如何在程序中使用资源文件
第四部分是关于资源文件的命名和部署。分别介绍.NET中资源文件的命名方式和如何在World-Ready程序中配置资源文件。
第一部分 概念
先来了解一些概念:
1. 什么是资源文件
顾名思义,资源文件当然包含的全是资源。不过,什么是资源?这里所谓的资源就是程序中可利用的任何数据,譬如:字符串、图片或任何二进制格式的数据。一个资源文件可以有多种语言文化版本,比如,一个Culture.resources 文件可以有英语版、简体中文版日文版等。ResourceManager可以自动根据Culture和资源文件名来确认调用哪个版本。只不过不同的资源版本需要在文件名中加入语言文化信息(.resource文件有一套严格的命名规范,参考第四部分:资源文件的命名和部署)。
2. 资源文件的类型
System.Resources名称空间支持三种类型的资源:
.txt文件,只能有字符串资源。因为不能被嵌入到Assembly中,所以很容易暴露,被其他程序或用户修改。最大缺点是仅支持字符串资源,不推荐使用。
.resx文件,由XML组成,可以加入任何资源,包括二进制格式的。同样不能被嵌入到Assembly中。在System.Resources 名称空间中有专用读写的类。VS.NET中创建的这种文件也是将其转成.resources 文件然后根据设置将其嵌入到Assembly中。
.resources文件,PE格式,可以加入任何资源。是唯一可以被嵌入到Assembly的文件,在System.Resources名称空间中有专用读写的类(ResourceManager)。
3. 调用资源文件的几种方法
ResourceManager可以根据不同的UICulture设置返回不同的本地资源,不同Culture的资源文件有一套严格的命名规则,只有按照这个规则命名,CRL才可以根据Culture找到这个本地资源。PS:因为这个很重要,所以才一再出现J。参考第四部分:资源文件的命名和部署)
.txt 文件:
不可以直接调用,得先将其转换成 .resources 文件才能使用。
.resx 文件:
可以用ResXResourceReader来读取,但是这种方法不直观也不安全,不推荐直接调用.resx文件。正确的方法是将其转换成.resources文件,然后用ResourceManager读取。注意,如果是在VS.NET中添加的.resx文件,那么它们自动被设为 Embedded Resource,然后被转成.resources文件后嵌入到Assembly中。
.resources 文件:
分成两种情况:
· 被嵌入或编译成卫星程序集(Satellite Assembly):
用ResourceManager的各种constructor来获得在Assembly中的资源。
· 单独文件,没有被编译或嵌入到Assembly中:
可以用ResourceManager.CreateFileBasedResourceManager来获得资源集(ResourceSet),就是所有的资源。
特殊情况:
还有一种特殊情况,那就是当你直接嵌入一资源时,也就是说,不通过一个资源文件(.resources)而直接将一资源(Object)嵌入到 Assembly 中。这可以通过AL.exe(Assembly Linker)的参数/embed:<object>把资源嵌入在Assembly中。在这种情况下ResourceManager就没有用了,因为它只能获取.resources资源文件(在或不在Assembly中)。
调用这类直接嵌入在Assembly中的资源,我们就需要利用Reflection的一些特性来完成。在System.Reflection.Assembly类中有一些相关函数可以帮助我们拿到这些资源。通过Assembly.GetManifestResourceNames可以拿到所有的资源的名字,然后我们就可以通过Assembly.GetManifestResourceStream(<object_name>)这个函数拿到对应的资源并以stream的方式返回,然后我们可以将这个stream转成在.NET中可用的对象。比如,如果嵌入资源是一图片,那么我们可以利用New Bitmap(Stream)的constructor获得这个图片资源的Bitmap对象。
第二部分 创建资源文件
创建资源文件有两种方式,一种是使用.NET SDK自带的resgen工具来创建,另外一种是自己写code来创建。分别来介绍:
1. Resgen:
这个工具是.NET自带的,它可以把.txt,.resX,转换为.resources文件。.resources文件是以一种以键-值方式对应存储的XML格式文件,每一个键<data>对应一个值<value>,这个<value>可以是任何的二进制格式。如果是格式为(键=值)对应得.txt文件,resgen会自动生成键-值对应的XML文件。但是resgen有一个局限性,它不能直接嵌入其他格式的文件,比如你就不能把.bmp以键-值得方式对应起来,因为你首先不能很容易得把.bmp以(键=值)对应的格式储存在.txt文件中。所以resgen主要是针对txt文件使用。
一个例子:company1.txt文件内容为:
Title = Company1
Address = Company1 Address
Phone = 12345678
----------------------------------------------------------------
Resgen company.txt <outputfilename>.resources
如果不指定<outputfilename>,默认会生成company1.resources。
然后就可以通过ResourceManager来使用了。
还可以再进一步,通过AL.exe把resources文件变为一个assembly(使用assembly有很多好处(比如可以加入版本信息和Culture信息等)详见(.NET系统学习----Assembly)。
Al /out:company1.dll /embed:company1.resources
通过设置ResourcesManager的不同的constructor就可以访问Assembly中包含的.resources文件(下面的例子会讲到)。
2. 通过编程使用IResourcsWrite来生成资源文件
上面的方法的一个最大的缺点是不能很方便的嵌入其他格式的资源,因为把其他格式的资源变为键-值对应得txt文件并不是一件很容易的事。所以我们介绍另一种方法,通过编程,使用.NET提供的IResourcesWrite类来实现把任何资源嵌入到resources文件中。
ResourceGenerator就是用这种方式实现的。
程序的主界面:
用到的主要方法就是:
private void OnGenerateResource(object sender, System.EventArgs e)
{
IResourceWriter rw = new ResourceWriter(“C:/test.resources”);
switch (sType)
{
case "system.string":
rw.AddResource(sKey,sValue);
break;
case "system.drawing.bitmap":
Bitmap bmp = new Bitmap(sValue);
rw.AddResource(sKey,bmp);
break;
case "system.drawing.image":
Image img= new Bitmap(sValue);
rw.AddResource(sKey,img);
break;
}
}
根据资源的类型,如果不是string类型的,我们就把它分实例化为相应的stream,然后加入到resoruces中即可(string类型可以直接加入)。生成的就是.NET可以直接使用的.resources文件。但是这样生成的资源CLR并不能根据不同的Culture自动识别。要想CRL自动识别并加载正确的资源文件,首先必须把.resources转换为Assembly,并根据严格的命名方式命名(参考第四部分:资源文件的命名和部署),并部署到正确的目录下,然后CLR就可以根据不同的Culture来加载正确的资源。
第三部分 在程序中使用资源文件
WorldApp.cs是一个World-Ready的程序,它的逻辑界面和资源界面是分开的,可以实现逻辑界面只Bulid一次,运行时根据当前的Culture调用相应的Satellite Assembly(卫星资源程序集)来实现本地化。添加一个新的Culture资源不需要重新Build源程序,只需要把相应的资源程序集部署到合适的目录就可以了。
下面说明WorldApp的实现方式:
程序主界面:
程序在启动的时候会根据当前的CurrentUICulutre去加载相应的资源文件。
读取资源文件的代码为:
private void SetCulture( CultureInfo ci )
{
// Change current UI culture
Thread.CurrentThread.CurrentUICulture = ci;
// Load culture resources.
String AssemblyPath = Application.StartupPath + "//Culture.dll";
Assembly asm = Assembly.LoadFrom(AssemblyPath);
// ResoruceManager constructor will load different resources acording to the
// CurrentUICulture. which means, if CurrentUICulutre is "en-US", rm will load
// "Culture.en-US.resources" automaticly.
// When loaded, give the resource name only.
ResourceManager rm = new ResourceManager("Culture", asm);
// Set title, culture info and logo.
this.lblTitle.Text = rm.GetString("Title");
this.lblCulture.Text = rm.GetString("Culture");
this.lblLogo.Text = rm.GetString("LogoTitle");
this.imgLogo.Image=(Bitmap)rm.GetObject("Logo");
}
如果当前的UICulture改变,可以通过显式调用SetCulture( CultureInfo ci )来加载相应的Culture资源。
现在如果我们有了一个新的Culture资源版本,我们只需要把它部署在对应的Culture目录下,WorldApp.exe就可以自动加载,WorldApp.exe程序本身并不用做任何更改(不需要编译)。
你可以通过上面制作的小工具ResoruceGenerator来生成对应不同Culture的资源,然后把生成的Assembly正确部署就可以了。WorldApp就又有了一个新的Culture版本。哈!!
第四部分 资源文件的命名和部署
这部分说明资源文件的部署方式和CLR是如何识别并加载不同的Culture资源的。
· 资源文件的命名方式
假设我们的应用程序名为WorldApp.exe,默认的资源文件为culture.resources,根据这个资源文件生成的Assembly为culutre.dll(这个是默认版本的资源文件)。然后我们有了一个en-US Culture版本的资源文件,则en-US的资源文件得名称必须为culture.en-US.resources,根据这个资源文件生成的en-US版本的Assembly必须命名为culture.resources.dll且必须加入Culture信息(把一个.resources生成一个Assembly:resgen /out:cluture.resources.dll /c:en-US /embedcluture.en-US.resources),生成的Assembly必须放在程序运行目录下的en-US目录下,这样CLR才能自动找到。同样,如果我们有了一个zh-CN版本的资源文件,则资源文件的名称必须为culture.zh-CN.resources,生成的Assembly必须为culture.resources.dll,并放在zh-CN目录下。
重要:因为生成的.resources文件本身并不包含Culture信息,它的Culture信息就体现在它的文件名上,所以.resources的命名必须加入Cluture信息(如果不加的话,生成的就是默认版本)。从.resources生成Assembly时,因为Assembly可以指定Culture信息(通过/c:<culture>来指定),所以Assembly的名称中不需要加入Culture信息,但是Assembly的名字必须是:默认版本名+<.resources>.dll,就是:culture.[resources].dll。
· 资源文件的部署方式
应用程序正确的部署方式(目录结构)应该是:
<WorldApp> (应用程序主目录)
WorldApp.exe (主程序)
Culture.dll (包含culture.resources资源文件)
<en-US> (en-US资源目录)
Culture.resources.dll (包含culture.en-US.resources资源文件)
<zh-CN> (zh-CN资源目录)
Culture.resources.dll (包含culture.zh-CN.resources资源文件)
<new-Culture> (net-Culture资源目录)
Culture.resources.dll (包含cluture.new-Culture.resources)
<…>
有了上面的部署,App.exe在运行时,会首先根当前Thread的CurrentUICuluture到对应的目录去寻找资源文件,比如当前的CurrentUICulture=”en-US”,则en-US目录下的Culture.resources.dll Assembly中的culture.en-US.resources会被加载。如果CLR遍历整个目录还没有找到对应的资源文件,则默认的资源文件版本就被加载(MSDN中称为Hub and Spoke model方式 详见:ms-help://MS.MSDNQTR.2004APR.1033/cpguide/html/cpconPackagingDeployingResources.htm)。
· CLR如何加载资源文件
重要:CLR在匹配资源文件的时候,不是按文件来匹配的,它是按照<data>字段一个key一个key的去匹配。举个例子:
默认版本的Culture资源文件中包含四个key:Title, Culture, LogoTitle,Logo。
中文版本的Culture资源文件中包含只有三个Key:Title, Culture, Logo。(没有LogoTitle)
如果当前的Culture是”zh-CN”,则zh-CN版本的Title, Culture, Logo都会被加载,但是因为zh-CN版本没有LogoTitle,所以CLR会自动加载和zh-CN文化最匹配的一个资源版本的LogoTitle。如果都没有,最后才会去加载默认版本的资源文件。
这样做有一个很大的好处:就是说并不是所有资源都必须要有对应Culture的版本,我们可以把共通的资源放在默认版本中,只把和特定Culture相关的资源隔离就可以了。
重要:关于Culture:
Culture信息是由主标记(文化)和次标记(地域)两部分组成的。举个例子:
en-US (英语-美国)
en-GB (英语-英国)
en-AU (英语-澳大利亚)
主标记是en,表示Culture都是英语文化,次标记(地域)区分了它们分别是哪个地区的英语。
说这个有什么用呢?
因为CLR在寻找资源的时候是以一种回退的方式来寻找的,就是说,他会首先去寻找最批的那个资源文件,如果没有,则会搜索文化层次结构,以查找最接近于请求的匹配资源文件,并把生成异常作为最后一种手段。比如CLR在寻找en-US资源的时候没有找到,CLR不会立即就去用默认版本匹配,而是会首先搜索文化层次结构,以查找最接近于en-US的资源(可能是en-GB或别的),如果找到,运行时就使用这个资源,如果还找不到,则会继续搜索下一层,最后才会用默认版本匹配(如果默认版本也没有,则会抛出一个异常)。
参考资料:
l Applied Microsoft .NET Framework Programming ---- Jeffrey Richter
l MSND Library