【问题标题】:Changing CurrentUICulture at runtime in a Localizable Form in WinForms在 WinForms 中以可本地化的形式在运行时更改 CurrentUICulture
【发布时间】:2018-02-10 22:56:02
【问题描述】:

我一直在搜索如何更改将 Localizable 属性设置为 true 的表单的语言。

https://msdn.microsoft.com/en-us/library/system.threading.thread.currentuiculture(v=vs.110).aspx

这是设置表单的语言,但是这个需要在我们实例化表单之前设置。此事件后无法调用。

搜索信息时,我看到了以下问题:https://stackoverflow.com/a/11738932/3286975 但是,正如评论所说,我在 TabControl 和 MenuStrip 中有控件,因此它们不受影响。

我试图通过获得表单的所有控件来修改这一点,但没有运气。

在此菜单中,我调用以下回调:

    private void englishToolStripMenuItem_Click_1(object sender, EventArgs e)
    {
        string lang = (string) ((ToolStripMenuItem) sender).Tag;
        base.Culture = CultureInfo.CreateSpecificCulture(lang);
    }

    private void spanishToolStripMenuItem_Click(object sender, EventArgs e)
    {
        string lang = (string) ((ToolStripMenuItem) sender).Tag;
        base.Culture = CultureInfo.CreateSpecificCulture(lang);
    }

我通过使用标签来改变文化。

当我点击它时,什么也没有发生。另外,我已经从提到的答案中稍微修改了 ApplyResources 方法。

private void ApplyResources(Control parent, CultureInfo culture)
{
        this.resManager.ApplyResources(parent, parent.Name, culture);

        foreach (Control ctl in parent.IterateAllChildren())
        {
            //this.ApplyResources(ctl, culture);
            this.resManager.ApplyResources(ctl, ctl.Name, culture);
        }
}

IterateAllChildren 如下:https://stackoverflow.com/a/16725020/3286975

另外,我尝试使用 (System.LINQ):Controls.OfType<Label>()(因为我有一个标签可以测试这个),但没有运气......

但是当我选择西班牙语时,文本没有改变。

所以也许,我对孩子们不满意。或者可能通过调用方法CreateCulture,我不知道。

提前致谢!

编辑:

我已经测试通过文化信息获取我表单的资源管理器,它每次都返回默认值:

 ResourceSet resourceSet = new ResourceManager(typeof(frmCredentials)).GetResourceSet(new CultureInfo(lang), true, true);
            foreach (DictionaryEntry entry in resourceSet)
            {
                string resourceKey = entry.Key.ToString();
                object resource = entry.Value; //resourceSet.GetString(resourceKey);
                if (resource.GetType().Equals(typeof(string)))
                    Console.WriteLine("Key: {0}\nValue: {1}\n\n", resourceKey, (string) resource);
            }

new CultureInfo(lang),我也测试过:new CultureInfo("es") & Thread.CurrentThread.CurrentCulture (CurrentUICulture) 没有运气。就像它永远不存在或被替换,但在我的设计和文件资源管理器中我可以看到这些文件。

EDIT2:

也许是因为我正在使用 ILMerge 将所有 dll 合并到一个唯一的 dll 中。我正在审查这个:Single-assembly multi-language Windows Forms deployment (ILMerge and satellite assemblies / localization) - possible?

回复 EDIT2:

是的,删除 ILMerge 问题就解决了,我给出的第一个解决方案解决了这个问题。但是由于某种原因,西班牙语被视为默认语言,当我尝试从中获取资源集时,它没有返回任何内容。

Aso,我已将 Localizable 属性设置为 false,并且它没有创建带有值的默认 resx 文件。我不知道这是否是一个好习惯。

我会尝试一些新的...

【问题讨论】:

  • 我承认下面的方法不是很好,但是既然你知道你有一个TabControl 和一个MenuStrip,你可以修改你的IterateAllChildren 方法来专门监视这些控件并进行特殊处理以确保您涵盖了他们所有的子控件。如果您可以处理表单中所有数据的清除(例如,首先保存),那么重新初始化表单(如@Isma 建议的那样)将是一种更好的方法。
  • > stackoverflow.com/questions/21067507/… StackOverflow 问题上指出的解决方案就像一个魅力。
  • 嗯...如果您这样做,这是一个很好的方法:stackoverflow.com/questions/5411694/… 感谢您的帮助,但我更喜欢更简单的方法,如果没有更多选择,我会试试这个。
  • 您可以使用AppDomain.CurrentDomain.AssemblyResolve,而不是使用ILMerge,这解决了ILMerge 经常引起的命名空间问题。 There is even a library that will automatically embed the files and add the assembly load code for you。 (其实 IL 合并的创建者在第一个链接的 cmets 中回应 “作为 ILMerge 的作者,我认为这太棒了!如果我知道这一点,我永远不会写 ILMerge。” )

