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 Mean, Golden 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 <= 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