Modify javafx Label properites while being displayed [duplicate] - java

I'm trying to do a maze resolver with a cellular automaton but I have a display problem
for each new generation of a grid of an automaton we try to display the cells in the form of rectangle.The initialization works well and the grid is displayed,the last generation of the simulation is displayed too but not the intermediate steps.
pic of first generation
last generation
//Initialise la liste des rectangles
public void initRectList() {
for(int height = 0; height < this.mazeA.grid.getHeight(); height++) {
for(int width = 0; width < this.mazeA.grid.getWidth(); width++) {
this.rectList[height][width] = new Rectangle(REC_HEIGHT,REC_WIDTH, getColorCell(this.mazeA.grid, height, width));
}
}
}
//Dessine le labyrinthe
public void drawGrid() {
for (int height = 0; height < this.mazeA.grid.getHeight(); height++) {
for(int width = 0; width < this.mazeA.grid.getWidth(); width++) {
tilePane.getChildren().add(this.rectList[height][width]);
}
}
}
public class MazeFX {
private HBox root = new HBox();
private Scene scene = new Scene(root,1100,800);
private TilePane tilePane = new TilePane();
private Grid grid = new Grid(30,30);
private Rectangle[][] rectList = new Rectangle[30][30];
private VBox buttons = new VBox();
private Button reset = new Button("Reset");
private Button pas = new Button("Play");
private Button load = new Button("Load");
private MazeAutomaton mazeA;
private final double REC_HEIGHT = 20.;
private final double REC_WIDTH = 20.;
public MazeFX(Stage stage) throws InterruptedException {
scene.getStylesheets().add(getClass().getResource("/src/application.css").toExternalForm());
initButton();
initLayout();
initGridByTopologie();
mazeA = new MazeAutomaton(this.grid);
initRectList();
drawGrid();
pressedButtons(stage);
setWindow(stage);
displayWindow(stage);
}
to start the next generation you press a button.
//Action de l'utilisateur sur l'un des bouttons
public void pressedButtons(Stage stage) {
pressReset();
pressPAS();
pressLoad(stage);
}
//Boutton Play/Stop préssé
public void pressPAS() {
this.pas.setOnMouseClicked(e -> {
for (int i = 0; i < 30; i++) {
mazeA.nextStep();
try {
Thread.sleep(100);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
updateRectColor();
}
}
);
}
the problem seems to be that we are stuck in the method setOnMouseClicked() and that the update of the rectangles is not done, with a textual display I see the evolution of the automaton which indicates us that the simulation works and that the problem seems to come from JavaFX

The JavaFX Application Thread runs as a loop. Effectively (the actual implementation details are far more complex) that loop does the following (in pseudocode):
while (applicationIsRunning()) {
if (thereAreEventsToHandle()) {
handleEvents();
}
if (thereAreAnimationsToUpdate()) {
updateAnimations();
}
if (itsTimeToRenderTheScene()) {
renderScene();
}
}
By default, JavaFX renders the scene at most 60 times per second.
The code in your event handler is executed on the FX Application Thread (it's invoked by the first block in the pseudocode loop above). Since it sleeps on that thread, the FX Application Thread never gets to the third part of the loop (rendering the Scene) until the entire event handler completes. Consequently, you never see the intermediate updates, because the scene is never rendered.
Assuming mazeA.nextStep() doesn't block (or take a long time to run), it's best to refactor this as an Animation, e.g. a Timeline:
public void pressPAS() {
this.pas.setOnMouseClicked(e -> {
KeyFrame updateMaze = new KeyFrame(Duration.ZERO, evt -> mazeA.nextStep());
KeyFrame updateRect = new KeyFrame(Duration.millis(100), evt -> updateRectColor());
Timeline timeline = new Timeline(updateMaze, updateRect);
timeline.setCycleCount(30);
timeline.play();
});
}
The timeline.play() method simply starts the animation and returns immediately, allowing the FX Application Thread to proceed. When the FX Application Thread checks for running animations, it will check if it's time to execute either of the handlers in the key frames, and if so will execute them. Then it will render the scene as usual.

Related

Trying to create a scrolling background effect with imageViews in javafx

Im trying to create a scrolling background effect with two imageViews where one picture is on top of another picture and out of the window; then i try to scroll both down the window to create a scrolling effect by changing their y coordinates. I made a loop to do so and put a thread.sleep so it wouldnt do it too quickly. Then i reset the picutres positions and do the loop again. However, when i try to run the program, the window will never open. Taking out the loop obviously properly shows the window with the picutre.
public class TestBackground extends Application{
#Override
public void start(Stage stage) throws Exception {
stage.setTitle("DRIFT STAGE");
Pane game = new Pane();
Scene gameScene = new Scene(game, 956, 740);
ImageView background = new ImageView(getClass().getResource("bg.png").toExternalForm());
game.getChildren().add(background);
ImageView background2 = new ImageView(getClass().getResource("bg.png").toExternalForm());
game.getChildren().add(background2);
background2.setY(-740);
//loop to scroll background vertically
for (int j = 0; j < 20; j++) {
for (double i = 1.0; i < 741.0; i++) {
background.setY(background.getY() + i);
background2.setY(background2.getY() + i);
try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
background.setY(0.0);
background2.setY(-740.0);
}
stage.setScene(gameScene);
stage.show();
}
public static void main(String[] args) { launch(args); }
}
Your loop is not the right thing to do. Use a Transition Animation on each ImageView. You want a TranslateTransition.
Something like this:
// Animation to scroll background vertically
TranslateTransition trans1 = new TranslateTransition(Duration.minutes(1), background);
trans1.setFromY(0);
trans1.setToY(740);
trans1.setCycleCount(20);
TranslateTransition trans2 = new TranslateTransition(Duration.minutes(1), background2);
trans2.setFromY(-740);
trans2.setToY(0);
trans2.setCycleCount(20);
ParallelTransition parTrans = new ParallelTransition(trans1, trans2);
parTrans.play();
If your intention is to have these images as a parallax background that scrolls "forever", set the transitions to cycle indefinitely
trans1.setCycleCount(Animation.INDEFINITE);
and use slightly different durations for each.
If you are using the same image, don't load it twice:
Image bgImg = new Image(getClass().getResource("bg.png").toExternalForm());
ImageView background = new ImageView(bgImg);
game.getChildren().add(background);
ImageView background2 = new ImageView(bgImg);
game.getChildren().add(background2);
Here's the whole start method with an added speed slider, just for fun:
#Override
public void start(Stage stage) {
stage.setTitle("DRIFT STAGE");
Pane game = new Pane();
Scene gameScene = new Scene(game, 956, 740);
Image bgImg = new Image(getClass().getResource("bg.png").toExternalForm());
ImageView background = new ImageView(bgImg);
ImageView background2 = new ImageView(bgImg);
Slider speedSlider = new Slider(0, 5, 1);
game.getChildren().addAll(background, background2, speedSlider);
// Animation to scroll background vertically
TranslateTransition trans1 = new TranslateTransition(Duration.seconds(10), background);
trans1.setFromY(0);
trans1.setToY(740);
trans1.setInterpolator(Interpolator.LINEAR);
trans1.setCycleCount(Animation.INDEFINITE);
TranslateTransition trans2 = new TranslateTransition(Duration.seconds(10), background2);
trans2.setFromY(-740);
trans2.setToY(0);
trans2.setCycleCount(Animation.INDEFINITE);
trans2.setInterpolator(Interpolator.LINEAR);
ParallelTransition parTrans = new ParallelTransition(trans1, trans2);
parTrans.rateProperty().bind(speedSlider.valueProperty());
parTrans.play();
stage.setScene(gameScene);
stage.show();
}

JavaFX rectangle doesn't update

I'm trying to do a maze resolver with a cellular automaton but I have a display problem
for each new generation of a grid of an automaton we try to display the cells in the form of rectangle.The initialization works well and the grid is displayed,the last generation of the simulation is displayed too but not the intermediate steps.
pic of first generation
last generation
//Initialise la liste des rectangles
public void initRectList() {
for(int height = 0; height < this.mazeA.grid.getHeight(); height++) {
for(int width = 0; width < this.mazeA.grid.getWidth(); width++) {
this.rectList[height][width] = new Rectangle(REC_HEIGHT,REC_WIDTH, getColorCell(this.mazeA.grid, height, width));
}
}
}
//Dessine le labyrinthe
public void drawGrid() {
for (int height = 0; height < this.mazeA.grid.getHeight(); height++) {
for(int width = 0; width < this.mazeA.grid.getWidth(); width++) {
tilePane.getChildren().add(this.rectList[height][width]);
}
}
}
public class MazeFX {
private HBox root = new HBox();
private Scene scene = new Scene(root,1100,800);
private TilePane tilePane = new TilePane();
private Grid grid = new Grid(30,30);
private Rectangle[][] rectList = new Rectangle[30][30];
private VBox buttons = new VBox();
private Button reset = new Button("Reset");
private Button pas = new Button("Play");
private Button load = new Button("Load");
private MazeAutomaton mazeA;
private final double REC_HEIGHT = 20.;
private final double REC_WIDTH = 20.;
public MazeFX(Stage stage) throws InterruptedException {
scene.getStylesheets().add(getClass().getResource("/src/application.css").toExternalForm());
initButton();
initLayout();
initGridByTopologie();
mazeA = new MazeAutomaton(this.grid);
initRectList();
drawGrid();
pressedButtons(stage);
setWindow(stage);
displayWindow(stage);
}
to start the next generation you press a button.
//Action de l'utilisateur sur l'un des bouttons
public void pressedButtons(Stage stage) {
pressReset();
pressPAS();
pressLoad(stage);
}
//Boutton Play/Stop préssé
public void pressPAS() {
this.pas.setOnMouseClicked(e -> {
for (int i = 0; i < 30; i++) {
mazeA.nextStep();
try {
Thread.sleep(100);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
updateRectColor();
}
}
);
}
the problem seems to be that we are stuck in the method setOnMouseClicked() and that the update of the rectangles is not done, with a textual display I see the evolution of the automaton which indicates us that the simulation works and that the problem seems to come from JavaFX
The JavaFX Application Thread runs as a loop. Effectively (the actual implementation details are far more complex) that loop does the following (in pseudocode):
while (applicationIsRunning()) {
if (thereAreEventsToHandle()) {
handleEvents();
}
if (thereAreAnimationsToUpdate()) {
updateAnimations();
}
if (itsTimeToRenderTheScene()) {
renderScene();
}
}
By default, JavaFX renders the scene at most 60 times per second.
The code in your event handler is executed on the FX Application Thread (it's invoked by the first block in the pseudocode loop above). Since it sleeps on that thread, the FX Application Thread never gets to the third part of the loop (rendering the Scene) until the entire event handler completes. Consequently, you never see the intermediate updates, because the scene is never rendered.
Assuming mazeA.nextStep() doesn't block (or take a long time to run), it's best to refactor this as an Animation, e.g. a Timeline:
public void pressPAS() {
this.pas.setOnMouseClicked(e -> {
KeyFrame updateMaze = new KeyFrame(Duration.ZERO, evt -> mazeA.nextStep());
KeyFrame updateRect = new KeyFrame(Duration.millis(100), evt -> updateRectColor());
Timeline timeline = new Timeline(updateMaze, updateRect);
timeline.setCycleCount(30);
timeline.play();
});
}
The timeline.play() method simply starts the animation and returns immediately, allowing the FX Application Thread to proceed. When the FX Application Thread checks for running animations, it will check if it's time to execute either of the handlers in the key frames, and if so will execute them. Then it will render the scene as usual.

Loading JavaFX GUI in background

I'm currently trying to create a Splash Screen for my program since it takes some time to start up.
The problem is that it takes a while to create the GUI (creating dialogues, updating tables etc.). And I can't move the GUI creation to a background thread (like the "Task" class), since I'll get an "Not on FXApplication Thread" exception.
I tried using:
Platform.runLater(new Runnable() {
public void run() {
//create GUI
}
}
And the "call" method of a Task:
public class InitWorker extends Task<Void> {
private Model model;
private ViewJFX view;
public InitWorker(Model model) {
this.model = model;
}
#Override
protected Void call() throws Exception {
View view = new View();
Collection collection = new Collection();
//do stuff
}
}
When I wrote the program in Swing I could just display and update the Splash Screen on the EventDispatchThread, without any real concurreny. The code looked like this:
public void build() {
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE++, "Creating Menus");
menuCreator = new MenuCreatorOld (model, this);
menuCreator.createMenu();
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE, "Creating Toolbar");
toolBar = menuCreator.createToolBar();
createWesternPanelToolBar();
shoppingPanel = new ShoppingListOld(model, this, collectionController, shoppingController, controller);
centerTabbedPane = new JTabbedPane();
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE++, "Creating Collection");
collectionPanel = new CollectionOld(model, collectionController, this, controller);
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE++, "Creating Wish List");
wishPanel = new WishListOld(model, this, collectionController, wishController, controller);
MainOld.updateProgressBar(MainOld.PROGRESSBAR_VALUE++, "Creating Folders Table");
//and so on
}
public static void updateProgressBar(int progressValue, String text) {
System.out.println("Loading Bar Value:"+progressValue);
progressBar.setValue(progressValue);
loadingLabel.setText(text);
progressBar.setString(text);
}
Is there any way to create the GUI in the background while displaying a Splash Screen with a loading bar?
Edit:
I had a look at my code and was able to decrease the startup time by 5 seconds. Most of the dialogs pull data from the database when they are created. So I moved the creation of the dialogs into their getter methods. That resulted in an improvement of 3 seconds. But I would still like to know if there is in a way to create the GUI on a background thread.
Edit:
As suggested, I also tried using "RunLater" in a "Task".
This way I can create the GUI and display the SplashScreen, but I can't update the progress bar and progress label, since the GUI creation blocks the JavaFX application thread. The progress bar and label are only updated, after the GUI has been fully created.
Here's an example you guys can run (I removed the splash screen and only kept the progress bar and progress label):
public class InitWorker extends Task<Void> {
private static ProgressBar progressBar;
private static Label progressLabel;
private static double PROGRESS_MAX = 5;
private double loadingValue;
public InitWorker() {
loadingValue = 0;
}
#Override
protected void succeeded() {
System.out.println("Succeeded");
}
#Override
protected void failed() {
System.out.println("Failed");
}
#Override
protected Void call() throws Exception {
System.out.println("RUNNING");
Platform.runLater(new Runnable() {
public void run() {
displaySplashScreen();
for(int i=0; i<10; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
updateProgressBar(loadingValue++, "Label "+i);
Stage stage = new Stage();
Label label = new Label("Label " + i);
VBox panel = new VBox();
panel.getChildren().add(label);
Scene scene = new Scene(panel);
stage.setScene(scene);
stage.centerOnScreen();
stage.show();
}
// updateProgressBar(1, "Initializing...");
}});
return null;
}
public void updateProgressBar(double loadingValue, String text) {
progressBar.setProgress(loadingValue / PROGRESS_MAX);
progressLabel.setText(text);
}
public static void displaySplashScreen() {
Stage progressBarStage = new Stage();
progressBar = new ProgressBar();
Scene progressBarScene = new Scene(progressBar);
progressBarStage.setScene(progressBarScene);
Stage progressLabelStage = new Stage();
progressLabel = new Label("Loading...");
progressLabel.setPadding(new Insets(5));
progressLabel.setStyle("-fx-background-color: red");
Scene progressLabelScene = new Scene(progressLabel);
progressLabelStage.setScene(progressLabelScene);
double progressBarWidth = 500;
double progressBarHeight = 75;
//muss angezeigt werden, um sie abhängig von Größe zu positionieren
progressBarStage.show();
progressLabelStage.show();
//
progressBarStage.setWidth(progressBarWidth);
progressBarStage.setHeight(progressBarHeight);
progressBarStage.centerOnScreen();
progressBarStage.centerOnScreen();
progressLabelStage.setY(progressLabelStage.getY() + 25);
}
}
See Task documentation titled "A Task Which Modifies The Scene Graph", which provides an example:
final Group group = new Group();
Task<Void> task = new Task<Void>() {
#Override protected Void call() throws Exception {
for (int i=0; i<100; i++) {
if (isCancelled()) break;
final Rectangle r = new Rectangle(10, 10);
r.setX(10 * i);
Platform.runLater(new Runnable() {
#Override public void run() {
group.getChildren().add(r);
}
});
}
return null;
}
};
The above example add the rectangles to the scene graph via a 100 runLater calls. A more efficient way to do this would be to add the rectangles to a group not attached to the active scene graph, then only add the group to the active scene graph in the runLater call. For example:
final Group groupInSceneGraph = new Group();
Task<Void> task = new Task<Void>() {
#Override protected Void call() throws Exception {
final Group localGroup = new Group();
for (int i=0; i<100; i++) {
if (isCancelled()) break;
final Rectangle r = new Rectangle(10, 10);
r.setX(10 * i);
localGroup.getChildren().add(r);
}
Platform.runLater(new Runnable() {
#Override public void run() {
groupInSceneGraph.add(localGroup);
}
});
return null;
}
};
You can create and modify most scene graph objects off of the JavaFX application thread (including loading FXML), as long as the objects aren't attached to the active scene graph. By active scene graph I mean a scene graph which is currently attached as a scene to a displayed stage. (A complicated control such as a WebView may be an exception to this rule and may require creation on the JavaFX application thread).
You must only attach the scene graph objects created off of the JavaFX application thread to the active scene graph on the JavaFX application thread (for example using Platform.runLater()). And, you must work with them on the JavaFX application thread as long they continue to be attached to the active scene graph.

