First steps with JavaFX

This tutorial will introduce

  • Clean separation of application UI and logic
  • Using Builders supporting Fluent API
  • Effects
  • Layouts
  • Property Bindings
  • EventHandler

Abstract

The Golden Ratio (Golden MeanGolden Section) is defined as φ = (√5 + 1) / 2.

Objective

Create an UI to find the golden section by using a slider. Add value labels and an indicator to get green when golden action is hit.

The Model

Create observable double a and b values and a boolean value to indicate whether the golden section has been hit:

private DoubleProperty aValue = new SimpleDoubleProperty(0.0);
private DoubleProperty bValue = new SimpleDoubleProperty(0.0);
private BooleanProperty sectioAurea = new SimpleBooleanProperty(false);

Method to calculate the golden section:

private void reCalculate(){

double b = aValue.substract(100).multiply(-1).get();
setBValue(b);

double result1 = aValue.get()/bValue.get();
result1 = Math.round(result1*100.0)/100.0;

double result2 = (aValue.get()+bValue.get())/aValue.get();
result2 = Math.round(result2*100.0)/100.0;

sectioAurea.set(result1 == result2);
}

Please note: get() returns the primitive double value, getValue() respectively returns the Double instance.

Next attach listeners to re-caculate the golden section indicator on value changes:

private void attachListener(){ChangeListener onChangeListener = new ChangeListener(){
@Override
public void changed(ObservableValue arg0, Number arg1, Number arg2) {
reCalculate();
}
};

getAValueProperty().addListener(onChangeListener);
getBValueProperty().addListener(onChangeListener);
}

Here is the complete model code width appropriate getter and setter methods:

package de.jensd.javafx.aurea;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

/**
*
* @author Jens Deters
*/
public class SectioModel {

private DoubleProperty aValue = new SimpleDoubleProperty(0.0);
private DoubleProperty bValue = new SimpleDoubleProperty(0.0);
private BooleanProperty sectioAurea = new SimpleBooleanProperty(false);

public SectioModel() {
attachListener();
}

private void attachListener(){

ChangeListener onChangeListener = new ChangeListener(){
@Override
public void changed(ObservableValue arg0, Number arg1, Number arg2) {
reCalculate();
}
};

getAValueProperty().addListener(onChangeListener);
getBValueProperty().addListener(onChangeListener);
}

public DoubleProperty getAValueProperty() {
return aValue;
}

public double getAValue() {
return aValue.get();
}

public void setAValue(double d) {
aValue.set(d);
}

public DoubleProperty getBValueProperty() {
return bValue;
}

public double getBValue() {
return bValue.get();
}

public void setBValue(double d) {
bValue.set(d);
}

public BooleanProperty getSectioAureaProperty() {
return sectioAurea;
}

public boolean isSectioAurea() {
return sectioAurea.get();
}

private void reCalculate(){

double b = aValue.substract(100).multiply(-1).get();
setBValue(b);

double result1 = aValue.get()/bValue.get();
result1 = Math.round(result1*100.0)/100.0;

double result2 = (aValue.get()+bValue.get())/aValue.get();
result2 = Math.round(result2*100.0)/100.0;

sectioAurea.set(result1 == result2);
}
}

The UI 

Title Label and usage hint

Text titleText = TextBuilder.create().
text("Sectio Aurea").
effect(ds).
font(Font.font(null, FontWeight.BOLD, 32)).
build();

Text descriptionText = TextBuilder.create().
text("Move the slider until your hit the golden section.\nHint: use the cursor keys for exact control.").
textAlignment(TextAlignment.CENTER).
build();

Indicator

Stop[] falseStops = new Stop[]{new Stop(0.0, Color.WHITE), new Stop(0.3, Color.RED), new Stop(1.0, Color.DARKRED)};
falsePaint = new RadialGradient(0.0, 0.0, -10.0, -10.0, 50, false, CycleMethod.NO_CYCLE, falseStops);

Stop[] trueStops = new Stop[]{new Stop(0.0, Color.WHITE), new Stop(0.3, Color.GREENYELLOW), new Stop(1.0, Color.DARKGREEN)};
truePaint = new RadialGradient(0.0, 0.0, -10.0, -10.0, 50, false, CycleMethod.NO_CYCLE, trueStops);

