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

如何解决JavaFX绘图项目中缩放时坐标轴错位问题?

JavaFX绘图缩放时坐标轴错位问题修复

问题根源

  1. 缩放偏移计算错误:原代码中调整xOffset和yOffset的公式逻辑错误,无法保证鼠标指向的点在缩放后保持原位,导致坐标轴偏移错位。
  2. 坐标系统混淆:屏幕坐标与世界坐标的转换逻辑不统一,坐标轴位置计算和网格绘制的起始位置规则不一致,缩放后两者出现错位。

修复方案

1. 修正缩放事件的偏移计算逻辑

以鼠标位置为缩放中心,确保缩放前后鼠标指向的世界坐标点不变,替换原setOnScroll方法:

canvas.setOnScroll((event) -> {
    double mouseX = event.getX();
    double mouseY = event.getY();
    double zoomFactor = event.getDeltaY() > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;

    double newGridSize = gridSize * zoomFactor;
    if (newGridSize >= MIN_GRID_SIZE && newGridSize <= MAX_GRID_SIZE) {
        // 转换鼠标位置到世界坐标
        double originX = CANVAS_WIDTH / 3.0 + xOffset;
        double originY = CANVAS_HEIGHT / 7.0 + yOffset;
        double worldX = (mouseX - originX) / gridSize;
        double worldY = (originY - mouseY) / gridSize;

        // 更新网格大小
        gridSize = newGridSize;

        // 计算新偏移量,保证鼠标点世界坐标不变
        double newOriginX = mouseX - worldX * gridSize;
        double newOriginY = mouseY + worldY * gridSize;
        xOffset = newOriginX - CANVAS_WIDTH / 3.0;
        yOffset = newOriginY - CANVAS_HEIGHT / 7.0;
    } else {
        // 重置缩放和偏移
        gridSize = INITIAL_GRID_SIZE;
        xOffset = 0;
        yOffset = 0;
    }
    drawGrid(gc);
});

2. 统一网格与坐标轴的坐标对齐

修改网格绘制的起始位置计算,确保网格线与坐标轴原点对齐:

// 绘制主网格线
gc.setStroke(Color.LIGHTGRAY);
double originX = CANVAS_WIDTH / 3.0 + xOffset;
double originY = CANVAS_HEIGHT / 7.0 + yOffset;

double startX = originX - Math.floor(originX / gridSize) * gridSize;
for (double x = startX; x < CANVAS_WIDTH; x += gridSize) {
    gc.strokeLine(x, 0, x, CANVAS_HEIGHT);
}

double startY = originY - Math.floor(originY / gridSize) * gridSize;
for (double y = startY; y < CANVAS_HEIGHT; y += gridSize) {
    gc.strokeLine(0, y, CANVAS_WIDTH, y);
}

// 高亮网格线同理修改
double highlightedLineDistance = gridSize * 5;
gc.setStroke(Color.rgb(128, 128, 128));
double highlightStartX = originX - Math.floor(originX / highlightedLineDistance) * highlightedLineDistance;
for (double x = highlightStartX; x < CANVAS_WIDTH; x += highlightedLineDistance) {
    gc.strokeLine(x, 0, x, CANVAS_HEIGHT);
}

double highlightStartY = originY - Math.floor(originY / highlightedLineDistance) * highlightedLineDistance;
for (double y = highlightStartY; y < CANVAS_HEIGHT; y += highlightedLineDistance) {
    gc.strokeLine(0, y, CANVAS_WIDTH, y);
}

3. 修正刻度计算逻辑

调整刻度的起始/结束值计算,保证刻度与坐标轴完全对应且仅在画布范围内显示:

// Y轴刻度
int startScaleY = (int) Math.floor((originY - CANVAS_HEIGHT) / highlightedLineDistance);
int endScaleY = (int) Math.ceil(originY / highlightedLineDistance);
for (int i = startScaleY; i <= endScaleY; i++) {
    double y = originY - i * highlightedLineDistance;
    if (y >= 0 && y <= CANVAS_HEIGHT && i != 0) {
        gc.strokeLine(originX + 5, y, originX - 5, y);
        gc.fillText(String.valueOf(i * 5), originX - 30, y + 3);
    }
}

// X轴刻度
int startScaleX = (int) Math.floor((-originX) / highlightedLineDistance);
int endScaleX = (int) Math.ceil((CANVAS_WIDTH - originX) / highlightedLineDistance);
for (int i = startScaleX; i <= endScaleX; i++) {
    double x = originX + i * highlightedLineDistance;
    if (x >= 0 && x <= CANVAS_WIDTH && i != 0) {
        gc.strokeLine(x, originY - 5, x, originY + 5);
        gc.fillText(String.valueOf(i * 5), x - 3, originY + 20);
    }
}

