65.9K
CodeProject 正在变化。 阅读更多。
Home

即时更改窗体语言

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (42投票s)

2004年12月6日

Zlib

6分钟阅读

viewsIcon

311436

downloadIcon

12674

如何即时切换窗体上的语言。

引言

.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` 有两种可能的情景。

  1. 首先通过调用 `ChangeCurrentThreadUICulture` 方法来更改当前线程的区域性,然后调用 `ChangeLanguage` 方法,像这样:
    CultureInfo newCulture = new CultureInfo("de");
    FormLanguageSwitchSingleton.Instance.ChangeCurrentThreadUICulture(newCulture);
    FormLanguageSwitchSingleton.Instance.ChangeLanguage(this);
  2. 只调用接受额外 `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 方法使获取资源更安全。它首先查找所选语言的本地化资源;如果未找到,则返回默认语言的值。同时(希望)修复了“找到不明确的匹配”错误。
© . All rights reserved.