3.5.1.3. 打开界面

可以通过主菜单URL 导航或从另外一个界面以编程方式打开。在本节,将介绍如何以编程的方式打开界面。


使用 Screens 接口

使用 ScreenBuilders bean

传递参数给界面

关闭界面后执行代码以及关闭界面返回值


使用 Screens 接口

Screens 接口允许创建和显示任何类型的界面。

假设有一个用于显示具有一些特殊格式的消息的界面:

界面控制器

  1. @UiController("demo_FancyMessageScreen")
  2. @UiDescriptor("fancy-message-screen.xml")
  3. @DialogMode(forceDialog = true, width = "300px")
  4. public class FancyMessageScreen extends Screen {
  5. @Inject
  6. private Label<String> messageLabel;
  7. public void setFancyMessage(String message) { (1)
  8. messageLabel.setValue(message);
  9. }
  10. @Subscribe("closeBtn")
  11. protected void onCloseBtnClick(Button.ClickEvent event) {
  12. closeWithDefaultAction();
  13. }
  14. }
1- 一个界面参数

界面描述

  1. <?xml version="1.0" encoding="UTF-8" standalone="no"?>
  2. <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Fancy Message">
  3. <layout>
  4. <label id="messageLabel" value="A message" stylename="h1"/>
  5. <button id="closeBtn" caption="Close"/>
  6. </layout>
  7. </window>

那么可以从另一个界面创建并打开它,如下所示:

  1. @Inject
  2. private Screens screens;
  3. private void showFancyMessage(String message) {
  4. FancyMessageScreen screen = screens.create(FancyMessageScreen.class);
  5. screen.setFancyMessage(message);
  6. screens.show(screen);
  7. }

请注意这里是如何创建界面实例、为其提供参数,然后显示界面。

如果界面不需要来自调用方的任何参数,可以仅用一行代码创建并打开它:

  1. @Inject
  2. private Screens screens;
  3. private void showDefaultFancyMessage() {
  4. screens.create(FancyMessageScreen.class).show();
  5. }

screens 不是 Spring bean,所以只能将它注入到界面控制器或使用 ComponentsHelper.getScreenContext(component).getScreens() 静态方法获取。

使用 ScreenBuilders bean

通过 ScreenBuilders bean 可以使用各种参数打开所有类型的界面。下面是用它打开界面并且在界面关闭之后执行一些代码的例子(更多细节参考这里):

  1. @Inject
  2. private ScreenBuilders screenBuilders;
  3. @Inject
  4. private Notifications notifications;
  5. private void openOtherScreen() {
  6. screenBuilders.screen(this)
  7. .withScreenClass(OtherScreen.class)
  8. .withAfterCloseListener(e -> {
  9. notifications.create().withCaption("Closed").show();
  10. })
  11. .build()
  12. .show();
  13. }

下面我们看看如何操作编辑界面和查找界面。

Customer 实体实例打开默认编辑界面的示例:

  1. @Inject
  2. private ScreenBuilders screenBuilders;
  3. private void editSelectedEntity(Customer entity) {
  4. screenBuilders.editor(Customer.class, this)
  5. .editEntity(entity)
  6. .build()
  7. .show();
  8. }

在这种情况下,编辑界面将更新实体,但调用界面将不会接收到更新后的实例。

最常见的情况是需要编辑某些用 TableDataGrid 组件显示的实体。那么应该使用以下调用方式,它更简洁且能自动更新表格组件:

  1. @Inject
  2. private GroupTable<Customer> customersTable;
  3. @Inject
  4. private ScreenBuilders screenBuilders;
  5. private void editSelectedEntity() {
  6. screenBuilders.editor(customersTable).build().show();
  7. }

要创建一个新的实体实例并打开它的编辑界面,只需在构建器上调用 newEntity() 方法:

  1. @Inject
  2. private GroupTable<Customer> customersTable;
  3. @Inject
  4. private ScreenBuilders screenBuilders;
  5. private void createNewEntity() {
  6. screenBuilders.editor(customersTable)
  7. .newEntity()
  8. .build()
  9. .show();
  10. }

默认编辑界面的确定过程如下:

  1. 如果存在使用@PrimaryEditorScreen注解的编辑界面,则使用它。

  2. 否则,使用 id 是 {entity_name}.edit 的编辑界面(例如,sales_Customer.edit)。

界面构建器提供了许多方法来设置被打开界面的可选参数。例如,以下代码以对话框的方式打开的特定编辑界面,同时新建并初始化实体:

  1. @Inject
  2. private GroupTable<Customer> customersTable;
  3. @Inject
  4. private ScreenBuilders screenBuilders;
  5. private void editSelectedEntity() {
  6. screenBuilders.editor(customersTable).build().show();
  7. }
  8. private void createNewEntity() {
  9. screenBuilders.editor(customersTable)
  10. .newEntity()
  11. .withInitializer(customer -> { // lambda to initialize new instance
  12. customer.setName("New customer");
  13. })
  14. .withScreenClass(CustomerEdit.class) // specific editor screen
  15. .withLaunchMode(OpenMode.DIALOG) // open as modal dialog
  16. .build()
  17. .show();
  18. }

