即时更改窗体语言






4.93/5 (42投票s)
如何即时切换窗体上的语言。
引言
.NET框架为编写多语言应用程序提供了相当好的支持。本地化资源存储在单独的资源文件中,这些文件被编译成单独的DLL。当应用程序启动时,会从相应的DLL加载所选本地化设置的资源。
要了解这实际上是如何工作的,你应该深入研究Visual Studio自动生成的代码。当窗体的 `Localizable` 属性设置为 true
时,Windows窗体设计器会修改 `InitializeComponent` 方法内的代码,添加一个 `ResourceManager` 类的实例并修改属性的 set 方法;所有控件的可本地化属性都是通过调用 `ResourceManager` 的 `GetObject` 或 `GetString` 方法来设置的,这使得 `ResourceManager` 负责加载相应的本地化资源。这些方法会为当前线程的区域性(culture)设置的语言加载相应的值。值得注意的是,不仅显示的标题被本地化,单个控件的其他设置(如位置、大小和可见性)也被本地化。这在控件需要针对不同语言进行不同布局的情况下非常有用。
更改窗体语言最简单的方法是在窗体构造函数中调用 `InitializeComponent` 之前,设置应用程序线程的UI语言。显然,这需要重启应用程序才能更改UI的语言。也就是说,`InitializeComponent` 不仅加载资源,还初始化窗体上的所有控件。在同一次应用程序运行中第二次调用它,会创建一组新的控件,并附加到现有的集合中。这些新控件将不可见,因为它们被最初创建的控件覆盖了,除非它们的位置和/或大小不同。此外,重新加载资源会重置像文本框这类控件的内容。
那么,如果你想在不重启应用程序的情况下更改UI语言,并保留用户所做的更改,该怎么办呢?本文提供了一种可能的解决方案。
背景
基本思想是重新扫描父窗体和所含控件的所有属性,并将其可本地化的值设置为新选择的区域性。最简单的方法是使用反射获取所有属性的列表。然而,这种“无选择性”的方法可能会导致意想不到的更改。例如,我们通常不想更改 `TextBox` 控件的 `Text` 属性,但希望更新 `Label` 控件的此属性。此外,重新加载父窗体的 `Location` 属性很可能会重新定位它。因此,属性是选择性地重新加载的,属性列表是硬编码的。
我们从父窗体开始,重新加载其可本地化的属性,然后递归遍历其包含的控件。对每个控件重复此过程,如果它们存在,则递归其包含的控件。
private void ChangeFormLanguage(Form form) {
form.SuspendLayout();
Cursor.Current = Cursors.WaitCursor;
ResourceManager resources = new ResourceManager(form.GetType());
// change main form resources
form.Text = resources.GetString("$this.Text", m_cultureInfo);
ReloadControlCommonProperties(form, resources);
ToolTip toolTip = GetToolTip(form);
// change text of all containing controls
RecurControls(form, resources, toolTip);
// change the text of menus
ScanNonControls(form, resources);
form.ResumeLayout();
}
`ReloadControlCommonProperties` 方法重新加载了大多数控件共有的硬编码属性列表。
protected virtual void ReloadControlCommonProperties(Control control,
ResourceManager resources) {
SetProperty(control, "AccessibleDescription", resources);
SetProperty(control, "AccessibleName", resources);
SetProperty(control, "BackgroundImage", resources);
SetProperty(control, "Font", resources);
SetProperty(control, "ImeMode", resources);
SetProperty(control, "RightToLeft", resources);
SetProperty(control, "Size", resources);
// following properties are not changed for the form
if (!(control is System.Windows.Forms.Form)) {
SetProperty(control, "Anchor", resources);
SetProperty(control, "Dock", resources);
SetProperty(control, "Enabled", resources);
SetProperty(control, "Location", resources);
SetProperty(control, "TabIndex", resources);
SetProperty(control, "Visible", resources);
}
if (control is ScrollableControl) {
// reloads properties specific to ScrollableControl:
// AutoScroll, AutoScrollMargin, AutoScrollMinSize
ReloadScrollableControlProperties((ScrollableControl)control, resources);
if (control is Form) {
// reloads properties specific to Form control only:
// AutoScaleBaseSize, Icon, MaximumSize and MinimumSize
ReloadFormProperties((Form)control, resources);
}
}
}
`SetProperty` 方法为传入名称的属性重新加载一个值。
private void SetProperty(Control control, string propertyName,
ResourceManager resources) {
PropertyInfo propertyInfo = control.GetType().GetProperty(propertyName);
if (propertyInfo != null) {
string controlName = control.Name;
if (control is Form)
controlName = "$this";
object resObject = resources.GetObject(controlName + "." +
propertyName, m_cultureInfo);
if (resObject != null)
propertyInfo.SetValue(control, Convert.ChangeType(resObject,
propertyInfo.PropertyType), null);
}
}
首先,它检查该控件是否存在具有此名称的属性。如果 `GetProperty` 方法返回一个非空值(这意味着该属性确实存在),`ResourceManager` 会尝试获取该资源的值;如果成功获取到该值,则相应的属性会被更改。
该方法利用反射来使属性设置变得通用。通过 `Convert` 类的静态 `ChangeType` 方法实现到适当类型的转换。
这种通用方法的替代方案是将每次对 `SetProperty` 方法的调用替换为带有硬编码类型转换的相应代码。虽然这种方法会使代码执行得更快,但它有一些陷阱。例如,不同类中存在名称相同但类型不同的属性:`TabControl` 中的 `Appearance` 属性是 `TabAppearance` 枚举类型,而在 `CheckBox` 中它是 `Appearance` 枚举类型。因此,这种方法需要更广泛的类型检查,并且更容易出错。
`RecurControls` 方法扫描 `Controls` 集合中的所有项,重新加载其属性并递归遍历其包含的控件。由于任何包含的控件都可能有关联的本地化文本,我们还必须传递对父窗体 `ToolTip` 对象的引用。
private void RecurControls(Control parent,
ResourceManager resources, ToolTip toolTip) {
foreach (Control control in parent.Controls) {
ReloadControlCommonProperties(control, resources);
ReloadControlSpecificProperties(control, resources);
if (toolTip != null)
toolTip.SetToolTip(control, resources.GetString(control.Name +
".ToolTip", m_cultureInfo));
if (control is UserControl)
RecurUserControl((UserControl)control);
else {
ReloadTextForSelectedControls(control, resources);
// change ListBox and ComboBox items
ReloadListItems(control, resources);
if (control is TreeView)
ReloadTreeViewNodes((TreeView)control, resources);
if (control.Controls.Count > 0)
RecurControls(control, resources, toolTip);
}
}
}
由于包含的控件可以是任何类型,除了所有控件共有的属性外,我们还必须检查某些控件特有的属性,这在下面给出的 `ReloadControlSpecificProperties` 方法中完成。
protected virtual void
ReloadControlSpecificProperties(System.Windows.Forms.Control control,
System.Resources.ResourceManager resources) {
// ImageIndex property for ButtonBase, Label,
// TabPage, ToolBarButton, TreeNode, TreeView
SetProperty(control, "ImageIndex", resources);
// ToolTipText property for StatusBar, TabPage, ToolBarButton
SetProperty(control, "ToolTipText", resources);
// IntegralHeight property for ComboBox, ListBox
SetProperty(control, "IntegralHeight", resources);
// ItemHeight property for ListBox, ComboBox, TreeView
SetProperty(control, "ItemHeight", resources);
// MaxDropDownItems property for ComboBox
SetProperty(control, "MaxDropDownItems", resources);
// MaxLength property for ComboBox, RichTextBox, TextBoxBase
SetProperty(control, "MaxLength", resources);
// Appearance property for CheckBox, RadioButton, TabControl, ToolBar
SetProperty(control, "Appearance", resources);
// CheckAlign property for CheckBox and RadioBox
SetProperty(control, "CheckAlign", resources);
// FlatStyle property for ButtonBase, GroupBox and Label
SetProperty(control, "FlatStyle", resources);
// ImageAlign property for ButtonBase, Image and Label
SetProperty(control, "ImageAlign", resources);
// Indent property for TreeView
SetProperty(control, "Indent", resources);
// Multiline property for RichTextBox, TabControl, TextBoxBase
SetProperty(control, "Multiline", resources);
// BulletIndent property for RichTextBox
SetProperty(control, "BulletIndent", resources);
// RightMargin property for RichTextBox
SetProperty(control, "RightMargin", resources);
// ScrollBars property for RichTextBox, TextBox
SetProperty(control, "ScrollBars", resources);
// WordWrap property for TextBoxBase
SetProperty(control, "WordWrap", resources);
// ZoomFactor property for RichTextBox
SetProperty(control, "ZoomFactor", resources);
}
可以看出,`RecurControls` 方法对任何包含的子控件进行递归调用。
如果包含的控件是 `UserControl`,则必须为其初始化一个新的 `ResourceManager`,以便可能从外部DLL加载资源。此外,必须获取 `UserControl` 的 `ToolTip` 对象以将引用传递给 `RecurControls` 方法。
private void RecurUserControl(UserControl userControl) {
ResourceManager resources = new ResourceManager(userControl.GetType());
ToolTip toolTip = GetToolTip(userControl);
RecurControls(userControl, resources, toolTip);
}
一些UI组件,如 `MenuItem`、`StatusBarPanel` 和 `ListView` 中的 `ColumnHeader`,不包含在 `Controls` 集合中。这些组件是父窗体的直接成员,所以我们使用反射来访问它们(下面的代码有所简化)。
protected virtual void ScanNonControls(Form form, ResourceManager resources) {
FieldInfo[] fieldInfo = form.GetType().GetFields(BindingFlags.NonPublic
| BindingFlags.Instance | BindingFlags.Public);
for (int i = 0; i < fieldInfo.Length; i++) {
object obj = fieldInfo[i].GetValue(form);
string fieldName = fieldInfo[i].Name;
if (obj is MenuItem) {
MenuItem menuItem = (MenuItem)obj;
menuItem.Enabled = (bool)(resources.GetObject(fieldName +
".Enabled", m_cultureInfo));
// etc.
}
if (obj is StatusBarPanel) {
StatusBarPanel panel = (StatusBarPanel)obj;
panel.Alignment =
(HorizontalAlignment)(resources.GetObject(fieldName +
".Alignment", m_cultureInfo));
// etc.
}
if (obj is ColumnHeader) {
ColumnHeader header = (ColumnHeader)obj;
header.Text = resources.GetString(fieldName + ".Text", m_cultureInfo);
header.TextAlign =
(HorizontalAlignment)(resources.GetObject(fieldName +
".TextAlign", m_cultureInfo));
header.Width = (int)(resources.GetObject(fieldName + ".Width", m_cultureInfo));
}
if (obj is ToolBarButton) {
ToolBarButton button = (ToolBarButton)obj;
button.Enabled = (bool)(resources.GetObject(fieldName +
".Enabled", m_cultureInfo));
// etc.
}
}
}
使用代码
所描述的过程实现为一个 `FormLanguageSwitchSingleton` 类,它有两个公共方法:`ChangeCurrentThreadUICulture` 和 `ChangeLanguage`,后者有两个重载实现。该类在 `System.Globalization` 命名空间中实现。
使用 `FormLanguageSwitchSingleton` 有两种可能的情景。
- 首先通过调用 `ChangeCurrentThreadUICulture` 方法来更改当前线程的区域性,然后调用 `ChangeLanguage` 方法,像这样:
CultureInfo newCulture = new CultureInfo("de"); FormLanguageSwitchSingleton.Instance.ChangeCurrentThreadUICulture(newCulture); FormLanguageSwitchSingleton.Instance.ChangeLanguage(this);
- 只调用接受额外 `CultureInfo` 参数的 `ChangeLanguage` 方法的重载版本。
CultureInfo newCulture = new CultureInfo("de"); FormLanguageSwitchSingleton.Instance.ChangeLanguage(this, newCulture);
在第二种情景中,只有当前打开的窗体会被“翻译”成所提供的区域性;所有后续打开的窗体将使用当前应用程序线程的区域性。读者可以在作为下载示例提供的 *TestMDIApp* 中观察这两种情景之间的差异;通过勾选/取消勾选*更改语言*对话框中的选项。
该单例类的以下方法被设为 virtual
,以允许用户重写它们。
- `ReloadTextForSelectedControls`;当前实现为 `AxHost`、`ButtonBase`、`GroupBox`、`Label`、`ScrollableControl`、`StatusBar`、`TabControl`、`ToolBar` 控件类型执行此操作。
- `ReloadControlCommonProperties`(实现见上文);
- `ReloadControlSpecificProperties`(参见上文);
- `ScanNonControls`(参见上文);
- `ReloadListItems` - 重新加载 `ComboBox` 和 `ListBox` 控件中的项。此外,如果项未排序,则保留项的选择。
`FormLanguageSwitchSingleton` 被编译成一个DLL。要使用该类,只需将相应的类库添加到引用列表中。在项目源码提供的两个测试示例中给出了使用示例。
关注点
`ListBox`、`ComboBox` 和 `DomainUpDown` 中的 `Items`,以及 `TreeView` 中的 `TreeNodes` 分别由 `ReloadListBoxItems`、`ReloadComboBoxItems`、`ReloadUpDownItems` 和 `ReloadTreeViewNodes` 方法重新加载。然而,需要注意的是,没有对单个项/节点的引用。它们是在相应方法的 `Items`/`Nodes` 属性的 `AddRange` 方法中作为无名对象加载的,例如:
this.listBox.Items.AddRange(new object[]
{
resources.GetString("listBox.Items.Items"),
resources.GetString("listBox.Items.Items1"),
resources.GetString("listBox.Items.Items2")
}
);
从上面的代码可以看出,项的名称是由控件名称,后跟两个“Items”字符串创建的,对于除第一项外的所有项,在第二个字符串后附加一个数字索引。因此,这些名称必须动态创建。
private void ReloadItems(string controlName, IList list, int itemsNumber,
System.Resources.ResourceManager resources) {
string resourceName = controlName + ".Items.Items";
list.Clear();
list.Add(resources.GetString(resourceName, m_cultureInfo));
for (int i = 1; i < itemsNumber; i++)
list.Add(resources.GetString(resourceName + i, m_cultureInfo));
}
历史
- 版本 1.0 - 初始发布 (2004年12月6日)。
- 版本 1.1 - 一些错误修复,包括 Gregory Bleiker 和 Piotr Sielski 注意到的问题 (2005年3月21日)。
- 版本 1.2 - 通过调用 GetSafeValue 方法使获取资源更安全。它首先查找所选语言的本地化资源;如果未找到,则返回默认语言的值。同时(希望)修复了“找到不明确的匹配”错误。