前言
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