下载本文的代码: BasicInstincts05.exe (146KB)
本页内容
可通过两种方法使用基于 Microsoft® .NET Framework 应用程序中的资源,如字符串、图像和基于文本的文件。可以将资源直接嵌入应用程序,或从外部文件加载它们。如果选择从外部源(而非嵌入资源)加载,则文件必须随程序集一起分发。还必须确保应用程序中的代码能够确定正确的路径,以及在运行时加载资源文件。如果 .exe 与它所依赖的文件分离,则该方法会导致问题。
采用嵌入选项并将所需资源直接编译到使用它们的程序集中可使分发更加可靠,并且不容易出错。本月,我将讨论资源的使用、如何及为什么要嵌入资源,以及资源文件在 .NET 中的作用。
如果使用Visual Studio,请直接参考Visual Studio 2005 中的资源文件一节,Visual Studio编译时会自动调相关工具处理资源文件。注:2005和2008有一些差异
嵌入资源
首先,我们来看一个简单的示例,了解如何实现嵌入。假设要在基于 Windows® 窗体的应用程序中嵌入一个名为 LitwareLogo.png 的图形图像。首先,要将该文件添加到 Visual Studio® 项目中。然后,在该文件的属性表中,将 Build Action 设置为 Embedded Resource,如图 1 所示。这样,就指示 Visual Studio 将该文件嵌入输出程序集 .exe 文件的物理图像了。
图 1 设置 Build Action
一旦将某个文件作为资源嵌入,就必须了解如何在运行时访问它。请参见以下代码片段,它先获得对当前程序集对象的引用,然后调用 GetManifestResourceStream 方法以获得对嵌入的资源文件基于流的访问。该代码假定导入了 System.Reflection 和 System.IO 命名空间:
'*** get current Assembly object.
Dim asm As Assembly = Assembly.GetExecutingAssembly()
'*** load embedded resource into stream
Dim ResourceName As String = "LitwareSmartClient.LitwareLogo.png"
Dim str As Stream = asm.GetManifestResourceStream(ResourceName)
'*** convert stream into image and load in '*** picture box
Dim img As Image = Image.FromStream(str)
PictureBox1.Image = img
正如您看到的那样,Assembly 对象公开 GetManifestResourceStream 方法,允许您传递标识嵌入资源的字符串名称。请注意,资源名称区分大小写,即使您使用不区分大小写的语言(如 Visual Basic®)也如此。在该示例中,代码调用 Image.FromStream 方法将包含图像文件的流转换为可加载到 PictureBox 控件中的 Image 对象。
除了嵌入图像文件外,还可以方便地嵌入包含 XML、SQL 或 JavaScript 的基于文本的文件。如果您对使用 Visual Basic 连接 XML、SQL 或 JavaScript 的较大字符串片段感到厌烦,那么这可以使您的工作轻松不少。
例如,假设您拥有应用程序所需的大型 XML 文档、SQL 语句或 JavaScript 函数。您可以在 Visual Studio 项目中将其作为独立的 .xml 文件、.sql 文件和 .js 文件进行维护。这样,您就可以利用 Visual Studio 的颜色代码和语句完成特性。您还可以针对 XML 文件利用 Visual Studio 架构驱动的智能感知®。您需要做的就是将这些源文件嵌入到输出程序集中,然后使用刚才看到的技术来访问它们。例如,如果您在基于 Windows 窗体的应用程序中嵌入了一个 SQL 文件和一个 XML 文件,则可使用如下面 所示的代码来访问它们。
'*** get current Assembly object. Dim asm As Assembly = Assembly.GetExecutingAssembly() '*** load embedded SQL resources file Dim SqlResourceName As String = "LitwareSmartClient.GetProducts.sql" Dim strSQL As Stream = asm.GetManifestResourceStream(SqlResourceName) Dim reader As New StreamReader(strSQL) Dim sql As String = reader.ReadToEnd reader.Close() '*** load embedded XML resources file Dim XmlResourceName As String = "LitwareSmartClient.Customers.xml" Dim strXML As Stream = asm.GetManifestResourceStream(XmlResourceName) Dim xmlDoc As New XmlDocument() xmlDoc.Load(strXML) strXML.Close()
资源文件
您刚才看到的技术涉及将资源文件直接嵌入程序集,以及使用 Assembly 类提供的 GetManifestResourceStream 方法加载它们。但 .NET 中还有另一个替代方法(即,资源文件),该方法在许多情况下可以更轻松地处理资源。此外,正如您将看到的,Visual Studio 2005 在使用资源和本地化应用程序时提供了一些便利。
使用资源文件
在 .NET 中,可以使用资源文件将资源嵌入程序集。使用资源文件的一个主要好处是,可以将应用程序或类库 DLL 中的所有特定于语言和区域设置的元素(如标题和用户消息)从应用程序代码中分离出来。为此,您需要为要支持的每种语言创建单独的资源文件。实际的资源文件是一个包含 XML 的基于文本的文件,其扩展名为 .resx。下面 显示某个资源文件中 XML 数据的节选示例。
<root> <data name="MainFormCaption"> <value>Litware Customer Managervalue> data> <data name="UserWelcome"> <value>Good dayvalue> data> <data name="ErrorMessage1"> <value>Oh no, Something went wrong!value> data> root>
尽管图中的 XML 片段不是完整的资源文件,但可以使您大致了解其内部内容。可以使用名为资源文件生成器 (Resgen.exe) 的基于 .NET 的实用工具将资源文件编译为二进制图像。编译后的资源文件通常具有 .resources 扩展名。例如,Litware 公司内的开发人员可以创建一个名为 LitwareStrings.resx 的资源文件,然后在批处理文件中或 Visual Studio 2005 命令提示符下执行以下命令,将该资源文件编译为名为 LitwareStrings.resources 的二进制图像:
RESGEN.EXE LitwareStrings.resx LitwareStrings.resources
将基于文本的 .resx 文件编译为二进制 .resource 文件后,仍然不能使用它,而必须将该二进制图像进一步编译为 .NET 程序集,才能在应用程序中使用。这可以使用另一个名为程序集链接器 (Al.exe) 的 .NET 工具完成。例如,要将 LitwareStrings.resources 编译为它自己的程序集 DLL,可从批处理文件中或 Visual Studio 2005 命令提示符下运行以下命令行指令:
AL.EXE /t:library /out:LitwareStrings.resources.dll /link:LitwareStrings.resources
一旦将资源文件编译为 .NET 程序集,就可使用 System.Resources 命名空间中定义的 ResourceManager 类访问其中的资源。下面显示一个使用 ResourceManager 访问字符串资源的简单代码示例:
Dim asm As Assembly = Assembly.Load("LitwareStrings.resources") Dim rm As New System.Resources.ResourceManager("LitwareStrings", asm) Dim caption As String = rm.GetString("MainFormCaption")
也可以使用 Resgen.exe 生成一个强类型资源类,以公开能够轻松访问其中资源的属性。例如,要在 Visual Basic 中生成强类型资源类,可在调用 Resgen.exe 时将 /str 参数和"vb"值添加到命令行:
RESGEN.EXE LitwareStrings.resx LitwareStrings.resources /str:vb
该命令行指令生成一个名为 LitwareStrings.vb 的 Visual Basic 源文件。该源文件包含一个名为 LitwareStrings 的类。在该类中,存在使用 ResourceManager 实现强类型化属性的代码,如下所示:
Shared ReadOnly Property MainFormCaption() As String Get Return ResourceManager.GetString("MainFormCaption", resourceCulture) End Get End Property
我刚才简要概述了如何将资源文件编译为程序集,以及如何使用 ResourceManager 类和强类型资源类来访问这些资源文件。这应该使您能更好地了解各个部分是如何配合的。
我不打算在此花费更多的时间来详细描述资源文件,因为您在开始本地化应用程序和类库 DLL 时不需要处理它们。这是因为 Visual Studio 2005 和 Visual Basic 在后台提供了许多有价值的便利。但谨记,在本地化大型开发项目时,您可能需要直接使用 Resgen.exe、Al.exe 以及某些其他基于 .NET 的实用工具和类。
Visual Studio 2005 中的资源文件
现在,我将重点说明如何在基于 Windows 窗体的应用程序中使用资源文件。我解释的大部分概念也将适用于在类库 DLL 中使用资源文件。Visual Studio 2005 通过提供一个可视编辑器,使您能轻松使用资源文件。通过使用 Add New Item 命令并选择 Resources File,您可以新建一个资源文件,如图 4 所示。
图 4 用于添加资源文件的新项目模板
一旦您在项目中添加了资源文件,就不需要直接处理 .resx 文件内部所需的 XML 格式。相反,Visual Studio 提供了一个友好的可视资源设计器,如图 5 所示。利用该资源文件设计器,能够轻松添加和维护字符串以及其他的基于文件的资源(如图形和 XML 文档)。
图 5 Visual Studio 资源编辑器
如果您编译一个包含资源文件的项目,Visual Studio 会将该 .resx 文件编译为 .resources 文件,然后将它链接到所得到输出程序集的物理图像的内部。这意味着,Visual Studio 将为您处理编译资源文件以及将该文件嵌入目标程序集的图像所涉及的所有细节。
Visual Studio 还内置一个强类型资源类,并使用 Visual Basic 2005 中引入的 My 命名空间功能在 Visual Basic 项目中公开该类。这意味着,可以让 .NET ResourceManager 类加载您的资源,而不必直接针对该类进行编程。例如,如果要访问已经添加到 LitwareStrings.resx 中的资源字符串,只需编写以下代码:
Sub Main_Load(sender As Object, e As EventArgs) Handles MyBase.Load Me.Text = _ My.Resources.LitwareStrings.MainFormCaption Me.lblUserWelcome.Text = _ My.Resources.LitwareStrings.UserWelcome End Sub
C#中应使用Properties.Settings.Default
项目级资源文件
虽然您可以将一个或多个资源文件显式添加到 Visual Studio 项目中,但通常不必这么做,因为每当新建项目时,Visual Studio 都会自动包含一个项目级资源文件。可通过 Visual Studio 编辑器中的 Project Properties 对话框访问该项目级资源文件。
如果要访问项目级资源文件中的资源(如字符串),可直接从 My.Resources 类中访问:
Private Sub LoadResources() '*** load project-level resources Me.Text = My.Resources.MainFormCaption Me.lblWelcomeMessage.Text = My.Resources.UserWelcome End Sub
正如您看到的,使用强类型资源类访问资源文件中的字符串比较简单。如果在资源文件中添加了包含 XML、SQL 和 JavaScript 之类内容的图形图像和文件,则可进一步对基于文件的资源进行相同的强类型访问。例如,假定您在项目级资源文件中添加了名为 LitwareLogo.png 的图形文件和名为 Customers.xml 的 XML 文件。这些资源将自动嵌入项目的输出程序集,您可以使用以下代码以强类型方式访问它们:
Me.picLogo.Image = My.Resources.LitwareLogo Dim xmlDoc As New Xml.XmlDocument xmlDoc.LoadXml(My.Resources.Customers)
您可以看到,强类型资源类自动将 .png 文件转换为能够直接加载到 PictureBox 中的 Image 对象。强类型资源类还将嵌入的 XML 文件转换为能够轻松加载到 XmlDocument 对象中的字符串。
区域设置与本地化
将软件项目本地化以供使用不同语言的人使用是一个常见要求。例如,假设要用 Visual Basic 和 Visual Studio 2005 开发 Litware Customer Manager 应用程序,并且需要为讲英语以及讲法语的用户编写该应用程序的本地化版本。幸运的是,Microsoft .NET Framework 和 Visual Studio 有许多功能都适用于本地化应用程序和类库 DLL。
如果您是初次设计和编写需要支持本地化的基于 .NET 的软件项目,则必须很快熟悉 System.Globalization 命名空间中定义的 CultureInfo 类。CultureInfo 对象负责跟踪标识某种语言的区域名称。
English 的区域名称是 "en",French 的区域名称是 "fr"。CultureInfo 对象的区域名称还可以包含标识特定区域的附加信息,如 "en-US" 代表 US English,"en-GB" 代表 British English,以及 "fr-BE" 代表 Belgian French。以下是使用有效的区域名称创建和初始化 CultureInfo 对象的示例:
Dim culture1 As CultureInfo = New CultureInfo("en-US") Dim culture2 As CultureInfo = New CultureInfo("en-GB") Dim culture3 As CultureInfo = New CultureInfo("fr") Dim culture4 As CultureInfo = New CultureInfo("fr-BE")
实际上,需要注意两个与当前线程相关联的 CultureInfo 对象。列出的第一个 CultureInfo 对象表示当前区域,而第二个 CultureInfo 对象表示当前的 UI 区域。可以使用以下代码确定这两个 CultureInfo 对象的区域名称:
'*** determine current culture and current UI culture Dim t As Thread = Thread.CurrentThread Dim currentCulture As CultureInfo = t.CurrentCulture Dim currentUICulture As CultureInfo = t.CurrentUICulture '*** display cultures in console Console.WriteLine("Current Culture: " & currentCulture.Name) Console.WriteLine("Current UI Culture: " & currentUICulture.Name)
第一个 CultureInfo 对象称为 CurrentCulture,它并不是用于本地化应用程序中的字符串。相反,它会影响 .NET Framework 如何设置日期、数字和货币的格式。例如,如果您将 CurrentCulture 对象在 en-US 和 en-GB 之间来回修改,就会产生奇怪的副作用,例如,单一货币值就会在美元 ($100.00) 和英镑 ($100.00) 之间来回切换。正如您看到的那样,切换货币值的货币格式会导致不正确的结果。为此,即使为了使用其他语言而本地化应用程序时,保持 CurrentCulture 静止也是一个常见做法。
如果您确定确实需要以编程方式更改 CurrentCulture,则应该记住必须使用包含区域标识符的区域名称。您可以使用 en-US、en-GB 或 fr-BE 区域名称。但是,如果您尝试更改 CurrentCulture 以使其区域名称为 en 或 fr,则将收到运行时错误,因为没有区域信息并且格式要求变得过于不明确。
第二个 CultureInfo 对象称为 CurrentUICulture,它对于讨论基于 .NET 的应用程序本地化非常重要。修改与当前线程相关联的 CurrentUICulture 对象将影响 .NET Framework 和 ResourceManager 类加载包含嵌入资源的程序集的方式。特别是,它可以对加载已本地化为当前用户首选语言的资源集产生影响。
要开始本地化,需要制作多个资源文件的副本 - 对于要支持的每种语言,每个资源文件一个副本。例如,首先找到名为 Resources.resx 的项目范围的资源文件,然后制作副本。可通过在右侧的 Visual Studio Solution Explorer 中进行简单的复制-粘贴操作来完成。
一旦复制了资源文件(如 Resources.resx),就可以通过在 .resx 扩展名前面添加区域名称来重命名所复制的资源文件,如图 6 所示。例如,对于一般的法语本地化字符串,应将资源文件命名为 Resources.fr.resx;对于专为比利时本地化的法语字符串,应该将另一个资源文件命名为 Resources.fr-BE.resx。
图 6 每种语言不同的资源文件
附属程序集
当使用如图 6 所示的本地化资源文件编译项目时,Visual Studio 不会将所有资源文件编译为一个输出程序集,而是将每个本地化资源文件编译为自己的单独程序集。每个本地化程序集只包含资源,不包含代码。这种只包含资源的本地化程序集称为附属 (satellite) 程序集。如果resource中间的是错误的本地化语法字符串,将不会生成satellite assembly
每个附属程序集都与称为非特定 (neutral) 程序集的主程序集相关联。非特定程序集包含所有代码,并加载所有必要的附属程序集,以获得当前用户所需的本地化资源。在我的示例中,LitwareSmartClient.exe 是非特定程序集,它包含所有应用程序代码。随后,还有几个与 LitwareSmartClient.exe 关联的附属程序集。每个附属程序集都具有相同的文件名 LitwareSmartClient.resources.dll。
当附属程序集与非特定程序集一起在 AppBase 目录中部署时,必须根据 .NET 程序集加载器的规则进行部署。特别是,每个附属程序集都必须部署在根据其本地化区域名称命名的目录中时。例如,包含 LitwareSmartClient.exe 的 AppBase 目录应该包含一个名为 fr-BE 的子目录,以保存为 Belgian French 本地化的名为 LitwareSmartClient.resources.dll 的附属程序集。只要遵守这些规则,.NET Framework 程序集加载器在 ResourceManager 类的协助下就可以在需要时加载正确的资源集。
幸运的是,Visual Studio 知道如何正确命名附属程序集,以及如何在 .NET 程序集加载器所期望的正确目录结构中部署它们。要更好地了解各个部分如何配合工作,您只需编译项目,然后查看所得到的保存附属程序集的 AppBase 目录及其子目录的结构。
加载本地化资源
一旦创建并编辑所有需要的本地化资源文件,并编译了项目之后,就该将注意力放到如何使应用程序加载当前用户首选的正确本地化字符串集上面。完成该操作的一种方法是:获取对当前线程的引用,为 CurrentUICulture 属性分配一个新建的 CultureInfo 对象。如果使用 Visual Basic 编写基于 Windows 窗体的应用程序,还可以使用以下代码:
My.Application.ChangeUICulture("fr-BE")
C#中不能使用ChangeUICulture,使用System.Threading.Thread.CurrentThread.CurrentUICulture
= new System.Globalization.CultureInfo(“fr-BE”);代替之
在本专栏随附的示例应用程序中,我为用户添加了某些支持,以便他们能够选择首选语言,并使应用程序在关闭和重启时能够跟踪用户语言首选项。尽管可以使用注册表项维护这些类型的用户首选项,但 Visual Studio 2005 通过添加一个能够在逐个用户基础上跟踪的应用程序设置来避免使用注册表。为此,示例应用程序跟踪一个名为 UserLanguagePreference 的用户范围的应用程序设置。该应用程序还包含一个应用程序启动事件(参见图 7)。
'*** code in ApplicationEvents.vb Namespace My Partial Friend Class MyApplication Private Sub MyApplication_Startup(ByVal sender As Object, _ ByVal e As StartupEventArgs) Handles Me.Startup '*** initialize application by setting preferred language Dim lang As String = My.Settings.UserLanguagePreference My.Application.ChangeUICulture(lang) End Sub End Class End Namespace
该示例应用程序还为用户提供一组单选按钮,以便从一种语言切换到另一种语言。图 8 显示事件处理程序中响应用户的更改语言请求的代码片段。也已经点击RadioButton时的事件。
*** retrieve user's language preference Dim lang As String = CType(sender, Control).Tag '*** save user setting My.Settings.UserLanguagePreference = lang My.Settings.Save() '*** change application's UI culture My.Application.ChangeUICulture(My.Settings.UserLanguagePreference) '*** call custom method to reload localized strings LoadResources()
在Visual Studio2008中,Properties文件夹中有一Settings.settings文件,可以用于设置用户或应用程序级别的参数设置,
其本质就是config文件中增加了一段UserSetting,以上代码调用Save方法,把语言设置保存到config文件中。
现在,您看到了允许用户切换语言的所有基本代码。.NET Framework 通过在 ResourceManager 下次检索项目级资源中的字符串时加载正确的附属程序集,来响应对 My.Application.ChangeUICulture 的调用。对 ChangeUICulture 进行调用之后,应用程序随后可以重新查询应用程序级资源字符串,然后使用您先前在自定义 LoadResources 方法中看到的相同代码将这些字符串载入窗体上的控件:
Me.Text = My.Resources.MainFormCaption Me.lblWelcomeMessage.Text = My.Resources.UserWelcome
请注意,.NET 程序集加载器首先会在请求的区域名称和可用附属程序集的区域名称之间尝试查找完全匹配的语言和区域。如果 .NET 程序集加载器找不到完全匹配项,则会查找具有匹配语言的可用附属程序集。例如,如果所请求的语言是 fr-CA(Canadian French),则 .NET Framework 将会首先查找具有该语言的附属程序集。如果找不到具有 fr-CA 的附属程序集,则会查找区域名称为 fr 的附属程序集。如果 .NET Framework 找不到区域名称为 fr 的附属程序集,则会使用非特定程序集(其中嵌入了一组默认的区域资源)中找到的资源。正如您刚才看到的那样,如果 .NET Framework 找不到更具体的附属程序集,则总是回退到非特定程序集中的默认区域资源。
编译非特定程序集时,可以使用特殊属性对其进行标识,以通知 .NET Framework 默认区域资源足以应付那些要求特定语言的用户。例如,可以在基于 Windows 窗体的应用程序项目的 AssemblyInfo.vb 文件中添加程序集级别的属性,如以下代码行所示:
此处使用的 NeutralResourcesLanguage 属性可以告诉 .NET 程序集加载器,每当当前用户请求将应用程序本地化为英语时,它都可以使用默认区域资源。
本地化窗体和控件设置
您刚才已经看到如何在项目范围的基础上本地化字符串资源。该技术涉及复制和维护已本地化的资源文件。当您需要本地化特定于窗体的字符串以及窗体所包含的控件时,Visual Studio 2005 会提供某些额外的帮助。
每个窗体都有一个可以设置为 true 或 false 的 Localizable 属性。如果将该属性设置为 true(如图 9 所示),Visual Studio 2005 将在幕后为您创建和维护一组已本地化的资源文件。
如果将 Localizable 属性设置为 true,则初始 Language 设置为默认值。如果您将属性值添加到窗体及其控件的属性表,则 Visual Studio 会在幕后于一个已编译为非特定程序集的资源文件中维护它们。如果您将窗体的 Language 属性更改为特定语言(如"French (Belgium)"),则 Visual Studio 会新建一个将编译为附属程序集的本地化资源文件。在幕后,其工作方式与处理项目范围资源的方式完全相同。Visual Studio 只是消除了直接使用资源文件的需要,并允许您在为控件 Text 属性之类的事物添加属性值时使用标准属性表。
图 9 Localizable 属性
Visual Studio 需要在幕后于窗体中添加一些额外的代码来支持窗体本地化。特别是,Visual Studio 需要添加代码来加载正确的本地化资源,并将它们的值分配给窗体和控件属性。Visual Studio 2005 生成的代码使用从 ResourceManager 派生的且名为 ComponentResourceManager 的更专用的类(该类在 System.ComponentModel 命名空间中定义),而不是使用 ResourceManager。
加载本地化窗体时,Visual Studio 生成的代码会为您完成加载本地化资源所需的所有操作。特别是,Visual Studio 提供创建 ComponentResourceManager 实例的代码,这会加载正确的资源集,并分配所有必要的控件值。
然而,如果窗体已经加载,然后用户切换了语言,则必须提供某些额外的代码以使用所请求语言的资源来刷新窗体。下面显示一个使用 ComponentResourceManager 完成该目标的代码示例: 意思是程序第一次运行时会加载相应的语言,但是如果有选项可以控制界面语言的话,要调用以下方法。ComponentResourceManager.AppleResources(Object value, string objectName), value是设置resource的目标,objectName是resource里的key值。
Dim crm As ComponentResourceManager crm = New ComponentResourceManager(GetType(Main)) crm.ApplyResources(cmdAddCustomer, cmdAddCustomer.Name) crm.ApplyResources(mnuFile, mnuFile.Name) crm.ApplyResources(mnuFileAddCustomer, mnuFileAddCustomer.Name)
正如您看到的那样,您可以通过传递有关窗体的类型信息(在本例中为 Main)来创建和初始化 ComponentResourceManager 的实例。
图 10 显示完整的示例应用程序,该示例应用程序演示本专栏中讨论的所有本地化技术。该应用程序目前支持 US English、British English 和 Belgian French。另请注意,由于有一个本地化为 fr(没有任何特定区域)的附属程序集,该应用程序还为世界上使用法语的用户支持一般形式的法语。
图 10 支持本地化版本
如果您希望将来添加对其他语言的支持,也不是很复杂。这只需创建并维护附加的资源文件即可。事实上,您可以添加对新语言的支持,而不必重新编译包含所有应用程序代码的中立程序集。对于本地化应用程序和类库 DLL 而言,这是 .NET Framework 策略中最有价值的功能之一。
小结
本月的专栏通过演练如何本地化一个基于 Windows 窗体的简单应用程序,讨论了 .NET Framework 中的资源和本地化的基本知识。在下一期 Basic Instincts 专栏中,我将在本月讨论的内容的基础上继续讨论 ASP.NET 2.0 中的资源和本地化,ASP.NET 2.0 具有一些利用资源和本地化应用程序的有价值的独特功能。