I am trying to build a JavaFX application, where I have a button named "Start" and an ImageView. With the robot class of JavaFX-12, I am trying to take a screenshot of the laptop screen when the button is clicked and show the images one by one during the runtime in the ImageView. My problem is that the JavaFX window does not respond and the program crashes (probably). Even putting the thread into sleep does not seem to work. I assume that it isn't working as I have not set any fps rule, but how can I do that? At the moment, I am creating writable images, converting them into a separate image with a number, saving them, and again reusing them. My goal is to create a screen sharing of the same laptop in the image view. I know that's difficult. I'm new to the JavaFx robot class (not he awt one). Any help is appreciated.
P.S.: The images are properly formed in the directory.
package sample;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Rectangle2D;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.VBox;
import javafx.scene.robot.Robot;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
ImageView iv = new ImageView();
iv.setFitWidth(100);
iv.setFitHeight(100);
Button b = new Button("Start");
VBox v = new VBox(10);
v.getChildren().addAll(b,iv);
b.setOnAction(event -> {
Robot r = new Robot();
WritableImage wi = new WritableImage(300,300);
WritableImage i;
Rectangle2D rect = Screen.getPrimary().getVisualBounds();
while(true){
i = r.getScreenCapture(wi,rect);
try {
ImageIO.write(SwingFXUtils.fromFXImage(i,null),"png",new File("F:/Pic/pic" + x + ".png"));
iv.setImage(new Image(new FileInputStream("F:/Pic/pic" + x + ".png")));
//Thread.sleep(500);
//iv.setImage(null);
} catch (Exception e) {
System.out.println(e);
}
}
});
primaryStage.setScene(new Scene(v, 500, 500));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
JavaFX is, like most UI frameworks, single-threaded. You must never block (e.g. sleep) or otherwise monopolize (e.g. while (true)) the JavaFX Application Thread. That thread is responsible for everything related to the UI and if it's not free to do its job then the UI will become unresponsive. Note that a render pass cannot happen until the FX thread returns from whatever it's doing, so setting the image of an ImageView in a loop will have no visible effect until some time after the loop terminates.
Also, a full-throttle while loop will attempt to get a screen capture as fast as the CPU can execute said loop. That is likely to be much faster than the rate at which your UI refreshes and is thus a waste of resources. The rate of screen captures should not exceed the frame rate.
If you need to loop on the FX thread and/or be constrained by the (JavaFX's) frame rate then use the javafx.animation API. In your case, an AnimationTimer seems apt. Here's an example which continuously screenshots the primary screen:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.StackPane;
import javafx.scene.robot.Robot;
import javafx.stage.Screen;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
ImageView view = new ImageView();
view.setFitWidth(720);
view.setFitHeight(480);
// Keep a reference to the AnimationTimer instance if
// you want to be able to start and stop it at will
new AnimationTimer() {
final Robot robot = new Robot();
final Rectangle2D bounds = Screen.getPrimary().getBounds();
#Override
public void handle(long now) {
WritableImage oldImage = (WritableImage) view.getImage();
WritableImage newImage = robot.getScreenCapture(oldImage, bounds);
if (oldImage != newImage) {
view.setImage(newImage);
}
}
}.start();
primaryStage.setScene(new Scene(new StackPane(view)));
primaryStage.show();
}
}
Some notes:
The AnimationTimer#handle(long) method is invoked once per frame. JavaFX tries to target 60 frames-per-second. If that's too fast (the above lags somewhat on my computer) then you can use the method's argument, which is the timestamp of the current frame in nanoseconds, to throttle the rate of screen captures. You could also look into using a Timeline or PauseTransition instead of an AnimationTimer. See JavaFX periodic background task for more information.
The above gives a Droste Effect (I think that's the term?) since the screen capture is displayed on the screen which is being captured.
My example does not include saving each image to a file. You seem to already understand how to do that so you should be able to easily adapt the code. It'd probably be a good idea, however, to move the I/O to a background thread. Unfortunately, that will likely require using different WritableImage instances for each capture to avoid the image being mutated by the FX thread while the background thread reads it. It may also require some tuning or dropped images; I'm not sure how well the I/O will keep up with the influx of screen captures (i.e. standard producer-consumer problems).
As an aside, your question explains you're attempting to share the entire screen. If that's the case then continue using Robot. However, if you only need to share something from the JavaFX application itself then consider using Node#snapshot(SnapshotParameters,WritableImage) or Scene#snapshot(WritableImage) (assuming you can't just send model data instead of images).
Related
I'm trying to have two boxes with one of them half transparent and the other in orange. Somehow it always just fully replaces the pixels but still kinda applies the transparency to the color. What am I missing? Same happens with loaded Obj files which have d/Tr set to 0.5 for example.
import javafx.application.Application;
import javafx.application.ConditionalFeature;
import javafx.application.Platform;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.CullFace;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class HelloFX extends Application {
#Override
public void start(Stage primaryStage) {
boolean is3DSupported = Platform.isSupported(ConditionalFeature.SCENE3D);
if (!is3DSupported) {
System.out.println("Sorry, 3D is not supported in JavaFX on this platform.");
return;
}
Box boxForeground = new Box(100, 500, 100);
boxForeground.setTranslateX(250);
boxForeground.setTranslateY(100);
boxForeground.setTranslateZ(400);
boxForeground.setMaterial(new PhongMaterial(new Color(0, 0, 0, 0.3)));
Box boxBackground = new Box(100, 100, 100);
boxBackground.setMaterial(new PhongMaterial(Color.ORANGE));
boxBackground.setTranslateX(250);
boxBackground.setTranslateY(200);
boxBackground.setTranslateZ(800);
boolean fixedEyeAtCameraZero = false;
PerspectiveCamera camera = new PerspectiveCamera(fixedEyeAtCameraZero);
camera.setTranslateX(150);
camera.setTranslateY(-100);
camera.setTranslateZ(250);
Group root = new Group(boxForeground, boxBackground);
// root.setDepthTest(DepthTest.ENABLE); // no effect
root.setRotationAxis(Rotate.X_AXIS);
root.setRotate(30);
Scene scene = new Scene(root, 500, 300, true);
scene.setCamera(camera);
primaryStage.setScene(scene);
primaryStage.setTitle("3D Example");
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}
I've played around with many options of depth buffer, depth test settings and colors but it seemed to have no good effect.
My expectation is that its possible to slightly see the orange box through the black box with strong transparency.
Actual result:
So based on the comments and my own investigation it seems to be a known bug thats hasn't been fixed in over 5 years. Basically transparency works like in the 2D space of JavaFx. The one last added to the graph paints over the already painted ones - potentially using transparency blending when your colors/pixels contain alpha information.
Bugtracker: Order-independent transparency for 3D objects
Workaround:
The current workaround is to reorder the graph so it matches the desired z-order. Depending on the use case you can group objects on a root level to get close to an ideal transparency handling. But for transparent objects that 'interact' with each other on the same level of the graph you gonna need constant reordering when the objects or the camera move.
When done right you get the expected result:
Group root = new Group(boxForeground, boxBackground); // bug shows
Group root = new Group(boxBackground, boxForeground); // workaround
Apparently JavaFx has a long history of transparency issues. Which are also discussed here: JavaFX 3D Transparency
I posted a question similar to this earlier but I wasn't specific enough. Here is a simplified version of my code:
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.scene.control.ProgressBar;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.layout.HBox;
import javafx.scene.Scene;
import javafx.concurrent.Task;
import javafx.stage.Stage;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Button;
import javafx.scene.control.MenuBar;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Separator;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.geometry.Pos;
import java.net.URL;
import javafx.scene.image.Image;
import javafx.scene.layout.GridPane;
import javafx.scene.control.Slider;
public class ProgressTest extends Application {
boolean play = true;
int x = 0;
#Override
public void start(Stage stage) {
GridPane pane = new GridPane(); //pane and Hboxes
HBox hbox = new HBox();
Button update = new Button("Start");
update.setOnAction( e -> {
while(play == true)
{
System.out.println(++x);
}
});
Button pause = new Button("Pause");
pause.setOnAction( e -> {
if(play == true)
{
pause.setText("Play");
play = false;
}
else
{
pause.setText("Pause");
play = true;
}
});
hbox.getChildren().addAll(update, pause);
pane.add(hbox, 0, 1);
Scene scene = new Scene(pane);
stage.setMaxWidth(655);
stage.setMaxHeight(620);
stage.setTitle("Gallery!");
stage.setScene(scene);
stage.sizeToScene();
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The idea with this code is that, when the user clicks the "Start" button, the program should print out x going up, and pause whenever the user hits "Pause", and resume again when the user hits "Play".
The problem is that whenever I click the "Play" button, the program goes into an infinite loop and I am unable to press the pause button to stop it. Is there something wrong with the way I am going about this? Are there any tricks to getting this to work? Any help would be very appreciated.
Also, I know that some of this may have syntax errors but I know it's correct on my copy of the code, I'm more considered with the logic behind how to get this to work.
There are 2 big problems with your implementation:
The GUI runs on the JavaFX thread. For it to remain responsive any operations in it must finish quickly. When you are running a long computation like your loop on this thread the whole GUI is blocked.
After you manage to set play to false the loop will just exit and clicking on resume will do nothing.
There are several ways to approach this. I will demonstrate one using Thread and CountDownLatch.
I can't explain what a thread is in the scope of the question (tons of material on SO and everywhere), but what is relevant here is that executing the costly operation on a thread which is not the JavaFX thread will solve point 1.
A CountDownLatch is used to block a thread's (or more than one) execution until the latch is released/broken, upon which the thread will continue. It is initialized with an int representing the number of times it needs to count down before it is released. A thread reaching the latch's await method blocks until the latch's countDown method is called the specified amount of times.
Here is a sample code:
import java.util.concurrent.CountDownLatch;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ProgressTest extends Application {
volatile CountDownLatch cl = new CountDownLatch(1);
#Override
public void start(Stage stage) {
Thread thread = new Thread(() -> {
int x = 0;
while (true) {
try {
cl.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println(++x);
}
});
thread.setDaemon(true);
Button update = new Button("Start");
update.setOnAction(e -> {
if (!thread.isAlive()) {
cl.countDown();
thread.start();
}
});
BooleanProperty running = new SimpleBooleanProperty(true);
Button pause = new Button("Pause");
pause.textProperty().bind(Bindings.when(running).then("Pause").otherwise("Play"));
pause.setOnAction(e -> {
if (running.get()) {
cl = new CountDownLatch(1);
}
else {
cl.countDown();
}
running.set(!running.get());
});
HBox hbox = new HBox(update, pause);
Scene scene = new Scene(hbox);
stage.setScene(scene);
stage.setTitle("Gallery!");
stage.sizeToScene();
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The thread spits out numbers "as fast as possible" with the while(true) loop as you did, only it can be paused by reaching the await method. When you press Start the latch is broken and the thread starts and executes continuously. When you press Pause a new latch is created in its place (a latch is a 1-time thing, it can't be reset), which causes the thread to wait in await until someone breaks it with the countDown method (one call is enough because we instantiated it with 1). That is what pressing Resume does.
Calling setDaemon(true) on the thread makes sure it allows the JVM to exit without waiting for it. If the thread must finish before the JVM exists (e.g., it is not a background thread), you can remove it.
Iv'e made the latch volatile which guarantees that different threads will see the same value for it. See also Do you ever use the volatile keyword in Java? and other available sources. In this specific case you don't need poinpoint thread synchronization so it won't have a noticeable effect, but it should be there nonetheless.
Note that I've added a small check on Start that the thread is not already running because starting a thread while it's running throws an exception. You didn't specify what to do if Start is pressed during execution.
While irrelevant to your question, Iv'e demonstrated how you can utilize the JavaFX binding API to have the button's text update automatically with the boolean's value. I "promoted" the control boolean to a property and bound the button's text to its value. It might not be so useful for this situation though.
Notes:
You're setting the height and width of the scene and then call sizeToScene, which makes the previous calls redundant.
You don't need to check a boolean with ==, you can use it directly: if (b == true) is equivalent to if (b) and if (b == false) is equivalent to if (!b).
A better name for your boolean would be one that represents a state ("running"/"paused") rather than an action ("run"/"pause").
I am making a game of snake, but whenever I try to update my canvas in the draw() method, the new snake won't draw. It draws in the run thread. I have tried a bunch of different things, but I can't seem to get it working.
Imports:
import javafx.application.Application;
import javafx.event.*;
import javafx.scene.*;
import javafx.stage.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.geometry.*;
import java.io.*;
import java.util.*;
import javafx.scene.input.KeyEvent;
import javafx.event.EventHandler;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.scene.text.*;
import javafx.scene.paint.*;
import javafx.scene.canvas.*;
Actual class:
public class Snake extends Application implements Runnable
{
//Snake parts and locations
boolean dead;
int headX=0;
int headY=0;
// for the game
Group root = new Group();
Scene snakeG = new Scene(root, 550, 550,Color.BLACK);
final Canvas canvas = new Canvas(550,550);
GraphicsContext gc = canvas.getGraphicsContext2D();
//Start Game
VBox newGame = new VBox(3);
Scene startC = new Scene(newGame, 200, 200);
Label info = new Label("Snake Game \nCreated by: Austin");
Label rules = new Label("Rules\n1.) If you hit the edge you die\n2.) If you touch your snake you die\n3.)Collect fruit to increase your snakes size");
Button startBut = new Button("Start Game");
Stage startS;
public static void main ( String[] args )
{
launch(args);
}
#Override
public void start ( Stage primaryStage )throws Exception
{
startS=primaryStage;
startS.setTitle("Snake");
newGame.getChildren().addAll(info,rules,startBut);
newGame.setAlignment(Pos.CENTER);
startS.setScene(startC);
startS.show();
startBut.setOnAction(e ->
{
startGame();
});
}
public void startGame()
{
System.out.println("Success");
headX=275;
headY=275;
dead = false;
gc.clearRect(0,0,800,800);
startS.setScene(snakeG);
gc.setFill(Color.GREEN);
gc.fillRect(headX,headY,10,10);
root.getChildren().add(canvas);
(new Thread ( new Snake())).start();
}
public void run()
{
draw();
}
// draws the snake
public void draw()
{
System.out.println("DRAW STARTED");
gc.setFill(Color.GREEN);
gc.fillRect(50,50,10,10);
}
}
If you know of a better way to draw graphics in JavaFX, please tell me. This is the only way I could find for what I am doing.
There are some problems with your approach.
You don't need your own thread.
You can only modify the active scene graph (including a canvas) using the JavaFX Thread. Read the JavaFX concurrency documentation:
The JavaFX scene graph, which represents the graphical user interface of a JavaFX application, is not thread-safe and can only be accessed and modified from the UI thread also known as the JavaFX Application thread.
Your main application class should not implement Runnable.
Some suggestions:
You may find it easier to use SceneGraph nodes for your game objects rather than a Canvas, but either should work.
You can implement your game loop using an AnimationTimer as demoed here.
Here is a sample of using an AnimationTimer for display.
I was trying out the code here for making a JavaFX app which allows my stylus pen to draw on a canvas: Canvas does not draw smooth lines
The performance is extremely painful when trying to draw on a canvas. It will freeze for half a second, and then start drawing. Only after it starts drawing is it fine. Beforehand though when you first press down with the mouse/pen, the delay is pretty brutal and makes it unusable.
Here is the code I used:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.BoxBlur;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.stage.Stage;
public class Test extends Application {
private GraphicsContext gc;
#Override
public void start(Stage stage) {
Canvas canvas = new Canvas(500, 500);
canvas.setOnMouseDragged(e -> {
gc.lineTo(e.getX(), e.getY());
gc.stroke();
});
canvas.setOnMousePressed(e -> gc.moveTo(e.getX(), e.getY()));
gc = canvas.getGraphicsContext2D();
gc.setLineCap(StrokeLineCap.ROUND);
gc.setLineJoin(StrokeLineJoin.ROUND);
gc.setLineWidth(1);
BoxBlur blur = new BoxBlur();
blur.setWidth(1);
blur.setHeight(1);
blur.setIterations(1);
gc.setEffect(blur);
Group root = new Group(canvas);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
stage.setFullScreen(true);
}
public static void main(String[] args) {
launch(args);
}
}
Is there any way to fix the performance issue? My desktop computer is pretty solid and can run high-end games, so its not the performance on my computer.
NOTE: I should also say that the delay that occurs when you first press the mouse on the canvas is bad enough to cause mouse clicks to drop.
EDIT: To confirm it wasnt the OnMousePressed, I commented it out and it did not help.
Apparently my driver was conflicting somehow with Java, which means JavaFX is fine. After getting help from the tablet company and some fixes, this doesn't happen with the latest drivers.
I have a question regarding JavaFX. I taught myself Java and now I'm learning JavaFX.
I've been trying to update the location of a 50x50 black block on the screen. I have a YAxis variable that when I change changes the location of the block.
I want the block to "flow" down the screen similar to Tetris.
My code is messy, as I'm just messing around with it, so please excuse that:
package gamefx;
import javafx.animation.Animation;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class GameFX extends Application {
public Image img = new Image("Block1.png");
public ImageView image = new ImageView(img);
public int YAxis = -200;
#Override
public void start(Stage primaryStage) throws InterruptedException{
primaryStage.setTitle("Game");
StackPane stckp1 = new StackPane();
Scene scn = new Scene(stckp1, 700, 700);
primaryStage.setScene(scn);
primaryStage.show();
image.setTranslateY(YAxis);
stckp1.getChildren().add(image);
}
}
Since you want to see your block moving, you need an animation. See Oracle tutorial for more information.
A sample code, quickly written:
TranslateTransition tt = new TranslateTransition(Duration.millis(2000), image);
tt.setByY(yAxis);
tt.setCycleCount(1);
tt.play();
Side-note: Variable names shall not start with an upper-case letter. Use yAxis instead of YAxis.