JavaFX:在 运行 上更改应用程序语言
JavaFX: Change application language on the run
我正在使用 FXML 中描述的核心组件制作 JavaFX 桌面应用程序,我想为用户提供更改语言的选项。但是,我还没有找到任何直接的方法来如何在从 FXML 加载组件后更改语言。
问题是有没有标准的方法来处理 JavaFX 中的语言切换。
我目前正在使用单例(在后期可能会通过 DI 框架注入)作为语言 ResourceBundle 的包装器。
我的计划是实现可观察模式并通知所有需要更改的组件(使用@FXML 语句注入的子组件)。
你可以这样做。正如在您的回答中,您要么希望将其实现为单例,要么使用 DI 框架在任何需要的地方注入单个实例:
public class ObservableResourceFactory {
private ObjectProperty<ResourceBundle> resources = new SimpleObjectProperty<>();
public ObjectProperty<ResourceBundle> resourcesProperty() {
return resources ;
}
public final ResourceBundle getResources() {
return resourcesProperty().get();
}
public final void setResources(ResourceBundle resources) {
resourcesProperty().set(resources);
}
public StringBinding getStringBinding(String key) {
return new StringBinding() {
{ bind(resourcesProperty()); }
@Override
public String computeValue() {
return getResources().getString(key);
}
};
}
}
现在您可以执行以下操作:
ObservableResourceFactory resourceFactory = .... ;
resourceBundle.setResources(...);
Label greetingLabel = new Label();
greetingLabel.textProperty().bind(resourceFactory.getStringBinding("greeting"));
任何时候你用
更新资源
resourceFactory.setResources(...);
将导致标签更新其文本。
这是一个 SSCCE(对于将 ResourceBundle
强制为单个可运行 class...的极其丑陋的方式表示歉意...)
import java.util.ListResourceBundle;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ResourceBundleBindingExample extends Application {
private static final String RESOURCE_NAME = Resources.class.getTypeName() ;
private static final ObservableResourceFactory RESOURCE_FACTORY = new ObservableResourceFactory();
static {
RESOURCE_FACTORY.setResources(ResourceBundle.getBundle(RESOURCE_NAME));
}
@Override
public void start(Stage primaryStage) {
ComboBox<Locale> languageSelect = new ComboBox<>();
languageSelect.getItems().addAll(Locale.ENGLISH, Locale.FRENCH);
languageSelect.setValue(Locale.ENGLISH);
languageSelect.setCellFactory(lv -> new LocaleCell());
languageSelect.setButtonCell(new LocaleCell());
languageSelect.valueProperty().addListener((obs, oldValue, newValue) -> {
if (newValue != null) {
RESOURCE_FACTORY.setResources(ResourceBundle.getBundle(RESOURCE_NAME, newValue));
}
});
Label label = new Label();
label.textProperty().bind(RESOURCE_FACTORY.getStringBinding("greeting"));
BorderPane root = new BorderPane(null, languageSelect, null, label, null);
root.setPadding(new Insets(10));
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class LocaleCell extends ListCell<Locale> {
@Override
public void updateItem(Locale locale, boolean empty) {
super.updateItem(locale, empty);
if (empty) {
setText(null);
} else {
setText(locale.getDisplayLanguage(locale));
}
}
}
public static class ObservableResourceFactory {
private ObjectProperty<ResourceBundle> resources = new SimpleObjectProperty<>();
public ObjectProperty<ResourceBundle> resourcesProperty() {
return resources ;
}
public final ResourceBundle getResources() {
return resourcesProperty().get();
}
public final void setResources(ResourceBundle resources) {
resourcesProperty().set(resources);
}
public StringBinding getStringBinding(String key) {
return new StringBinding() {
{ bind(resourcesProperty()); }
@Override
public String computeValue() {
return getResources().getString(key);
}
};
}
}
public static class Resources extends ListResourceBundle {
@Override
protected Object[][] getContents() {
return new Object[][] {
{"greeting", "Hello"}
};
}
}
public static class Resources_fr extends ListResourceBundle {
@Override
protected Object[][] getContents() {
return new Object[][] {
{"greeting", "Bonjour"}
};
}
}
public static void main(String[] args) {
launch(args);
}
}
和@Chiggiddi 一样,我也喜欢国际化的字符串方法。在 java 端绑定每个标签太繁琐了。所以我想出了一种在 fxml 端结合绑定和表达式绑定的混合方法。我今天在这里分享这个解决方案,因为我在我访问过的所有 Whosebug 问题中都没有找到类似的解决方案。希望对大家有所帮助。
首先创建一个由 ResourceBundle 的键填充的地图的可观察地图,如下所示:
public class LocaleManager extends SimpleMapProperty<String, Object> {
private String bundleName = "language"; // a file language.properties must be present at the root of your classpath
public LocaleManager() {
super(FXCollections.observableHashMap());
reload();
}
public void changeLocale(Locale newLocale) {
Locale.setDefault(newLocale);
reload();
}
private void reload() {
ResourceBundle bundle = ResourceBundle.getBundle(bundleName);
Enumeration<String> keys = bundle.getKeys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
String value = bundle.getString(key);
String[] parts = key.split("\.");
MapProperty<String, Object> map = this;
for (int i=0;i < parts.length; i++) {
String part = parts[i];
if (i == parts.length - 1) {
map.put(part, value);
} else {
if (!map.containsKey(part)) {
map.put(part, new SimpleMapProperty<>(FXCollections.observableHashMap()));
}
map = (MapProperty<String, Object>)map.get(part);
}
}
}
}
public StringBinding bind(String key) {
String[] parts = key.split("\.");
MapProperty<String, Object> map = this;
for (int i=0;i < parts.length; i++) {
String part = parts[i];
if (i == parts.length - 1) {
return Bindings.valueAt(map, part).asString();
} else {
if (!map.containsKey(part)) {
map.put(part, new SimpleMapProperty<>(FXCollections.observableHashMap()));
}
map = (MapProperty<String, Object>)map.get(part);
}
}
throw new NullPointerException("Unknown key : " + key);
}
}
现在您需要为您的视图创建一个基础 class,将 LocaleManager 公开为一个包含 getter 和 setter 的 属性:
public class BaseView {
private LocaleManager lang;
public BaseView() {
lang = new LocaleManager();
}
public LocaleManager langProperty() {
return lang;
}
public ObservableMap<String, Object> getLang() {
return lang.get();
}
public void setLang(MapProperty<String, Object> resource) {
this.lang.set(resource);
}
}
现在,如果您的视图扩展了 BaseView
public MyView extends BaseView {}
fxml 中的任何表达式,如 ${controller.lang.my.resource.key} 都将绑定到 ResourceBundle
中的相同键
java 端的绑定仍然可以使用:
someField.textProperty().bind(langProperty().bind(BUNDLE_KEY));
现在要即时更改语言,只需使用:
langProperty().changeLocale(newLocale);
如果您想更改所有应用程序的语言,请记住在您的应用程序中将 LocaleManager 设为单例。
尚不支持在 SceneBuilder 上为字符串字段绑定表达式。
但如果将来接受以下拉取请求可能会有所帮助:
编辑:
为了回答@Novy 评论,绑定密钥包含 2 个部分。第一部分“controller.lang”让您可以访问控制器的语言属性。这与在您的控制器中写入 this.getLang()
相同 class
因此,如果在您的 属性 文件中,您具有以下属性:
item1 = "somestring"
item1.item2 = "someotherstring"
item1.item2.item3 = "someotherotherstring"
然后
${controller.lang.item1} == "somestring"
${controller.lang.item1.item2} == "someotherstring"
${controller.lang.item1.item2.item3} == "someotherotherstring"
${controller.lang.item1.item2} 可以在您的控制器中转换为以下 java 代码 class
((Map<String, Object)this.getLang().get("item1")).get("item2").toString()
或使用 javafx
提供的隐式转换
this.getLang().get("item1").get("item2")
我正在使用 FXML 中描述的核心组件制作 JavaFX 桌面应用程序,我想为用户提供更改语言的选项。但是,我还没有找到任何直接的方法来如何在从 FXML 加载组件后更改语言。
问题是有没有标准的方法来处理 JavaFX 中的语言切换。
我目前正在使用单例(在后期可能会通过 DI 框架注入)作为语言 ResourceBundle 的包装器。
我的计划是实现可观察模式并通知所有需要更改的组件(使用@FXML 语句注入的子组件)。
你可以这样做。正如在您的回答中,您要么希望将其实现为单例,要么使用 DI 框架在任何需要的地方注入单个实例:
public class ObservableResourceFactory {
private ObjectProperty<ResourceBundle> resources = new SimpleObjectProperty<>();
public ObjectProperty<ResourceBundle> resourcesProperty() {
return resources ;
}
public final ResourceBundle getResources() {
return resourcesProperty().get();
}
public final void setResources(ResourceBundle resources) {
resourcesProperty().set(resources);
}
public StringBinding getStringBinding(String key) {
return new StringBinding() {
{ bind(resourcesProperty()); }
@Override
public String computeValue() {
return getResources().getString(key);
}
};
}
}
现在您可以执行以下操作:
ObservableResourceFactory resourceFactory = .... ;
resourceBundle.setResources(...);
Label greetingLabel = new Label();
greetingLabel.textProperty().bind(resourceFactory.getStringBinding("greeting"));
任何时候你用
更新资源resourceFactory.setResources(...);
将导致标签更新其文本。
这是一个 SSCCE(对于将 ResourceBundle
强制为单个可运行 class...的极其丑陋的方式表示歉意...)
import java.util.ListResourceBundle;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ResourceBundleBindingExample extends Application {
private static final String RESOURCE_NAME = Resources.class.getTypeName() ;
private static final ObservableResourceFactory RESOURCE_FACTORY = new ObservableResourceFactory();
static {
RESOURCE_FACTORY.setResources(ResourceBundle.getBundle(RESOURCE_NAME));
}
@Override
public void start(Stage primaryStage) {
ComboBox<Locale> languageSelect = new ComboBox<>();
languageSelect.getItems().addAll(Locale.ENGLISH, Locale.FRENCH);
languageSelect.setValue(Locale.ENGLISH);
languageSelect.setCellFactory(lv -> new LocaleCell());
languageSelect.setButtonCell(new LocaleCell());
languageSelect.valueProperty().addListener((obs, oldValue, newValue) -> {
if (newValue != null) {
RESOURCE_FACTORY.setResources(ResourceBundle.getBundle(RESOURCE_NAME, newValue));
}
});
Label label = new Label();
label.textProperty().bind(RESOURCE_FACTORY.getStringBinding("greeting"));
BorderPane root = new BorderPane(null, languageSelect, null, label, null);
root.setPadding(new Insets(10));
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class LocaleCell extends ListCell<Locale> {
@Override
public void updateItem(Locale locale, boolean empty) {
super.updateItem(locale, empty);
if (empty) {
setText(null);
} else {
setText(locale.getDisplayLanguage(locale));
}
}
}
public static class ObservableResourceFactory {
private ObjectProperty<ResourceBundle> resources = new SimpleObjectProperty<>();
public ObjectProperty<ResourceBundle> resourcesProperty() {
return resources ;
}
public final ResourceBundle getResources() {
return resourcesProperty().get();
}
public final void setResources(ResourceBundle resources) {
resourcesProperty().set(resources);
}
public StringBinding getStringBinding(String key) {
return new StringBinding() {
{ bind(resourcesProperty()); }
@Override
public String computeValue() {
return getResources().getString(key);
}
};
}
}
public static class Resources extends ListResourceBundle {
@Override
protected Object[][] getContents() {
return new Object[][] {
{"greeting", "Hello"}
};
}
}
public static class Resources_fr extends ListResourceBundle {
@Override
protected Object[][] getContents() {
return new Object[][] {
{"greeting", "Bonjour"}
};
}
}
public static void main(String[] args) {
launch(args);
}
}
和@Chiggiddi 一样,我也喜欢国际化的字符串方法。在 java 端绑定每个标签太繁琐了。所以我想出了一种在 fxml 端结合绑定和表达式绑定的混合方法。我今天在这里分享这个解决方案,因为我在我访问过的所有 Whosebug 问题中都没有找到类似的解决方案。希望对大家有所帮助。
首先创建一个由 ResourceBundle 的键填充的地图的可观察地图,如下所示:
public class LocaleManager extends SimpleMapProperty<String, Object> {
private String bundleName = "language"; // a file language.properties must be present at the root of your classpath
public LocaleManager() {
super(FXCollections.observableHashMap());
reload();
}
public void changeLocale(Locale newLocale) {
Locale.setDefault(newLocale);
reload();
}
private void reload() {
ResourceBundle bundle = ResourceBundle.getBundle(bundleName);
Enumeration<String> keys = bundle.getKeys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
String value = bundle.getString(key);
String[] parts = key.split("\.");
MapProperty<String, Object> map = this;
for (int i=0;i < parts.length; i++) {
String part = parts[i];
if (i == parts.length - 1) {
map.put(part, value);
} else {
if (!map.containsKey(part)) {
map.put(part, new SimpleMapProperty<>(FXCollections.observableHashMap()));
}
map = (MapProperty<String, Object>)map.get(part);
}
}
}
}
public StringBinding bind(String key) {
String[] parts = key.split("\.");
MapProperty<String, Object> map = this;
for (int i=0;i < parts.length; i++) {
String part = parts[i];
if (i == parts.length - 1) {
return Bindings.valueAt(map, part).asString();
} else {
if (!map.containsKey(part)) {
map.put(part, new SimpleMapProperty<>(FXCollections.observableHashMap()));
}
map = (MapProperty<String, Object>)map.get(part);
}
}
throw new NullPointerException("Unknown key : " + key);
}
}
现在您需要为您的视图创建一个基础 class,将 LocaleManager 公开为一个包含 getter 和 setter 的 属性:
public class BaseView {
private LocaleManager lang;
public BaseView() {
lang = new LocaleManager();
}
public LocaleManager langProperty() {
return lang;
}
public ObservableMap<String, Object> getLang() {
return lang.get();
}
public void setLang(MapProperty<String, Object> resource) {
this.lang.set(resource);
}
}
现在,如果您的视图扩展了 BaseView
public MyView extends BaseView {}
fxml 中的任何表达式,如 ${controller.lang.my.resource.key} 都将绑定到 ResourceBundle
中的相同键java 端的绑定仍然可以使用:
someField.textProperty().bind(langProperty().bind(BUNDLE_KEY));
现在要即时更改语言,只需使用:
langProperty().changeLocale(newLocale);
如果您想更改所有应用程序的语言,请记住在您的应用程序中将 LocaleManager 设为单例。
尚不支持在 SceneBuilder 上为字符串字段绑定表达式。 但如果将来接受以下拉取请求可能会有所帮助:
编辑:
为了回答@Novy 评论,绑定密钥包含 2 个部分。第一部分“controller.lang”让您可以访问控制器的语言属性。这与在您的控制器中写入 this.getLang()
相同 class
因此,如果在您的 属性 文件中,您具有以下属性:
item1 = "somestring"
item1.item2 = "someotherstring"
item1.item2.item3 = "someotherotherstring"
然后
${controller.lang.item1} == "somestring"
${controller.lang.item1.item2} == "someotherstring"
${controller.lang.item1.item2.item3} == "someotherotherstring"
${controller.lang.item1.item2} 可以在您的控制器中转换为以下 java 代码 class
((Map<String, Object)this.getLang().get("item1")).get("item2").toString()
或使用 javafx
提供的隐式转换this.getLang().get("item1").get("item2")