Bind CSS Style Property to Node in JavaFX - css

I want to set up a model for my project so my controllers can communicate with each other. I want it to have a setter and getter, to allow easy access to styling certain nodes from either class.
My question: is it possible to bind a style property (ex. "-fx-background-color: blue") to a node?
From my research, I see that this is definitely possible with text values for labels (explained by James_D here: JavaFX - How to use a method in a controller from another controller?), but I am having a hard time figuring out what the syntax for doing a similar thing with "setStyle" would be.
The model I have so far:
public class Model {
private final StringProperty shadow = new SimpleStringProperty("-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.24), 10,0,0,0)");
public StringProperty shadowProperty() {
return shadow;
}
public final String getShadow() {
return shadowProperty().get();
}
public final void setShadow(String shadow) {
shadowProperty().set(shadow);
}
}
I understand how I would set the "shadow" value from a controller, but what I don't understand is how I can bind a node from another controller to listen to that change.
Let's say the node is something like:
#FXML AnchorPane appBar
I want "appBar" to take on any changes made to "shadow" in the model. What would that look like?

You need to add a listener to the shadowProperty to listen to its changes.
something.shadowProperty() .addListener( (observable, oldValue, newValue) -> {
//do something with appBar
}) ;
I'm not entirely sure what you want to achieve, but this should answer your question about how to listen to property changes.
PS: im on mobile, so no guarantees regarding typos
Edit: you can also bind the property of one object to the property of another. Use bind() for that.
EDIT: Here is an example:
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class Main extends Application {
Property<Background> backgroundProperty;
StringProperty styleProperty;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
VBox root = new VBox(10);
backgroundProperty = new SimpleObjectProperty<>();
styleProperty = new SimpleStringProperty();
// Pane that changes background by listener
Pane pane1 = new Pane();
pane1.setMinHeight(40);
backgroundProperty.addListener( (observable, oldValue, newValue) -> {
pane1.setBackground(backgroundProperty.getValue());
});
// Pane that changes background by property binding
Pane pane2 = new Pane();
pane2.setMinHeight(40);
pane2.backgroundProperty().bind(backgroundProperty);
// Pane that binds the style property
Pane pane3 = new Pane();
pane3.setMinHeight(40);
pane3.styleProperty().bind(styleProperty);
backgroundProperty.setValue(new Background(new BackgroundFill(Color.RED, null, null)));
styleProperty.setValue("-fx-background-color: black");
root.getChildren().add(pane1);
root.getChildren().add(pane2);
root.getChildren().add(pane3);
Scene scene = new Scene(root, 200, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
}

Related

Why is my javafx label not showing after it being changed?

I'm new to javafx programming, and i dont understand why my javafx Text isn't getting updated, when it is changed.
I want to make a timer, that counts from 60 to 0. I'm trying to change the timeCounter Text, for every second that has passed.
Help would be appreciated!
Here's my controller code:
public class Controller {
TimerUtil timerUtil;
#FXML
private Button startButton;
#FXML
private Text timeCounter;
#FXML
private Text pointCounter;
#FXML
private Circle circle;
#FXML
private void handleStartButtonClick(ActionEvent event) {
timerUtil = new TimerUtil();
}
private class TimerUtil extends Pane {
private int tmp = 60;
private Timeline animation;
public TimerUtil(){
getChildren().add(timeCounter);
animation = new Timeline(new KeyFrame(Duration.seconds(1), e -> timeLabel()));
animation.setCycleCount(Timeline.INDEFINITE);
animation.play();
}
private void timeLabel(){
if(tmp > 0){
tmp--;
}
timeCounter.setText(String.valueOf(tmp));
System.out.println(tmp);
}
}
}
Your error occurs because the label has been silently removed from it's displayed parent node:
You have your TimerUtil class extend Pane (I have no idea why).
You add the timeCounter text to the TimeUtil pane (again, I have no idea why).
Adding the timeCounter text to the TimeUtil pane will silently remove it from the parent which the FXML loader injected it into.
You are probably only displaying the parent which the FXML loader injected.
You are never displaying the TimerUtil pane.
Therefore, even though the text is getting updated by your timeline, you never see it.
To better understand your error, read:
JavaFX - Why does adding a node to a pane multiple times or to different panes result in an error?
From the Node javadoc:
If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent.
Once you fix your error, the basic concept works for me. Here is the runnable example I created from your code:
import javafx.animation.*;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Timer extends Application {
private int tmp = 60;
private Text counter = new Text();
private Timeline animation = new Timeline(
new KeyFrame(Duration.seconds(1), e -> updateCounter())
);
#Override
public void start(Stage stage) {
animation.setCycleCount(Timeline.INDEFINITE);
animation.play();
StackPane layout = new StackPane(counter);
layout.setPadding(new Insets(20));
stage.setScene(new Scene(layout));
stage.show();
}
private void updateCounter() {
if (tmp > 0){
tmp--;
} else {
animation.stop();
}
counter.setText(String.valueOf(tmp));
System.out.println(tmp);
}
public static void main(String[] args) {
launch(args);
}
}

How to concatenate event handler with another method call?

I have two buttons in two separate classes, and I want to change the onAction of the first button when the second button has been pressed to be the original action plus one additional method call. Once the first button has been pressed I want its onAction to revert to the original.
What I currently have working is essientially
Button b1 = new Button("b1");
b1.setOnAction((event)-> {
oldActionMethod();
});
public void oldActionMethod(){
//actual code
}
b2.setOnAction((event)-> {
//some stuff
Button b1 = getB1();
EventHandler<ActionEvent> temp = b1.getOnAction();
b1.setOnAction((event) -> {
b1class.oldActionMethod();
additionalMethod();
b1.setOnAction(temp);
});
});
In order to make this work I had to move the block of code that was originally in the setOnAction lambda expression to a helper function. Is there a cleaner way to do this? Something like this which would eliminate the need for the helper function?
b1.setOnAction((event)-> {
//actual code
});
b2.setOnAction((event) -> {
//stuff
Button b1 = getB1();
EventHandler<ActionEvent> temp = b1.getOnAction();
b1.setOnAction(b1.getOnAction() + methodCall());
b1.setOnAction(temp);
//stuff
});
The way I have it currently does work but it feels really hack-y so I am just interested to know if there is a better option where you could essentially concatenate an actionEvent with another method. Also if there is a way to not require storing the original event in a temp object and resetting it at the end. A possible solution would be if I could tell b2 to listen for the next time b1 is pressed, but I don't know if there is any way to do that when they are in two separate classes.
One solution is to have a shared model class between the two classes containing the buttos.
See the following mcve. For conviniense the entire code can be copy-pasted into one file (FaMain.java) and run:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class FxMain extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
Model model = new Model();
AClass aClass = new AClass(model);
BClass bClass = new BClass(model);
Label show = new Label();
show.textProperty().bind(model.getTextProperty());
VBox root = new VBox(10);
root.getChildren().addAll(aClass.getButton(),show, bClass.getButton());
primaryStage.setScene(new Scene(root, 400,100));
primaryStage.sizeToScene();
primaryStage.show();
}
public static void main(final String[] args) {
launch(args);
}
}
class Model {
private final BooleanProperty aButtonSelected;
private final SimpleStringProperty textProperty;
Model(){
aButtonSelected = new SimpleBooleanProperty();
textProperty= new SimpleStringProperty();
}
ObservableValue<? extends String> getTextProperty() {
return textProperty;
}
BooleanProperty aButtonSelectedProperty(){
return aButtonSelected;
}
void bButtonClicked() {
textProperty.set(aButtonSelected.get() ? "Button clicked. Toggle IS selected" :
"Button clicked. Toggle is NOT selected");
}
}
class AClass{
private final ToggleButton aButton;
AClass(Model model) {
aButton = new ToggleButton("Toogle");
model.aButtonSelectedProperty().bind(aButton.selectedProperty());
}
ToggleButton getButton(){
return aButton;
}
}
class BClass{
private final Button bButton;
BClass(Model model) {
bButton = new Button("Click");
bButton.setOnAction(e->model.bButtonClicked());
}
Button getButton(){
return bButton;
}
}

