You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaFX高分辨率副屏截图位置错误问题排查

高分辨率显示器截图位置偏移问题修复

问题描述

使用JavaFX实现的截图工具,在高分辨率第二显示器上截图时,捕获的区域位置不正确。工具逻辑为:创建与屏幕尺寸匹配的透明窗口,用户通过拖拽选择截图区域。

问题原因

JavaFX使用逻辑坐标(受屏幕DPI缩放影响),而JavaFX的Robot.getScreenCapture()方法接收的是物理屏幕坐标。当显示器启用DPI缩放(如125%、150%)时,逻辑坐标与物理坐标不匹配,导致截图位置偏移。原代码中使用localToScreen()转换的是逻辑坐标到屏幕逻辑坐标,而非物理坐标,这是核心问题。

修复方案

  1. 获取目标屏幕的输出缩放因子(即DPI缩放比例),通过Screen.getOutputScaleX()和Screen.getOutputScaleY()获取。
  2. 将用户选择的逻辑坐标转换为物理坐标,再传入Robot.getScreenCapture()方法。
  3. 修正截图区域的宽高计算,避免因缩放导致的区域偏差。

修改后的关键代码

核心修正:takeSnapshot方法

private boolean takeSnapshot() {
    double x = Math.min(clickX, dragX);
    double y = Math.min(clickY, dragY);
    double width = Math.abs(dragX - clickX);
    double height = Math.abs(dragY - clickY);
    
    if (width > 0 && height > 0) {
        // 获取屏幕缩放因子
        double scaleX = screen.getOutputScaleX();
        double scaleY = screen.getOutputScaleY();
        
        // 将逻辑坐标转换为物理坐标
        Rectangle2D screenVisualBounds = screen.getVisualBounds();
        double physicalX = (screenVisualBounds.getMinX() + x) * scaleX;
        double physicalY = (screenVisualBounds.getMinY() + y) * scaleY;
        double physicalWidth = width * scaleX;
        double physicalHeight = height * scaleY;
        
        // 使用物理坐标进行截图
        final WritableImage image = new javafx.scene.robot.Robot().getScreenCapture(
                null, physicalX, physicalY, physicalWidth, physicalHeight, false);
        
        File file;
        int i = 0;
        while ((file = new File(storeURI.resolve("snapshot_" + i + ".png"))).exists()) {
            i++;
        }
        try {
            ImageIO.setUseCache(false);
            ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", file);
            snapshotFile = file;
        } catch (IOException ex) {
            Log.error(ex);
            new Alert$(Alert.AlertType.ERROR)
                    .withOwner(getScene())
                    .withTitle("错误")
                    .withContextText(ex.toString())
                    .withException(ex)
                    .show();
        }
        return true;
    }
    return false;
}

额外优化:repaintCanvas方法

原代码中截图区域的绘制逻辑存在坐标判断漏洞,修正后确保选中区域的高亮框与用户拖拽范围完全匹配。

完整修改后代码

import com.wisecoders.dbs.sys.Log;
import com.wisecoders.dbs.sys.fx.Alert$;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Alert;
import javafx.scene.image.WritableImage;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import javax.imageio.ImageIO;
import java.io.File;
import java.io.IOException;
import java.net.URI;

class SnapshotStage extends Stage {

    private final Screen screen;
    private final Canvas mainCanvas = new Canvas();
    private double clickX, clickY, dragX, dragY;
    private enum Status { waitForSelection, selectArea };
    private Status status = Status.waitForSelection;
    private File snapshotFile;

    private final URI storeURI;

