7

I have this very simple form and I set the min width and height of all UI controls to be USE_PREF_SIZE, so, they cannot get too small:

enter image description here

The code looks like this:

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>

<AnchorPane minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="10.0" layoutX="64.0" layoutY="122.0" vgap="10.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
        <columnConstraints>
          <ColumnConstraints halignment="RIGHT" hgrow="NEVER" />
          <ColumnConstraints hgrow="SOMETIMES" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints vgrow="NEVER" />
          <RowConstraints vgrow="NEVER" />
          <RowConstraints vgrow="NEVER" />
            <RowConstraints valignment="TOP" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label minHeight="-Infinity" minWidth="-Infinity" text="Log in" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" />
            <Label minHeight="-Infinity" minWidth="-Infinity" text="Email address:" GridPane.rowIndex="1" />
            <Label minHeight="-Infinity" minWidth="-Infinity" text="Password:" GridPane.rowIndex="2" />
            <Button minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" text="Log in" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="3" />
            <PasswordField minHeight="-Infinity" minWidth="-Infinity" GridPane.columnIndex="1" GridPane.rowIndex="2" />
            <TextField minHeight="-Infinity" minWidth="-Infinity" GridPane.columnIndex="1" GridPane.rowIndex="1" />
         </children>
         <padding>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </padding>
      </GridPane>
   </children>
</AnchorPane>

My problem is that the window can still get too small:

enter image description here

How do I prevent that from happening?

I could of course set a fixed min width and height, that's trivial:

stage.setMinWidth(280);
stage.setMinHeight(180);

but that requires remembering to update it every time the UI changes and it wouldn't be sufficient in other languages that have longer strings, such as Spanish:

enter image description here

Since it seems this cannot be achieved with FXML, I'm trying with code. The basic code I'm starting with is the usual FXML loading method:

public void start(Stage stage) throws Exception {
    Parent root = FXMLLoader.load(getClass().getResource("/simple_form.fxml"));
    Scene scene = new Scene(root);
    stage.setScene(scene);
    stage.show();
}

The FXML loaded object, root, has a prefWidth and prefHeight property that look promising. I added this debugging prints:

public void start(Stage stage) throws Exception {
    Parent root = FXMLLoader.load(getClass().getResource("/simple_form.fxml"));
    Scene scene = new Scene(root);
    stage.setScene(scene);
    System.out.println("Preferred width before showing: " + root.prefWidth(-1));
    System.out.println("Preferred height before showing: " + root.prefHeight(-1));
    stage.show();
    System.out.println("Preferred width after showing: " + root.prefWidth(-1));
    System.out.println("Preferred height after showing: " + root.prefHeight(-1));
    System.out.println("Actual width: " + scene.getWidth());
    System.out.println("Actual height: " + scene.getHeight());
}

and the result is:

Preferred width before showing: 30.0
Preferred height before showing: 50.0
Preferred width after showing: 255.0
Preferred height after showing: 142.0
Actual width: 255.0
Actual height: 142.0

Since the initial size is the preferred size, the one I want as a minimum for this UI, a sensible person would have expected to have the answer (you just need to show() the stage first):

public void start(Stage stage) throws Exception {
    Parent root = FXMLLoader.load(getClass().getResource("/simple_form.fxml"));
    Scene scene = new Scene(root);
    stage.setScene(scene);
    stage.show();
    stage.setMinWidth(root.prefWidth(-1));
    stage.setMinHeight(root.prefHeight(-1));
}

But the result of this code is that I can still make the Window smaller, but only a little bit:

enter image description here

I'm utterly confused by that result. I tried removing the paddings and gaps from the fxml, but the odd behavior remained.

