在 WinForms 中以可本地化的形式在运行时更改 CurrentUICulture
Changing CurrentUICulture at runtime in a Localizable Form in WinForms
我一直在寻找如何更改 Localizable
属性设置为 true 的表单的语言。
https://msdn.microsoft.com/en-us/library/system.threading.thread.currentuiculture(v=vs.110).aspx
这是设置窗体的语言,但是需要在我们实例化窗体之前设置。在此事件后无法调用。
搜索信息,我看到了以下问题:但是,正如评论所说,我在 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 如下:
此外,我尝试使用 (System.LINQ): Controls.OfType<Label>()
(因为我有一个标签来测试它)但运气不佳...
但是当我 select 西班牙语时,没有文本发生变化。
所以也许,我对孩子们感到失望。或者也许通过调用方法 CreateCulture
,我不知道。
提前致谢!
编辑:
我已经测试过通过 Culture Info 获取我的表单的资源管理器,它 return 每次都是默认值:
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) 运气不好。好像它从不存在或被替换,但在我的设计和文件资源管理器中我可以看到这些文件。
编辑2:
可能是因为我正在使用 ILMerge 将所有 dll 合并到一个唯一的 dll 中。我正在审查这个:Single-assembly multi-language Windows Forms deployment (ILMerge and satellite assemblies / localization) - possible?
回复EDIT2:
是的,删除ILMerge问题解决了,我给出的第一个解决方案解决了这个问题。但出于某种原因,西班牙语被用作默认语言,当我试图从中获取资源集时,它什么也没给我。return。
此外,我已将 Localizable 属性设置为 false,它没有创建带有值的默认 resx 文件。我不知道这是不是一个好习惯。
我会尝试新的东西...
MVVM(模型 - 视图 - ViewModel)方法有一些好处,对你的情况很有用。
为将用于本地化的语言创建新的资源文件。使用表单自己的资源文件可能有点棘手,因为每次您在设计器中进行更改时它都会重新生成 - 所以我认为自己的资源文件将更容易维护,甚至与其他表单甚至项目共享。
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 生成静态 class LocalizationValues
并将属性作为 .resx 文件的键。所以 "Title" 可以访问为 LocalizationValues.Title
创建 "viewmodel" class 代表您在本地化中使用的所有文本。 Class 应该实现 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");
}
}
上述方法将提供比仅更新控件更多的好处。您可以清楚地分离应用程序的各个部分,可以相互独立地进行测试。
这个问题有不同的解决方案,包括其他答案中提到的MVVM。但是你也可以考虑一些其他的选择。
在所有控件上调用 ApplyResource
您可以设置当前线程的 cur运行t UI 文化和对所有控件的调用 ApplyResource
。为此,您需要为 return 所有控件创建一个方法,然后只需在所有控件上调用 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 区域性来简单地在语言之间切换。看看这个想法的例子,看看这个 post:
- How to set text to a control from resource file in design time?
经过一些尝试,我意识到有几件事失败了。
我还必须说,非常感谢在此问题中提供的所有帮助,而且 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 属性.
的那些 Controls 的名称
下一步是找到 Control 并替换它的文本...
好吧,我对 派生的 类 有点困惑,这就是为什么我创建了一个 try-catch 系统 ,因为,Controls.Find 在所有 Derived 类 中搜索我希望更具体一点,但我不知道如何......(那是为什么我创建了 this question)
这样我们就不必保存任何对象,因为我们不会清除并重新创建它们。
这里的主要问题不是我这样做的方式,因为它是正确的。
问题是 程序集合并后会做一些奇怪的事情 当你这样调用时:
ResourceSet resourceSet = new ComponentResourceManager(GetType()).GetResourceSet(value, true, true);
value
将是默认值...类似这样的 文化不存在 ,这是因为 合并程序集 找不到 resx 文件。
所以,我会尝试 AppDomain.CurrentDomain.AssemblyResolve
@ScottChamberlain 向我建议的。或者 ILRepack 也许吧。
任何优化帮助或想法(在评论中)为什么这不起作用将不胜感激!
我一直在寻找如何更改 Localizable
属性设置为 true 的表单的语言。
https://msdn.microsoft.com/en-us/library/system.threading.thread.currentuiculture(v=vs.110).aspx
这是设置窗体的语言,但是需要在我们实例化窗体之前设置。在此事件后无法调用。
搜索信息,我看到了以下问题:但是,正如评论所说,我在 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 如下:
此外,我尝试使用 (System.LINQ): Controls.OfType<Label>()
(因为我有一个标签来测试它)但运气不佳...
但是当我 select 西班牙语时,没有文本发生变化。
所以也许,我对孩子们感到失望。或者也许通过调用方法 CreateCulture
,我不知道。
提前致谢!
编辑:
我已经测试过通过 Culture Info 获取我的表单的资源管理器,它 return 每次都是默认值:
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) 运气不好。好像它从不存在或被替换,但在我的设计和文件资源管理器中我可以看到这些文件。
编辑2:
可能是因为我正在使用 ILMerge 将所有 dll 合并到一个唯一的 dll 中。我正在审查这个:Single-assembly multi-language Windows Forms deployment (ILMerge and satellite assemblies / localization) - possible?
回复EDIT2:
是的,删除ILMerge问题解决了,我给出的第一个解决方案解决了这个问题。但出于某种原因,西班牙语被用作默认语言,当我试图从中获取资源集时,它什么也没给我。return。
此外,我已将 Localizable 属性设置为 false,它没有创建带有值的默认 resx 文件。我不知道这是不是一个好习惯。
我会尝试新的东西...
MVVM(模型 - 视图 - ViewModel)方法有一些好处,对你的情况很有用。
为将用于本地化的语言创建新的资源文件。使用表单自己的资源文件可能有点棘手,因为每次您在设计器中进行更改时它都会重新生成 - 所以我认为自己的资源文件将更容易维护,甚至与其他表单甚至项目共享。
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 生成静态 class LocalizationValues
并将属性作为 .resx 文件的键。所以 "Title" 可以访问为 LocalizationValues.Title
创建 "viewmodel" class 代表您在本地化中使用的所有文本。 Class 应该实现 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");
}
}
上述方法将提供比仅更新控件更多的好处。您可以清楚地分离应用程序的各个部分,可以相互独立地进行测试。
这个问题有不同的解决方案,包括其他答案中提到的MVVM。但是你也可以考虑一些其他的选择。
在所有控件上调用 ApplyResource
您可以设置当前线程的 cur运行t UI 文化和对所有控件的调用 ApplyResource
。为此,您需要为 return 所有控件创建一个方法,然后只需在所有控件上调用 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 区域性来简单地在语言之间切换。看看这个想法的例子,看看这个 post:
- How to set text to a control from resource file in design time?
经过一些尝试,我意识到有几件事失败了。
我还必须说,非常感谢在此问题中提供的所有帮助,而且 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 属性.
的那些 Controls 的名称下一步是找到 Control 并替换它的文本...
好吧,我对 派生的 类 有点困惑,这就是为什么我创建了一个 try-catch 系统 ,因为,Controls.Find 在所有 Derived 类 中搜索我希望更具体一点,但我不知道如何......(那是为什么我创建了 this question)
这样我们就不必保存任何对象,因为我们不会清除并重新创建它们。
这里的主要问题不是我这样做的方式,因为它是正确的。 问题是 程序集合并后会做一些奇怪的事情 当你这样调用时:
ResourceSet resourceSet = new ComponentResourceManager(GetType()).GetResourceSet(value, true, true);
value
将是默认值...类似这样的 文化不存在 ,这是因为 合并程序集 找不到 resx 文件。
所以,我会尝试 AppDomain.CurrentDomain.AssemblyResolve
@ScottChamberlain 向我建议的。或者 ILRepack 也许吧。
任何优化帮助或想法(在评论中)为什么这不起作用将不胜感激!