    SnapshotStage(Screen screen, URI storeURI ){
        this.screen = screen;
        this.storeURI = storeURI;
        this.initStyle(StageStyle.TRANSPARENT);
        setAlwaysOnTop(true);

        final StackPane stackPane = new StackPane();
        stackPane.setStyle("-fx-background-color:transparent;");
        stackPane.getChildren().add( mainCanvas );

        Rectangle2D screenBounds = screen.getVisualBounds();
        final Scene scene = new Scene(stackPane, screenBounds.getWidth(), screenBounds.getHeight(), Color.TRANSPARENT);
        scene.setFill(Color.TRANSPARENT);
        setScene(scene);
        setX(screenBounds.getMinX());
        setY(screenBounds.getMinY());

        scene.addEventHandler( KeyEvent.KEY_PRESSED, e -> {
            if ( e.getCode() == KeyCode.ESCAPE ){
                SnapshotStage.this.close();
            }
        });
        mainCanvas.setWidth( screenBounds.getWidth() );
        mainCanvas.setHeight( screenBounds.getHeight() );
        mainCanvas.setOnMouseMoved( e -> {
            clickX = e.getX();
            clickY = e.getY();
            repaintCanvas();
        });
        mainCanvas.setOnMousePressed( e -> {
            if (e.getButton() == MouseButton.PRIMARY) {
                clickX = e.getX();
                clickY = e.getY();
            }
        });
        mainCanvas.setOnMouseDragged( m -> {
            if ( m.getButton() == MouseButton.PRIMARY ) {
                status = Status.selectArea;
                dragX = m.getX();
                dragY = m.getY();
                repaintCanvas();
            }
        });
        mainCanvas.setOnMouseReleased( m -> {
            if ( status == Status.selectArea ) {
                try {
                    if ( takeSnapshot()) {
                        final Media sound = new Media( String.valueOf( SnapshotStage.class.getResource("/icons/sounds/camera_shooter.mp3") ));
                        final MediaPlayer mediaPlayer = new MediaPlayer(sound);
                        mediaPlayer.play();
                    }
                } catch ( Throwable ex ){
                    Log.error( ex );
                }
                SnapshotStage.this.close();
            }
        });
        Platform.runLater( this::repaintCanvas );
    }

    private boolean takeSnapshot() {
        double x = Math.min( clickX, dragX );
        double y = Math.min( clickY, dragY );
        double width = Math.abs(dragX - clickX);
        double height = Math.abs(dragY - clickY);
        
        if( width > 0 && height > 0 ) {
            // 获取屏幕缩放因子
            double scaleX = screen.getOutputScaleX();
            double scaleY = screen.getOutputScaleY();
            
            // 将逻辑坐标转换为物理坐标
            Rectangle2D screenVisualBounds = screen.getVisualBounds();
            double physicalX = (screenVisualBounds.getMinX() + x) * scaleX;
            double physicalY = (screenVisualBounds.getMinY() + y) * scaleY;
            double physicalWidth = width * scaleX;
            double physicalHeight = height * scaleY;
            
            // 使用物理坐标进行截图
            final WritableImage image = new javafx.scene.robot.Robot().getScreenCapture(
                    null, physicalX, physicalY, physicalWidth, physicalHeight, false);
            
            File file;
            int i = 0;
            while ((file = new File( storeURI.resolve("snapshot_" + i + ".png"))).exists()) {
                i++;
            }
            try {
                ImageIO.setUseCache(false);
                ImageIO.write( SwingFXUtils.fromFXImage( image, null ), "png", file);
                snapshotFile = file;
            } catch (IOException ex) {
                Log.error(ex);
                new Alert$(Alert.AlertType.ERROR)
                        .withOwner(getScene())
                        .withTitle("错误")
                        .withContextText(ex.toString())
                        .withException(ex)
                        .show();
            }
            return true;
        }
        return false;
    }

    private void repaintCanvas(){
        if ( mainCanvas.isVisible() && mainCanvas.getScene() != null ) {
            GraphicsContext gc = mainCanvas.getGraphicsContext2D();

            gc.clearRect(0, 0, mainCanvas.getWidth(), mainCanvas.getHeight());
            gc.setFill(Color.rgb(0, 0, 0, 0.2));
            gc.fillRect(0, 0, mainCanvas.getWidth(), mainCanvas.getHeight());

            if (status == Status.waitForSelection) {
                gc.setStroke( Color.web("#672824"));
                gc.setFill( Color.web("#672824"));
                gc.setLineWidth(1d);
                gc.strokeLine(clickX, 0, clickX, mainCanvas.getHeight());
                gc.strokeLine(0, clickY, mainCanvas.getWidth(), clickY);
                gc.fillText("拖拽选择截图区域", clickX + 10, clickY - 15 );
            } else {
                double rectX = Math.min(clickX, dragX);
                double rectY = Math.min(clickY, dragY);
                double rectWidth = Math.abs(dragX - clickX);
                double rectHeight = Math.abs(dragY - clickY);
                
                gc.clearRect(rectX, rectY, rectWidth, rectHeight);
                gc.setStroke(Color.RED);
                gc.setLineWidth(1d);
                gc.strokeRect(rectX - 1, rectY - 1, rectWidth + 2, rectHeight + 2);
            }
        }
    }

    File getSnapshotFile(){
        return snapshotFile;
    }
}

内容的提问来源于stack exchange,提问作者DbSchema

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 22:14:58