Pablo Fernandez
  • 279,434
  • 135
  • 377
  • 622
  • Is it manually resized? Then I would say disable manual resizing. – creativecreatorormaybenot Aug 27 '17 at 12:54
  • Yes, I manually resized it to that small size. I don't want to disable manual resizing because someone might want to make it bigger. I just don't want them to make it smaller and hide part of it. – Pablo Fernandez Aug 27 '17 at 12:55
  • What about handling that in code since the attribute will only apply on creation? Like save the window size and make a resize listener, which does not allow resizing smaller than the saved dimension? – creativecreatorormaybenot Aug 27 '17 at 12:57
  • I could do it with code, but I don't trust the starting size to be the minimum (at some point the app will remember the size of the window when you closed it). – Pablo Fernandez Aug 27 '17 at 12:59
  • set the min height and width. – SedJ601 Aug 27 '17 at 14:59
  • In the last part, you are setting the min height/width of the *stage* to the preferred width/height of the *root*. Obviously, the stage takes up some space of its own (e.g. for the title bar), for which you haven't accounted. – James_D Aug 27 '17 at 23:32

3 Answers3

16

Calling Stage.sizeToScene() and then setting the minimum width and height to the current width and height after showing the stage works for me:

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

public class Login extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(FXMLLoader.load(getClass().getResource("LoginForm.fxml")));
        primaryStage.setScene(scene);
        primaryStage.sizeToScene();
        primaryStage.show();
        primaryStage.setMinWidth(primaryStage.getWidth());
        primaryStage.setMinHeight(primaryStage.getHeight());
    }

    public static void main(String[] args) {
        launch(args);
    }
}

sizeToScene() is redundant here, as this is the default behavior in this scenario, though you might need this if you replace the stage's scene at a later time (and making things explicit is never a bad thing).

If you want to explicitly set the size of the stage to something other than the size that allows the scene to be its preferred size, but still enforce the same minimum size requirements, it gets a bit trickier. After showing the stage, you can calculate the difference between the size of the stage and the size of the root of the scene (essentially the space taken up by the window decorations), and compute the preferred size of the root, and then use those to compute the minimum size of the stage. This looks like

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Login extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(FXMLLoader.load(getClass().getResource("LoginForm.fxml")));


        primaryStage.setScene(scene);

        primaryStage.setWidth(400);
        primaryStage.setHeight(280);

        primaryStage.show();


        Node root = scene.getRoot();
        Bounds rootBounds = root.getBoundsInLocal();
        double deltaW = primaryStage.getWidth() - rootBounds.getWidth();
        double deltaH = primaryStage.getHeight() - rootBounds.getHeight();

        Bounds prefBounds = getPrefBounds(root);

        primaryStage.setMinWidth(prefBounds.getWidth() + deltaW);
        primaryStage.setMinHeight(prefBounds.getHeight() + deltaH);
    }

    private Bounds getPrefBounds(Node node) {
        double prefWidth ;
        double prefHeight ;

        Orientation bias = node.getContentBias();
        if (bias == Orientation.HORIZONTAL) {
            prefWidth = node.prefWidth(-1);
            prefHeight = node.prefHeight(prefWidth);
        } else if (bias == Orientation.VERTICAL) {
            prefHeight = node.prefHeight(-1);
            prefWidth = node.prefWidth(prefHeight);
        } else {
            prefWidth = node.prefWidth(-1);
            prefHeight = node.prefHeight(-1);
        }
        return new BoundingBox(0, 0, prefWidth, prefHeight);
    }

    public static void main(String[] args) {
        launch(args);
    }
}

