Search code examples
javaswingawtmouselistener

Difficulty capturing fast mouse movements in Java Swing paint app


I'm currently developing my first project, a simple paint application using Java Swing and AWT. While implementing the painting functionality, I encountered an issue with accurately capturing mouse movements, especially when moving the mouse quickly.

I've designed the application to update the drawing coordinates in response to mouse events (mouseDragged and mouseMoved methods in the PaintPanel class), triggering repaints to render the drawings. However, despite my efforts, I've noticed that fast mouse movements sometimes result in skipped points, leading to gaps in the drawn lines.

Here's my PaintPanel class, which manages the painting functionality:

public class PaintPanel extends JPanel implements MouseMotionListener{
    public Point mouseCoordinates;
    boolean painting = false;
    
    
    public PaintPanel() {
        this.setPreferredSize(new Dimension(1000,550));
        this.setBackground(Color.white);
        this.addMouseMotionListener(this);
        
        
        
        
    }
    public void paintComponent(Graphics g) {
        Graphics2D g2D = (Graphics2D) g;
        g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    
        if(painting == false) {
        super.paintComponent(g2D);
        }
        if(mouseCoordinates != null) {
            g2D.setColor(UtilePanel.col);
            g2D.fillOval((int)mouseCoordinates.getX(),(int)mouseCoordinates.getY(),UtilePanel.brushSize, UtilePanel.brushSize);
             this.setCursor( this.getToolkit().createCustomCursor(
                       new BufferedImage( 1, 1, BufferedImage.TYPE_INT_ARGB ),
                       new Point(),
                       null ) );
        }
    }
    @Override
    public void mouseDragged(MouseEvent e) {
        mouseCoordinates = e.getPoint();
        painting = true;
        repaint();
    }
    @Override
    public void mouseMoved(MouseEvent e) {
        mouseCoordinates = e.getPoint();
        repaint();
    }
}

Here's the UtilePanel class if it help :

public class UtilePanel extends JPanel {
        static Color col=Color.black;
        static int brushSize = 5;
    
        public UtilePanel(){
            this.setPreferredSize(new Dimension (1000,150));
            JPanel container = new JPanel();
            container.setLayout(new GridLayout(1,0));
            this.setLayout(new GridLayout(0,1));
            Brushes brushes = new Brushes();
            Shapes shapes = new Shapes();
            LoadImage loadImage = new LoadImage();
            container.add(brushes);
            container.add(shapes);
            container.add(loadImage);
            this.add(new JPanel());
            this.add(container);
            this.add(new JPanel());
            setBorder(BorderFactory.createEtchedBorder(0));
        }
        public class Brushes extends JPanel{
            JRadioButton Size1;
            JRadioButton Size2;
            JRadioButton Size3;
            JRadioButton Size4;
            ButtonGroup  group;
            JButton color;
            
            public Brushes() {
                
                Size1 = new JRadioButton();
                Size2 = new JRadioButton();
                Size3 = new JRadioButton();
                Size4 = new JRadioButton();
                group = new ButtonGroup();
                color = new JButton();
                color.setBackground(col);
                color.setBorder(BorderFactory.createEtchedBorder(0));
                color.setPreferredSize(new Dimension(20,20));
                Size1.setSelected(true);
                JColorChooser  colorchooser= new JColorChooser();
                color.addActionListener(e->{
                    col = JColorChooser.showDialog(null, "Pick a color ",Color.black);
                    color.setBackground(col);
                });
                Size1.addActionListener(e->{
                    brushSize = 5;
                });
                Size2.addActionListener(e->{
                    brushSize = 10;
                });
                Size3.addActionListener(e->{
                    brushSize = 15;
                });
                Size4.addActionListener(e->{
                    brushSize = 20;
                });
                group.add(Size1);
                group.add(Size2);
                group.add(Size3);
                group.add(Size4);
                this.add(Size1);
                this.add(Size2);
                this.add(Size3);
                this.add(Size4);
                this.add(color);
                this.setLayout(new FlowLayout());
            }
        }
        public class Shapes extends JPanel{
            public Shapes() {
                ShapeButton  circule = new  ShapeButton ("circule");
                ShapeButton  rect = new  ShapeButton ("Rectangle");
                ShapeButton triangle = new  ShapeButton ("Tri");
                ShapeButton line = new  ShapeButton ("Line");
                this.setLayout(new FlowLayout());
                
                ButtonGroup bg = new ButtonGroup();
                bg.add(rect);
                bg.add(triangle);
                bg.add(circule);
                bg.add(line);
                this.add(circule);
                this.add(rect);
                this.add(triangle);
                this.add(line);
            }
            class ShapeButton extends JRadioButton {
                public ShapeButton(String s) {
                    setIcon((new ImageIcon(creatImage(new Color(0x00FFFFFF, true),s))));
                    setSelectedIcon(new ImageIcon(creatImage(Color.gray,s)));
                        
                    
                    
                }
            }
            
