FXML referencing CSS variables - css

I have google'd around a bit without an answer. In FXML I know how to reference a css, a style, etc using the styleClass and style tags. I'd like to know if it is possible to reference a single css variable adhoc.
For instance, if I want to set the padding of a pane is it possible to achieve the following, or something similar:
example.css
/* ---------- Constants ---------- */
*{
margin_small: 1.0em;
margin_large: 2.0em;
}
example fxml
<padding>
<Insets bottom="margin_small" left="margin_small" right="margin_small" top="margin_large" />
</padding>
The alternative would be to make a css style for every combination of these, or to reference them with the style tag. I'd prefer to avoid both of those options.
Is this possible ?

I couldn't get to the solution I actually wanted, I would've hoped to be able to include a file (fxml, css, values, etc) and reference directly. The best I could do is to create a POJO with a property for each constant, then defining an instance of the POJO in fxml.
The problem with this is each fxml will be creating a new instance of a class, which is a little wasteful seeing as the constants are static in nature.
The following is what I did:
FXMLConstants.java
// Class instance containing global variables to be referenced in fxml files. This is to allow us to use constants similarly to how Android's xml structure does
public class FXMLConstants
{
private static final DoubleProperty marginSmall = new SimpleDoubleProperty(10);
private static final DoubleProperty marginMedium = new SimpleDoubleProperty(15);
private static final DoubleProperty marginLarge = new SimpleDoubleProperty(25);
public Double getMarginSmall()
{
return marginSmall.getValue();
}
public Double getMarginMedium()
{
return marginMedium.getValue();
}
public Double getMarginLarge()
{
return marginLarge.getValue();
}
}
Example.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import com.example.FXMLConstants?>
<GridPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<fx:define>
<FXMLConstants fx:id="fxmlConsts" />
</fx:define>
<padding>
<Insets bottom="$fxmlConsts.marginMedium" left="$fxmlConsts.marginLarge" right="$fxmlConsts.marginLarge" top="$fxmlConsts.marginMedium" />
</padding>
<children>
<Label text="Test" GridPane.columnIndex="0" GridPane.rowIndex="0" />
</children>
</GridPane>

Related

JavaFx : tab item does not fill the content

I have a TabPane declared like this :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TabPane?>
<TabPane fx:id="rootNode" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="#dark_theme.css" tabClosingPolicy="UNAVAILABLE" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="controllers.AppController" />
And I want to add tabs from my controller. So I do :
jsonConfig.getAvailableChannelIds().forEach( chId -> {
try {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("tab_item.fxml"));
Tab item = fxmlLoader.load();
item.setText(String.format("%d", chId));
rootNode.getTabs().add(item);
}catch (Exception e) {
e.printStackTrace();
}
});
"tab_item.fxml" looks as follows :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.layout.VBox?>
<Tab xmlns:fx="http://www.w3.org/1999/XSL/Transform">
<VBox>
<fx:include source="test.fxml"/>
</VBox>
</Tab>
And finally "test.fxml" :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" style="-fx-background-color: red;" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" />
And here is what I have :
What am I missing to fill the Tab content with the red square ?
Most layout panes, such as VBox, will by default try to size their content to the preferred size, and will make every attempt to keep the sizes within the constraints specified as the minimum and maximum.
I recommend reading the old Oracle layout tutorial. It was written a long time ago (before Java 8), so the code style is a bit out of date, but the layout concepts are all still relevant. Also read the Javadocs for the layout package, and presumably you already read the Javadocs for any layout component you are using (if not, you should always read the docs for the classes you use).
You have
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity"
prefHeight="400.0" prefWidth="600.0"
style="-fx-background-color: red;"
xmlns="http://javafx.com/javafx/19"
xmlns:fx="http://javafx.com/fxml/1" />
which explicitly sets the preferred size to 600x400 pixels, and additionally sets the minimum and maximum sizes to -Infinity, which is the sentinel value Region.USE_PREF_SIZE. Therefore, the VBox will always size the content to 600x400 pixels (as long as there is enough space in the VBoxto do so).
Remove all those settings (I corrected the namespace too, though you don't really need it here):
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane style="-fx-background-color: red;"
xmlns:fx="http://javafx.com/fxml" />
Now the VBox will still attempt to set the content to its preferred size, which by default is computed from its own content. Since the anchor pane is empty, that preferred size will be computed as 0x0. However, since the max size is now unconstrained, you can tell the VBox to let it grow:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.layout.VBox?>
<Tab xmlns:fx="http://javafx.com/fxml/">
<VBox fillWidth="true">
<fx:include source="test.fxml" VBox.vgrow="ALWAYS" />
</VBox>
</Tab>
Now the content defined in test.fxml will fill the VBox. The behavior of a Tab is to let its content fill the entire region of the tab, so the VBox will fill the tab.
Of course, it's not really clear why you are wrapping test.fxml in a VBox in the first place. Since the tab will be filled by its content by default, why not just simplify to
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Tab?>
<Tab xmlns:fx="http://javafx.com/fxml/">
<fx:include source="test.fxml" />
</Tab>
Here is a complete example, with all the unnecessary hard-coded sizes and redundant layout panes removed:
HelloApplication.java:
package org.jamesd.examples.tab;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
#Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
hello-view.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TabPane?>
<TabPane fx:id="rootNode"
tabClosingPolicy="UNAVAILABLE"
xmlns:fx="http://javafx.com/fxml/"
fx:controller="org.jamesd.examples.tab.AppController" />
AppController.java:
package org.jamesd.examples.tab;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
public class AppController {
#FXML
private TabPane rootNode ;
public void initialize() {
try {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("tab_item.fxml"));
Tab item = fxmlLoader.load();
item.setText(String.format("%d", 42));
rootNode.getTabs().add(item);
}catch (Exception e) {
e.printStackTrace();
}
}
}
tab-item.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Tab?>
<Tab xmlns:fx="http://javafx.com/fxml/">
<fx:include source="test.fxml" />
</Tab>
and test.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane style="-fx-background-color: red;"/>
Ok, so I found at least a workaround here -> just wrap fx:include with AnchorsPane and it works :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.layout.AnchorPane?>
<Tab xmlns:fx="http://www.w3.org/1999/XSL/Transform">
<content>
<AnchorPane>
<fx:include source="test.fxml" AnchorPane.topAnchor="0" AnchorPane.rightAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.bottomAnchor="0"/>
</AnchorPane>
</content>
</Tab>