I haven't tested this on all platforms.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks for the answer. I haven't implemented it yet, but I want the window to remember its last size. It would look like sizeToScene would prevent that from working correctly, right? – Pablo Fernandez Aug 27 '17 at 21:51
  • I'm not sure I understand what you mean by that. – James_D Aug 27 '17 at 22:28
  • @Pablo I think I understand what you mean. I updated the answer with another (more complex) approach that should work with that requirement. – James_D Aug 27 '17 at 23:20
  • I was just saying his question/requirements are morphing with every new answer. – SedJ601 Aug 27 '17 at 23:59
  • @SedrickJefferson I meant I didn't understand Pablo's comment. I think I understand it now. – James_D Aug 28 '17 at 00:01
  • minor nit-pick: we probably should do the minSize adjust in a listener to the stage's showing property – kleopatra Aug 28 '17 at 09:04
  • That actually works very well @James_D. To completely emulate setting min size to pref size, I somehow need to catch when the root change sizes and re-calculate, but I can take it from here. – Pablo Fernandez Aug 28 '17 at 10:20
  • Thanks for the comment @kleopatra, that makes sense. – Pablo Fernandez Aug 28 '17 at 10:32
  • Looking at this: https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html shouldn't we use getBoundsInParent instead of getBoundsInLocal? – Pablo Fernandez Aug 28 '17 at 10:36
  • @Pablo It shouldn't make any difference, but `getBoundsInLocal()` makes more sense to me, since the root node doesn't have a parent. – James_D Aug 28 '17 at 11:17
0

Building on James_D's answer, I wrote these two functions. In my application I call applyRootSizeContraints after Stage.show():

private static Bounds getBounds(final Node node,
                                final BiFunction<Node, Double, Double> widthFunction,
                                final BiFunction<Node, Double, Double> heightFunction) {
    final Orientation bias = node.getContentBias();
    double prefWidth;
    double prefHeight;
    if (bias == Orientation.HORIZONTAL) {
        prefWidth = widthFunction.apply(node, (double) -1);
        prefHeight = heightFunction.apply(node, prefWidth);
    } else if (bias == Orientation.VERTICAL) {
        prefHeight = heightFunction.apply(node, (double) -1);
        prefWidth = widthFunction.apply(node, prefHeight);
    } else {
        prefWidth = widthFunction.apply(node, (double) -1);
        prefHeight = heightFunction.apply(node, (double) -1);
    }
    return new BoundingBox(0, 0, prefWidth, prefHeight);
}

private static void applyRootSizeConstraints(final Stage stage) {
    final Parent root = stage.getScene().getRoot();
    stage.sizeToScene();
    final double deltaWidth = stage.getWidth() - root.getLayoutBounds().getWidth();
    final double deltaHeight = stage.getHeight() - root.getLayoutBounds().getHeight();
    final Bounds minBounds = getBounds(root, Node::minWidth, Node::minHeight);
    stage.setMinWidth(minBounds.getWidth() + deltaWidth);
    stage.setMinHeight(minBounds.getHeight() + deltaHeight);
    final Bounds prefBounds = getBounds(root, Node::prefWidth, Node::prefHeight);
    stage.setWidth(prefBounds.getWidth() + deltaWidth);
    stage.setHeight(prefBounds.getHeight() + deltaHeight);
    final Bounds maxBounds = getBounds(root, Node::maxWidth, Node::maxHeight);
    stage.setMaxWidth(maxBounds.getWidth() + deltaWidth);
    stage.setMaxHeight(maxBounds.getHeight() + deltaHeight);
}

Also note, that I used Node.getLayoutBounds instead of Node.getBoundsInLocal.

-3

You should block the window resize by writing:

primaryStage.setResizable(false);
  • 1
    His requirement is to allow user to make it bigger, your solution forbits that. – Tomas Bisciak Aug 27 '17 at 13:02
  • i see the problem now . So the solution as I think is to create a frame listener and block the resize if it is under a certain limit . `frame.addComponentListener(new ComponentListener() { public void componentResized(ComponentEvent e) { Rectangle r = frame.getBounds(); h = r.height; w = r.width; if (h < 400 || w < 400) frame.setResizable(false); } });` – Yasser Kotrsi Aug 27 '17 at 13:13
  • Then the problem is how to programmatically find those limits. – Pablo Fernandez Aug 27 '17 at 16:57
  • Test your GUI by changing the frame's size until you find the frame's limit. I suggested a certain limit which is 400 x 400. – Yasser Kotrsi Aug 27 '17 at 19:50