【问题标题】:How to create a custom dialog in JavaFx without using any "ButtonType"-controls?如何在不使用任何“ButtonType”控件的情况下在 JavaFx 中创建自定义对话框?
【发布时间】:2020-02-06 19:27:37
【问题描述】:

我想创建一个自定义 Dialog,它只显示选项(见图 1)。如果用户选择其中一个选项,对话框应该立即关闭并返回相应的结果。

到目前为止,我只能通过将任意ButtonType 添加到Dialog、使用setVisible(false) 隐藏它并在单击选项的EventHandler 中应用fire() 来实现此目的。

这种奇怪的解决方法实际上效果很好,但在我看来非常不专业......
那么,在不使用 ButtonType 技巧的情况下,如何以更专业或更恰当的方式进行操作

我的解决方法代码如下所示(Dialog 类):

public class CustomDialog extends Dialog<String> {

    private static final String[] OPTIONS
            = new String[]{"Option1", "Option2", "Option3", "Option4"};
    private String selectedOption = null;
    Button applyButton;

    public CustomDialog() {
        super();
        initStyle(StageStyle.DECORATED);
        VBox vBox = new VBox();
        for (String option : OPTIONS) {
            Button optionButton = new Button(option);
            optionButton.setOnAction((event) -> {
                selectedOption = option;
                applyButton.fire();
            });
            vBox.getChildren().add(optionButton);
        }
        getDialogPane().setContent(vBox);
        getDialogPane().getButtonTypes().add(ButtonType.APPLY);
        applyButton = (Button) getDialogPane().lookupButton(ButtonType.APPLY);
        applyButton.setVisible(false);

        setResultConverter((dialogButton) -> {
            return selectedOption;
        });
    }
}

使用对话框类:

    CustomDialog dialog = new CustomDialog();
    Optional<String> result = dialog.showAndWait();
    String selected = null;
    if (result.isPresent()) {
        selected = result.get();
    } else if (selected == null) {
        System.exit(0);
    }

【问题讨论】:

  • 你需要去docs.oracle.com/javase/8/javafx/api/javafx/scene/control/…。阅读标记为Dialog Closing Rules的部分
  • 抱歉,您没有阅读我的代码和问题吗?我的问题是如何在不需要使用任何 ButtonType 控件的情况下使用对话框?通用规则说这是不可能的,所以我的解决方法似乎是我问题的唯一解决方案,你所做的就是引用某事。我们都已经很清楚了 ;-) ...
  • A DialogDialogPane 的重量级容器,如文档中所述,“DialogPaneButtonType 的概念上运行。”因此,如果您不想要任何 ButtonType 功能,似乎 Dialog 不是要使用的正确类。也许您的问题中没有说明您想要的其他一些功能,但普通的Stage 不会在这里做您需要的事情吗?
  • 所以你问了一个你知道做不到的问题?

标签: java javafx dialog


【解决方案1】:

Dialog 只是一个显示DialogPane 的窗口,并且引用Javadocs for DialogPane

DialogPaneButtonType 的概念上运行。 ButtonType 是一个 应在视觉上表示的单个按钮的描述符 DialogPane。因此,创建DialogPane 的开发人员必须 指定他们想要显示的按钮类型

(我的重点)。因此,虽然您展示了一种可能的解决方法,而在另一个答案中,Slaw 展示了另一种解决方法,但如果您尝试使用 Dialog 而不使用 ButtonType 及其相关的结果转换器,那么您实际上是在使用 @987654332 @class 用于不打算用于的东西。

您描述的功能完全可以通过常规模式Stage 实现。例如,以下给出了您描述的相同基本行为并且不涉及ButtonTypes:

package org.jamesd.examples.dialog;

import java.util.Optional;
import java.util.stream.Stream;

import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;

public class CustomDialog {

    private static final String[] OPTIONS 
        = {"Option 1", "Option 2", "Option 3", "Option 4"};

    private final Stage stage ;

    private String selectedOption = null ;

    public CustomDialog() {
        this(null);
    }

    public CustomDialog(Window parent) {
        var vbox = new VBox();
        // Real app should use an external style sheet:
        vbox.setStyle("-fx-padding: 12px; -fx-spacing: 5px;");
        Stream.of(OPTIONS)
            .map(this::createButton)
            .forEach(vbox.getChildren()::add);
        var scene = new Scene(vbox);
        stage = new Stage();
        stage.initOwner(parent);
        stage.initModality(Modality.WINDOW_MODAL);
        stage.setScene(scene);
    }

