最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Capturing The App Window Exactly Using JavaFX - Stack Overflow

programmeradmin1浏览0评论

I'm using JavaFX [21] and want to precisely capture a snapshot of my entire app window.

So, from the primary stage I get the scene and from the scene get the window:

    Scene scene = primaryStage.getScene();
    Window appWindow = scene.getWindow();

As far as I'm aware there is no JavaFX method for taking a snapshot of a Window in a similar way to the Scene.snapshot() method. And so I then resort to using the Robot.getScreenCapture() method:

    Scene scene = primaryStage.getScene();
    Window appWindow = scene.getWindow();
    double screenX = appWindow.getX();
    double screenY = appWindow.getY();
    double appW = appWindow.getWidth();
    double appH = appWindow.getHeight();
    Rectangle2D rectangle = new Rectangle2D(screenX, screenY, appW, appH);

    Robot robot = new Robot();
    WritableImage image = robot.getScreenCapture(null, rectangle, true);

All makes sense apart from the resulting image being slightly wider and higher than my app window. Also, it is clear that the screen (x,y) do not coincide exactly with the top left corner of the app.

It's as though the dimensions of an unknown border/inset are being added but I see no mention of this in the JavaDoc.

It's a simple request to obtain a snapshot of my entire app and nothing else, but I can't seem to solve this request using JavaFX. Has anyone else encountered this issue and found a solution?

Thanks.

I'm using JavaFX [21] and want to precisely capture a snapshot of my entire app window.

So, from the primary stage I get the scene and from the scene get the window:

    Scene scene = primaryStage.getScene();
    Window appWindow = scene.getWindow();

As far as I'm aware there is no JavaFX method for taking a snapshot of a Window in a similar way to the Scene.snapshot() method. And so I then resort to using the Robot.getScreenCapture() method:

    Scene scene = primaryStage.getScene();
    Window appWindow = scene.getWindow();
    double screenX = appWindow.getX();
    double screenY = appWindow.getY();
    double appW = appWindow.getWidth();
    double appH = appWindow.getHeight();
    Rectangle2D rectangle = new Rectangle2D(screenX, screenY, appW, appH);

    Robot robot = new Robot();
    WritableImage image = robot.getScreenCapture(null, rectangle, true);

All makes sense apart from the resulting image being slightly wider and higher than my app window. Also, it is clear that the screen (x,y) do not coincide exactly with the top left corner of the app.

It's as though the dimensions of an unknown border/inset are being added but I see no mention of this in the JavaDoc.

It's a simple request to obtain a snapshot of my entire app and nothing else, but I can't seem to solve this request using JavaFX. Has anyone else encountered this issue and found a solution?

Thanks.

Share Improve this question asked Feb 14 at 14:13 Graham SeedGraham Seed 8843 gold badges12 silver badges27 bronze badges 3
  • See if stackoverflow/a/32709332/2423906 can help. – SedJ601 Commented Feb 14 at 14:58
  • Thanks for your reply but this appears to be equivalent to taking a snapshot of the primary scene and clips the app title and other decorations. I'm after the entire app. – Graham Seed Commented Feb 14 at 15:09
  • 4 "It's as though the dimensions of an unknown border/inset are being added but I see no mention of this in the JavaDoc" -- The default window decorations come from the operating system, so the Javadoc won't give precise details. Though properties like Window.width do say, "This value includes any and all decorations which may be added by the Operating System such as resizable frame handles". – Slaw Commented Feb 14 at 20:32
Add a comment  | 

1 Answer 1

Reset to default 3

The default decorations of a window come from the operating system, so the Javadoc will not give precise details. However, the documentation of properties like Window.width do say:

This value includes any and all decorations which may be added by the Operating System such as resizable frame handles.

JavaFX does not, at least as of version 23, provide an API to query the size of these decorations or to get the bounds of the "true" window. And unfortunately, there's no snapshot method for Window or its subtypes like there is for Scene and Node.

You'll have to find a way to get the bounds of the window you want. That will likely involve relying on native libraries. Which means you'll need a separate solution for each platform you're having a problem with.


Example - Windows 11

Here's an example that works on Windows 11 with Java 23 and JavaFX 23. It may work on Windows 10 as well, but I'm not sure. Also not sure if it will work with other versions of Java or JavaFX.

This example uses the Foreign Function & Memory API, which means it requires Java 22+. Other options include using JNI or JNA.