JavaFx property binding with multiple objects on on screen

I use JavaFx with property binding.
I got a object 'Person' with the properties 'name' and age.
These objects are stored in a ObservableList.
The properties are bound to labels on the gui. When I change the person in the ListBox the data also change on the right hand side.
GUI with person list:
And now it comes to my problem.
I want to disply all persons on one window, like the next picture shows.
GUI with multiple persons on one view:
How can I handle this. I thought about HBox but the binding doesn't work.
FYI: Here you can find the tutorial I used.
https://code.makery.ch/library/javafx-tutorial/part1/
This looks like a perfect time to use a ListView with custom ListCell implementations.
The sample application below shows a very basic application that displays each Person object in a ListView. We will provide our own ListCell so we can control exactly how each Person gets displayed.
I also added a profile photo just for fun :)
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Separator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewDetailSample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple interface
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// First, let's create our list of Persons
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.addAll(
new Person("John", 34),
new Person("Cheyenne", 24),
new Person("Micah", 17),
new Person("Katelyn", 28)
);
// Create a ListView
ListView<Person> listView = new ListView<>();
// Bind our list to the ListView
listView.setItems(persons);
// Now, for the magic. We'll create our own ListCells for the ListView. This allows us to create a custom
// layout for each individual cell. For this sample, we'll include a profile picture, the name, and the age.
listView.setCellFactory(new Callback<ListView<Person>, ListCell<Person>>() {
#Override
public ListCell<Person> call(ListView<Person> param) {
return new ListCell<Person>() {
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
// Set any empty cells to show nothing
if (person == null || empty) {
setText(null);
setGraphic(null);
} else {
// Here we can build our layout. We'll use a HBox for our root container
HBox cellRoot = new HBox(5);
cellRoot.setAlignment(Pos.CENTER_LEFT);
cellRoot.setPadding(new Insets(5));
// Add our profile picture
ImageView imgProfilePic = new ImageView("/sample/user.png");
imgProfilePic.setFitHeight(24);
imgProfilePic.setFitWidth(24);
cellRoot.getChildren().add(imgProfilePic);
// A simple Separator between the photo and the details
cellRoot.getChildren().add(new Separator(Orientation.VERTICAL));
// Now, create a VBox to hold the name and age
VBox vBox = new VBox(5);
vBox.setAlignment(Pos.CENTER_LEFT);
vBox.setPadding(new Insets(5));
// Add our Person details
vBox.getChildren().addAll(
new Label("Name: " + person.getName()),
new Label("Age: " + person.getAge())
);
// Add our VBox to the cellRoot
cellRoot.getChildren().add(vBox);
// Finally, set this cell to display our custom layout
setGraphic(cellRoot);
}
}
};
}
});
// Now, add our ListView to the root layout
root.getChildren().add(listView);
// Show the Stage
primaryStage.setWidth(450);
primaryStage.setHeight(400);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
// Simple Person class
class Person {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty age = new SimpleIntegerProperty();
public Person(String name, int age) {
this.name.set(name);
this.age.set(age);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public int getAge() {
return age.get();
}
public IntegerProperty ageProperty() {
return age;
}
public void setAge(int age) {
this.age.set(age);
}
}
The Result:
Without ListView:
If you'd prefer not to use a ListView for this display, you can keep another list of your Person displays and bind that to the children list of whichever container you want:
// Create a list to hold our individual Person displays
ObservableList<Node> personDisplays = FXCollections.observableArrayList();
// Now add a new PersonDisplay to the list for each Person in the personsList
persons.forEach(person -> personDisplays.add(new PersonDisplay(person)));
// Bind our personsDisplay list to the children of our root VBox
Bindings.bindContent(root.getChildren(), personDisplays);
PersonDisplay class:
class PersonDisplay extends HBox {
public PersonDisplay(Person person) {
// First, let's configure our root layout
setSpacing(5);
setAlignment(Pos.CENTER_LEFT);
setPadding(new Insets(5));
// Add our profile picture
ImageView imgProfilePic = new ImageView("/user.png");
imgProfilePic.setFitHeight(24);
imgProfilePic.setFitWidth(24);
getChildren().add(imgProfilePic);
// A simple Separator between the photo and the details
getChildren().add(new Separator(Orientation.VERTICAL));
// Now, create a VBox to hold the name and age
VBox vBox = new VBox(5);
vBox.setAlignment(Pos.CENTER_LEFT);
vBox.setPadding(new Insets(5));
// Add our Person details
vBox.getChildren().addAll(
new Label("Name: " + person.getName()),
new Label("Age: " + person.getAge())
);
// Add our VBox to the layout
getChildren().add(vBox);
}
}
The Result:

In JavaFX, how do I tell which Stage is in front of another?

I have a JavaFX application with several Stages open to provide floating windows. I want to iterate through these Stages from front to back. I have a list of all the Stages which I'd like to sort and am looking for a method that will let me compare two of them and determine which is in front. Is there a way to do this?
This is just one possibility.
Store each open Stage in a list that can be observed for changes.
Add a listener on each Stage's focusedProperty. When it changes to true,
Remove the stage from our List and readd it at index 0
Now, create a listener on the List and your "focused" Stage will always be at index 0.
You now have an ArrayList that stores the open stages, in order.
Here is a simple MCVE to demonstrate. There are certainly areas to be improved upon and I welcome suggestions, but this does provide some basic functionality.
OpenStages.java:
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
/**
* Implementation of a SimpleListProperty that will store our ObservableArrayList
*/
public class OpenStages<Stage> extends SimpleListProperty<Stage> {
/**
* Constructor that creates an ObservableArrayList
*/
public OpenStages() {
super(FXCollections.observableArrayList());
}
/**
* Removes this Stage from the list and re-adds it at index 0
*/
public void focusStage(Stage stage) {
this.remove(stage);
this.add(0, stage);
}
}
Main.java:
import javafx.application.Application;
import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Custom SimpleListProperty that holds our ObservableArrayList of open stages
OpenStages<Stage> openStages = new OpenStages<>();
// Simple interface
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
Button btnCreateStage = new Button("Create New Stage");
btnCreateStage.setOnAction(event -> {
// Create a new Stage
Stage newStage = new Stage();
// Add a listener to the focusedProperty of the Stage. When focus changes to true, we
// need to update our openStages list
newStage.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
openStages.focusStage(newStage);
}
});
// Add the stage to our OpenStages list.
openStages.add(newStage);
// Simple layout for the new Stage
VBox stageRoot = new VBox();
stageRoot.setPrefSize(300, 100);
stageRoot.setPadding(new Insets(10));
stageRoot.setAlignment(Pos.CENTER);
// Let's add a label and title to indicate which Stage this is
stageRoot.getChildren().add(new Label("Stage #" + openStages.size()));
newStage.setTitle("Stage #" + openStages.size());
newStage.setScene(new Scene(stageRoot));
// Finally, let's show the stage
newStage.show();
});
// Now, let's create a simple listener for our openStages list to print out the focused Stage
openStages.addListener(new ListChangeListener<Stage>() {
#Override
public void onChanged(Change<? extends Stage> c) {
// Only interested in a stage being added
if (c.next() && c.wasAdded()) {
System.out.println("Focused stage: " + openStages.get(0).getTitle());
}
}
});
// Add the Button to our main layout
root.getChildren().add(btnCreateStage);
// Show the Stage
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}

Javafx add a dropdown event listener for choicebox

I have a ChoiceBox and I want to refresh it's content whenever user expands it. I haven't found a proper listener for this. All the stuff google gives is related to handling ChangeValue events.
I reckon I should add eventListener<ActionEvent> to ChoiceBox since what I'm handling is click on a ChoiceBox, but my implementation doesn't work.
ActionEvent fires when I click on any List value, not when I click ChoiceBox itself.
Register a listener with the choice box's showingProperty:
choiceBox.showingProperty().addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing) {
// choice box popup is now displayed
} else {
// choice box popup is now hidden
}
});
Here is a quick demo:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ChoiceBoxPopupTest extends Application {
private int nextValue ;
#Override
public void start(Stage primaryStage) {
ChoiceBox<Integer> choiceBox = new ChoiceBox<>();
choiceBox.getItems().add(nextValue);
choiceBox.setValue(nextValue);
choiceBox.showingProperty().addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing) {
choiceBox.getItems().setAll(++nextValue, ++nextValue, ++nextValue);
}
});
BorderPane root = new BorderPane();
root.setTop(choiceBox);
BorderPane.setAlignment(choiceBox, Pos.CENTER);
root.setPadding(new Insets(5));
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Resources