Alex Johnson
Alex Johnson

Reputation: 534

Can you pass through a variable to Insets in FXML?

I am doing some work that requires me to have different values for elements based on a scalar value. I have the work to come up with the scalar value in a class called Sizer with a method called getScalar() which returns a double. Below is the partial FXML that I have right now.

<VBox id="mainWindow" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="controller">
   <fx:define>
      <Sizer fx:id="SCALAR" fx:factory="getScalar"/>
   </fx:define>
   <children>
     <HBox alignment="CENTER_LEFT">
        <children>
           <Label fx:id="titleLabel" text="Label" HBox.hgrow="ALWAYS" />
           <HBox fx:id="titleBar" HBox.hgrow="ALWAYS" />
        </children>
        <padding>
           <!-- This part DOESN'T work at runtime -->
           <Insets left="${SCALAR * 10.0}" right="${SCALAR * 10.0}" top="${SCALAR * 5.0}" />
        </padding>
     </HBox>
     <!-- This part DOES work at runtime -->
     <Separator prefWidth="${SCALAR * 200.0}" />
  </children>
</Vbox>

In this code, if I take out the Insets references and just make those values 10.0, 5.0, etc, it works at runtime and my dialog appears. The 'prefWidth' setting using SCALAR seems to be working fine, but when I add in the reference to the Insets values it throws and error pointing to the line with Insets on it.

javafx.fxml.LoadException: Cannot bind to untyped object.

at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2621)
at javafx.fxml.FXMLLoader.access$100(FXMLLoader.java:105)
at javafx.fxml.FXMLLoader$Element.processPropertyAttribute(FXMLLoader.java:306)
at javafx.fxml.FXMLLoader$Element.processInstancePropertyAttributes(FXMLLoader.java:242)
at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:757)
at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2722)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2552)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2466)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2435)

My Understanding

You are able to create these defined objects through define blocks and then reference them in other attributes by way of expression binding and variable resolution, but somehow I cannot do this with Insets. The only thing that I've found is that 'prefWidth' has a setter taking a double whereas 'left' or other positions with Insets have getters. I thought maybe it could have issues with binding to an object that doesn't appear to have a setter.

Any help would be appreciated as I'm not that knowledgable about FXML, and I come from a JavaFX world where this is much easier.

EDIT Below is the contents of the Sizer class

public static Toolkit getToolKit() {
    return Toolkit.getDefaultToolkit();
}
public static Dimension getScreenSize() {
    return getToolKit().getScreenSize();
}
public static int getPercentOfHeight(double percent) {
    return (int)(getScreenSize().getHeight() * percent / 100.0);
}
public static int getPercentOfWidth(double percent) {
    return (int)(getScreenSize().getWidth() * percent / 100.0);
}
public static double getScaleFactor() {
    if ((int) getScreenSize().getHeight() > 1800) return 2.0;    //4K
    else if ((int)getScreenSize().getHeight() > 1260) return 1.33; //1440p
    else if ((int)getScreenSize().getHeight() > 900) return 1.0;     //1080p
    else return 0.667;                                   //720p
}

Upvotes: 0

Views: 741

Answers (1)

Slaw
Slaw

Reputation: 46156

The ${} syntax is used to create an expression binding which in turn is used to bind a Property to the resulting ObservableValue. The problem with your code is Insets does not expose its state as Property instances. On top of that, the Insets class is immutable which means the state is set via one of the constructors and both constructors only accept double arguments. You can't pass the result of an expression binding as a double and FXMLLoader does not "cleverly" extract the current value at the time it's creating the Insets instance.

From what I understand, you want to multiply each side of the Insets by a single double value and this only needs to happen once (i.e. you don't need to update the value over time). In that case, one solution is to subclass Insets and "add a parameter" to each constructor for your scalar argument.

package com.example;

import javafx.beans.NamedArg;
import javafx.geometry.Insets;

public class ScaledInsets extends Insets {

    public ScaledInsets(@NamedArg(value = "scale", defaultValue = "1") double scale,
                        @NamedArg("top") double top, @NamedArg("right") right,
                        @NamedArg("bottom") double bottom, @NamedArg("left") double left) {
        super(scale * top, scale * right, scale * bottom, scale * left);
    }

    public ScaledInsets(@NamedArg(value = "scale", defaultValue = "1") double scale,
                        @NamedArg("topRightBottomLeft") double topRightBottomLeft) {
        super(scale * topRightBottomLeft);
    }

}

Which would let you use the following in an FXML file:

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

<?import com.example.ScaledInsets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?>

<StackPane xmlns="http://javafx.com/javafx/" xmlns:fx="http://javafx.com/fxml">
    <fx:define>
        <Sizer fx:id="SCALAR" fx:factory="getScaleFactor"/>
    </fx:define>
    <padding>
        <ScaledInsets scale="$SCALAR" top="5" left="10" right="10"/>
    </padding>
    <Label text="Hello, World!"/>
</StackPane>

Note that FXML also supports scripting:

The <fx:script> tag allows a caller to import scripting code into or embed script within a FXML file. Any JVM scripting language can be used, including JavaScript, Groovy, and Clojure, among others. Script code is often used to define event handlers directly in markup or in an associated source file, since event handlers can often be written more concisely in more loosely-typed scripting languages than they can in a statically-typed language such as Java.

I've never used scripting in FXML before and don't know how to use it in an example at the moment. Suffice it to say, you can mess around with it but I don't know if scripting is a viable solution.


Unrelated to your problem, but you appear to be using AWT classes in order to get the screen dimensions. JavaFX provides its own API for this: javafx.stage.Screen. You can use:

Rectangle2D visualBounds = Screen.getPrimary().getVisualBounds();

To get the dimensions of the primary screen. To get the dimensions of the entire screen and not just the visual area use getBounds() instead of getVisualBounds().

Upvotes: 2

Related Questions