The native functions used are GetForegroundWindow (to get the window handle) and DwmGetWindowAttribute (to get the window's bounds without the drop shadow).

Output

The screenshot of the primary Stage is being displayed in a Dialog. Unfortunately, the corners of the screenshot are square despite the corners of the window being round. Though note this is the same behavior seen by Window's Snipping Tool which is what I used to take the screenshot of the Dialog. The other unfortunate thing is that the screenshot taken by JavaFX's Robot is of noticeably lower quality than the screenshot taken by Window's Snipping Tool. Not sure if there's a way to fix that.

For contrast, here's what it looks like when getting the window's bounds from the x, y, width, and height properties of the Window.

Source Code

Native.java

package com.example.

import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;
import static java.lang.foreign.ValueLayout.JAVA_LONG;

import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;

// Note: not a great class name
public final class Native {

  public static long GetForegroundWindow() {
    try {
      return (long) GetForegroundWindow.invokeExact();
    } catch (Throwable t) {
      throw new RuntimeException(t);
    }
  }

  public static int DwmGetWindowAttribute(
      long hwnd, WindowAttribute dwAttribute, MemorySegment pvAttribute, MemoryLayout cbAttribute) {
    try {
      return (int) DwmGetWindowAttribute.invokeExact(
          hwnd, dwAttribute.flag(), pvAttribute, Math.toIntExact(cbAttribute.byteSize()));
    } catch (Throwable t) {
      throw new RuntimeException(t);
    }
  }

  private Native() {}

  /* **************************************************************************
   *                                                                          *
   * FFM Handling                                                             *
   *                                                                          *
   ****************************************************************************/

  private static final MethodHandle GetForegroundWindow;
  private static final MethodHandle DwmGetWindowAttribute;

  static {
    System.loadLibrary("user32");
    System.loadLibrary("dwmapi");

    var symbols = SymbolLookup.loaderLookup();
    var linker = Linker.nativeLinker();

    GetForegroundWindow = linker.downcallHandle(
        symbols.findOrThrow("GetForegroundWindow"), FunctionDescriptor.of(JAVA_LONG));

    DwmGetWindowAttribute = linker.downcallHandle(
        symbols.findOrThrow("DwmGetWindowAttribute"),
        FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_INT, ADDRESS, JAVA_INT));
  }
}

Rect.java

package com.example;

import static java.lang.foreign.MemoryLayout.PathElement.groupElement;
import static java.lang.foreign.MemoryLayout.structLayout;
import static java.lang.foreign.ValueLayout.JAVA_INT;

import java.lang.foreign.MemorySegment;
import java.lang.foreign.SegmentAllocator;
import java.lang.foreign.StructLayout;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public final class Rect {

  private final MemorySegment segment;

  public Rect(SegmentAllocator allocator) {
    this.segment = allocator.allocate(LAYOUT).asReadOnly();
  }

  public MemorySegment segment() {
    return segment;
  }

  public int left() {
    return (int) LEFT.get(segment);
  }

  public int top() {
    return (int) TOP.get(segment);
  }

  public int right() {
    return (int) RIGHT.get(segment);
  }

  public int bottom() {
    return (int) BOTTOM.get(segment);
  }

  public int width() {
    return right() - left();
  }

  public int height() {
    return bottom() - top();
  }

  /* **************************************************************************
   *                                                                          *
   * FFM Handling                                                             *
   *                                                                          *
   ****************************************************************************/

  public static final StructLayout LAYOUT = structLayout(
          JAVA_INT.withName("left"),
          JAVA_INT.withName("top"),
          JAVA_INT.withName("right"),
          JAVA_INT.withName("bottom"))
      .withName("RECT");

  private static final VarHandle LEFT = handle("left");
  private static final VarHandle TOP = handle("top");
  private static final VarHandle RIGHT = handle("right");
  private static final VarHandle BOTTOM = handle("bottom");

  private static VarHandle handle(String groupElementName) {
    var handle = LAYOUT.varHandle(groupElement(groupElementName));
    return MethodHandles.insertCoordinates(handle, 1, 0L);
  }
}

WindowAttribute.java

package com.example;

public enum WindowAttribute {
  EXTENDED_FRAME_BOUNDS(9);

  private final int flag;

  WindowAttribute(int flag) {
    this.flag = flag;
  }

  public int flag() {
    return flag;
  }
}

Main.java

package com.example;

import static com.example.WindowAttribute.EXTENDED_FRAME_BOUNDS;

import java.lang.foreign.Arena;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.robot.Robot;
import javafx.stage.Stage;
import javafx.stage.Window;

public class Main extends Application {

  public static void main(String[] args) {
    launch(Main.class);
  }

  @Override
  public void start(Stage primaryStage) {
    var button = new Button("Take screenshot");
    button.setOnAction(e -> {
      e.consume();
      takeAndShowScreenshot(primaryStage);
    });
    var root = new StackPane(button);
    primaryStage.setScene(new Scene(root, 500, 300));
    primaryStage.setTitle("Window Screenshot Demo");
    primaryStage.show();
  }

  private void takeAndShowScreenshot(Window window) {
    var area = getWindowBoundsForScreenshot(window);
    var image = new Robot().getScreenCapture(null, area);

    var content = new StackPane(new ImageView(image));
    content.setPadding(new Insets(10));

    var dialog = new Dialog<Void>();
    dialog.initOwner(window);
    dialog.setTitle("Screenshot");
    dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
    dialog.getDialogPane().setContent(content);
    dialog.setResizable(true);
    dialog.show();
  }

  private Rectangle2D getWindowBoundsForScreenshot(Window window) {
    if (!window.isFocused()) throw new IllegalStateException();
    long hwnd = Native.GetForegroundWindow();
    try (var arena = Arena.ofConfined()) {
      var rect = new Rect(arena);

      int hresult =
          Native.DwmGetWindowAttribute(hwnd, EXTENDED_FRAME_BOUNDS, rect.segment(), Rect.LAYOUT);
      if (hresult < 0) {
        throw new RuntimeException("DwmGetWindowAttribute returned error code: " + hresult);
      }

      // The RECT returned by DWM is already scaled, but JavaFX's API works
      // on the unscaled values.
      double x = rect.left() / window.getOutputScaleX();
      double y = rect.top() / window.getOutputScaleY();
      double w = rect.width() / window.getOutputScaleX();
      double h = rect.height() / window.getOutputScaleY();
      return new Rectangle2D(x, y, w, h);
    }
  }
}
发布评论

评论列表(0)

  1. 暂无评论