标签: c# winforms currentculture


【解决方案1】:

MVVM(模型 - 视图 - 视图模型)方法有一些对您的情况有用的好处。

为您将用于本地化的语言创建新的资源文件。使用表单自己的资源文件可能有点棘手,因为每次您在设计器中进行更改时它都会重新生成 - 所以我认为自己的资源文件将更容易维护,甚至与其他表单甚至项目共享。

LocalizationValues.resx // (default english), set Access Modifier to "Internal" or "Public"
    "Title": "Title"
    "Description": "Description"

LocalizationValues.es.resx
    "Title": "Título"
    "Description": "Descripción"

Visual Studio 生成静态类LocalizationValues,其属性作为 .resx 文件的键。所以“标题”可以访问为LocalizationValues.Title

创建代表您在本地化中使用的所有文本的“viewmodel”类。类应该实现INotifyPropertyChanged 接口。

public class LocalizationViewModel : INotifyPropertyChanged
{
    public string Title
    {
        get
        {
            return LocalizationValues.Title;
        }
    }

    public string Description
    {
        get
        {
            return LocalizationValues.Description;
        }
    }

    public void SetLanguage(string language)
    {
        var culture = new CultureInfo(language);
        Thread.CurrentThread.CurrentUICulture = culture;

        // This is important, 
        // By raising PropertyChanged you notify Form to update bounded controls
        NotifyAllPropertyChanged();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyAllPropertyChanged()
    {
        // Passing empty string as propertyName
        // will indicate that all properties of viewmodel have changed
        NotifyPropertyChanged(string.Empty);
    }

    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

然后在Form中绑定viewmodel到控件

public partial class YourForm : Form
{
    private LocalizationViewModel _viewmodel;

    public YourForm()
    {
        InitializeComponent();

        _viewmodel = new LocalizationViewModel();

        // Bound controls to correspondent viewmodel's properties
        LblTitle.DataBindings.Add("Text", _viewmodel, "Title", true);
        LblDescription.DataBindings.Add("Text", _viewmodel, "Description", true);
    }

    // Menu buttons to change language
    private void SpanishToolStripMenuItem_Click(object sender, EventArgs e)
    {
        _viewmodel.SetLanguage("es");
    }

    private void EnglishToolStripMenuItem_Click(object sender, EventArgs e)
    {
        _viewmodel.SetLanguage("en");
    }
}

上述方法将比仅更新控件提供更多好处。您会得到应用程序的清晰分离的部分,可以相互独立地进行测试。

【讨论】:

  • 您对NotifyPropertyChanged(nameof(...)) 的两次调用可以简化。如果您刚刚对PropertyChanged 事件执行NotifyPropertyChanged("") 引发空字符串或null,则表明所有属性都已更改并且必须重新评估所有属性。注释在the remarks section of the documentation for the event itself
  • @ScottChamberlain - 很好,不知道这一点。应仔细阅读文档 - 实际上会节省时间。谢谢
【解决方案2】:

解决问题有不同的解决方案,包括其他答案中提到的 MVVM。但您也可以考虑其他一些选择。

在所有控件上调用 ApplyResource

您可以在所有控件上设置当前线程的当前 UI 文化和调用 ApplyResource。为此,您需要创建一个返回所有控件的方法,然后在所有控件上调用ApplyResource,例如:

private void englishToolStripMenuItem_Click(object sender, EventArgs e)
{
    SetCulture("en-US");
}
private void persianToolStripMenuItem_Click(object sender, EventArgs e)
{
    SetCulture("fa-IR");
}
public void SetCulture(string cultureName)
{
    System.Threading.Thread.CurrentThread.CurrentUICulture =
       System.Globalization.CultureInfo.GetCultureInfo(cultureName);
    var resources = new System.ComponentModel.ComponentResourceManager(this.GetType());
    GetChildren(this).ToList().ForEach(c => {
        resources.ApplyResources(c, c.Name);
    });
}
public IEnumerable<Control> GetChildren(Control control)
{
    var controls = control.Controls.Cast<Control>();
    return controls.SelectMany(ctrl => GetChildren(ctrl)).Concat(controls);
}

创建文本本地化扩展器组件

您还可以创建可在设计时和运行时使用的扩展器组件,并将一些资源文件和资源键分配给控件。这样,您可以通过更改当前线程的当前 UI 文化来简单地在语言之间切换。只是想看看这个想法的一个例子,看看这个帖子:

【讨论】:

  • 我现在的主要问题是使用 ILMerge 的程序集不起作用。所以,我必须弄清楚,谢谢你的回答!
  • 这可能是您的下一个问题。不要在一个帖子中混合多个问题:)
  • 我需要这样做,我会发布我的解决方案,但是谢谢!
  • 我可以说提供的解决方案将解决您在常见应用程序中的问题。我对不同情况下的问题没有任何想法。如果您发布一些 minimal reproducible example ,我会看看您的其他问题 :)
  • 对了,不要错过第二个想法。它具有高度可扩展性。在大多数情况下,我更喜欢我的第一个解决方案,但在某些情况下,我扩展了第二个想法,并使用它通过设计时支持为控件文本提供本地化。
