Java Processing 3 PAplet in JavaFX scene as FXNode

前端 未结 4 1054
小鲜肉
小鲜肉 2021-01-13 14:16

I am trying to make a program for visual analyzing Fractal sets. I choose Processing 3 as drawing library and JavaFX for the user interface. There are some screenshots of th

4条回答
  •  庸人自扰
    2021-01-13 14:36

    I have devised two approaches: in the first, we bypass Processing's JavaFX stage creation and point Processing to draw into a JavaFX stage loaded from an FXML file; in the second, we replace Processing's default JavaFX scene with one loaded from an FXML file during runtime.

    1. Launching from an FXML

    With the first approach we launch the application like we would a JavaFX app (using Application.launch(Launcher.class);), completely bypassing Processing's JavaFX stage creation code.

    You'll have to download a slightly modified core.jar for this approach to work, where I've changed the visibility of a few members of the PSurfaceFX and PGraphicsFX2D classes from Protected to Public. The changes allow us to launch JavaFX from our own ... extends Application class, while maintaining access to the members that Processing needs to set during the launch to function.

    Processing 3 crashes in FX2D mode when the JDK in use is above Java 8, so I've also made a working version for 8+, since the FXML files usually need at least Java 9 to work.

    • Download core.jar (Java 8 & below)
    • Download core.jar (Above Java 8)

    This is the FXML file I am working with in this example:

    With the modified core.jar added to your project's classpath, override initSurface() in your PApplet class with the following snippet. With this code, we bypass the PApplet's call to initFrame() - this is where processing creates its own JavaFX stage, which we do not want it to do.

    @Override
    protected PSurface initSurface() {
        g = createPrimaryGraphics();
        PSurface genericSurface = g.createSurface();
        PSurfaceFX fxSurface = (PSurfaceFX) genericSurface;
    
        fxSurface.sketch = this;
    
        Launcher.surface = fxSurface;
    
        new Thread(new Runnable() {
            public void run() {
                Application.launch(Launcher.class);
            }
        }).start();
    
        while (fxSurface.stage == null) {
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
            }
        }
    
        this.surface = fxSurface;
        return fxSurface;
    }
    

    Set the PApplet's renderering mode to FX2D like so:

    @Override
    public void settings() {
        size(0, 0, FX2D);
    }
    

    Put the following, or similar, in your Launcher class. In this example, I have manually found the Node that I want to add the canvas object into. There are better, more programmatic, ways of doing this (such as .lookup() using the fx:id of the desired node -- this can be defined in the FXML file). I have also bound the dimensions of the canvas to those of its parent, so when the divisor separating the Master and View panes is dragged, the Processing canvas resizes accordingly.

    public class Launcher extends Application {
    
        public static PSurfaceFX surface;
    
        @Override
        public void start(Stage primaryStage) throws Exception {
    
            Canvas canvas = (Canvas) surface.getNative(); // boilerplate
            GraphicsContext graphicsContext = canvas.getGraphicsContext2D(); // boilerplate
            surface.fx.context = graphicsContext; // boilerplate
    
            primaryStage.setTitle("FXML/Processing");
    
            VBox root = FXMLLoader.load(new File("c:/Users/Mike/desktop/test.fxml").toURI().toURL());
            SplitPane pane = (SplitPane) root.getChildren().get(1); // Manually get the item I want to add canvas to
            AnchorPane pane2 = (AnchorPane) pane.getItems().get(0); // Manually get the item I want to add canvas to
            pane2.getChildren().add(canvas); // Manually get the item I want to add canvas to
    
            canvas.widthProperty().bind(pane2.widthProperty());
            canvas.heightProperty().bind(pane2.heightProperty());
    
            Scene scene = new Scene(root, 800, 800);
            primaryStage.setScene(scene);
            primaryStage.show();
    
            surface.stage = primaryStage; // boilerplate
        }
    }
    

    This is the result:

    Also see this Github project -- a basic project showing how a Processing sketch and a FXML JavaFX stage may be integrated using this first approach, but includes a JavaFX Controller to populate @FXMLannotated fields (providing an easy way to first get, and then reference, JavaFX objects in code).


    2. Launching, then loading a FXML

    This approach works with vanilla Processing. Here, we launch Processing like normal and then replace the default scene with new scene loaded from an FXML file during runtime. This is a simpler approach (and doesn't require using a modified .jar!) but will make JavaFX/Processing interoperability more difficult because we can't use a JavaFX Controller to get fields via FXML injection.

    Example PDE code:

    import java.util.Map;
    import java.nio.file.Paths;
    
    import javafx.application.Platform;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.SceneAntialiasing;
    import javafx.scene.canvas.Canvas;
    import javafx.scene.layout.AnchorPane;
    import javafx.stage.Stage;
    
    import processing.javafx.PSurfaceFX;
    
    public void setup() {
      size(800, 800, FX2D);
      strokeWeight(3);
    }
    
    protected PSurface initSurface() {
      surface = (PSurfaceFX) super.initSurface();
      final Canvas canvas = (Canvas) surface.getNative();
      final Scene oldScene = canvas.getScene();
      final Stage stage = (Stage) oldScene.getWindow();
    
      try {
        FXMLLoader loader = new FXMLLoader(Paths.get("C:\\path--to--fxml\\stage.fxml").toUri().toURL()); // abs path to fxml file
        final Parent sceneFromFXML = loader.load();
        final Map namespace = loader.getNamespace();
    
        final Scene newScene = new Scene(sceneFromFXML, stage.getWidth(), stage.getHeight(), false, 
          SceneAntialiasing.BALANCED);
        final AnchorPane pane = (AnchorPane) namespace.get("anchorPane"); // get element by fx:id
    
        pane.getChildren().add(canvas); // processing to stackPane
        canvas.widthProperty().bind(pane.widthProperty()); // bind canvas dimensions to pane
        canvas.heightProperty().bind(pane.heightProperty()); // bind canvas dimensions to pane
    
        Platform.runLater(new Runnable() {
          @Override
            public void run() {
            stage.setScene(newScene);
          }
        }
        );
      } 
      catch (IOException e) {
        e.printStackTrace();
      }
      return surface;
    }
    
    public void draw() {
      background(125, 125, 98);
      ellipse(200, 200, 200, 200);
      line(0, 0, width, height);
      line(width, 0, 0, height);
    }
    

    Result:

    …using this FXML file:

提交回复
热议问题