使用语言转换器工具在 WPF 中进行可插拔的样式和资源






4.96/5 (17投票s)
在本文中,我展示了如何为样式、语言或任何静态对象等构建可插拔资源。因此,构建新样式不会影响您的代码,即使应用程序已在生产环境中,您也可以轻松地将任何新样式插入应用程序中。
引言
随着 WPF 的发展,我们面临许多非常基础但又非常重要的问题。在我最近的 WPF 应用程序中,我发现为应用程序中使用的 `Styles` 和 `Themes` 构建坚实的基础非常重要。在构建应用程序时,我们创建资源。有些资源我们放在资源字典中,有些则放在窗口本身。因此,当我们最终发布代码时,我们发现更改主题是一项巨大的任务。在本文中,我将讨论如何通过将 `ResourceDictionary` 对象放入另一个库并使用 **.NET Reflection 将资源直接插入到您的应用程序中**来轻松操作样式。
关于反射的注意事项
反射是任何 Windows 应用程序中非常重要的一部分。我们需要反射来调用未与项目直接引用的对象。在两个程序集之间保持强引用通常会使它们紧密耦合。这意味着这两个组件完全相互依赖,并且单个元素不能独立存在。
反射允许您通过指定 UNC 路径从任何位置读取 DLL,并允许您创建对象和调用方法。`System.Reflection` 带有 `Assembly`、`Type`、`Module`、`MemberInfo`、`PropertyInfo`、`ConstructorInfo`、`FieldInfo`、`EventInfo`、`ParameterInfo`、`Enum` 等类,用于调用任何 .NET 对象的各种功能。例如
Assembly thisAssembly = Assembly.LoadFile(fi.FullName);
var object = thisAssembly.CreateInstance("MyType");
因此,对象将持有 `MyType` 的实例。与此类似,每个 `Type` 都有方法可以获取所有 `MethodInfo`、`FieldInfo`、`PropertyInfo` 等,您可以通过上面创建的对象调用它们并完成您的工作。
在本文中,我们将从反射中添加几行代码,以从特定文件夹中插入样式和语言。您可以从 MSDN 反射 阅读更多关于反射的信息。
属性的实现
由于我们将从应用程序中引用外部 DLL,因此为每个外部实体定义入口点非常重要。为了定义外部实体,我创建了一个类库,它在 `MainApplication` 和 `Resources` 之间进行协调。`ResourceBase` 库将定义一些属性,这些属性稍后将用于从 DLL 调用成员。
需要注意的是,我们将资源创建为单独的 DLL。这些属性将允许我们获取 DLL 本身的元数据。