            public BufferedImage creatImage(Color color,String shape) {
                BufferedImage bi = new BufferedImage(40,40, BufferedImage.TYPE_INT_ARGB);
                Graphics2D g = (Graphics2D) bi.getGraphics();
                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g.setStroke(new BasicStroke(5));
                g.setColor(color);
                g.fillRect(0,0,40,40);
                
                g.setColor(Color.black);
                switch(shape) {
                    case "circule": 
                        g.drawOval(5, 5, 30,30);
                        break;
                    case "Rectangle":
                        g.drawRect(5,5,30,30);
                        break;
                    case "Tri":
                        int[] X= {5,16,30};
                        int[] Y= {30,5,30};
                        g.drawPolygon(X,Y,3);
                        break;
                    case "Line":
                        g.drawLine(5,30,30,5);
                        break;
                }
                
                
                
                //g.dispose();
                return bi;
            }
            
        }
        public class LoadImage extends JPanel{
            public LoadImage() {
                JButton loadButton = new JButton("Import Image");
                loadButton.setPreferredSize(new Dimension(100,50));
                JFileChooser f = new JFileChooser();
                loadButton.addActionListener(e->{
                    int resp = f.showOpenDialog(null);
                    if(resp == JFileChooser.APPROVE_OPTION) {
                        File file = new File(f.getSelectedFile().getAbsolutePath());
                        
                        System.out.println(file.getAbsolutePath());
                    }
                });
                this.add(loadButton);
            }
        }
}

Additionally, I attempted to incorporate a game loop to continuously poll for mouse input, hoping it would improve the accuracy of mouse movement capturing. However, even with the game loop in place, the problem persists.

I'm unsure if my approach to painting by omitting super.paintComponent(g) in paintComponent is the correct way or there's a better way to do it.

Could someone provide insights or suggestions on how to improve the mouse event capturing to guarantee precise rendering, especially during rapid mouse movements?

Your assistance would be greatly appreciated. Thank you!


Solution

  • You will want to:

    1. Create the BufferedImage first and outside of any painting method. The painting method should be lean and fast, and not hold code that might be expensive with regards to time or memory, since slowing it down unnecessarily has a great effect on the perceived responsiveness of your code.
    2. Draw lines that connect current point with prior point, not points.
    3. Always call the super.paintComponent(g) method within your override.

    For example:

    import java.awt.*;
    import java.awt.event.*;
    import java.awt.image.BufferedImage;
    import javax.swing.*;
    
    @SuppressWarnings("serial")
    public class PaintPanel02 extends JPanel {
        private static final float BRUSH_SIZE = 10f;
        private static final Stroke DRAWING_STROKE = new BasicStroke(BRUSH_SIZE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
        private static final int PREF_W = 800;
        private static final int PREF_H = 550;
        private BufferedImage image;
        private Color brushColor = null;
        private JButton clearButton = new JButton("Clear");
    
        public PaintPanel02() {
            setBackground(Color.white);
            MyMouseAdapter myMouseAdapter = new MyMouseAdapter();
            addMouseListener(myMouseAdapter);
            addMouseMotionListener(myMouseAdapter);
            image = new BufferedImage(PREF_W, PREF_H, BufferedImage.TYPE_INT_ARGB);
    
            clearButton.addActionListener(e -> {
                image = new BufferedImage(PREF_W, PREF_H, BufferedImage.TYPE_INT_ARGB);
                repaint();
            });
            add(clearButton);
        }
    
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(PREF_W, PREF_H);
        }
    
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2D = (Graphics2D) g;
            g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            if (image != null) {
                g2D.drawImage(image, 0, 0, this);
            }
        }
    
        private class MyMouseAdapter extends MouseAdapter {
            private Point p0 = null;
            private Point p1 = null;
    
            public void mousePressed(MouseEvent e) {
                p0 = e.getPoint();
    
                // let's create a random color for each curve that we draw
                float hue = (float) Math.random();
                float sat = (float) Math.random() / 2f + 0.5f;
                float bright = (float) Math.random() / 2f + 0.5f;
                brushColor = Color.getHSBColor(hue, sat, bright);
            }
    
            public void mouseReleased(MouseEvent e) {
                p0 = null;
            }
    
            @Override
            public void mouseDragged(MouseEvent e) {
                if (p0 == null) {
                    // if we're not drawing, get out of here
                    return;
                }
                p1 = e.getPoint();
                Graphics2D g2D = image.createGraphics();
                g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2D.setColor(brushColor);
                g2D.setStroke(DRAWING_STROKE);
                g2D.drawLine(p0.x, p0.y, p1.x, p1.y);
                g2D.dispose();
                p0 = p1;
                repaint();
            }
        }
    
        public static void main(String[] args) {
            JFrame frame = new JFrame("PaintPanel02");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new PaintPanel02());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
    }