【解决方案3】:

经过一些尝试,我意识到有几件事失败了。

我还不得不说,非常感谢在这个问题上提供的所有帮助,而且LocalizedForm snippet 非常有用。

第一个问题是我已经意识到,使用此解决方案的子层次结构中级别超过 1 的控件不起作用。

遍历所有控件是一项昂贵的任务。也许我的最差,但迭代次数较少(因为我只搜索带有文本的控件)

    /// <summary>
    /// Current culture of this form
    /// </summary>
    [Browsable(false)]
    [Description("Current culture of this form")]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public CultureInfo Culture
    {
        get { return this.culture; }
        set
        {
            if (this.culture != value)
            {
                ResourceSet resourceSet = new ComponentResourceManager(GetType()).GetResourceSet(value, true, true);
                IEnumerable<DictionaryEntry> entries = resourceSet
                    .Cast<DictionaryEntry>()
                    .Where(x => x.Key.ToString().Contains(".Text"))
                    .Select(x => { x.Key = x.Key.ToString().Replace(">", "").Split('.')[0]; return x; });

                foreach (DictionaryEntry entry in entries)
                {
                    if (!entry.Value.GetType().Equals(typeof(string))) return;

                    string Key = entry.Key.ToString(),
                           Value = (string) entry.Value;

                    try
                    {
                        Control c = Controls.Find(Key, true).SingleOrDefault();
                        c.Text = Value;
                    }
                    catch
                    {
                        Console.WriteLine("Control {0} is null in form {1}!", Key, GetType().Name);
                    }
                }

                this.culture = value;
                this.OnCultureChanged();
            }
        }
    }

我做的是以下,首先,我搜索ResourceManager,小心!因为这是第二个问题,如果您使用 CultureInfo.CreateSpecificCulture 而不是 CultureInfo.GetCultureInfo 在某些情况下将创建新的文化并返回默认值(Form1.resx 值而不是 Form1.es.resx 值(例如)) .

一旦我们从 resx 文件中加载了所有的值,我们就会遍历所有的值,并删除双倍 >>(在某些情况下会出现),然后我们会得到它们的名称控件只声明了 Text 属性

下一步是找到Control并替换它的Text...

好吧,我对派生类有点混乱,这就是我创建try-catch系统的原因,因为,Controls.Find 搜索派生类我希望更具体一点,但我不知道如何......(这就是我创建this question的原因)

有了这个,我们不需要保存任何对象,因为我们不会清除和重新创建它们。

这里的主要问题不是我这样做的方式,因为它是正确的。 问题是 Assemblies 合并做奇怪的事情 当你调用例如这个:

ResourceSet resourceSet = new ComponentResourceManager(GetType()).GetResourceSet(value, true, true);

value 将是默认值...类似这样的文化不存在,是因为,合并的大会找不到resx 文件。

所以,我会尝试@ScottChamberlain 向我建议的AppDomain.CurrentDomain.AssemblyResolve。或者 ILRepack 可能。

任何关于优化的帮助或关于为什么这不起作用的想法(在 cmets 中)将不胜感激!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多