JavaFXDev: Screen capture tool with 200 lines and 500ms startup time

Post to Twitter

Hi!

Here is my next test with the JavaFX 2.0 ea release. I’ve created a 200-liner to capture parts from the desktop. The “Snipper” detects mouse dragging and two different key strokes (Escape and the letter ‘A’). The captured picture is automatically stored in the user.home/snapshot path.

Before I start to explain the code, I show a small picture about the different scene graph nodes:

At the bottom is the Desktop (I use only the primary screen, but it’s possible to detect all additional screens). The stage is created by the JavaFX Launcher class. I modify the stage to a transparent style and fullscreen mode. The scene (embedded in the stage) captures the mouse events (button pressed, released and dragging). The scene itself has a transparent fill color and contains one group with three different nodes. In the KeyPane-Node I capture the key events. The node is focusable and transparent to mouse events (any mouse event sinks down to the scene). GlassPane is a node with a Shape created by the screen bounds with a rectangular hole. This hole is calculated by the mouse gestures from the user. At last I’ve a visual representation for the user interaction: a red rectangular lasso node.

Disclaimer: The solution here is based on the early access release through the JavaFX partner program. This “best practice” may change significantly between now and the final version. However, I’ll show only a concept, not compilable code.

Here the start up:


  @Override
  public void start(final Stage stage) {
    primaryStage = stage;
    Group group = new Group();
    Scene scene = new Scene(group);
    scene.setFill(Color.TRANSPARENT); // default is white
    scene.setCursor(Cursor.CROSSHAIR);

    stage.setStyle(StageStyle.TRANSPARENT);
    stage.setFullScreen(true);
    stage.setScene(scene);
    stage.setVisible(true);

    screenBounds = new Rectangle (
      Screen.getPrimary().getBounds().getWidth(),
      Screen.getPrimary().getBounds().getHeight()
    );

Nothing special. Only few notes: The Scene-fill must be transparent and the Screen class provides more than the primary screen. But this small example captures only from the primary screen.

I have a special helper class named Rebounder. This class creates and manipulates the lasso rectangle and calculates the shape with the hole for the GlassPane. The KeyPane needs the Rebounder for a fullscreen capture. The scene mouse handlers are call the rebounder with the current mouse points.


  class Rebounder {
    Rectangle lasso;
    double px;
    double py;
    Rebounder() {
      lasso = new Rectangle(0, 0);
      lasso.setFill(null);
      lasso.setSmooth(false);
      lasso.setStroke(Color.RED);
      lasso.setStrokeWidth(1);
      lasso.setStrokeType(StrokeType.OUTSIDE);
      lasso.setMouseTransparent(true);
      lasso.setVisible(false);
    }

    Rectangle start(double x, double y) {
      lasso.setX(x);
      lasso.setY(y);
      lasso.setWidth(0);
      lasso.setHeight(0);
      px = x;
      py = y;
      lasso.setVisible(true);
      return lasso;
    }

    Rectangle rebound(double x, double y) {
      lasso.setWidth(Math.abs(x - px));
      lasso.setHeight(Math.abs(y - py));
      lasso.setX(Math.min(x, px));
      lasso.setY(Math.min(y, py));
      return lasso;
    }

    Rectangle stop(double x, double y) {
      rebound(x, y);
      lasso.setVisible(false);
      return lasso;
    }

    boolean isStopped() { return !lasso.isVisible(); }

    Node getLasso() { return lasso; }

    Shape shapeBuilder(Rectangle r) {
      return r != null
        ? Path.subtract(screenBounds, r)
        : new Rectangle(screenBounds.getWidth(), screenBounds.getHeight());
    }
  }

In the constructor I create the lasso node. The three methods start(x,y), rebound(x,y) and stop (x,y) are called by mousePressed, mouseDragged and mouseReleased. The shapeBuilder-method returns a shape for the GlassPane.

Here are my mouse handlers:


    scene.setOnMousePressed(new EventHandler() {
      @Override public void handle(MouseEvent me) {
        glassPane.setShape(
          rebounder.shapeBuilder(rebounder.start(me.getX(), me.getY())), false);
      }
    });

    scene.setOnMouseDragged(new EventHandler() {
      @Override public void handle(MouseEvent me) {
        glassPane.setShape(
          rebounder.shapeBuilder(rebounder.rebound(me.getX()+1, me.getY()+1)), false);
      }
    });

    scene.setOnMouseReleased(new EventHandler() {
      @Override public void handle(MouseEvent me) {
        if ( !rebounder.isStopped() ) {
          capture(rebounder.stop(me.getX()+1, me.getY()+1));
        }
      }
    });

A very straight forward implementation.

Ok we miss the KeyPane and the GlassPane. The KeyPane is complete transparent, contains only a screen wide rectangle which is focusable and captures key events:


  class KeyPane extends Group {
    public KeyPane(final Stage stage, final Rebounder rebounder) {
      final Rectangle keyEventPane =
        new Rectangle(screenBounds.getWidth(), screenBounds.getHeight());
      keyEventPane.setFill(Color.TRANSPARENT);
      keyEventPane.setStroke(null);
      keyEventPane.setMouseTransparent(true);
      keyEventPane.setFocusTraversable(true); // for keyPressed events
      keyEventPane.setOnKeyPressed(new EventHandler() {
        @Override public void handle(KeyEvent key) {
          if (key.getCode() == KeyCode.VK_ESCAPE) {
            stage.setVisible(false); // == Close
          } else if (key.getCode() == KeyCode.VK_A) {
            capture(new Rectangle(screenBounds.getWidth()+1, screenBounds.getHeight()+1));
          }
        }
      });
      getChildren().add (keyEventPane);
    }
  }

In my GlassPane I’ve only the calculated shape and a small help text:


  class GlassPane extends Group {
    void setShape(Shape shape, boolean showInfo) {
      while (!getChildren().isEmpty()) {
        getChildren().remove(0);
      }
      if ( shape != null ) {
        double BLUE = 0.95;
        Stop[] stops = new Stop[] {
          new Stop(0.00, Color.color(BLUE, BLUE, 1, 0.3)),
          new Stop(0.2, Color.color(BLUE, BLUE, 1, 0.7)),
          new Stop(0.25, Color.color(BLUE, BLUE, 1, 0.5)),
          new Stop(0.4, Color.color(BLUE, BLUE, 1, 0.3)),
          new Stop(0.70, Color.color(BLUE, BLUE, 1, 0.7)),
          new Stop(1.0, Color.color(BLUE, BLUE, 1, 0.3)),
        };
        LinearGradient lg = new LinearGradient(0, 0, 1, 1, true, CycleMethod.NO_CYCLE, stops);
        shape.setFill(lg);
        shape.setMouseTransparent(true);
        shape.setStroke(null);
        getChildren().add(shape);

        if ( showInfo ) {
          Text info = new Text("'ESC' to leave the Snipper\n"
                        + "'A' to capture the whole screen\n"
                        + "Drag the mouse to capture a rectangle");
          info.setFill(Color.WHITE);
          info.setTranslateX(200);
          info.setTranslateY(60);
          info.setScaleX(2);
          info.setScaleY(2);
          info.setEffect(new DropShadow());
          info.setMouseTransparent(true);
          getChildren().add (info);
        }
      }
    }
  }

I fill the shape with a LinearGradient paint.

In the start method I stick all panes together:


    KeyPane keyPane = new KeyPane(stage, rebounder = new Rebounder());
    glassPane = new GlassPane();
    glassPane.setShape(rebounder.shapeBuilder(null), true);

    group.getChildren().addAll(new Node[] {keyPane, glassPane, rebounder.getLasso()});

Ok where is the heart of my Snipper application? The capture method?

The capture method is currently developed with the java.awt.Robot class. So we need to leave the JavaFX thread and dive into the EventQueue. But first we must hide the whole stage window to get an good image from the desktop. But don’t use stage.setVisible (false) (it closes the application). I resize the stage to a zero dimension. Here my capture method:


  public void capture(final Rectangle finished) {
    primaryStage.setWidth(0);
    primaryStage.setHeight(0);
    EventQueue.invokeLater(new Runnable() {
      @Override public void run() {
        if ( (((finished.getWidth()-1) * (finished.getHeight()-1) ) > 0.0d ) ) {
          try {
            Robot robot = new Robot();
            BufferedImage img = robot.createScreenCapture(
              new java.awt.Rectangle(
                (int)finished.getX(), (int)finished.getY(),
                (int)finished.getWidth()-1, (int)finished.getHeight()-1));
            File folder = new File (System.getProperty("user.home"), "snapshots");
            folder.mkdirs();
            File file = File.createTempFile("jfx2_screen_capture", ".jpg", folder);
            ImageIO.write(img, "jpg", file);
          } catch (Exception ex) {
            Logger.getLogger(Snipper.class.getName()).log(Level.SEVERE, null, ex);
          }
        }
        Platform.runLater(new Runnable() {
          @Override public void run() {
            glassPane.setShape(rebounder.shapeBuilder(null), true);
            primaryStage.setWidth(screenBounds.getWidth());
            primaryStage.setHeight(screenBounds.getHeight());
          }
        });
      }
    });
  }

With this few 200 lines I’ve created a very useful screen capture application. The start up time is incredible: Between the main method and the complete visible stage the JavaFX framework needs only 500ms on my one year old double-core notebook. The complete startup (double-click the jar) is under a second (hard to stop the time). I need only one AWT dependency to the Robot class. I hope this could be changed to increase the startup time (e.g. with the internal FXRobot class).

best regards,
Josh.

Dieser Beitrag wurde unter JavaFX abgelegt und mit , , verschlagwortet. Setze ein Lesezeichen auf den Permalink.

13 Antworten auf JavaFXDev: Screen capture tool with 200 lines and 500ms startup time

  1. Great stuff …

    A bit too verbose (deh,…Java is Java) but hopefully with various Scala/Groovy/Visage specialized DSL/builders we can see some source code “minimization”.

    Any hints on JavaFX 2.0 GUI Builders support ? It should be simpler now.

    According to your code, it seams JavaFX 2.0 follows the single event handler/listener approach, setOnXXX instead of addXX/removeXXX ones in Swing (similarly to Android).

  2. Hopefully, DataBox/JfxBuilder will be also updated: http://www.reportmill.com/jfx/

    Any plan to have some articles covering Javascript/DOM WebPane/WebSource integration ? And don’t forgot some about CSS styling/theming …. ;)

    • Hi!

      CSS/Theming is my next topic.

      DOM manipulation is pretty easy and works a expected. JavaScript-Execution is possible. After CSS/Theming I’ll create a little sample application + article – if I get the permission to publish ;-)

      br, josh.

  3. Pingback: JavaFX 2.0으로 만든 스크린 캡쳐 프로그램 « turtle9

  4. Sakuraba sagt:

    Thank you for this insightful sample.

    I still baffles me though, that most UI-toolkits provide a declarative approach to UI-layout, that in a redesigned jfx 2.0 the choice was still made in favor of a programmatic UI building.

    In my mind for JFX, the syntax was not the problem. Rather its sluggish, slow and ugly apps combined with the slow startup time was preventing it from mainstream usage.

    And now? We still have the same runtime (the JRE) that will make it slow to load compared to Flash and HTML5, but we lose the innovative syntax and get a more verbose alternative.

    I am not sure how this will end.

    • Hi!

      Yes, we have the same runtime. But JavaFX don’t need Swing or AWT (ok my sample use Robot, but we are in EA). Without Swing and AWT you have less classes to load. The JavaFX launcher loads some modules in background.

      The scenegraph performance is very good. Anything is rendered by the prism engine and is accelerated by the graphics card.

      I think, the current JavaFX 2.0 runtime could be a big surprise for Flash, Silverlight and HTML5/JavaScript users.

      br, josh.

      • Thierry sagt:

        Oracle must do 2 more things before they can hope to tikle Adbe-Flash to my point of vue :
        - Upgrade their jvm installer so it doesn’t look like a old 90′s installer ( a bit like flash installer).
        - Continue to speedup the jvm at cold start. Very important for the Web.

        Thierry

  5. Pingback: Java desktop links of the week, March 14 | Jonathan Giles

  6. Pingback: JavaFX links of the week, March 21 // JavaFX News, Demos and Insight // FX Experience

  7. Pingback: JavaFX 2.0 EA, co wiemy? | Java-FX.pl

  8. Nicolas Lorain (@javafx4you) sagt:

    Now that JavaFX 2 is GA, do you plan to post an updated version?

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

eMail-Benachrichtigung bei weiteren Kommentaren.
Auch möglich: Abo ohne Kommentar.