JavaFX File Chooser for an MVC Application

AFAIK, JavaFX doesn't have a FileChooser Component in Scene Builder/FXML. It is needed to set it using Java, using javafx.stage.FileChooser class. What is the best way to do it an MVC Application - where all my Views are in FXML?
Thanks.
Could you give me an example of FileChooser in FXML? cc #mrmcwolf
<?import javafx.scene.layout.GridPane?>
<?import javafx.stage.FileChooser?>
<GridPane fx:controller="sample.Controller" xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<fx:define>
<FileChooser fx:id="chooser"/>
</fx:define>
</GridPane>
public class Controller {
#FXML
private FileChooser chooser;
}
By fx:define are declared objects that are not placed in the generated view but can be accessed in the controllers and / or in the other declarations via id.

JavaFX controller injection does not work

I have two fxml files. I connect them with an include statement:
The "main" fxmlfile looks like that:
<?import javafx.geometry.*?>
// ...
<BorderPane prefHeight="962" prefWidth="1280" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MyMainController">
<center>
<SplitPane dividerPositions="0.63" BorderPane.alignment="CENTER">
<items>
<fx:include source="AnotherFile.fxml" />
// ...
</items>
</SplitPane>
</center>
<top>
// ...
</top>
</BorderPane>
And the second one (= "AnotherFile.fxml") like that:
<?import java.lang.*?>
// ...
<SplitPane dividerPositions="0.15" orientation="VERTICAL" prefHeight="400.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1">
<items>
// ...
<Label fx:id="oneOfMyLabels" text="myText" GridPane.columnIndex="2" GridPane.rowIndex="1" />
</items>
</SplitPane>
Now, I am using injections in the "main"-controller application.MyMainController:
#FXML
private Label oneOfMyLabels;
If I run the controller I get a java.lang.NullPointerException exception, respectively a java.lang.reflect.InvocationTargetException one. In debugging mode I found out, that the injected Label is null!
Now, my question:
Can't reach the MyMainController from the "main fxml file" the components of the included fxml file?? Do I have to use an own controller on each fxml file, if it is included or not?!
Thanks for your help!!
You need to have a different controller for each FXML file, and the fx:id-annotated elements of each file will be injected into the corresponding controller instance.
When you have included FXML files, you can inject the controller for the included file into the controller for the including file, by setting an fx:id attribute on the fx:include element:
"main" fxml file:
<?import javafx.geometry.*?>
// ...
<BorderPane prefHeight="962" prefWidth="1280" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MyMainController">
<center>
<SplitPane dividerPositions="0.63" BorderPane.alignment="CENTER">
<items>
<fx:include fx:id="another" source="AnotherFile.fxml" />
// ...
</items>
</SplitPane>
</center>
<top>
// ...
</top>
</BorderPane>
and in the "main controller":
public class MyMainController {
#FXML
private AnotherController anotherController ;
// ...
}
(the rule being that the field name is the value of the fx:id attribute with "Controller" appended). Here AnotherController is the controller class for AnotherFile.fxml.
Now you can, for example, expose the data you need to access in the "included controller":
public class AnotherController {
#FXML
private Label oneOfMyLabels ;
public StringProperty textProperty() {
return oneOfMyLabels.textProperty();
}
public final String getText() {
return textProperty().get();
}
public final setText(String text) {
textProperty().set(text);
}
// ...
}
and then your main controller can do things like
anotherController.setText(...);
which will of course update the label. This preserves encapsulation, so that if you choose to use another control instead of a label, those changes do not have to propagate outside of the immediate controller.