实体查找界面也能使用不同参数打开。

下面是打开 User 实体的默认查找界面的示例:

  1. @Inject
  2. private TextField<String> userField;
  3. @Inject
  4. private ScreenBuilders screenBuilders;
  5. private void lookupUser() {
  6. screenBuilders.lookup(User.class, this)
  7. .withSelectHandler(users -> {
  8. User user = users.iterator().next();
  9. userField.setValue(user.getName());
  10. })
  11. .build()
  12. .show();
  13. }

如果需要将找到的实体设置到字段,可使用更简洁的方式:

  1. @Inject
  2. private PickerField<User> userPickerField;
  3. @Inject
  4. private ScreenBuilders screenBuilders;
  5. private void lookupUser() {
  6. screenBuilders.lookup(User.class, this)
  7. .withField(userPickerField) // set result to the field
  8. .build()
  9. .show();
  10. }

默认查找界面的确定过程如下:

  1. 如果存在使用@PrimaryLookupScreen注解的查找界面,则使用它。

  2. 否则,如果存在 id 为 {entity_name}.lookup 的界面,则使用它(例如,sales_Customer.lookup)。

  3. 否则,使用 id 为 {entity_name}.browse 的界面(例如,sales_Customer.browse)。

与使用编辑界面一样,使用构建器方法设置打开界面的可选参数。例如,以下代码以对话框的方式打开特定的查找界面,在这个界面中查找 User 实体:

  1. @Inject
  2. private TextField<String> userField;
  3. @Inject
  4. private ScreenBuilders screenBuilders;
  5. private void lookupUser() {
  6. screenBuilders.lookup(User.class, this)
  7. .withScreenId("sec$User.browse") // specific lookup screen
  8. .withLaunchMode(OpenMode.DIALOG) // open as modal dialog
  9. .withSelectHandler(users -> {
  10. User user = users.iterator().next();
  11. userField.setValue(user.getName());
  12. })
  13. .build()
  14. .show();
  15. }

为界面传递参数

为打开界面传递参数的推荐方式是使用界面控制器的公共 setter 方法,如上面界面接口部分示范。

使用这个方式,可以为任意类型的界面传递参数,包括使用ScreenBuilders或者从主菜单打开的实体编辑和查找界面。带有传参使用 ScreenBuilders 来调用 FancyMessageScreen 如下所示:

  1. @Inject
  2. private ScreenBuilders screenBuilders;
  3. private void showFancyMessage(String message) {
  4. FancyMessageScreen screen = screenBuilders.screen(this)
  5. .withScreenClass(FancyMessageScreen.class)
  6. .build();
  7. screen.setFancyMessage(message);
  8. screen.show();
  9. }

另一个方式是为参数定义一个特殊的类,然后在界面构造器中将该类的实例传递给标准的 withOptions() 方法。参数类必需实现 ScreenOptions 标记接口。示例:

  1. import com.haulmont.cuba.gui.screen.ScreenOptions;
  2. public class FancyMessageOptions implements ScreenOptions {
  3. private String message;
  4. public FancyMessageOptions(String message) {
  5. this.message = message;
  6. }
  7. public String getMessage() {
  8. return message;
  9. }
  10. }

在打开的 FancyMessageScreen 界面,可以通过InitEventAfterInitEvent处理器获取参数:

  1. @Subscribe
  2. private void onInit(InitEvent event) {
  3. ScreenOptions options = event.getOptions();
  4. if (options instanceof FancyMessageOptions) {
  5. String message = ((FancyMessageOptions) options).getMessage();
  6. messageLabel.setValue(message);
  7. }
  8. }

带有传递 ScreenOptions 参数使用 ScreenBuilders 来调用 FancyMessageScreen 如下所示:

  1. @Inject
  2. private ScreenBuilders screenBuilders;
  3. private void showFancyMessage(String message) {
  4. screenBuilders.screen(this)
  5. .withScreenClass(FancyMessageScreen.class)
  6. .withOptions(new FancyMessageOptions(message))
  7. .build()
  8. .show();
  9. }

可以看到,这个方式需要在控制界接收参数的时候进行类型转换,所以需要考虑清楚再使用。推荐还是用上面介绍的类型安全的使用 setter 的方式。

如果界面是基于legacy API从另一个界面打开的,那么使用 ScreenOptions 对象是唯一能获取到参数的方法。此时,参数对象是 MapScreenOptions 类型的,可以在打开的界面中按照如下处理:

  1. @Subscribe
  2. private void onInit(InitEvent event) {
  3. ScreenOptions options = event.getOptions();
  4. if (options instanceof MapScreenOptions) {
  5. String message = (String) ((MapScreenOptions) options).getParams().get("message");
  6. messageLabel.setValue(message);
  7. }
  8. }

