Search code examples
javajavafxpreview

JavaFX create a small live preview of pane


I have a project where I have two windows, basically you could compare it with powerpoint in presentation mode. In one of my windows I would like do show a small preview in a corner of what is shown on the entire second window. If there would be a copy method for nodes I think i could manage to realize it but I couldn't find any.

For better understanding I tried to visualize my problem.

how my setup should look


Solution

  • You could always take continuous snapshots of the other Stage's Scene (or some arbitrary Node) and display them in your "live image" area. Here's an example:

    Main.java

    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class Main extends Application {
    
      @Override
      public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
        Scene scene = new Scene(loader.load());
        primaryStage.setScene(scene);
        primaryStage.setTitle("Main Window");
        primaryStage.setResizable(false);
        primaryStage.show();
        ((MainController) loader.getController()).displayOtherWindow();
      }
    
    }
    

    MainController.java

    import java.io.IOException;
    import javafx.animation.AnimationTimer;
    import javafx.fxml.FXML;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.scene.image.ImageView;
    import javafx.stage.Stage;
    
    public class MainController {
    
      @FXML
      private ImageView imageView;
      private AnimationTimer imageTimer;
    
      public void displayOtherWindow() throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("Other.fxml"));
        Scene scene = new Scene(loader.load(), 500, 300);
    
        Stage stage = new Stage();
        stage.setScene(scene);
        stage.setTitle("Other Window");
        stage.setResizable(false);
        stage.show();
    
        ((OtherController) loader.getController()).startAnimation();
    
        imageTimer = new ScreenshotsAnimationTimer(scene);
        imageTimer.start();
      }
    
      private class ScreenshotsAnimationTimer extends AnimationTimer {
    
        private final Scene scene;
    
        private ScreenshotsAnimationTimer(Scene scene) {
          this.scene = scene;
        }
    
        @Override
        public void handle(long now) {
          imageView.setImage(scene.snapshot(null));
        }
    
      }
    }
    

    Main.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.BorderPane?>
    <?import javafx.scene.layout.VBox?>
    <?import javafx.scene.control.Button?>
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.layout.HBox?>
    <?import javafx.scene.control.Separator?>
    <?import javafx.scene.image.ImageView?>
    <BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
                fx:controller="MainController" prefHeight="300.0" prefWidth="500.0">
    
        <center>
            <VBox spacing="10" alignment="CENTER_LEFT">
                <padding>
                    <Insets topRightBottomLeft="15"/>
                </padding>
                <Label text="Some controls. These buttons do nothing."/>
                <Button text="Button #1"/>
                <Button text="Button #2"/>
                <Button text="Button #3"/>
            </VBox>
        </center>
    
        <bottom>
            <VBox>
                <Separator/>
                <HBox minHeight="105" maxHeight="105">
                    <padding>
                        <Insets topRightBottomLeft="5"/>
                    </padding>
                    <Label text="Some other information could go here. Live image is to the right."
                           maxWidth="Infinity" HBox.hgrow="ALWAYS" wrapText="true"/>
                    <Separator orientation="VERTICAL"/>
                    <ImageView fx:id="imageView" fitWidth="166.66" fitHeight="100"/>
                </HBox>
            </VBox>
        </bottom>
    
    </BorderPane>
    

    OtherControler.java

    import javafx.animation.Animation;
    import javafx.animation.SequentialTransition;
    import javafx.animation.TranslateTransition;
    import javafx.fxml.FXML;
    import javafx.scene.shape.Rectangle;
    import javafx.util.Duration;
    
    public class OtherController {
    
      @FXML
      private Rectangle rect;
    
      public void startAnimation() {
        SequentialTransition transition = new SequentialTransition(
            createTransition(500 - rect.getWidth(), 0),
            createTransition(500 - rect.getWidth(), 300 - rect.getHeight()),
            createTransition(0, 300 - rect.getHeight()),
            createTransition(0, 0)
        );
        transition.setCycleCount(Animation.INDEFINITE);
        transition.play();
      }
    
      private TranslateTransition createTransition(double x, double y) {
        TranslateTransition tt = new TranslateTransition(Duration.seconds(1), rect);
        tt.setToX(x);
        tt.setToY(y);
        return tt;
      }
    
    }
    

    Other.fxml

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.Group?>
    <?import javafx.scene.shape.Rectangle?>
    
    <Group xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
           fx:controller="OtherController">
    
        <Rectangle fx:id="rect" width="200" height="100"/>
    
    </Group>
    

    Image of result

    enter image description here


    I did notice some stuttering when trying to move either Window around. A more complicated application may also cause some lag. In other words, you should tune things for your application's performance. For instance, do you really need a screenshot every frame? Maybe you could take a screenshot every other frame or maybe every n frames. Another possible optimization is to use the same WritableImage for the snapshot (only creating a new image if the Scene's dimensions change).

    Also note that in my example I use a lot of hard coded values. You'll want to change that for a real application.