JavaFX: Button's width in custom gridpane

I would like to have buttons with equal(maximum) width in a gridpane.
When I'm trying to set it directly in FXML - it works perfectly.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="TestController">
<children>
<GridPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" AnchorPane.bottomAnchor="0"
AnchorPane.topAnchor="0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" percentWidth="50.0"/>
<ColumnConstraints hgrow="SOMETIMES" percentWidth="50.0"/>
</columnConstraints>
<children>
<Button maxWidth="Infinity" text="Button1" GridPane.columnIndex="0"/>
<Button maxWidth="Infinity" text="Button2" GridPane.columnIndex="1"/>
</children>
</GridPane>
</children>
</AnchorPane>
But when I wanted to separate the whole grid into a custom control, the buttons stopped to fill the available width.
Here's a FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import CustomGridPane?>
<AnchorPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="TestController">
<children>
<CustomGridPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.topAnchor="0">
</CustomGridPane>
</children>
</AnchorPane>
And an extension:
public class CustomGridPane extends GridPane {
private Button button1 = new Button("button1");
private Button button2 = new Button("button2");
public CustomGridPane() {
super();
button1.maxWidth(Double.MAX_VALUE);
button2.maxWidth(Double.MAX_VALUE);
getColumnConstraints().add(new ColumnConstraints());
getColumnConstraints().add(new ColumnConstraints());
getColumnConstraints().get(0).setPercentWidth(50);
getColumnConstraints().get(0).setHgrow(Priority.SOMETIMES);
getColumnConstraints().get(1).setPercentWidth(50);
getColumnConstraints().get(1).setHgrow(Priority.SOMETIMES);
add(button1, 0, 0);
add(button2, 1, 0);
}
}
Am I missing something?
You are using maxWidth property getter instead of the method setMaxWidth (setter).
Please see the documentation of Button here
public final double maxWidth(double height)
Called during layout to determine the maximum width for this node. Returns the value from computeMaxWidth(forHeight) unless the application overrode the maximum width by setting the maxWidth property.
Replace the two lines with maxWidth(...) by these ones:
button1.setMaxWidth(Double.MAX_VALUE);
button2.setMaxWidth(Double.MAX_VALUE);
This can be achieved inside scene builder also...
I required this, while creating a calculator, with the buttons inside gridpane.
Happy Coding (:

JavaFX: how to make a proper vertical toolbar?

I want to make a vertical toolbar with buttons arranged vertically. Using JavaFX 2.2 that is included in JDK 7, in Linux Mint.
The screenshot shows the problem:
The FXML I am using looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<?language javascript?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane prefHeight="800.0" prefWidth="700.0" styleClass="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<top>
<ToolBar>
<items>
<Button text="Test" />
</items>
</ToolBar>
</top>
<left>
<ToolBar orientation="VERTICAL" style="-fx-background-color: blue;">
<items>
<Region style="-fx-padding:10;" />
<Button rotate="-90" text="Project" style="-fx-label-padding:1;"/>
<Region style="-fx-padding:10;" />
<Button rotate="-90" text="Structure" />
</items>
</ToolBar>
</left>
<center>
<HBox>
<children>
</children>
</HBox>
</center>
<bottom>
<ToolBar prefHeight="18.0" prefWidth="472.0">
<items>
<Region styleClass="spacer" />
<HBox>
<children>
</children>
</HBox>
</items>
</ToolBar>
</bottom>
</BorderPane>
The proper toolbar in my definition is: buttons are placed correctly and the toolbar is as wide as the width of the buttons. The blue color indicates how wide the toolbar currently is.
Wrap your rotated tool items in a Group, then the in-built layout of toolbar will know that the rotation is a permanent one which should be taken into account for layout calculations and not just a temporary thing which might be used for animations. Read the javadoc for Group, where it talks about layout bounds calculations to better understand this.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<HBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="100.0" style="-fx-background-color: cornsilk;" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<children>
<ToolBar orientation="VERTICAL" style="-fx-base: palegreen;">
<items>
<Group>
<children>
<Button rotate="-90.0" style="-fx-base: gold;" text="Project" />
</children>
</Group>
<Group>
<children>
<Button rotate="-90.0" style="-fx-base: khaki;" text="Structure" />
</children>
</Group>
</items>
</ToolBar>
</children>
</HBox>
Update 24th April 2017
The above solution is fine as far as it goes, but does suffer from an issue in that the buttons in the toolbar misalign when they receive focus.
What a group does is size itself based upon its contents. When the size of the contents changes, the size of the group also changes. When a button or control gets focus in JavaFX it gets a focus ring around the control. The display for the focus ring is defined in CSS and contains negative values for background inset display. The result is that, when a control is focused, it is slightly larger than when it is not focused. Normally, when you use a standard layout pane, this is not an issue as the layout pane will just ignore the background insets for layout purposes. A group however takes the full size into account and does not ignore the focus ring. The result is that a group that consists of only a control will change in size slightly when it is focused or unfocused. This presents an issue with the solution above because, when a button becomes focused, it will get slightly larger and the shift in layout in the toolbar, which is not ideal.
The solution to the focus shift problem in the above code is to just rotate the entire ToolBar within a group rather than rotating each button within a group per button. This works fine, but then presents some other issues such as the ToolBar not taking up the entire available area at the left side of the scene (due to wrapping it in a group removing the dynamic layout properties of the ToolBar). To get around this, a binding in code can be used to size the ToolBar to the available area of its parent layout container.
So we end up with the slightly more verbose solution below:
skinsample/toolbar.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.Group?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.control.ToggleGroup?>
<BorderPane fx:id="border" prefHeight="200.0" prefWidth="100.0" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="skinsample.VerticalToolbarController">
<left>
<Group>
<fx:define>
<ToggleGroup fx:id="selectedTool"/>
</fx:define>
<ToolBar fx:id="toolbar" rotate="-90.0" style="-fx-base: palegreen;">
<Pane HBox.hgrow="ALWAYS" />
<ToggleButton style="-fx-base: khaki;" text="Structure" toggleGroup="${selectedTool}"/>
<ToggleButton style="-fx-base: gold;" text="Project" toggleGroup="${selectedTool}" selected="true"/>
</ToolBar>
</Group>
</left>
</BorderPane>
skinsample/VerticalToolbarController.java
package skinsample;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
public class VerticalToolbarController {
#FXML
private BorderPane border;
#FXML
private ToolBar toolbar;
public void initialize() {
toolbar.minWidthProperty().bind(Bindings.max(border.heightProperty(), toolbar.prefWidthProperty()));
}
}
skinsample/ToolDisplayApp.java
package skinsample;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class ToolDisplayApp extends Application {
#Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("toolbar.fxml"));
Scene scene = new Scene(loader.load());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Notes:
This solution also demonstrates use of ToggleButtons rather than standard buttons within the ToolBar.
We also eliminate the default overflow behavior of the ToolBar (as it seems a little annoying in the vertical toolbar situation), using:
toolbar.minWidthProperty().bind(Bindings.max(border.heightProperty(), toolbar.prefWidthProperty()));
If you want to retain the overflow behavior, then use:
toolbar.prefWidthProperty().bind(border.heightProperty());
An alternate solution to the focus issue (using CSS to remove the focus ring entirely), is presented in:
JavaFX - How to prevent Toolbar from changing width on button state changes.

Resources