ellipse = EllipseBuilder.create().
radiusX(30).
radiusY(30).
fill(falsePaint).
build();

DropShadow ds = new DropShadow();
ds.setOffsetY(3.0f);
ds.setColor(Color.color(0.4f, 0.4f, 0.4f));

ellipse.setEffect(ds);

Slider

slider = SliderBuilder.create().
min(model.getAValue()).
max(model.getBValue()).
showTickMarks(true).
showTickLabels(true).
majorTickUnit(20).
minorTickCount(3).
prefWidth(300).
blockIncrement(0.1f).
build();

Status Labels

aValueText = TextBuilder.create().build();
bValueText = TextBuilder.create().build();
aureaText = TextBuilder.create().build();

Solve Button

solveButton = ButtonBuilder.create().text("Solve").build();

Add the solve action

solveButton.setOnAction(new EventHandler() {
@Override
public void handle(ActionEvent arg0) {
model.setAValue(61.8);
}
});

Put it all together

Using the GridPane layout:

GridPane root = new GridPane();
root.setPadding(new Insets(20, 20, 20, 20));
root.setGridLinesVisible(true);</h2>
RowConstraints rowInfo = new RowConstraints(100);
ColumnConstraints colInfo = new ColumnConstraints(100);

root.getRowConstraints().
add(rowInfo);

for (int j = 0; j &lt;= 2; j++) {
root.getColumnConstraints().
add(colInfo);
}

GridPane.setHalignment(titleText, HPos.CENTER);
GridPane.setHalignment(descriptionText, HPos.CENTER);
GridPane.setHalignment(slider, HPos.CENTER);
GridPane.setHalignment(aValueText, HPos.CENTER);
GridPane.setHalignment(ellipse, HPos.CENTER);
GridPane.setHalignment(bValueText, HPos.CENTER);
GridPane.setHalignment(solveButton, HPos.CENTER);

GridPane.setConstraints(titleText, 0, 0, 3, 1);
GridPane.setConstraints(descriptionText, 0, 1, 3, 1);
GridPane.setConstraints(slider, 0, 2, 3, 1);
GridPane.setConstraints(aValueText, 0, 3);
GridPane.setConstraints(ellipse, 1, 3);
GridPane.setConstraints(bValueText, 2, 3);
GridPane.setConstraints(solveButton, 2, 4);

Connect UI to Model

model.getAValueProperty().
bindBidirectional(slider.valueProperty());

aValueText.textProperty().
bind(model.getAValueProperty().
asString("%.1f"));
bValueText.textProperty().
bind(model.getBValueProperty().
asString("%.1f"));

aureaText.textProperty().
bind(model.getSectioAureaProperty().
asString());
<h2>Connect listener to refresh the indicator:</h2>
<pre>model.getSectioAureaProperty().
            addListener(new ChangeListener() {</pre>
@Override
public void changed(ObservableValue arg0, Boolean arg1, Boolean arg2) {
refreshIndicator();
}
});

The complete UI code

package de.jensd.javafx.aurea;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBuilder;
import javafx.scene.control.Slider;
import javafx.scene.control.SliderBuilder;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.RowConstraints;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.Paint;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.EllipseBuilder;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextBuilder;
import javafx.stage.Stage;