Animations in JavaFX using RxJava without blocking GUI

This is a simple class that uses bubble sort to sort a List of Integers and push the result of every iteration to a Subject from RxJava library, with some delay.
public class Sorter {
private PublishSubject<List<Integer>> subject = PublishSubject.create();
public void bubbleSort(int delay) throws InterruptedException {
List<Integer> ints = randomIntegers(0,200,10);
for (int i = 0; i < ints.size(); i++) {
for (int j = 0; j < ints.size() - 1; j++) {
if (ints.get(j) > ints.get(j + 1)) {
Collections.swap(ints, j, j + 1);
subject.onNext(ints);
Thread.sleep(delay);
}
}
}
}
private List<Integer> randomIntegers(int origin, int bound, int limit) {
return new Random()
.ints(origin, bound)
.limit(limit)
.boxed()
.collect(Collectors.toList());
}
public PublishSubject<List<Integer>> getSubject() {
return subject;
}
}
I can observe the result very easily with a sample code:
int delay = 100;
Sorter sorter = new Sorter();
PublishSubject<List<Integer>> subject = sorter.getSubject();
subject.subscribe(System.out::println);
sorter.bubbleSort(delay);
I want, however, to see the results of the iterations as moving rectangles of different height representing the numbers. I use JavaFX. After every iteration, I want to clear the application window and redraw everything.
public class Main extends Application {
private Group root;
#Override
public void start(Stage primaryStage) throws InterruptedException {
root = new Group();
Scene scene = new Scene(root, 500, 500, Color.WHITE);
primaryStage.show();
primaryStage.setScene(scene);
Sorter sorter = new Sorter();
PublishSubject<List<Integer>> subject = sorter.getSubject();
subject.subscribe(this::drawRectangles);
sorter.bubbleSort(100);
}
private void drawRectangles(List<Integer> integers) {
root.getChildren().clear();
rectangles.clear();
for (int i = 0; i<integers.size(); i++) {
Rectangle rectangle = new Rectangle(i * 20, 500 - integers.get(i), 10, integers.get(i));
rectangle.setFill(Color.BLACK);
rectangles.add(rectangle);
}
root.getChildren().addAll(rectangles);
}
public static void main(String[] args) {
launch(args);
}
}
However, I only see the last iteration's result. Prior to that, I can only see that the application is running and the GUI thread is blocked.
How can I make the background RxJava calculations non-blocking to my GUI?
EDIT: I solved the issue by running the computation code in another thread, like so:
#Override
public void start(Stage primaryStage) throws InterruptedException {
new Thread(() -> {
Sorter sorter = new Sorter();
sorter.getSubject().subscribe(this::drawRectangles);
try {
sorter.bubbleSort(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
root = new Group();
Scene scene = new Scene(root, 1500, 500, Color.WHITE);
primaryStage.show();
primaryStage.setScene(scene);
}
And wrapper the drawRectangles in Platform.runLater(() -> {} expression.
This seems to be working, although I feel like there might me a possibility to run this with observeOn somehow.
Use RxJavaFX scheduler your UI tasks.
You need do the sort task sorter.bubbleSort in another thread instead of UI thread. And
observeOn(JavaFxScheduler.getInstance())
to schedule UI task back into UI thread.
Only change your start method to:
#Override
public void start(Stage primaryStage) throws InterruptedException {
root = new Group();
Scene scene = new Scene(root, 500, 500, Color.WHITE);
primaryStage.show();
primaryStage.setScene(scene);
Sorter sorter = new Sorter();
sorter.getSubject()
.observeOn(JavaFxScheduler.platform())
.subscribe(this::drawRectangles);
new Thread(() -> uncheck(() -> sorter.bubbleSort(100))).start();
}

JavaFX scroll started and ended

I have a very costly action to do on a mouse scroll on a pane. I currently use
pane.setOnScroll({myMethod()}).
The problem is that if you scroll a lot it computes everything many times. So what I want is to do my actions only when the scroll is finished. I hoped to use setOnScrollStarted, save the starting value and setOnScrollFinished to do my actions.
But I don't know why these two methods are never called. As a test I used
pane.setOnScroll({System.out.println("proof of action"});
and it was clearly never called.
Any idea on how to call my method only at the end of the scroll?
Thanks in advance, A
From the javadoc of ScrollEvent (emphasis mine):
When the scrolling is produced by a touch gesture (such as dragging a
finger over a touch screen), it is surrounded by the SCROLL_STARTED
and SCROLL_FINISHED events. Changing number of involved touch points
during the scrolling is considered a new gesture, so the pair of
SCROLL_FINISHED and SCROLL_STARTED notifications is delivered each
time the touchCount changes. When the scrolling is caused by a mouse
wheel rotation, only a one-time SCROLL event is delivered, without the
started/finished surroundings.
A possible workaround:
Increment a counter variable every time a scroll is detected. In the listener start a new thread that waits 1 second and performs the action that you want only if the counter equals to 1 (the last scrolling) then decrements the counter.
I created a Gist, but I copy here the code:
public class ScrollablePane extends Pane {
private Integer scrollCounter = 0;
private final ObjectProperty<EventHandler<? super ScrollEvent>> onScrollEnded = new SimpleObjectProperty<>();
public final ObjectProperty<EventHandler<? super ScrollEvent>> onScrollEndedProperty() {
return onScrollEnded;
}
public ScrollablePane() {
this.setOnScroll(e -> {
scrollCounter++;
Thread th = new Thread(() -> {
try {
Thread.sleep(1000);
if (scrollCounter == 1)
onScrollEnded.get().handle(e);
scrollCounter--;
} catch (Exception e1) {
e1.printStackTrace();
}
});
th.setDaemon(true);
th.start();
});
}
public void setOnScrollEnded(EventHandler<? super ScrollEvent> handler) {
onScrollEnded.setValue(handler);
}
}
To use it:
public class MyApplication extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
Scene scene = new Scene(root, 400, 400);
ScrollablePane pane = new ScrollablePane();
pane.setOnScrollEnded(e -> System.out.println("Scroll just has been ended"));
root.setCenter(pane);
primaryStage.setScene(scene);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}

Categories

Resources