【问题标题】:Making a zoomable coordinate system in JavaFX [closed]在 JavaFX 中制作可缩放坐标系 [关闭]
【发布时间】:2021-10-26 21:23:03
【问题描述】:

我想在 javafx 中制作一种“可缩放图形计算器”,所以基本上是一个可以用鼠标滚轮放大的坐标系。我在画布上绘制所有内容,但不确定如何进行缩放部分...我可以想到三种方法:

  1. 我可以使用 GraphicsContext.transform() 将转换矩阵应用于所有内容
  2. 我可以制作某种坐标转换方法,将我的所有坐标都通过它,然后将它们移动到屏幕上的正确位置
  3. 为我在画布上绘制的所有内容手动执行所有数学运算(这看起来会非常繁琐且很难维护)

你们会建议我做什么?

编辑:另外,如果我采用第一种方法或类似方法,我是否需要担心在画布之外“绘制”的元素?线条是否会保持美观和清晰,还是会因为抗锯齿而变得模糊? (我更喜欢前者)

【问题讨论】:

  • 检查了一个相关的例子here

标签: java javafx canvas graphics


【解决方案1】:

我从这个答案更新了图形解决方案以添加缩放功能:

为了添加交互式缩放,我为滚动事件添加了一个处理程序。在滚动事件处理程序中,我计算坐标轴和绘图坐标的低值和高值的新值,然后将它们应用于坐标轴和绘图。

我使用在鼠标滚轮或触摸板或触摸屏滚动手势上工作的scroll event handler。但您也可以(或改为)使用zoom event handler,它利用触摸表面上的缩放(捏合)手势。

当检测到滚动时,我只是放大或缩小固定量(当前缩放系数的 10%),直到最小或最大缩放值。更复杂的解决方案可以查询滚动或缩放事件的增量值,以根据滚动事件的速度实现惯性滚动和更大或更少的滚动。

为了实现缩放,我重新创建了缩放的节点,而不是更新现有节点的属性,这可能不是那么有效。但是,在我的简单测试用例中,性能似乎还不错,所以我认为不值得付出额外的努力来优化。

这只是这个问题的众多潜在解决方案之一(我不会在这里讨论其他潜在的解决方案)。此答案中提供的特定解决方案似乎很适合我。

另外,请注意,此解决方案不使用画布,它基于场景图。我建议为此任务使用场景图,但如果您愿意,也可以使用画布。对于画布解决方案,该解决方案可能与此处介绍的解决方案大不相同(我不提供任何关于如何创建基于画布的解决方案的建议)。

用于处理缩放的事件处理程序

此处理程序附加到包含图形节点子节点的父窗格。

private class ZoomHandler implements EventHandler<ScrollEvent> {
    private static final double MAX_ZOOM = 2;
    private static final double MIN_ZOOM = 0.5;

    private double zoomFactor = 1;

    @Override
    public void handle(ScrollEvent event) {
        if (event.getDeltaY() == 0) {
            return;
        } else if (event.getDeltaY() < 0) {
            zoomFactor = Math.max(MIN_ZOOM, zoomFactor * 0.9);
        } else if (event.getDeltaY() > 0) {
            zoomFactor = Math.min(MAX_ZOOM, zoomFactor * 1.1);
        }

        Plot plot = plotChart(zoomFactor);

        Pane parent = (Pane) event.getSource();
        parent.getChildren().setAll(plot);
    }
}

缩放图表图像示例

一直放大

默认缩放级别

一直放大

完整的示例解决方案代码

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;

import java.util.function.Function;

public class ZoomableCartesianPlot extends Application {

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

    @Override
    public void start(final Stage stage) {
        Plot plot = plotChart(1);

        StackPane layout = new StackPane(
                plot
        );
        layout.setPadding(new Insets(20));
        layout.setStyle("-fx-background-color: rgb(35, 39, 50);");
        layout.setOnScroll(new ZoomHandler());

        stage.setTitle("y = \u00BC(x+4)(x+1)(x-2)");
        stage.setScene(new Scene(layout, Color.rgb(35, 39, 50)));
        stage.show();
    }

    private Plot plotChart(double zoomFactor) {
        Axes axes = new Axes(
                400, 300,
                -8 * zoomFactor, 8 * zoomFactor, 1,
                -6 * zoomFactor, 6 * zoomFactor, 1
        );

        Plot plot = new Plot(
                x -> .25 * (x + 4) * (x + 1) * (x - 2),
                -8 * zoomFactor, 8 * zoomFactor, 0.1,
                axes
        );

        return plot;
    }