    private Button createButton(String text) {
        var button = new Button(text);
        button.setOnAction(e -> {
            selectedOption = text ;
            stage.close();
        });
        return button ;
    }

    public Optional<String> showDialog() {
        selectedOption = null ;
        stage.showAndWait();
        return Optional.ofNullable(selectedOption);
    }
}

这是一个使用此自定义对话框的简单应用程序类:

package org.jamesd.examples.dialog;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        var root = new VBox();
        // Real app should use an external style sheet:
        root.setStyle("-fx-padding: 12px; -fx-spacing: 5px;");
        var showDialog = new Button("Show dialog");
        var label = new Label("No option chosen");
        showDialog.setOnAction(e -> 
            new CustomDialog(stage)
                .showDialog()
                .ifPresentOrElse(label::setText, Platform::exit));
        root.getChildren().addAll(showDialog, label);
        stage.setScene(new Scene(root));
        stage.show();
    }

}

【讨论】:

    【解决方案2】:

    正如@Sedrick 和@James_D 所指出的,Dialog API 是围绕“按钮类型”的概念构建的。不使用 ButtonType 会违反 API,因此,总是会显得 hacky/错误。也就是说,您可以对当前的代码进行细微的修改,以满足您的“不使用任何 'ButtonType'-controls”目标。它似乎没有记录,但如果您手动设置 result 属性,它会触发关闭并返回结果过程。这意味着您不需要添加任何ButtonType 并且可以完全绕过resultConverter。这是一个概念验证

    OptonsDialog.java

    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.control.Button;
    import javafx.scene.control.Dialog;
    import javafx.scene.layout.VBox;
    
    public class OptionsDialog<T extends OptionsDialog.Option> extends Dialog<T> {
    
      public interface Option {
    
        String getDisplayText();
      }
    
      @SafeVarargs
      public OptionsDialog(T... options) {
        if (options.length == 0) {
          throw new IllegalArgumentException("must provide at least one option");
        }
    
        var content = new VBox(10);
        content.setAlignment(Pos.CENTER);
        content.setPadding(new Insets(15, 25, 15, 25));
    
        for (var option : options) {
          var button = new Button(option.getDisplayText());
          button.setOnAction(
              event -> {
                event.consume();
                setResult(option);
              });
          content.getChildren().add(button);
        }
    
        getDialogPane().setContent(content);
      }
    }
    

    App.java

    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.Alert;
    import javafx.scene.control.Alert.AlertType;
    import javafx.scene.control.Button;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    import javafx.stage.Window;
    
    public class App extends Application {
    
      private enum Foo implements OptionsDialog.Option {
        OPTION_1("Option Number 1"),
        OPTION_2("Option Number 2"),
        OPTION_3("Option Number 3"),
        OPTION_4("Option Number 4");
    
        private final String displayText;
    
        Foo(String displayText) {
          this.displayText = displayText;
        }
    
        @Override
        public String getDisplayText() {
          return displayText;
        }
      }
    
      @Override
      public void start(Stage primaryStage) {
        var button = new Button("Click me!");
        button.setOnAction(
            event -> {
              event.consume();
              showChosenOption(primaryStage, promptUserForOption(primaryStage));
            });
        primaryStage.setScene(new Scene(new StackPane(button), 500, 300));
        primaryStage.show();
      }
    
      private static Foo promptUserForOption(Window owner) {
        var dialog = new OptionsDialog<>(Foo.values());
        dialog.initOwner(owner);
        dialog.setTitle("Choose Option");
        return dialog.showAndWait().orElseThrow();
      }
    
      private static void showChosenOption(Window owner, OptionsDialog.Option option) {
        var alert = new Alert(AlertType.INFORMATION);
        alert.initOwner(owner);
        alert.setHeaderText("Chosen Option");
        alert.setContentText(String.format("You chose the following: \"%s\"", option.getDisplayText()));
        alert.show();
      }
    }
    

    这与您当前的解决方法没有什么不同,它仍然在使用 API。这也依赖于未记录的行为(即手动设置 result 属性会关闭对话框并返回结果)。底部的ButtonBar 仍然占用一些空间,尽管比添加不可见按钮时要少。但是,可以通过添加以下 CSS 来摆脱这个空白区域:

    .options-dialog-pane .button-bar {
      -fx-min-height: 0;
      -fx-pref-height: 0;
      -fx-max-height: 0;
    }
    

    请注意,以上假设您已修改代码以将"options-dialog-pane" 样式类添加到与OptionsDialog 一起使用的DialogPane

    【讨论】:

      【解决方案3】:

      我认为您应该从Java Docs 阅读以下内容:

      对话结束规则:

      了解 Dialog 关闭时会发生什么以及如何关闭 Dialog 非常重要,尤其是在异常关闭情况下(例如单击对话框标题栏中的“X”按钮时,或者在操作时系统特定的键盘快捷键(例如 Windows 上的 alt-F4)。幸运的是,在这些情况下,结果是明确定义的,最好总结为以下要点:

      • JavaFX 对话框只能在 两种情况:

        1. 当对话框只有一个按钮时,或
        2. 当对话框有多个按钮时,只要其中一个满足以下要求之一即可:
          1. 按钮具有 ButtonType,其 ButtonBar.ButtonData 的类型为 ButtonBar.ButtonData.CANCEL_CLOSE。
          2. 该按钮有一个 ButtonType,其 ButtonBar.ButtonData 在调用 ButtonBar.ButtonData.isCancelButton() 时返回 true。
      • 在所有其他情况下,对话框将拒绝响应所有关闭请求,并保持打开状态,直到用户单击对话框的 DialogPane 区域中的可用按钮之一。

      • 如果对话框异常关闭,并且对话框包含满足上述两个条件之一的按钮,则对话框将尝试将 result 属性设置为通过调用结果转换器返回的任何值第一个匹配的 ButtonType。

      • 如果由于任何原因结果转换器返回 null,或者如果对话框在仅存在一个非取消按钮时关闭,则结果属性将为 null,并且 showAndWait() 方法将返回 Optional.empty()。这一点意味着,如果您使用选项 2 或选项 3(如本类文档前面所述),则永远不会调用 Optional.ifPresent(java.util.function.Consumer) lambda,并且代码将继续执行好像对话框根本没有返回任何值一样。

      如果您不介意Buttons 是水平的,您应该使用ButtonTypesetResultConverter 根据按下的按钮返回一个字符串。

      import javafx.scene.control.Alert;
      import javafx.scene.control.ButtonType;
      import javafx.scene.control.Dialog;
      import javafx.stage.StageStyle;
      import javafx.util.Callback;
      
      /**
       *
       * @author blj0011
       */
      public class CustomDialog extends Dialog<String>
      {
          String result = "";
      
          public CustomDialog()
          {
              super();
              initStyle(StageStyle.DECORATED);
      
              setContentText(null);
              setHeaderText(null);
      
              ButtonType buttonOne = new ButtonType("Option1");
              ButtonType buttonTwo = new ButtonType("Option2");
              ButtonType buttonThree = new ButtonType("Option3");
              ButtonType buttonFour = new ButtonType("Option4");
      
              getDialogPane().getButtonTypes().addAll(buttonOne, buttonTwo, buttonThree, buttonFour);
      
              setResultConverter(new Callback<ButtonType, String>()
              {
                  @Override
                  public String call(ButtonType param)
                  {
                      if (param == buttonOne) {
                          return buttonOne.getText();
                      }
                      else if (param == buttonTwo) {
                          return buttonTwo.getText();
                      }
                      else if (param == buttonThree) {
                          return buttonThree.getText();
                      }
                      else if (param == buttonFour) {
                          return buttonFour.getText();
                      }
      
                      return "";
                  }
              });
          }
      }
      

      更新:正如 @Slaw 在 cmets 中所述,您可以将 setResultConverter(...) 替换为 setResultConverter(ButtonType::getText)

      【讨论】:

      • 有了这个设置,我相信结果转换器可以简化为ButtonType::getText
      • 感谢ButtonType::getText 和编辑块引用。我做错了。我会看看你做了什么。 @斯劳
      • 抱歉,这根本不是我想要的。正如我所说,我不想使用 ButtonType 也不想让它们水平放置,它们必须垂直放置,如图所示。所以我的解决方法似乎是我唯一的解决方案。
      • 我真的在问我,你的帖子有什么新内容,实际上什么都没有?你读过我的代码吗?我已经使用过像 u 和 ResultConverter 这样的 ButtonType,但这根本不是重点。您只需提供常见文档中已经众所周知的解决方案:((...
      • 我还添加了指向 JavaDoc 的链接。它基本上指出,如果要扩展 Dialog,则必须满足某些条件。如果你根本不使用 ButtonType,你就不符合这些条件。
      猜你喜欢
      • 2017-07-15
      • 1970-01-01
      • 2016-09-20
      • 2021-11-21
      • 2023-04-07
      • 1970-01-01
      • 2013-04-11
      • 1970-01-01
      • 2023-03-21
      相关资源
      最近更新 更多