/**
*
* @author Jens Deters
*/
public class SectioAureaApplication extends Application {

private SectioModel model = new SectioModel();
private Slider slider;
private Text aValueText;
private Text bValueText;
private Text aureaText;
private Ellipse ellipse;
private Paint falsePaint;
private Paint truePaint;
private Button solveButton;

public static void main(String[] args) {
Application.launch(SectioAureaApplication.class, args);
}

@Override
public void start(Stage stage) throws Exception {
stage.setTitle("Sectio Aurea");

model = new SectioModel();
model.setAValue(0);
model.setBValue(100.9);

slider = SliderBuilder.create().
min(model.getAValue()).
max(model.getBValue()).
showTickMarks(true).
showTickLabels(true).
majorTickUnit(20).
minorTickCount(3).
prefWidth(300).
blockIncrement(0.1f).
build();

aValueText = TextBuilder.create().
build();

bValueText = TextBuilder.create().
build();

aureaText = TextBuilder.create().
build();

solveButton = ButtonBuilder.create().
text("Solve").
build();

Stop[] falseStops = new Stop[]{new Stop(0.0, Color.WHITE), new Stop(0.3, Color.RED), new Stop(1.0, Color.DARKRED)};
falsePaint = new RadialGradient(0.0, 0.0, -10.0, -10.0, 50, false, CycleMethod.NO_CYCLE, falseStops);

Stop[] trueStops = new Stop[]{new Stop(0.0, Color.WHITE), new Stop(0.3, Color.GREENYELLOW), new Stop(1.0, Color.DARKGREEN)};
truePaint = new RadialGradient(0.0, 0.0, -10.0, -10.0, 50, false, CycleMethod.NO_CYCLE, trueStops);

ellipse = EllipseBuilder.create().
radiusX(30).
radiusY(30).
fill(falsePaint).
build();

DropShadow ds = new DropShadow();
ds.setOffsetY(3.0f);
ds.setColor(Color.color(0.4f, 0.4f, 0.4f));

ellipse.setEffect(ds);

Text titleText = TextBuilder.create().
text("Sectio Aurea").
effect(ds).
font(Font.font(null, FontWeight.BOLD, 32)).
build();

Text descriptionText = TextBuilder.create().
text("Move the slider until your hit the golden section.\nHint: use the cursor keys for exact control.").
textAlignment(TextAlignment.CENTER).
build();

// Put it all together
GridPane root = new GridPane();
root.setPadding(new Insets(20, 20, 20, 20));
root.setGridLinesVisible(false);

RowConstraints rowInfo = new RowConstraints(100);
ColumnConstraints colInfo = new ColumnConstraints(100);

root.getRowConstraints().
add(rowInfo);
root.getRowConstraints().
add(rowInfo);

for (int j = 0; j <= 2; j++) {
root.getColumnConstraints().
add(colInfo);
}

GridPane.setHalignment(titleText, HPos.CENTER);
GridPane.setHalignment(descriptionText, HPos.CENTER);
GridPane.setHalignment(slider, HPos.CENTER);
GridPane.setHalignment(aValueText, HPos.CENTER);
GridPane.setHalignment(ellipse, HPos.CENTER);
GridPane.setHalignment(bValueText, HPos.CENTER);
GridPane.setHalignment(solveButton, HPos.CENTER);

GridPane.setConstraints(titleText, 0, 0, 3, 1);
GridPane.setConstraints(descriptionText, 0, 1, 3, 1);
GridPane.setConstraints(slider, 0, 2, 3, 1);
GridPane.setConstraints(aValueText, 0, 3);
GridPane.setConstraints(ellipse, 1, 3);
GridPane.setConstraints(bValueText, 2, 3);
GridPane.setConstraints(solveButton, 2, 4);

root.getChildren().
addAll(titleText, descriptionText, slider, aValueText, ellipse, bValueText, solveButton);

bindProperties();

stage.setScene(new Scene(root, 350, 380));
stage.show();

}

private void refreshIndicator() {
if (model.getSectioAureaProperty().
get()) {
ellipse.fillProperty().
set(truePaint);

} else {
ellipse.fillProperty().
set(falsePaint);
}
}

private void bindProperties() {

model.getAValueProperty().
bindBidirectional(slider.valueProperty());

aValueText.textProperty().
bind(model.getAValueProperty().
asString("%.1f"));
bValueText.textProperty().
bind(model.getBValueProperty().
asString("%.1f"));

aureaText.textProperty().
bind(model.getSectioAureaProperty().
asString());

model.getSectioAureaProperty().
addListener(new ChangeListener() {

@Override
public void changed(ObservableValue arg0, Boolean arg1, Boolean arg2) {
refreshIndicator();
}
});

solveButton.setOnAction(new EventHandler() {

@Override
public void handle(ActionEvent arg0) {
model.setAValue(61.8);
}
});

}
}

_____________________________________________________

here’s the NetBeans project: SectioAurea.zip

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.