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

使用AffineTransform绘制箭头时位置与角度异常问题排查

问题原因与修复方案

核心问题分析

  • AffineTransform 变换顺序搞反:AffineTransform 的变换是逆序执行的,比如代码中先调用 translate 再 rotate,实际生效顺序是先旋转再平移,和你预期的操作顺序完全相反。你原本想先平移到起点再旋转,但实际是先旋转再平移,导致位置偏移。
  • 箭头头的变换叠加错误:你复用了同一个 AT 对象来处理箭头头,导致箭头头的变换包含了之前箭杆的平移+旋转,再叠加后续的平移、旋转、剪切,最终变换完全混乱。
  • 箭头头锚点与变换顺序错误:你对箭头头的剪切、旋转顺序不符合预期,且没有将箭头头的基准点对齐到箭杆末端。
  • 箭头长度计算偏差:arrowheadLength 的计算逻辑有误,导致箭杆长度和箭头头的实际尺寸不匹配。

修复后的代码

import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;

class CanvasPane extends JPanel {
    Rectangle start = new Rectangle();
    Line2D.Double arrowShaft = new Line2D.Double();
    Rectangle arrowhead = new Rectangle(new Dimension(16, 16));
    double shearMultiplier = 0.5;
    // 修正箭头头长度计算:直接用矩形宽度乘以(1+剪切系数),对应剪切后的箭头尾部宽度
    double arrowheadLength = arrowhead.width * (1 + shearMultiplier);
    MouseEvent me;
    double length, angle;

    CanvasPane() {
        setPreferredSize(new Dimension(1000, 600));

        addMouseMotionListener(new MouseInputAdapter() {
            @Override
            public void mouseMoved(MouseEvent e) {
                me = e;
                repaint();
            }
        });
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        start.setBounds(getWidth()/2 - 5, getHeight()/2 - 5, 10, 10);
        Graphics2D g2d = (Graphics2D) g;
        g2d.fill(start);

        if (me != null) {
            length = Point.distance(start.getCenterX(), start.getCenterY(), me.getX(), me.getY());
            // 确保长度至少能容纳箭头头,避免负数
            if (length < arrowheadLength) {
                length = arrowheadLength;
            }
            angle = Math.atan2(me.getY() - start.getCenterY(), me.getX() - start.getCenterX());

            AffineTransform storedTransform = g2d.getTransform();

            // 绘制箭杆:先旋转,再平移到起点(变换顺序逆序)
            arrowShaft.setLine(0, 0, length - arrowheadLength, 0);
            AffineTransform shaftTransform = new AffineTransform();
            shaftTransform.rotate(angle);
            shaftTransform.translate(start.getCenterX(), start.getCenterY());
            g2d.transform(shaftTransform);
            g2d.draw(arrowShaft);

            // 重置变换,单独处理箭头头
            g2d.setTransform(storedTransform);

            // 绘制箭头头:变换顺序为:剪切 → 旋转-45° → 平移到箭杆末端 → 整体旋转到目标角度 → 平移到起点
            // 注意AffineTransform的逆序:代码写的顺序是最后执行的先调用
            AffineTransform headTransform = new AffineTransform();
            // 最后一步:平移到起点
            headTransform.translate(start.getCenterX(), start.getCenterY());
            // 倒数第二步:整体旋转到目标角度
            headTransform.rotate(angle);
            // 倒数第三步:平移到箭杆末端
            headTransform.translate(length - arrowheadLength, 0);
            // 倒数第四步:旋转-45°(让剪切后的矩形变成箭头形状)
            headTransform.rotate(Math.toRadians(-45));
            // 第一步:剪切矩形,形成箭头的斜边
            headTransform.shear(shearMultiplier, 0); // 只做水平剪切,避免垂直方向变形

            g2d.transform(headTransform);
            // 调整箭头头的位置,让其左上角对齐到变换原点(即箭杆末端)
            g2d.draw(new Rectangle(0, -arrowhead.height/2, arrowhead.width, arrowhead.height));

            g2d.setTransform(storedTransform);
        }
    }
}

public class DrawLine {
    Container createContentPane() {
        JPanel contentPane = new JPanel(new BorderLayout());
        contentPane.setOpaque(true);
        contentPane.add(new CanvasPane(), BorderLayout.CENTER);
        return contentPane;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

            DrawLine drawLine = new DrawLine();
            frame.setContentPane(drawLine.createContentPane());

            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

关键修复点说明

  1. 修正变换顺序:所有变换按照实际想要的逆序调用,比如要实现「先平移到起点,再旋转」,代码里先调用rotate再调用translate。
  2. 分离箭杆与箭头头的变换:分别创建独立的AffineTransform对象处理箭杆和箭头头,避免变换叠加污染。
  3. 调整箭头头锚点:将箭头头矩形的垂直中心对齐到箭杆的轴线,避免箭头上下偏移,同时只做水平剪切(shear(shearMultiplier, 0)),让箭头形状更符合预期。
  4. 修正箭头长度计算:简化arrowheadLength的计算,确保箭杆长度加上箭头头长度等于鼠标到起点的距离。
  5. 边界处理:添加长度判断,当鼠标距离过近时避免箭杆长度为负数。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 23:00:01