How to draw an arrow using Java2D?

8

I'm trying to draw an arrow inside a circle (similar to a clock hand), but I can not align the tip of the arrow with the rest of the line.

I made the this "arrow" based on this SOEn response, but I can not get it to be properly positioned with the line I draw.

The tip of the arrow is to the left of the line, as follows in the image:

Follow my class LineArrow :

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.geom.AffineTransform;

public class LineArrow {

    int x;
    int y;
    int endX;
    int endY;
    Color color;
    int thickness;

    public LineArrow(int x, int y, int x2, int y2, Color color, int thickness) {
        super();
        this.x = x;
        this.y = y;
        this.endX = x2;
        this.endY = y2;

        this.color = color;
        this.thickness = thickness;
    }

    public void draw(Graphics g) {
        Graphics2D g2 = (Graphics2D) g.create();

        g2.setColor(color);
        g2.setStroke(new BasicStroke(thickness));
        g2.drawLine(x, y, endX, endY);;
        drawArrowHead(g2);
        g2.dispose();
    }

    private void drawArrowHead(Graphics2D g2) {

        Polygon arrowHead = new Polygon();
        AffineTransform tx = new AffineTransform();
        arrowHead.addPoint(0, 5);
        arrowHead.addPoint(-5, -5);
        arrowHead.addPoint(5, -5);

        tx.setToIdentity();
        double angle = Math.atan2(endY - y, endX - x);
        tx.translate(endX, endY);
        tx.rotate(angle - Math.PI / 2d);

        g2.setTransform(tx);
        g2.fill(arrowHead);
    }

}
  

Note: I did not add circle drawing code because the above class is self-sufficient to simulate the image problem.

Here's an example:

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.geom.AffineTransform;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;

public class LineArrowTest extends JFrame {

    private static final long serialVersionUID = 1L;
    private JPanel contentPane;
    private JPanel DrawPanel;

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            new LineArrowTest().setVisible(true);
        });
    }

    public LineArrowTest() {
        initComponents();
        pack();
    }

    private void initComponents() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setPreferredSize(new Dimension(400, 300));
        this.contentPane = new JPanel(new BorderLayout(0, 0));
        this.contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        setContentPane(this.contentPane);

        this.DrawPanel = new JPanel() {

            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                LineArrow line = new LineArrow(getWidth() / 2, getHeight() / 2, getWidth() / 2, getHeight(),
                        Color.black, 3);
                line.draw(g);
            }
        };
        this.contentPane.add(this.DrawPanel, BorderLayout.CENTER);
    }

    class LineArrow {

        int x;
        int y;
        int endX;
        int endY;
        Color color;
        int thickness;

        public LineArrow(int x, int y, int x2, int y2, Color color, int thickness) {
            super();
            this.x = x;
            this.y = y;
            this.endX = x2;
            this.endY = y2;

            this.color = color;
            this.thickness = thickness;
        }

        public void draw(Graphics g) {
            Graphics2D g2 = (Graphics2D) g.create();

            g2.setColor(color);
            g2.setStroke(new BasicStroke(thickness));
            g2.drawLine(x, y, endX, endY);
            ;
            drawArrowHead(g2);
            g2.dispose();
        }

        private void drawArrowHead(Graphics2D g2) {

            Polygon arrowHead = new Polygon();
            AffineTransform tx = new AffineTransform();

            arrowHead.addPoint(0, 5);
            arrowHead.addPoint(-5, -5);
            arrowHead.addPoint(5, -5);

            tx.setToIdentity();
            double angle = Math.atan2(endY - y, endX - x);
            tx.translate(endX, endY);
            tx.rotate(angle - Math.PI / 2d);

            g2.setTransform(tx);
            g2.fill(arrowHead);
        }

    }

}
    
asked by anonymous 09.10.2017 / 16:30

1 answer

3

