摘要:Chris Sells 討論無類型清單資源和有類型資源,它們是受 Microsoft .NET 框架支持的兩種資源。他定義了這兩種資源,并介紹了如何在您自己的應用程序中使用它們。
假設要在應用程序中通過從文件加載位圖來設置窗體的背景圖像: public Form1() {
...
// Load a file from the file system
this.BackgroundImage =
new Bitmap(@"C:\WINDOWS\Web\Wallpaper\Azul.jpg");
}
該代碼的問題是,并非所有 Microsoft Windows 的安裝實例都有 Azul.jpg,即使是那些確實具有該文件的安裝實例,該文件可能也不在安裝實例的相同位置。即使您與應用程序一起交付該圖片,節省空間的用戶也可能決定刪除它,這會導致您的應用程序出錯。確保圖片或任何文件與代碼在一起的唯一安全方式是將它作為資源嵌入并加載。
清單資源
資源是在編譯時添加到程序集中的。例如,如果您使用命令行編譯器,則可以使用 /resource 開關嵌入資源: C:\>csc.exe myApp.cs /resource:c:\windows\web\wallpaper\Azul.jpg
/resource 開關將文件作為資源嵌入,嵌入時使用文件名(沒有路徑)作為資源名稱。文件嵌入到程序集的清單 資源集中。程序集的清單由一組作為程序集一部分的元數據組成。該元數據的一部分是與每個嵌入資源關聯的名稱和數據。執行 ildasm 時,可以在清單部分看見程序集清單資源的列表,如圖 1 所示。 C:\>ildasm.exe myApp.exe
圖 1. ildasm 顯示嵌入資源
可以像 ildasm 一樣枚舉清單資源的列表,這需要使用 System.Reflection.Assembly 類的 GetManifestResourceNames 方法: using System.Reflection;
...
// Get this type's assembly
Assembly assem = this.GetType().Assembly;
// Enumerate the assembly's manifest resources
foreach( string resourceName in assem.GetManifestResourceNames() ) {
MessageBox.Show(resourceName);
}
一旦通過枚舉清單資源或硬編碼一個您想要的清單資源而知道了清單資源的名稱,就可以通過 Assembly 類的 GetManifestResourceStream 方法將該清單資源作為原始字節流進行加載,如下所示: using System.IO;
public Form1() {
...
// Get this type's assembly
Assembly assem = this.GetType().Assembly;
// Get the stream that holds the resource
// NOTE1: Make sure not to close this stream!
// NOTE2: Also be very careful to match the case
// on the resource name itself
Stream stream =
assem.GetManifestResourceStream("Azul.jpg");
// Load the bitmap from the stream
this.BackgroundImage = new Bitmap(stream);
}
因為資源可以像類型名稱一樣有沖突,所以最好用資源自己的“命名空間”來嵌入資源,該操作可以使用 /resource 開關的擴展格式來完成: C:\>csc myApp.cs /resource:c:\...\azul.jpg,ResourcesApp.Azul.jpg
注意在要嵌入的文件名的逗號后面使用的備用資源名稱。備用資源名稱允許您為資源任意地提供時間嵌套名稱,不管文件名是什么。它是設置在程序集中的備用名稱,如圖 2 所示。
圖 2. 使用備用名稱的嵌入資源
public Form1() {
...
// Get this type's assembly
Assembly assem = this.GetType().Assembly;
// Load a resource with an alternate name
Stream stream =
assem.GetManifestResourceStream("ResourcesApp.Azul.jpg");
// Load the bitmap from the stream
this.BackgroundImage = new Bitmap(stream);
}
為了更方便,如果您的資源和加載資源的類碰巧使用了相同的命名空間,則可以將類的類型作為可選的第一參數傳遞給 GetManifestResourceStream: namespace ResourcesApp {
public class Form1 : Form {
public Form1() {
...
// Get this type's assembly
Assembly assem = this.GetType().Assembly;
// Load the resource using a namespace
// Will load resource named "ResourcesApp.Azul.jpg"
Stream stream =
assem.GetManifestResourceStream(this.GetType(), "Azul.jpg");
// Load the bitmap from the stream
this.BackgroundImage = new Bitmap(stream);
}
...
}
}
GetManifestResourceStream 將使用如下格式編寫資源名稱: <namespace>.<fileName>
在加載某些類型(比如 Bitmap 類)時,使用類型和文件名也是有用的,這樣可以通過提供構造函數避免由您自己打開流:
namespace ResourcesApp { public class Form1 : Form { public Form1() { ... // Get this type's assembly Assembly assem = this.GetType().Assembly; // Load the bitmap directly from the manifest resources this.BackgroundImage = new Bitmap(this.GetType(), "Azul.jpg"); } ... }}
Visual Studio .NET 中的清單資源
如果(大多數情況下)您使用 Visual Studio?NET 來開發和構建程序集,則用命令行嵌入清單資源的方法不可能非常吸引您。這種情況下,您可以將資源添加到 Windows 窗體項目中,該方法將把合適的命令行參數傳遞給編譯器。
要將資源添加到項目中,請在 Solution Explorer 中右鍵單擊項目,然后選擇 Add New Item,并選擇您想作為資源嵌入的文件。文件將復制到項目的目錄中,但仍然不會被嵌入。要使文件作為資源嵌入,請右鍵單擊文件,并選擇 Properties,然后將 Build Action 從 Content(默認)更改為 Embedded Resource,如圖 3 所示。
圖 3. 將文件的 Build Action 設置為 Embedded Resource
這種嵌入資源的方法會使 Visual Studio .NET 為您創建一個備用資源名,其組成類似這樣: <defaultNamespace>.<folderName>.<fileName>
資源名稱的默認命名空間部分是項目本身的默認命名空間,它是通過 Solution Explorer->(右鍵單擊)->Properties->Common Properties->General->Default Namespace 來設置的。由于這是在生成新類時,新類得到的相同命名空間,所以這就使通過使用類型和部分資源名稱來加載資源變得很方便。如果文件碰巧位于項目的子文件夾中,就需要在文件夾名稱中包括子文件夾,并用點替換反斜杠。例如,一個名為 Azul.jpg 的位圖位于項目根下面的 foo\bar 文件夾中,要加載它就需要這樣做: // If this code called within the ResourcesApp.Form1 class,
// and the file is \foo\bar\Azul.jpg,
// will load ResourcesApp.foo.bar.Azul.jpg
this.BackgroundImage =
new Bitmap(this.GetType(), "foo.bar.Azul.jpg");
有類型資源
盡管文件有擴展名,但清單資源是在沒有類型信息的情況下嵌入的。例如,如果 Azul.jpg 文件的名稱實際上是 Azul.quux,這對于 Bitmap 類來說是沒有差別的,因為這個類將通過查看數據本身來確定其類型(JPEG、PNG、GIF 等)。這就需要由您來將每個資源的類型正確映射為加載該資源所需的對象的類型。
但如果您愿意多走一步,則可以用一個類型來標記資源。.NET 框架支持用于資源的一組擴展元數據,其中包括兩種格式的 MIME 類型信息,一個是文本格式,另一個是二進制格式。這兩種格式都有內置的讀取器,以便在運行時取得類型正確的資源。
基于文本的格式是特定于.NET 框架的 XML 格式,稱為 ResX(.resx 文件)。不考慮其 XML 基礎,該格式不是專門為人工閱讀而設計的(XML 格式很少是這樣的)。但是,Visual Studio .NET 仍然為 .resx 文件提供了一個基本編輯器。要將新的 .resx 文件添加到 Visual Studio .NET 項目中,請從 Project 菜單中選擇 Add New Item,然后選擇 Assembly Resource File 模板,如圖 4 所示。
圖 4. 將 .resx 文件添加到項目中
到寫本文時為止,即使空的 .resx 文件也是 42 行 XML,而其中大多數是架構信息。架構允許 .resx 文件中有任意數目的項,每項都有名稱、值、注釋、類型和 MIME 類型。圖 5 顯示了有兩個項的 .resx 文件,即名為 MyString 的字符串和名為 MyImage 的圖像。
圖 5. 設計器的數據視圖中簡單的 .resx 文件
遺憾的是,只有字符串項能夠在 .resx 編輯器的數據視圖中實際進行編輯。任何二進制數據都需要手動直接輸入到 XML 中(而且只能是 base64 編碼)。因此,直接使用 .resx 文件只對字符串資源有用(盡管間接使用會使 .resx 文件對任何種類的數據都非常有用,我們隨后將討論這一點)。
來自 System.Resources 命名空間的 ResXResourceReader 類將分析 XML 文件,并公開一組命名的、有類型的值。要取得具體的項需要查找它: using System.Collections;
using System.Resources;
...
public Form1() {
...
using( ResXResourceReader reader =
new ResXResourceReader(@"Resource1.resx") ) {
foreach( DictionaryEntry entry in reader ) {
if( entry.Key.ToString() == "MyString" ) {
// Set form caption from string resource
this.Text = entry.Value.ToString();
}
}
}
}
使用 Add New Item 對話框將 .resx 文件添加到項目中會使該文件作為 Embedded Resource 添加進項目,而編譯項目時則會導致 .resx 數據作為嵌套資源 嵌入(“嵌套資源”是分組到命名容器中的資源)。容器的名稱與作為資源添加的任何文件相同,只是不使用 .resx 擴展名,使用 .resource 擴展名。假定一個項目的默認命名空間是 ResourcesApp 而 .resx 文件名為 Resources1.resx,則嵌套資源的容器名為 ResourcesApp.Resources1.resx,如圖 6 中的 ildasm 所示。
圖 6. 嵌入的 .resources 文件
.resources 擴展名來自于在將 .resx 文件作為資源嵌入之前 Visual Studio .NET 處理該文件時所使用的工具。工具名稱是 resgen.exe,它用來將 .resx XML 格式“編譯”為二進制格式?梢允謩訉 .resx 文件編譯成 .resources 文件,如下所示: C:\> resgen.exe Resource1.resx
在將 .resx 文件編譯成 .resources 文件以后,就可以使用 System.Resources 命名空間中的 ResourceReader 來枚舉它: using( ResourceReader reader =
new ResourceReader(@"Resource1.resources") ) {
foreach( DictionaryEntry entry in reader ) {
string s = string.Format("{0} ({1})= '{2}'",
entry.Key, entry.Value.GetType(), entry.Value);
MessageBox.Show(s);
}
}
除了類的名稱和輸入格式,ResourceReader 類的使用方法與 ResXResourceReader 相同,包括都不能隨機訪問命名項。
所以,雖然您可以將 .resx 文件轉換成 .resources 文件,并使用 /resource 編譯器命令行開關嵌入它,但容易得多的方法是直接在項目中讓 Visual Studio .NET 接受被標記為 Embedded Resources 的 .resx 文件,然后將它編譯進 .resources 文件并嵌入它,如圖 4、圖 5 和圖 6 所示。一旦將 .resources 文件捆綁為資源,訪問 .resources 文件中的資源就只需執行兩個步驟的過程: // 1. Load embedded .resources file
using( Stream stream =
assem.GetManifestResourceStream(
this.GetType(), "Resource1.resources") ) {
// 2. Find resource in .resources file
using( ResourceReader reader = new ResourceReader(stream) ) {
foreach( DictionaryEntry entry in reader ) {
if( entry.Key.ToString() == "MyString" ) {
// Set form caption from string resource
this.Text = entry.Value.ToString();
}
}
}
}
因為 ResourceReader 和 ResXResourceReader 都需要該兩步過程才能找到具體的資源,因此 .NET 框架提供了 ResourceManager 類,該類公開了一個更簡單的使用模型。
資源管理器ResourceManager 類也來自 System.Resources 命名空間,該類包裝了 ResourceReader,用于在構造時枚舉資源,并使用其名稱公開它們:
public Form1() {
...
// Get this type's assembly
Assembly assem = this.GetType().Assembly;
// Load the .resources file into the ResourceManager
// Assumes a file named "Resource1.resx" as part of the project
ResourceManager resman =
new ResourceManager("ResourcesApp.Resource1", assem);
// Set form caption from string resource
this.Text = (string)resman.GetObject("MyString"); // The hard way
this.Text = resman.GetString("MyString"); // The easy way
}
用來查找 .resources 文件的命名方式與命名任何其他種類的資源相同(注意追加到 Resource1.resources 文件中的項目默認命名空間的使用方法),只是 .resources 擴展名是假定的,并且不能包括在名稱中。為了更方便,如果您碰巧將一個 .resx 文件命名為類型名稱,則 .resources 文件和程序集的名稱將從類型確定: // Use the type to determine resource name and assembly
ResourceManager resman = new ResourceManager(this.GetType());
一旦已經創建了資源管理器的實例,就可以通過使用 GetObject 方法并強制轉換為合適的類型,從而按名稱找到嵌套資源。如果使用 .resx 文件來處理字符串資源,則可以使用 GetString 方法,該方法將執行到 System.String 類型的強制轉換。
設計器資源
缺少用于 .resx 文件的合適的編輯器使它們在使用除字符串資源以外的任何其他資源時非常困難。您不僅必須通過手動編寫代碼才能在運行時輸入數據,而且無法在設計時看見資源的使用情況;例如,窗體的背景圖像。
幸運的是,設計器再次在這里幫助了我們。如果打開 Visual Studio .NET Solution Explorer,并選擇 Show All Files 按鈕,您將看見每個組件(無論它是窗體、控件還是簡單的組件)都有相應的 .resx 文件。這是為了讓資源與組件的屬性保持關聯,這種關聯是在 Property Browser 中設置的。例如,如果設置窗體的 BackgroundImage 屬性,那么不僅在設計器中窗體將顯示背景圖像,而且窗體的 .resx 文件將包含該圖像的對應項。同樣,如果在相同窗體上設置 PictureBox 控件的 Image 屬性,則 .resx 文件同樣會增大以便包括該資源。這兩項都可以在圖 7 中看到。
圖 7. 組件的 .resx 文件
每個組件的 .resx 文件將作為 .resources 文件進行編譯和嵌入,就像已經將您自己的 .resx 文件添加到項目中一樣,這將使資源能夠在運行時被組件使用。除了組件的 .resx 文件中的項之外,設計器還會將代碼添加到 InitializeComponent 中,以便加載組件的資源管理器,并使用從資源獲得的對象來填充組件的屬性: namespace ResourcesApp {
public class Form1 : Form {
...
private void InitializeComponent() {
ResourceManager resources = new ResourceManager(typeof(Form1));
...
this.pictureBox1.Image =
(System.Drawing.Bitmap)resources.GetObject("pictureBox1.Image");
...
this.BackgroundImage =
(System.Drawing.Bitmap)resources.GetObject("$this.BackgroundImage");
...
}
}
}
注意 ResourceManager 對象是使用組件的類型來構造的,該類型用來構造組件的 .resources 資源名稱。還要注意設計器在命名資源時所使用的命名約定。對于組件字段上的屬性,名稱的格式是: <fieldName>.<propertyName>
$this.<propertyName>
如果您想添加供組件本身使用的自定義字符串屬性,您可以這樣做,但要確保與設計器生成的名稱格式不同。
我們所處的位置
Microsoft .NET 框架支持兩種資源 — 無類型清單資源和有類型資源。通過將文件的 Build Action 設置為 Embedded Resource,可以讓 Visual Studio .NET 支持無類型清單資源,并通過 .resx 文件(可以是自定義文件或作為組件資源的備份存儲)支持有類型資源。清單資源的好處是,它們可在 IDE 中直接編輯,而有類型資源需要做特別的工作才能編輯,但可提供有類型訪問。兩種資源類型都有某些嚴格的命名要求,所以在編寫方法調用來加載它們時要格外小心。 |