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
相关产品推荐
相关产品推荐