I made some changes to your code:

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Polygon;
import java.awt.geom.AffineTransform;
import javax.swing.BorderFactory;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class LineArrowTest extends JFrame {

    private static final long serialVersionUID = 1L;

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> new LineArrowTest().setVisible(true));
    }

    public LineArrowTest() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setPreferredSize(new Dimension(400, 300));
        JPanel contentPane = new JPanel(new BorderLayout(0, 0));
        contentPane.setBorder(BorderFactory.createLineBorder(Color.YELLOW, 5));
        setContentPane(contentPane);

        JPanel drawPanel = new JPanel() {

            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                g.setColor(Color.PINK);
                g.drawRect(0, 0, this.getWidth() - 1, this.getHeight() - 1);
                Insets insets = getInsets();
                LineArrow line1 = new LineArrow(this.getWidth() / 2, this.getHeight() / 2, this.getWidth() / 2, this.getHeight(), Color.BLACK, 3);
                line1.draw(g);
                LineArrow line2 = new LineArrow(20, 40, 60, 80, Color.RED, 3);
                line2.draw(g);
                LineArrow line3 = new LineArrow(0, 0, this.getWidth(), this.getHeight(), Color.GREEN, 3);
                line3.draw(g);
                LineArrow line4 = new LineArrow(this.getWidth(), 0, 0, this.getHeight(), Color.MAGENTA, 3);
                line4.draw(g);
                LineArrow line5 = new LineArrow((insets.right + insets.left) / 2, (insets.top + insets.bottom) / 2, 140, 170, Color.BLUE, 3);
                line5.draw(g);
                LineArrow line6 = new LineArrow(this.getWidth() / 2, this.getHeight() / 2, this.getWidth(), this.getHeight() / 2, Color.CYAN, 3);
                line6.draw(g);
            }
        };
        contentPane.add(drawPanel, BorderLayout.CENTER);
        pack();
    }

    private static final Polygon ARROW_HEAD = new Polygon();

    static {
        ARROW_HEAD.addPoint(0, 0);
        ARROW_HEAD.addPoint(-5, -10);
        ARROW_HEAD.addPoint(5, -10);
    }

    public static class LineArrow {

        private final int x;
        private final int y;
        private final int endX;
        private final int endY;
        private final Color color;
        private final int thickness;

        public LineArrow(int x, int y, int x2, int y2, Color color, int thickness) {
            super();
            this.x = x;
            this.y = y;
            this.endX = x2;
            this.endY = y2;
            this.color = color;
            this.thickness = thickness;
        }

        public void draw(Graphics g) {
            Graphics2D g2 = (Graphics2D) g;

            // Calcula o ângulo da seta.
            double angle = Math.atan2(endY - y, endX - x);

            g2.setColor(color);
            g2.setStroke(new BasicStroke(thickness));

            // Desenha a linha. Corta 10 pixels na ponta para a ponta não ficar grossa.
            g2.drawLine(x, y, (int) (endX - 10 * Math.cos(angle)), (int) (endY - 10 * Math.sin(angle)));

            // Obtém o AffineTransform original.
            AffineTransform tx1 = g2.getTransform();

            // Cria uma cópia do AffineTransform.
            AffineTransform tx2 = (AffineTransform) tx1.clone();

            // Translada e rotaciona o novo AffineTransform.
            tx2.translate(endX, endY);
            tx2.rotate(angle - Math.PI / 2);

            // Desenha a ponta com o AffineTransform transladado e rotacionado.
            g2.setTransform(tx2);
            g2.fill(ARROW_HEAD);

            // Restaura o AffineTransform original.
            g2.setTransform(tx1);
        }
    }
}

Here's how it went:

Resizeworksasexpected:

Your problem was that by adding the border to contentPane , everything started to get wrong because the border would interfere with the drawing result.

What I did to solve was basically this:

  • I've added more rows to test better.

  • I changed the invisible border to a yellow border.

  • I drew a pink rectangle to clearly delimit drawPanel .

  • Do not neglect the AffineTransform that already comes in Graphics by resetting it with setToIdentity() . Instead, save the original AffineTransform , create a new AffineTransform as a copy, translate and rotate it, draw based on this AffineTransform after putting it in Graphics2D and place back to the original% .

  • You do not have to abuse AffineTransform and create() .

  • Your triangle is wrong. The first tip has a Y = 5 coordinate. This means that it will exceed its target by 5 pixels. The tip has to have a Y = 0. Because of this, I have changed the Y of the other two vertices from -5 to -10.

  • I drew the line 10 pixels shorter. The reason for this is because it has a considerable thickness, while the arrow has a sharp point. If you draw the line at full length, the arrow would stick with a thick tip instead the line would be drawn to its end. So the solution is to decrease 10 pixels of the line length since the arrow travels the last 10 pixels in length. Since the line may not be perfectly horizontal or vertical, I use the sine and cosine to know how much to cut in X and Y, from the angle you already calculate with dispose() .

    Other simple standardization changes - put atan2 in the fields, make the inner class be private final , use static instead of DrawPanel , embed the drawPanel in the constructor, leave the triangle of the tip as an immutable and reusable polygon, etc.

09.10.2017 / 20:27