Search code examples
javaswingjpanelgraphics2d

Paint on a glass pane without repainting other components


Let's say I have a JPanel called content with its paintComponent(g) overridden. This panel usually paints very quickly, but sometimes (Clarification: the user generates a graphics object that can have thousands of points, which slows down the rendering. It needs to be this way and it's not part of my question) it may take a whole second to repaint due to all of the graphics stuff going on in it.

I want to paint a dot near the location of the mouse, but I want this to update quickly and not have to repaint content every time the mouse is moved.

What I tried to do was paint it on a glass pane, like this:

class LabelGlassPane extends JComponent {
    public LabelGlassPane(JApplet frame) {
        this.frame = frame;
    }
    public JApplet frame;
    public void paintComponent(Graphics g) {
        if(ell != null){
            g2.setColor(Vars.overDot);
            g2.fill(ell); //this is an ellipse field that was created in the mouesmoved() listener
        }
    }
}

My issue is that even though I now only update this glass pane when the mouse is moved, it's repainting all of the other components in the applet when I call repaint() on it.

How can I paint to this glass pane (or have another way of painting) without it painting the components below?

Thanks.

EDIT: Here is a simple example. jp should not update when the mouse is moved, but it is.

public class Glasspanetest extends JApplet implements Runnable{

public void init() {
    setLayout(new BorderLayout());
    GraphicsPanel jp = new GraphicsPanel();
    add(jp, BorderLayout.CENTER);
    glass = new LabelGlassPane(this);
    this.setGlassPane(glass);
    glass.setVisible(true);
}


class GraphicsPanel extends JPanel{
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        g.setColor(Color.red);
        g.fillOval(50, 50, 50, 50);
        System.err.println("Painting bottom panel");
    }
}

public LabelGlassPane glass = null;
public Ellipse2D.Double ell = null;

class LabelGlassPane extends JComponent {
    public LabelGlassPane(JApplet frame) {
        this.frame = frame;
        this.addMouseMotionListener(new MoveInfoListener());
    }
    public JApplet frame;
    public void paintComponent(Graphics g) {
        //g.setColor(Color.red);
        //Container root = frame.getRootPane();
        //          g.setColor(new Color(255,100,100,100));
        //          g.fillRect(100, 100, 500, 500);
        if(ell != null){
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g;
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g.setColor(Color.black);
            g2.fill(ell);
            System.out.println("Painted glass pane");
        }

        //rPaint(root,g);
    }
}

public class MoveInfoListener implements MouseMotionListener{
    public void mouseMoved(MouseEvent e) {
        ell = new Ellipse2D.Double(e.getX()-3, e.getY()-3, 6, 6);
        glass.repaint();
    }

    public void mouseDragged(MouseEvent arg0) {}
}

public void run() {}

}


Solution

  • The problem is that Swing has no way to know that it should only repaint a small part of your LabelGlassPane and possibly a small part or no part of your GraphicsPanel.

    This is due to the fact that you perform custom painting in your components and that, although you only paint a small area of your panel's, you could also very well paint over the entire panel.

    Now, as to why your GraphicsPanel gets repainted every time your LabelGlassPane is repainted, this is due to the fact that LabelGlassPane is not opaque, meaning that, in order to properly display not painted areas, it needs to paint whatever is "under" the glasspane.

    The combination of those 2 aspects forces Swing to paint entirely both panels everytime you call repaint.

    But there are ways to avoid this bad combination. For this to work, you need to make your painting methods more clever by:

    1. Only ask to repaint the rectangle that has been made dirty (ie, the union of the old ellipse and the new one)
    2. In your painting components, only do the repaint if the clip of the Graphics intersects the area where you plan to paint. If there are no intersection, then you can just skip the paint operation.

    Small demo code showing that:

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.Rectangle;
    import java.awt.RenderingHints;
    import java.awt.Shape;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseMotionListener;
    import java.awt.geom.Ellipse2D;
    import java.awt.geom.Ellipse2D.Double;
    
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.SwingUtilities;
    
    public class Glasspanetest extends JFrame {
    
        public JFrame init() {
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            setLayout(new BorderLayout());
            GraphicsPanel jp = new GraphicsPanel();
            add(jp, BorderLayout.CENTER);
            LabelGlassPane glass = new LabelGlassPane();
            this.setGlassPane(glass);
            glass.setVisible(true);
            setSize(400, 400);
            return this;
        }
    
        class GraphicsPanel extends JPanel {
    
            private Shape oval = new Ellipse2D.Double(50, 50, 50, 50);
    
            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                if (g.getClip().intersects(oval.getBounds2D())) {
                    g.setColor(Color.red);
                    ((Graphics2D) g).fill(oval);
                    System.out.println("Painting bottom panel");
                }
            }
        }
    
        public Ellipse2D.Double ell = null;
    
        class LabelGlassPane extends JComponent {
            public LabelGlassPane() {
                this.addMouseMotionListener(new MoveInfoListener());
            }
    
            public class MoveInfoListener implements MouseMotionListener {
                @Override
                public void mouseMoved(MouseEvent e) {
                    Ellipse2D.Double oldEll = ell;
                    ell = new Ellipse2D.Double(e.getX() - 3, e.getY() - 3, 6, 6);
                    LabelGlassPane.this.repaint(oldEll, ell);
                }
    
                @Override
                public void mouseDragged(MouseEvent arg0) {
                }
            }
    
            public void repaint(Double oldEll, Double ell) {
                Rectangle rect = ell.getBounds();
                if (oldEll != null) {
                    rect = rect.union(oldEll.getBounds());
                }
                // repaint(rect);
                repaint();
            }
    
            @Override
            public void paintComponent(Graphics g) {
                // g.setColor(Color.red);
                // Container root = frame.getRootPane();
                // g.setColor(new Color(255,100,100,100));
                // g.fillRect(100, 100, 500, 500);
                if (ell != null) {
                    super.paintComponent(g);
                    Graphics2D g2 = (Graphics2D) g;
                    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                    g.setColor(Color.black);
                    g2.fill(ell);
                    System.out.println("Painted glass pane");
                }
    
                // rPaint(root,g);
            }
    
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new Runnable() {
    
                @Override
                public void run() {
                    new Glasspanetest().init().setVisible(true);
                }
            });
        }
    
    }
    

    Note: this wouldn't be my favourite approach to your application, but it will work. Unfortunately, I don't have the full picture of your application to provide an alternate approach to this.