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

JavaFX实现X光图像缩放平移并保持标注点及标签固定尺寸

JavaFX X光图像缩放平移+点位固定实现方案

核心实现逻辑

  • 点位存储归一化相对坐标:所有标记点位存相对于原图宽高的0~1范围坐标,缩放平移时根据当前显示比例动态计算屏幕坐标,保证点位永远对应正确的解剖位置
  • 元素逆缩放实现大小固定:标记圆圈、标签的缩放值设置为当前图像缩放比例的倒数,保证无论图像缩放多少,标记元素视觉大小不变

1 调整FXML布局

首先修改FXML结构,将ScrollPane的内容换成AnchorPane,同时承载ImageView和所有标记元素:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="650.0" prefWidth="851.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="main.java.ZoomClass" fx:id="xRayAnchorPane">
   <children>
      <ScrollPane layoutX="56.0" layoutY="71.0" prefHeight="533.0" prefWidth="743.0" fx:id="xRayScrollPane" pannable="false">
         <content>
            <AnchorPane fx:id="contentPane" prefHeight="736.0" prefWidth="739.0">
               <children>
                  <ImageView fitHeight="736.0" fitWidth="739.0" pickOnBounds="true" preserveRatio="true" fx:id="xRayImage">
                     <image>
                        <Image url="@ceph2.jpg" />
                     </image>
                  </ImageView>
               </children>
            </AnchorPane>
         </content>
      </ScrollPane>
   </children>
</AnchorPane>

2 完整控制器实现

package main.java;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Circle;
import java.util.ArrayList;
import java.util.List;

// 存储标记点位的归一化坐标和对应UI元素
class MarkPoint {
    double normX; // 相对于原图宽度的0~1坐标
    double normY; // 相对于原图高度的0~1坐标
    Circle circle;
    Label label;

    public MarkPoint(double normX, double normY, Circle circle, Label label) {
        this.normX = normX;
        this.normY = normY;
        this.circle = circle;
        this.label = label;
    }
}

public class ZoomClass {
    @FXML
    AnchorPane xRayAnchorPane;
    @FXML
    ImageView xRayImage;
    @FXML
    ScrollPane xRayScrollPane;
    @FXML
    AnchorPane contentPane;

    private final double MIN_SCALE = 0.5;
    private final double MAX_SCALE = 5.0;
    private final double SCALE_STEP = 0.1;
    private double currentScale = 1.0;
    // 拖拽平移用的临时变量
    private double dragStartX, dragStartY;
    private double scrollStartH, scrollStartV;
    // 所有标记点位集合
    private final List<MarkPoint> markPoints = new ArrayList<>();

    public void initialize() {
        // 初始化图像大小绑定
        xRayImage.fitWidthProperty().bind(contentPane.prefWidthProperty().multiply(currentScale));
        xRayImage.fitHeightProperty().bind(contentPane.prefHeightProperty().multiply(currentScale));

        // 1. 鼠标滚轮缩放事件
        contentPane.addEventFilter(ScrollEvent.SCROLL, event -> {
            event.consume();
            double delta = event.getDeltaY() > 0 ? SCALE_STEP : -SCALE_STEP;
            double newScale = Math.clamp(currentScale + delta, MIN_SCALE, MAX_SCALE);
            if (newScale == currentScale) return;

            // 计算鼠标在当前图像上的归一化坐标
            double mouseX = event.getX();
            double mouseY = event.getY();
            double normX = mouseX / contentPane.getPrefWidth() / currentScale;
            double normY = mouseY / contentPane.getPrefHeight() / currentScale;

            // 应用新缩放比例
            currentScale = newScale;
            xRayImage.setFitWidth(contentPane.getPrefWidth() * currentScale);
            xRayImage.setFitHeight(contentPane.getPrefHeight() * currentScale);
            contentPane.setPrefSize(contentPane.getPrefWidth() * currentScale, contentPane.getPrefHeight() * currentScale);

            // 调整滚动位置,保证鼠标指向的图像位置不变
            double newMouseX = normX * contentPane.getPrefWidth();
            double newMouseY = normY * contentPane.getPrefHeight();
            xRayScrollPane.setHvalue(xRayScrollPane.getHvalue() + (newMouseX - mouseX) / (contentPane.getPrefWidth() - xRayScrollPane.getViewportBounds().getWidth()));
            xRayScrollPane.setVvalue(xRayScrollPane.getVvalue() + (newMouseY - mouseY) / (contentPane.getPrefHeight() - xRayScrollPane.getViewportBounds().getHeight()));

            // 更新所有标记点位位置和逆缩放
            updateAllMarkPoints();
        });

        // 2. 拖拽平移事件
        contentPane.setOnMousePressed(event -> {
            if (event.getButton() == MouseButton.SECONDARY) { // 右键拖动平移
                dragStartX = event.getSceneX();
                dragStartY = event.getSceneY();
                scrollStartH = xRayScrollPane.getHvalue();
                scrollStartV = xRayScrollPane.getVvalue();
                event.consume();
            }
        });

        contentPane.setOnMouseDragged(event -> {
            if (event.getButton() == MouseButton.SECONDARY) {
                double deltaX = (event.getSceneX() - dragStartX) / contentPane.getPrefWidth();
                double deltaY = (event.getSceneY() - dragStartY) / contentPane.getPrefHeight();
                xRayScrollPane.setHvalue(Math.clamp(scrollStartH - deltaX, 0, 1));
                xRayScrollPane.setVvalue(Math.clamp(scrollStartV - deltaY, 0, 1));
                event.consume();
            }
        });

        // 3. 点击添加标记点
        xRayImage.setOnMouseClicked(event -> {
            if(event.getButton().equals(MouseButton.PRIMARY)) {
                // 计算归一化坐标
                double normX = event.getX() / xRayImage.getBoundsInLocal().getWidth();
                double normY = event.getY() / xRayImage.getBoundsInLocal().getHeight();

                // 创建标记元素
                Circle circle = new Circle();
                circle.setRadius(2);
                Label label = new Label("A");

                // 添加到容器
                contentPane.getChildren().addAll(circle, label);
                MarkPoint point = new MarkPoint(normX, normY, circle, label);
                markPoints.add(point);

                // 刷新点位位置
                updateMarkPoint(point);
            }
        });
    }

    // 更新单个标记点位的位置和大小
    private void updateMarkPoint(MarkPoint point) {
        double x = point.normX * xRayImage.getFitWidth();
        double y = point.normY * xRayImage.getFitHeight();
        point.circle.setCenterX(x);
        point.circle.setCenterY(y);
        point.label.setLayoutX(x + 5);
        point.label.setLayoutY(y);
        // 逆缩放保证大小固定
        point.circle.setScaleX(1 / currentScale);
        point.circle.setScaleY(1 / currentScale);
        point.label.setScaleX(1 / currentScale);
        point.label.setScaleY(1 / currentScale);
    }

    // 更新所有标记点位
    private void updateAllMarkPoints() {
        for (MarkPoint point : markPoints) {
            updateMarkPoint(point);
        }
    }
}

关键逻辑说明

  • 所有点位都存储相对于原图的归一化坐标,不受缩放平移影响,永远对应正确的解剖位置
  • 缩放时对标记元素应用1/当前缩放比例的逆缩放,保证圆圈和标签视觉大小固定,不会随图像放大
  • 缩放时自动调整滚动条位置,实现鼠标指向位置为中心的缩放效果
  • 右键拖拽实现图像平移,所有点位跟随图像同步移动无偏移

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 20:09:03