JavaFX (8) Alert: different button sizes

2019-02-28 16:12发布

问题:

Consider a JavaFX (8) Alert dialog with two buttons:

Alert alert = new Alert(AlertType.CONFIRMATION);

ButtonType bttYes = new ButtonType("Yes");
ButtonType bttNo = new ButtonType("No, continue without restart");

alert.getButtonTypes().clear();
alert.getButtonTypes().addAll(bttYes, bttNo);

alert.showAndWait();

The resulting dialog has two buttons, both same width which looks quite silly.

Is there a way to adjust both buttons to the actual text length?

回答1:

An Alert, which extends Dialog, wraps a DialogPane. A DialogPane is split into different sections and one of those sections is the "button bar". This section is, as expected, where all the buttons go. The button bar can can be any arbitrary Node, but the default implementation of DialogPane uses the aptly named ButtonBar control. This is documented in DialogPane.createButtonBar() (emphasis mine):

This method can be overridden by subclasses to provide the button bar. Note that by overriding this method, the developer must take on multiple responsibilities:

  1. The developer must immediately iterate through all button types and call createButton(ButtonType) for each of them in turn.
  2. The developer must add a listener to the button types list, and when this list changes update the button bar as appropriate.
  3. Similarly, the developer must watch for changes to the expandable content property, adding and removing the details button (created via createDetailsButton() method).

The default implementation of this method creates and returns a new ButtonBar instance.

A ButtonBar, by default, resizes all the buttons to be the width of the widest one.

Uniform button sizing

By default all buttons are uniformly sized in a ButtonBar, meaning that all buttons take the width of the widest button. It is possible to opt-out of this on a per-button basis, but calling the setButtonUniformSize(Node, boolean) method with a boolean value of false.

If a button is excluded from uniform sizing, it is both excluded from being resized away from its preferred size, and also excluded from the measuring process, so its size will not influence the maximum size calculated for all buttons in the ButtonBar.

As mentioned in the above Javadoc, this behavior can be disabled with the static method ButtonBar.setButtonUniformSize(Node, boolean). Simply loop over the buttons using getButtonTypes() and lookupButton(ButtonType) and set the uniform sizing to false for each one.

Alert alert = ...;
DialogPane pane = alert.getDialogPane();
pane.getButtonTypes().stream()
        .map(pane::lookupButton)
        .forEach(btn-> ButtonBar.setButtonUniformedSize(btn, false));

Note that this requires you to have configured the ButtonTypes beforehand, if not using the defaults.

This becomes implementation dependent if custom DialogPanes are involved. For instance, maybe the custom DialogPane doesn't use a ButtonBar. However, your code shows no indication of using a custom DialogPane so the above solution should work fine.



回答2:

Here are examples of Custom Dialogs. One pure code and one MCV.

Pure Code

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;

/**
 *
 * @author blj0011
 */
public class JavaFXApplication7 extends Application {

    @Override
    public void start(Stage primaryStage) {    
        Button button = new Button("press me");
        button.setOnAction((event)->{
            showDialog(primaryStage);                
        });


        VBox root = new VBox();        
        root.getChildren().add(button);

        Scene scene = new Scene(root, 300, 250);

        primaryStage.setTitle("Hello World!");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    private Stage showDialog(Window parent)
    {
        final Stage dialog = new Stage();
        dialog.setTitle("Test Dialog");
        dialog.initOwner(parent);
        dialog.initStyle(StageStyle.DECORATED);                

        //Create Header
        Label lblHeaderText = new Label("Header Text Goes Here!");
        lblHeaderText.setFont(new Font("System", 16));
        lblHeaderText.setPadding(new Insets(0, 0, 0, 20));
        StackPane headerLabelContainer = new StackPane();
        headerLabelContainer.getChildren().add(lblHeaderText);

        ImageView ivHeaderImageView = new ImageView();
        ivHeaderImageView.setFitHeight(80);
        ivHeaderImageView.setFitWidth(80);

        HBox hBoxHeader = new HBox();
        hBoxHeader.setMinHeight(80);
        hBoxHeader.setMaxHeight(80);
        hBoxHeader.setStyle("-fx-background-color: #E4E4E4");
        hBoxHeader.getChildren().addAll(headerLabelContainer, ivHeaderImageView);

        //Create Body
        Label lblBodyText = new Label("Body Text Goes Here!");
        lblBodyText.setPadding(new Insets(0, 0, 0, 20));
        lblBodyText.wrapTextProperty().set(true);
        AnchorPane.setTopAnchor(lblBodyText, 0.0);
        AnchorPane.setBottomAnchor(lblBodyText, 0.0);
        AnchorPane.setLeftAnchor(lblBodyText, 0.0);
        AnchorPane.setRightAnchor(lblBodyText, 0.0);

        AnchorPane apBodyLabelContainer = new AnchorPane();
        apBodyLabelContainer.setMinHeight(130);
        apBodyLabelContainer.getChildren().add(lblBodyText);        

        Button yesButton = new Button("Yes");
        Button noButton = new Button("No, continue without restart");
        yesButton.setOnAction((aEvent)->{
            System.out.println("continue with restart!");
            dialog.close();
        });
        noButton.setOnAction((aEvent)->{
            System.out.println("continue without restart!");
            //code to restart
            dialog.close();
        });   

        HBox hBoxBodyFooter = new HBox();
        hBoxBodyFooter.setSpacing(10);
        hBoxBodyFooter.setPadding(new Insets(0, 20, 10, 0));
        hBoxBodyFooter.alignmentProperty().set(Pos.CENTER_RIGHT);
        hBoxBodyFooter.getChildren().addAll(yesButton, noButton);

        VBox vBoxBody = new VBox();
        vBoxBody.getChildren().addAll(apBodyLabelContainer, hBoxBodyFooter);

        VBox rootLayout = new VBox();
        rootLayout.getChildren().addAll(hBoxHeader, vBoxBody);


        Scene scene = new Scene(rootLayout, 400, 250);//You can adjust this!

        dialog.setScene(scene);
        dialog.showAndWait();

        return dialog;      
    }
}

Model-Controller-View (more like Controller-View)

Main

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

/**
 *
 * @author blj0011
 */
public class JavaFXApplication8 extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

Controller

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

/**
 *
 * @author blj0011
 */
public class FXMLDocumentController implements Initializable {

