使用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); }); } }
关键修复点说明
- 修正变换顺序:所有变换按照实际想要的逆序调用,比如要实现「先平移到起点,再旋转」,代码里先调用
rotate再调用translate。 - 分离箭杆与箭头头的变换:分别创建独立的
AffineTransform对象处理箭杆和箭头头,避免变换叠加污染。 - 调整箭头头锚点:将箭头头矩形的垂直中心对齐到箭杆的轴线,避免箭头上下偏移,同时只做水平剪切(
shear(shearMultiplier, 0)),让箭头形状更符合预期。 - 修正箭头长度计算:简化
arrowheadLength的计算,确保箭杆长度加上箭头头长度等于鼠标到起点的距离。 - 边界处理:添加长度判断,当鼠标距离过近时避免箭杆长度为负数。
内容的提问来源于stack exchange,提问作者Jacky
相关产品推荐
相关产品推荐