关闭界面后执行代码以及关闭界面返回值

每个界面在关闭时都会发送 AfterCloseEvent 事件。可以为界面添加监听器,这样可以在界面关闭时收到通知,示例:

  1. @Inject
  2. private Screens screens;
  3. @Inject
  4. private Notifications notifications;
  5. private void openOtherScreen() {
  6. OtherScreen otherScreen = screens.create(OtherScreen.class);
  7. otherScreen.addAfterCloseListener(afterCloseEvent -> {
  8. notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show();
  9. });
  10. otherScreen.show();
  11. }

当使用 ScreenBuilders 时,可以在 withAfterCloseListener() 方法中提供监听器:

  1. @Inject
  2. private ScreenBuilders screenBuilders;
  3. @Inject
  4. private Notifications notifications;
  5. private void openOtherScreen() {
  6. screenBuilders.screen(this)
  7. .withScreenClass(OtherScreen.class)
  8. .withAfterCloseListener(afterCloseEvent -> {
  9. notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show();
  10. })
  11. .build()
  12. .show();
  13. }

事件对象能提供关于界面是如何关闭的信息:其 getCloseAction() 方法返回带 CloseAction 接口的对象。界面控制器实现的 FrameOwner 接口包含几个常量,定义了框架使用的 CloseAction 接口的实现。在应用程序中,可以直接使用这些常量或者创建自己的实现。

看一个简单的自定义界面:

  1. package com.company.demo.web.screens;
  2. import com.haulmont.cuba.gui.components.Button;
  3. import com.haulmont.cuba.gui.screen.*;
  4. @UiController("demo_OtherScreen")
  5. @UiDescriptor("other-screen.xml")
  6. public class OtherScreen extends Screen {
  7. private String result;
  8. public String getResult() {
  9. return result;
  10. }
  11. @Subscribe("okBtn")
  12. private void onOkBtnClick(Button.ClickEvent event) {
  13. result = "Done";
  14. close(WINDOW_COMMIT_AND_CLOSE_ACTION); (1)
  15. }
  16. @Subscribe("cancelBtn")
  17. private void onCancelBtnClick(Button.ClickEvent event) {
  18. closeWithDefaultAction(); (2)
  19. }
  20. }
1- 在点击 “OK” 按钮时,设置一些结果状态,并使用标准的 WINDOW_COMMIT_AND_CLOSE_ACTION 操作关闭界面。
2- 在点击 “Cancel” 按钮时,使用默认操作关闭界面。

于是,在 AfterCloseEvent 监听器我们能分析界面是如何关闭的,并且如果需要的话可以读取结果:

  1. @Inject
  2. private ScreenBuilders screenBuilders;
  3. @Inject
  4. private Notifications notifications;
  5. private void openOtherScreen() {
  6. screenBuilders.screen(this)
  7. .withScreenClass(OtherScreen.class)
  8. .withAfterCloseListener(afterCloseEvent -> {
  9. OtherScreen otherScreen = afterCloseEvent.getScreen();
  10. if (afterCloseEvent.getCloseAction().equals(WINDOW_COMMIT_AND_CLOSE_ACTION)) {
  11. String result = otherScreen.getResult();
  12. notifications.create().withCaption("Result: " + result).show();
  13. }
  14. })
  15. .build()
  16. .show();
  17. }

从界面返回值的另一个方法是使用自定义的 CloseAction 实现。重写一下上面的示例,使用如下操作类:

  1. package com.company.demo.web.screens;
  2. import com.haulmont.cuba.gui.screen.StandardCloseAction;
  3. public class MyCloseAction extends StandardCloseAction {
  4. private String result;
  5. public MyCloseAction(String result) {
  6. super("myCloseAction");
  7. this.result = result;
  8. }
  9. public String getResult() {
  10. return result;
  11. }
  12. }

然后可以使用该操作类关闭界面:

  1. @Inject
  2. private Screens screens;
  3. @Inject
  4. private Notifications notifications;
  5. private void openOtherScreen() {
  6. Screen otherScreen = screens.create("demo_OtherScreen", OpenMode.THIS_TAB);
  7. otherScreen.addAfterCloseListener(afterCloseEvent -> {
  8. CloseAction closeAction = afterCloseEvent.getCloseAction();
  9. if (closeAction instanceof MyCloseAction) {
  10. String result = ((MyCloseAction) closeAction).getResult();
  11. notifications.create().withCaption("Result: " + result).show();
  12. }
  13. });
  14. otherScreen.show();
  15. }

可以看到,当使用自定义的 CloseAction 返回值时,调用方不需要知道打开的界面类是什么,因为不会调用具体的界面控制器内的方法。所以界面可以只通过其字符串 id 来创建。

当然,在使用 ScreenBuilders 打开界面时,也可以使用相同的方式通过关闭操作返回结果。