    @FXML
    private void handleButtonAction(ActionEvent event) {
        showDialog((Node)event.getSource());
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // TODO
    }    

    private Stage showDialog(Node sourceNode)//You can add String title and String headerText and String bodyText
    {
        Stage dialog = new Stage();
        try 
        {
            Parent rootDialog = FXMLLoader.load(getClass().getResource("test.fxml"));
            dialog.setTitle("Title of Alert");
            dialog.initOwner(sourceNode.getScene().getWindow());
            dialog.initStyle(StageStyle.UTILITY);
            dialog.setScene(new Scene(rootDialog));

            ArrayList<Node> childrenNodes = getAllNodes(rootDialog);
            Button yesButton = (Button)childrenNodes.stream().filter((node) -> (node instanceof Button && ((Button)node).getText().equals("Yes"))).collect(Collectors.toList()).get(0);//Find the yes button in the test fxml
            Button noButton = (Button)childrenNodes.stream().filter((node) -> (node instanceof Button && ((Button)node).getText().contains("No"))).collect(Collectors.toList()).get(0);//Fin the no button in the test fxml

            //Yes button event handler
            yesButton.setOnAction((aEvent)->{
                System.out.println("continue with restart!");
                dialog.close();
            });

            //No button event handler
            noButton.setOnAction((aEvent)->{
                System.out.println("continue without restart!");
                //code to restart
                dialog.close();
            });  

            dialog.show();
        } 
        catch (IOException ex) 
        {
            System.out.println(ex.toString());
        }

        return dialog;
    }

    public static ArrayList<Node> getAllNodes(Parent root) {
        ArrayList<Node> nodes = new ArrayList();
        addAllDescendents(root, nodes);
        return nodes;
    }

    private static void addAllDescendents(Parent parent, ArrayList<Node> nodes) {
        for (Node node : parent.getChildrenUnmodifiable()) {
            nodes.add(node);
            if (node instanceof Parent)
                addAllDescendents((Parent)node, nodes);
        }
    }    
}

FXML

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane id="AnchorPane" prefHeight="200" prefWidth="320" xmlns:fx="http://javafx.com/fxml/1" fx:controller="javafxapplication8.FXMLDocumentController">
    <children>
        <Button layoutX="126" layoutY="90" text="Click Me!" onAction="#handleButtonAction" fx:id="button" />
        <Label layoutX="126" layoutY="120" minHeight="16" minWidth="69" fx:id="label" />
    </children>
</AnchorPane>

Dialog FXML

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>

<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="250.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <HBox maxHeight="80.0" style="-fx-background-color: #E4E4E4;">
         <children>
            <StackPane prefWidth="300.0" style="-fx-background-color: #E4E4E4;">
               <children>
                  <Label text="Header Text Goes Here!" StackPane.alignment="CENTER_LEFT">
                     <padding>
                        <Insets left="20.0" />
                     </padding>
                     <font>
                        <Font size="16.0" />
                     </font>
                  </Label>
               </children>
            </StackPane>
            <ImageView fitHeight="80.0" fitWidth="80.0" pickOnBounds="true" preserveRatio="true" />
         </children>
      </HBox>
      <VBox maxHeight="170.0" style="-fx-background-color: #F8F8F8;">
         <children>
            <AnchorPane minHeight="130.0">
               <children>
                  <Label alignment="TOP_LEFT" text="Body Text Goes Here!" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
                     <padding>
                        <Insets left="20.0" top="15.0" />
                     </padding>
                  </Label>
               </children>
            </AnchorPane>
            <HBox alignment="CENTER_RIGHT" spacing="10.0">
               <children>
                  <Button mnemonicParsing="false" text="Yes" />
                  <Button mnemonicParsing="false" text="No, continue without restart" />
               </children>
               <padding>
                  <Insets bottom="10.0" right="20.0" />
               </padding>
            </HBox>
         </children>
      </VBox>
   </children>
</VBox>

This will probably work as a workaround until someone comes up with the answer.



回答3:

Partial solution (not exactly text lenght but you can set your own size):

((Button) alert.getDialogPane().lookupButton(bttYes)).setMaxWidth(50);