Search code examples
javaswingjpaneljtextfieldjtextarea

JAVA - JTextArea has a fake visual location after translate its parent graphics


I'm trying to create a canvas(JPanel) that can have text Boxes(JTextArea) on it and I can also drag the canvas or the text boxes around. Now the dragging text boxes functions are ok. I can use setBounds() to set the location for the JTextArea.

However, when it comes to dragging the canvas, problems come. I use Graphics2D.transform to move the canvas. After the canvas is moving, there will be a visual text area and a hidden real text area. I have reset the bounds of JTextArea to fit the moving offset of the canvas but it doesn't work.

Below is an example,

the GIF of the problem

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;

/**
 * @author Zhijie Lan<p>
 * create date: 2020/11/25<p>
 **/
public class TranslateJPanel extends JFrame
{
    public TranslateJPanel() throws HeadlessException
    {
        Canvas canvas = new Canvas();
        add(canvas,BorderLayout.CENTER);

        this.setSize(1280, 720);     
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);   
        this.setLocationRelativeTo(null);                               
        this.setVisible(true);
    }

    public static void main(String[] args)
    {
        new TranslateJPanel();
    }
}

class Canvas extends JPanel
{
    private final JTextArea jTextArea;
    private int offset = 0;

    public Canvas()
    {
        setLayout(null);

        jTextArea = new JTextArea("hello!!!!!!!!!!!");

        jTextArea.setBounds(0,0,100,100);
        jTextArea.setBorder(BorderFactory.createLineBorder(Color.BLACK));
        add(jTextArea);

        this.addMouseMotionListener(new MouseMotionAdapter()
        {
            @Override
            public void mouseDragged(MouseEvent e)
            {
                super.mouseDragged(e);
                offset++;
                revalidate();
                repaint();
            }
        });

        this.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseClicked(MouseEvent e)
            {
                super.mouseClicked(e);
                System.out.println("M: "+ e.getX()+"  "+e.getY());
            }
        });

    }

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

        Graphics2D g2 = (Graphics2D) g;

        AffineTransform at = new AffineTransform();
        at.translate(offset, offset);
        g2.transform(at);

        jTextArea.setBounds(offset,offset,100,100);

        System.out.println(offset);
        System.out.println(jTextArea.getX()+"      "+jTextArea.getY());
        System.out.println("***************************");

    }
}

Solution

  • I suggest the following for draggable text areas:

    1. Do not override paintComponent or any painting method, don't use AffineTransforms or anything that moves pixels around. Instead move the components themselves.
    2. This might be one of the few times that I might use a null layout. Note that there are layout managers that can be used in its place (and I think Camickr has written code for this and may be able to give you a link to this).
    3. Never set the size of the JTextArea or even its preferred size.
    4. Instead, set the column and row properties.
    5. Add the JTextArea to a JScrollPane (to its viewport actually)
    6. Add a MouseListener and MouseMotionListener to the JScrollPane that moves it
    7. Add a MouseListener and MouseMotionListener to the JTextArea that forwards mouse actions to the JScrollPane that holds it, but be sure to change the MouseEvent's source property to reflect the JScrollPane, not the JTextArea.

    For example:

    import java.awt.BorderLayout;
    import java.awt.Component;
    import java.awt.Container;
    import java.awt.Dimension;
    import java.awt.Point;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    import javax.swing.*;
    
    @SuppressWarnings("serial")
    public class DraggingBoxesPanel extends JPanel {
        public DraggingBoxesPanel() {
            int w = 900;
            int h = 600;
            setPreferredSize(new Dimension(w, h));
    
            setLayout(null); // I usually avoid doing this
            add(createTextBox()); // add first "box" to this JPanel
        }
    
        // exposed method to allow adding a new "box" to GUI
        public void newBox() {
            add(createTextBox());
            revalidate(); // The JScrollPane requires this
            repaint();
        }
    
        private JComponent createTextBox() {
            int rows = 10; // jtextarea property
            int cols = 20; // jtextarea property
    
            // create text area and scroll pane
            JTextArea textArea = new JTextArea(rows, cols);
            JScrollPane scrollPane = new JScrollPane(textArea);
    
            // let scrollpane size itself
            scrollPane.setSize(scrollPane.getPreferredSize());
    
            // scrollpane's mouse listeners
            MyMouse myMouse = new MyMouse();
            scrollPane.addMouseListener(myMouse);
            scrollPane.addMouseMotionListener(myMouse);
    
            // mouse adapter that forwards mouse actions in
            // text area into its containing scrollpane
            MouseAdapter textAreaMouseAdapter = new MouseAdapter() {
                @Override
                public void mouseReleased(MouseEvent e) {
                    // make sure source is correct
                    e.setSource(scrollPane);
                    myMouse.mouseReleased(e);
                }
    
                @Override
                public void mousePressed(MouseEvent e) {
                    e.setSource(scrollPane);
                    myMouse.mousePressed(e);
                }
    
                @Override
                public void mouseDragged(MouseEvent e) {
                    e.setSource(scrollPane);
                    myMouse.mouseDragged(e);
                }
            };
    
            textArea.addMouseListener(textAreaMouseAdapter);
            textArea.addMouseMotionListener(textAreaMouseAdapter);
    
            return scrollPane;
        }
    
        // code that allows a component to be dragged
        private class MyMouse extends MouseAdapter {
            private Point mousePt1;
            private Point compPt1;
    
            @Override
            public void mousePressed(MouseEvent e) {
                mousePt1 = e.getLocationOnScreen();
                Component comp = (Component) e.getSource();
                compPt1 = comp.getLocation();
                
                Container container = comp.getParent();
                container.setComponentZOrder(comp, 0);
                container.revalidate();
                container.repaint();
            }
    
            @Override
            public void mouseDragged(MouseEvent e) {
                if (mousePt1 == null) {
                    return;
                }
                dragComponent(e);
            }
    
            @Override
            public void mouseReleased(MouseEvent e) {
                dragComponent(e);
                mousePt1 = null;
            }
    
            private void dragComponent(MouseEvent e) {
                Point mousePt2 = e.getLocationOnScreen();
                int x = compPt1.x + mousePt2.x - mousePt1.x;
                int y = compPt1.y + mousePt2.y - mousePt1.y;
                Component comp = (Component) e.getSource();
                comp.setLocation(x, y);
                
                Container container = comp.getParent();
                container.revalidate();
                container.repaint();
            }
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> createAndShowGui());
        }
    
        private static void createAndShowGui() {
            final DraggingBoxesPanel mainPanel = new DraggingBoxesPanel();
            JButton newBoxBtn = new JButton("New Box");
            newBoxBtn.addActionListener(e -> mainPanel.newBox());
            JPanel bottomPanel = new JPanel();
            bottomPanel.add(newBoxBtn);
    
            JFrame frame = new JFrame("DraggingBoxesPanel");
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            frame.add(mainPanel);
            frame.add(bottomPanel, BorderLayout.PAGE_END);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
    }