为了使每个资源都兼容,我创建了一个接口
public interface IResourceAttribute
{
string Name { get; set; }
string Description { get; set; }
string ResourceClassPath { get; set; }
}
`IResourceAttribute` 定义了三个属性。`Name` 我们将用于调用资源,`Description` 资源的描述,以及 `ResourceClassPath`,这对于识别在程序集中创建适当资源的类的路径非常重要。
现在让我们创建一个具体的属性,允许我们输入特定于每种类型的元数据。
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple=true, Inherited=true)]
public class LocalizationResourceAttribute : Attribute, IResourceAttribute
{
public CultureInfo Culture { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string ResourceClassPath { get; set; }
public LocalizationResourceAttribute(string culture)
{
this.Culture = new CultureInfo(culture);
}
}
在这里您可以看到 `LocalizationResourceAttribute` 引入了 `CultureInfo` 对象。这将允许您定义特定于应用程序当前文化的文化。
与此类似
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = true)]
public class StyleResourceAttribute : Attribute, IResourceAttribute
{
public string Name { get; set; }
public string Description { get; set; }
public string ResourceClassPath { get; set; }
/// <summary>
/// Defines the Color Base to be used
/// </summary>
public Color BaseColor { get; set; }
public StyleResourceAttribute(string name)
{
this.Name = name;
}
}
这里的 `BaseColor` 将允许您的 DLL 为默认应用程序公开颜色基色。
请注意,我们使用 `AttributeTargets.Assembly`,因为我们需要属性存在于程序集级别。`AllowMultiple = true` 允许您在同一个程序集中创建多个资源。因此,我们可以在同一个 DLL 中拥有多个样式。
样式的实现
现在我们准备好了,让我们尝试创建一些样式,看看它们在应用程序上的效果如何。首先,让我们创建一个新的类库,并引用 *PresentationCore.dll*、*PresentationFramework.dll* 和 *WindowsBase.dll*,因为任何应用程序都明确需要它们。
注意:您还需要添加您希望从样式中引用的 DLL。例如,如果您需要 WPFToolKit,您可以在此处进行。
接下来,您需要添加我们刚刚生成的自定义 DLL。您可以在上图中看到我添加了我们自己的自定义 *ResourceBase.dll*,我们将使用它来用特殊属性标记程序集。
现在是为您的应用程序实现样式的时候了。
<ResourceDictionary x:Class="GoldenBlack.GoldResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Custom="http://schemas.microsoft.com/wpf/2008/toolkit">
<Color x:Key="ShadeHover">#F9E4B7</Color>
<Color x:Key="ShadeBack">#F48519</Color>
<Color x:Key="ShadeHighlight">#F9BE58</Color>
<!-- Your code goes here -->
<Style TargetType="{x:Type TextBlock}"
x:Key="tbBasic">
<Setter Property="FontFamily"
Value="Calibri" />
<Setter Property="FontSize"
Value="18"></Setter>
<Setter Property="ScrollViewer.CanContentScroll"
Value="true" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility"
Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Auto" />
<Setter Property="Foreground"
Value="{StaticResource ShadeHighlightBrush}"></Setter>
</Style>
</ResourceDictionary>
完成创建自定义资源后,您需要创建一个类。只需从解决方案资源管理器中添加一个新的类文件并将其设为 `public`。在您的资源文件中,您需要添加 `x:Class="YourNamespace.Yourclass"`。因此,您需要将 `x:Class` 添加为类的确切逻辑路径。在我的例子中,它是 `x:Class="GoldenBlack.GoldResource"`。所以类将如下所示
namespace GoldenBlack
{
public partial class GoldResource : ResourceDictionary
{
public GoldResource()
{
InitializeComponent();
}
}
}
实际上,在添加任何资源时,.NET 会隐式地为其创建一个类,然后添加它。由于我们需要使用反射手动完成此操作,因此您需要添加自定义类并在其构造函数中添加 `InitializeComponent`。换句话说,您需要创建一个继承自 `ResourceDictionary` 的自定义类,并在其默认构造函数中使用 `InitializeComponent`。
所以最后,是时候编译并生成一个您可以用于主应用程序的程序集了。
在此之前,您需要在 `AssemblyInfo` 文件中添加几行,您可以在 *Properties* 文件夹中找到它。
[assembly: StyleResourceAttribute("GoldenBlack",
Description = "Theme with Black and Gold",
ResourceClassPath = "GoldenBlack.GoldResource")]
这将在 `Assembly` 的元数据中添加一个特殊属性,以确保程序集实际上是一个 `Style`。我们稍后将从应用程序中解析此属性并生成我们的实际程序集。
`ResourceClassPath` 起着至关重要的作用。它让我们了解实际资源存在的位置。因此,为库中的资源指定确切的 `classPath` 非常重要。
注意:我使用了 AllowMultiple=true,这将使您能够将多个样式添加到同一个程序集中。
创建主应用程序
现在是时候转到主应用程序并查看如何动态应用样式了。为了简单起见,我添加了一个名为 `ResourceUtil` 的新类,并使用 *app.config* 在程序加载时动态加载 `Style`。
public static class ResourceUtil
{
public static Dictionary<IResourceAttribute, Assembly> AvailableStyles =
new Dictionary<IResourceAttribute, Assembly>();
public static Color BaseColor { get; set; }
public static ResourceDictionary GetAppropriateDictionary()
{
//Get Styles Folder path
string path = ConfigurationSettings.AppSettings["stylefolderpath"];
string currentTheme = ConfigurationSettings.AppSettings["CurrentTheme"];
ResourceUtil.LoadAssemblies(AvailableStyles, path);
IResourceAttribute currentResource =
AvailableStyles.Keys.FirstOrDefault<IResourceAttribute>(item =>
item.Name.Equals(currentTheme));
StyleResourrceAttribute sra= currentResource as StyleResourceAttribute;
if(sra != null)
BaseColor = sra.BaseColor;
// We can do this as we are fetching from AvailableStyles.
if (currentResource != null)
{
Assembly currentAssembly = AvailableStyles[currentResource];
Type resourceType =
currentAssembly.GetType(currentResource.ResourceClassPath);
ConstructorInfo cinfo = resourceType.GetConstructor(Type.EmptyTypes);
ResourceDictionary dictionary = cinfo.Invoke(new object[] { })
as ResourceDictionary;
return dictionary;
}
return null;
}
private static void LoadAssemblies(Dictionary<IResourceAttribute, Assembly>
resource, string path)
{
DirectoryInfo di = new DirectoryInfo(Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), path));
foreach (FileInfo fi in di.GetFiles())
{
try
{
Assembly thisAssembly = Assembly.LoadFile(fi.FullName);
var attributes = thisAssembly.GetCustomAttributes(true);
IEnumerable<object> resourceAttributes =
attributes.Where(item => item is IResourceAttribute);
foreach (IResourceAttribute raatr in resourceAttributes)
AvailableStyles.Add(raatr, thisAssembly);
}
catch { }
}
}
}
在上面的代码中,您可以看到 Application `LoadAssemblies` 实际上从提供的文件夹路径加载程序集。因此,在我们的例子中,我们从为样式明确指定的文件夹中加载所有程序集。所以 `ResourceUtil.LoadAssemblies` 将加载指定为 `AvailableStyles` 路径的文件夹中的所有程序集。
现在是调用资源并获取 `ResourceDictionary` 对象的时候了。由于我们现在没有实际的 `Dictionary` 对象,因为我们没有对加载的程序集的强引用,因此我们为此目的使用反射。
IResourceAttribute currentResource =
AvailableStyles.Keys.FirstOrDefault<IResourceAttribute>(item =>
item.Name.Equals(currentTheme));
上述行筛选出 `AvailableStyles` 中加载的所有程序集,并仅给出 *app.config* 中指定的 `currentTheme` 匹配的 `Resource` 对象。
由于属性还具有 `BaseColor`,我们还需要添加该功能。因此,我们将颜色放置到 `BaseColor` 对象中。
所以最后,让我们为 `Application.Startup` 创建一个处理程序,并放置几行来加载 `Dictionary`。
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
ResourceDictionary dictionary = ResourceUtil.GetAppropriateDictionary();
if (dictionary != null)
this.Resources.MergedDictionaries.Add(dictionary);
}
}
这将向应用程序添加新的 `ResourceDictionary`。因此,样式已应用。
等等,这还没结束。您还需要对您的应用程序进行一些调整。这意味着您只能通过使用 `DynamicResource` 而不是 `StaticResource` 来引用样式。`StaticResource` 尝试在编译时查找资源,因此在我们的例子中它将找不到它。所以我们的示例代码将如下所示
<Window x:Class="MainWpfApplication.MyMainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MyMainWindow"
Background="{DynamicResource ShadeFadedBackgroundBrush}">
<Grid>
<DockPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock DockPanel.Dock="Top" MinWidth="400"
Style="{DynamicResource tbBasic}" Text="This is my custom Text"/>
<Button Content="Accept" Click="Button_Click"
DockPanel.Dock="Top" Style="{DynamicResource btnBasic}"/>
</DockPanel>
</Grid>
</Window>
您可以看到我已将所有 `StaticResource` 元素替换为 `DynamicResource`,因此我们开放了在运行时更改样式的能力。
现在,将 DLL 放置到 *app.config* 中指定的应用程序目录中,然后运行应用程序。
因此,当您将 *app.config* 的 `CurrentTheme` 键从 `SilverRed` 更改为 `GoldenBlack` 时,您可以看到 `Style` 已更改。瞧,我们完成了。
如果您愿意,可以动态加载资源。为此,您需要持有已添加到应用程序中的当前资源,然后移除当前主题并使用以下代码添加 `ResourceDictionary`
((App)Application.Current).Resources.MergedDictionaries.Add(dictionary);
因此,您可以轻松地使应用程序根据用户交互动态加载资源。
使用其他资源
这还不是结束。几天前,我介绍了一种将资源作为多语言应用程序技术的方法。如果您不记得,可以从
所以让我们将其扩展到基于插件的语言应用程序。
以几乎相同的方式工作,我们向 `ResourceUtil` 添加了一个新方法,用于根据用户的当前语言设置返回适当的 `ResourceDictionary`。
所以 `ResourceDictionary` 将如下所示
<ResourceDictionary x:Class="LanguageResource.EnglishResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="rKeychangetheme" >Change Theme</system:String>
<system:String x:Key="rKeycustomtext" >This is my custom Text</system:String>
<system:String x:Key="rKeyaccept" >Accept</system:String>
</ResourceDictionary>
与此类似,我们添加了一个法语字典。为了向您展示如何在同一个库中使用多个资源,我将 `FrenchResourceDictionary` 添加到同一个文件夹中
<ResourceDictionary x:Class="LanguageResource.FrenchResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="rKeychangetheme">Changer de thème</system:String>
<system:String x:Key="rKeycustomtext">C'est mon texte personnalisé</system:String>
<system:String x:Key="rKeyaccept">Accepter</system:String>
</ResourceDictionary>
您会注意到键都以相同的方式维护,而值已修改。现在是编译程序集的时候了。在此之前,让我们将自定义属性添加到项目的 *AssemblyInfo.cs* 文件中。
[assembly: LocalizationResource("en-US",
Name = "English dictionary", Description = "For English Dictionary",
ResourceClassPath = "LanguageResource.EnglishResource")]
[assembly: LocalizationResource("fr-Fr",
Name = "French dictionary", Description = "For French Dictionary",
ResourceClassPath = "LanguageResource.FrenchResource")]
我已将这两种资源都添加到同一个 DLL 中,因此您必须将它们都添加到 `AssemblyInfo` 中。我们稍后会将它们加载到主应用程序中。
现在与此类似,让我们使用 `DynamicResources` 修改主 XAML 代码中的 `string`s
<Grid>
<DockPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock DockPanel.Dock="Top" FontSize="20"
Style="{DynamicResource tbBasic}" x:Name="tbCurrentTheme" />
<TextBlock DockPanel.Dock="Top" MinWidth="400"
Style="{DynamicResource tbBasic}"
Text="{DynamicResource rKeycustomtext}"/>
<Button Content="{DynamicResource rKeyaccept}"
Click="Button_Click" DockPanel.Dock="Top"
Style="{DynamicResource btnBasic}"/>
</DockPanel>
</Grid>
所以 `ResourceUtil` 语言方法的微小改动如下
public static ResourceDictionary GetAppropriateLanguage()
{
//Get Language Folder path
string path = ConfigurationSettings.AppSettings["languagefolderpath"];
CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
ResourceUtil.LoadAssemblies(AvailableDictionaries, path);
IResourceAttribute currentResource =
AvailableDictionaries.Keys.FirstOrDefault<IResourceAttribute>(
item =>
{
LocalizationResourceAttribute la = item as
LocalizationResourceAttribute;
if (la != null)
return la.Culture.Equals(currentCulture);
return false;
});
if (currentResource != null)
{
Assembly currentAssembly = AvailableDictionaries[currentResource];
Type resourceType =
currentAssembly.GetType(currentResource.ResourceClassPath);
ConstructorInfo cinfo = resourceType.GetConstructor(Type.EmptyTypes);
ResourceDictionary dictionary = cinfo.Invoke(new object[] { })
as ResourceDictionary;
return dictionary;
}
return null;
}
因此,我们根据区域设置加载语言。您可以根据自己的需要更改逻辑。
要更改语言设置,您可以尝试
所以应用程序看起来像
因此,您可以看到文本根据添加到应用程序中的语言进行了修改。与此类似,对象资源也可以插入。
语言工具
创建语言资源对我来说常常非常无聊。所以我想如果能为您提供一个将一种资源转换为另一种资源的工具会很棒。所以,如果您只构建了一个字符串资源并希望为您的客户提供对多种资源的支持,请尝试我的语言转换器为您生成资源文件。
您可以从 使用 Bing 翻译的语言资源转换器 获取带完整源代码的语言转换工具,或阅读我的博客文章 WPF 资源生成工具。
运行应用程序后,您将看到如上所示的用户界面。您需要选择目标文件,即我们要构建的文件。让我先创建英文资源,并将目标选为同一个文件。英文文件如下所示
<ResourceDictionary x:Class="LanguageResource.EnglishResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="rKeychangetheme" >Change Theme</system:String>
<system:String x:Key="rKeycustomtext" >This is my custom Text</system:String>
<system:String x:Key="rKeyaccept" >Accept</system:String>
</ResourceDictionary>
在目标位置,您需要指定转换器将转换为的名称,然后单击“转换”。
资源将立即转换为法语,并且键将保持不变。
<ResourceDictionary x:Class="LanguageResource.FrenchResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="rKeychangetheme">Changer de thème</system:String>
<system:String x:Key="rKeycustomtext">C'est mon texte personnalisé</system:String>
<system:String x:Key="rKeyaccept">Accepter</system:String>
</ResourceDictionary>
这就是我们应用程序所需的。我已将 Bing 翻译支持的所有语言添加到此工具中,以便您可以将资源从任何语言更改为任何其他语言。
要了解有关此工具的更多信息,请继续阅读 WPF 资源生成工具。
结论
我认为可插拔资源是每个应用程序都需要的。我们早在需要样式和资源之前就构建了应用程序。功能是任何应用程序的主要内容。但是遵循这些基本准则将使您非常容易地添加可插拔主题。
希望这篇文章对您有所帮助。感谢阅读。