完整修复后的代码

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class GraphingCal extends Application {

    private static final int CANVAS_WIDTH = 900;
    private static final int CANVAS_HEIGHT = 700;
    private static final double INITIAL_GRID_SIZE = 20;
    private static final double ZOOM_FACTOR = 1.1;
    private static final double MIN_GRID_SIZE = INITIAL_GRID_SIZE * 0.6;
    private static final double MAX_GRID_SIZE = INITIAL_GRID_SIZE / 0.6;

    private double gridSize = INITIAL_GRID_SIZE;
    private double xOffset = 0;
    private double yOffset = 0;
    private double prevX;
    private double prevY;

    @Override
    public void start(Stage primaryStage) {
        Group root = new Group();
        Scene scene = new Scene(root, CANVAS_WIDTH, CANVAS_HEIGHT);

        Canvas canvas = new Canvas(CANVAS_WIDTH, CANVAS_HEIGHT);
        GraphicsContext gc = canvas.getGraphicsContext2D();
        drawGrid(gc);

        canvas.setOnMousePressed(e -> {
            prevX = e.getX();
            prevY = e.getY();
        });

        canvas.setOnMouseDragged(e -> {
            double dx = e.getX() - prevX;
            double dy = e.getY() - prevY;
            xOffset += dx;
            yOffset += dy;
            prevX = e.getX();
            prevY = e.getY();
            drawGrid(gc);
        });

        canvas.setOnScroll((event) -> {
            double mouseX = event.getX();
            double mouseY = event.getY();
            double zoomFactor = event.getDeltaY() > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;

            double newGridSize = gridSize * zoomFactor;
            if (newGridSize >= MIN_GRID_SIZE && newGridSize <= MAX_GRID_SIZE) {
                double originX = CANVAS_WIDTH / 3.0 + xOffset;
                double originY = CANVAS_HEIGHT / 7.0 + yOffset;
                double worldX = (mouseX - originX) / gridSize;
                double worldY = (originY - mouseY) / gridSize;

                gridSize = newGridSize;

                double newOriginX = mouseX - worldX * gridSize;
                double newOriginY = mouseY + worldY * gridSize;
                xOffset = newOriginX - CANVAS_WIDTH / 3.0;
                yOffset = newOriginY - CANVAS_HEIGHT / 7.0;
            } else {
                gridSize = INITIAL_GRID_SIZE;
                xOffset = 0;
                yOffset = 0;
            }
            drawGrid(gc);
        });

        root.getChildren().add(canvas);
        primaryStage.setTitle("Graphing Calculator");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void drawGrid(GraphicsContext gc) {
        gc.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

        double originX = CANVAS_WIDTH / 3.0 + xOffset;
        double originY = CANVAS_HEIGHT / 7.0 + yOffset;
        double highlightedLineDistance = gridSize * 5;

        // 主网格线
        gc.setStroke(Color.LIGHTGRAY);
        double startX = originX - Math.floor(originX / gridSize) * gridSize;
        for (double x = startX; x < CANVAS_WIDTH; x += gridSize) {
            gc.strokeLine(x, 0, x, CANVAS_HEIGHT);
        }
        double startY = originY - Math.floor(originY / gridSize) * gridSize;
        for (double y = startY; y < CANVAS_HEIGHT; y += gridSize) {
            gc.strokeLine(0, y, CANVAS_WIDTH, y);
        }

        // 高亮网格线
        gc.setStroke(Color.rgb(128, 128, 128));
        double highlightStartX = originX - Math.floor(originX / highlightedLineDistance) * highlightedLineDistance;
        for (double x = highlightStartX; x < CANVAS_WIDTH; x += highlightedLineDistance) {
            gc.strokeLine(x, 0, x, CANVAS_HEIGHT);
        }
        double highlightStartY = originY - Math.floor(originY / highlightedLineDistance) * highlightedLineDistance;
        for (double y = highlightStartY; y < CANVAS_HEIGHT; y += highlightedLineDistance) {
            gc.strokeLine(0, y, CANVAS_WIDTH, y);
        }

        // 坐标轴
        gc.setStroke(Color.BLACK);
        gc.strokeLine(0, originY, CANVAS_WIDTH, originY);
        gc.strokeLine(originX, 0, originX, CANVAS_HEIGHT);

        // 坐标轴标签
        gc.setFill(Color.BLACK);
        gc.fillText("0", originX + 5, originY + 5);
        gc.fillText("x", CANVAS_WIDTH - 10, originY + 15);
        gc.fillText("y", originX + 5, 15);

        // Y轴刻度
        int startScaleY = (int) Math.floor((originY - CANVAS_HEIGHT) / highlightedLineDistance);
        int endScaleY = (int) Math.ceil(originY / highlightedLineDistance);
        for (int i = startScaleY; i <= endScaleY; i++) {
            double y = originY - i * highlightedLineDistance;
            if (y >= 0 && y <= CANVAS_HEIGHT && i != 0) {
                gc.strokeLine(originX + 5, y, originX - 5, y);
                gc.fillText(String.valueOf(i * 5), originX - 30, y + 3);
            }
        }

        // X轴刻度
        int startScaleX = (int) Math.floor((-originX) / highlightedLineDistance);
        int endScaleX = (int) Math.ceil((CANVAS_WIDTH - originX) / highlightedLineDistance);
        for (int i = startScaleX; i <= endScaleX; i++) {
            double x = originX + i * highlightedLineDistance;
            if (x >= 0 && x <= CANVAS_WIDTH && i != 0) {
                gc.strokeLine(x, originY - 5, x, originY + 5);
                gc.fillText(String.valueOf(i * 5), x - 3, originY + 20);
            }
        }
    }

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

修复说明

  • 通过世界坐标与屏幕坐标的双向转换实现中心缩放,确保鼠标指向点位置不变,解决坐标轴偏移问题。
  • 统一网格与坐标轴的起始位置计算规则,保证两者始终对齐。
  • 修正刻度边界条件,避免刻度超出画布范围,同时确保刻度与坐标轴完全对应。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 01:52:03