    class Axes extends Pane {
        private NumberAxis xAxis;
        private NumberAxis yAxis;

        public Axes(
                int width, int height,
                double xLow, double xHi, double xTickUnit,
                double yLow, double yHi, double yTickUnit
        ) {
            setMinSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
            setPrefSize(width, height);
            setMaxSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);

            xAxis = new NumberAxis(xLow, xHi, xTickUnit);
            xAxis.setSide(Side.BOTTOM);
            xAxis.setMinorTickVisible(false);
            xAxis.setPrefWidth(width);
            xAxis.setLayoutY(height / 2);

            yAxis = new NumberAxis(yLow, yHi, yTickUnit);
            yAxis.setSide(Side.LEFT);
            yAxis.setMinorTickVisible(false);
            yAxis.setPrefHeight(height);
            yAxis.layoutXProperty().bind(
                Bindings.subtract(
                    (width / 2) + 1,
                    yAxis.widthProperty()
                )
            );

            getChildren().setAll(xAxis, yAxis);
        }

        public NumberAxis getXAxis() {
            return xAxis;
        }

        public NumberAxis getYAxis() {
            return yAxis;
        }
    }

    class Plot extends Pane {
        public Plot(
                Function<Double, Double> f,
                double xMin, double xMax, double xInc,
                Axes axes
        ) {
            Path path = new Path();
            path.setStroke(Color.ORANGE.deriveColor(0, 1, 1, 0.6));
            path.setStrokeWidth(2);

            path.setClip(
                    new Rectangle(
                            0, 0, 
                            axes.getPrefWidth(), 
                            axes.getPrefHeight()
                    )
            );

            double x = xMin;
            double y = f.apply(x);

            path.getElements().add(
                    new MoveTo(
                            mapX(x, axes), mapY(y, axes)
                    )
            );

            x += xInc;
            while (x < xMax) {
                y = f.apply(x);

                path.getElements().add(
                        new LineTo(
                                mapX(x, axes), mapY(y, axes)
                        )
                );

                x += xInc;
            }

            setMinSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
            setPrefSize(axes.getPrefWidth(), axes.getPrefHeight());
            setMaxSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);

            getChildren().setAll(axes, path);
        }

        private double mapX(double x, Axes axes) {
            double tx = axes.getPrefWidth() / 2;
            double sx = axes.getPrefWidth() / 
               (axes.getXAxis().getUpperBound() - 
                axes.getXAxis().getLowerBound());

            return x * sx + tx;
        }

        private double mapY(double y, Axes axes) {
            double ty = axes.getPrefHeight() / 2;
            double sy = axes.getPrefHeight() / 
                (axes.getYAxis().getUpperBound() - 
                 axes.getYAxis().getLowerBound());

            return -y * sy + ty;
        }
    }

    private class ZoomHandler implements EventHandler<ScrollEvent> {
        private static final double MAX_ZOOM = 2;
        private static final double MIN_ZOOM = 0.5;

        private double zoomFactor = 1;

        @Override
        public void handle(ScrollEvent event) {
            if (event.getDeltaY() == 0) {
                return;
            } else if (event.getDeltaY() < 0) {
                zoomFactor = Math.max(MIN_ZOOM, zoomFactor * 0.9);
            } else if (event.getDeltaY() > 0) {
                zoomFactor = Math.min(MAX_ZOOM, zoomFactor * 1.1);
            }

            Plot plot = plotChart(zoomFactor);

            Pane parent = (Pane) event.getSource();
            parent.getChildren().setAll(plot);
        }
    }
}

【讨论】:

  • 一个非常有用的 MVCE,非常感谢发布它。我正在编写自定义烛台图表,需要处理屏幕调整大小事件。您提到重新创建节点与更新它们的属性,但没有绑定。是否可以将节点的 X 和 Y 布局值绑定到各自轴上的显示位置?我一直试图弄清楚但无处可去。我正在使用轴的getDisplayPosition() 方法绘制烛台,但找不到要绑定的“displayPositionProperty”。干杯。
  • @GreenZebra 我不太明白你在问什么。我建议您用自己的 mcve 提出一个新问题。我不能保证你会得到答案,而且这将是一个难以表达和提出的问题,但这是你最有可能获得一些有用信息的方法。
  • 好的,谢谢,会的。
猜你喜欢
  • 2016-01-26
  • 2019-12-23
  • 2014-04-11
  • 1970-01-01
  • 1970-01-01
  • 2018-04-23
  • 1970-01-01
  • 2013-03-28
  • 1970-01-01
相关资源
最近更新 更多