How to draw an arrow using Java2d?

Asked

Viewed 612 times

7

I’m trying to draw an arrow inside a circle (similar to a clock pointer), but I’m not able to align the arrowhead with the rest of the line.

I made the "arrow" based in this reply by Soen, but I’m not getting it positioned properly with the line I draw.

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

Screenshot do relógio

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 class above is self-sufficient to simulate the image problem.


Follow 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);
        }

    }

}
  • I can try an approach using rotation matrix... only for later

  • 4

    Post also the class that makes the JFrame, so that I (or anyone else who will try to answer) don’t have to recode it and also leave your MCVE question.

  • By the way, I did a quick test drawing the arrow inside a JPanel (without the red circle and the button) and it was drawn correctly. I did not change a comma from your code. I tested with arrow of various sizes and to various directions and worked with all. So post what’s in the rest of the code because your problem must be somewhere else.

  • @Victorstafusa posted.

  • The edge of the component is mocking its AffineTransform. That’s why I didn’t find the problem when I did a quick test as I didn’t put edge. However, I still haven’t found the solution.

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);
        }
    }
}

This is how it turned out:

Screenshot 1

Resize works as expected:

Screenshot 2

His problem was that by adding the edge to the contentPane, everything started to get wrong because the edge interfered with the result of the drawing.

What I did to fix was basically this:

  • I added more lines to test better.

  • I changed the edge from invisible to a yellow edge.

  • I drew a pink rectangle to clearly delimit the drawPanel.

  • Do not despise the AffineTransform which is already in the Graphics resetting him with the setToIdentity(). Instead, save the AffineTransform original, create another AffineTransform new as copy, make in it translation and rotation, draw based on this AffineTransform after putting it on Graphics2D and put back the AffineTransform original.

  • There’s no reason to abuse create() and dispose().

  • Your triangle is wrong. The first tip has a Y = 5 coordinate. This means it will overtake your target by 5 pixels. The tip has to have a Y = 0. Because of this, I 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 tip. If you draw the line at full length, the arrow would have a thick tip as the line would be drawn to its end. Therefore, the solution is to decrease 10 pixels of the line length once the arrow travels these last 10 pixels of length. As the line may not be perfectly horizontal or vertical, I use the sine and the cosine to know how much should be cut in X and Y, from the angle you already calculate with the atan2.

  • Other simple standardization changes - put private final in the fields, make the inner class static, use DrawPanel instead of drawPanel, incorporate the initComponents() in the builder, leave the tip triangle as an immutable and reusable polygon, etc.

  • I took this excerpt from the answer, I didn’t even know that this detail of the triangle would make a difference. But I didn’t understand those drawline formulas to shorten the size.

  • Another question, for arrow size to be relative to line thickness, I can do tx2.scale(thickness) or it would not be enough to maintain the proportion?

  • @Article If I have a diagonal line at an angle x, I multiply the co-sine of x by 10 pixels and I have the measure of how those 10 pixels on the diagonal correspond to horizontal pixels. When doing this also with the sine I have how much they correspond vertically. Let’s assume that the angle x is 30º. In this case cos(30º) = 0.866 and sen(30º) = 0.5. Multiplying both by 10 pixels, I have 8 pixels horizontally and 5 vertically. That’s what I subtract from the line length.

  • @Articuno If you do tx2.scale(thickness), you will draw the tip with the triple size in all directions, since the thickness is 3.

  • @Articuno I did a test with the tx2.scale(thickness, thickness); and with the thickness varying from one line to another. The effect turned out pretty cool, but I don’t think that’s what you want.

  • I tested it here, it looks giant even. I ended up adopting tx2.scale(thickness/2, thickness/2);, At least the arrow is more proportional to the size. As for the formula, I’m terrible at kkkk geometry I’ll read your comment more calmly later while compiling the code to understand it better.

  • @Articuno Faça tx2.scale(thickness / 2.0, thickness / 2.0); - Because without that .0, will be whole division and not give what you want. And multiply that 10 which appears together with the sine and the co-sine by thickness / 2.0.

Show 2 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.