Can I tell the JPanel paintComponent(Graphics g) function to only repaint the object I'm editing?

38 Views Asked by At

I'm writing an app in java using a JPanel to draw maps, I've used Landmass as a class, and the workspace (extends JPanel) uses multiple landmasses to represent each continent/island.

currently, the paintComponent runs a for loop over every Landmass every time I repaint(), which is everytime the mouse is moved. This seems inefficient as most Landmasses aren't changing, and is causing my program to slow down when too many Landmass objects are added. I want the landmasses to still be displayed by the graphics, I'm looking for a way to not redraw the ones I haven't edited.

protected void paintComponent(Graphics g)
{
   super.paintComponent(g);
   Graphics2D g2= (Graphics2D) g;
            zoom(g2);
            
   int radius = zoomHandler.divZ(this.radius);
   graphicsHandler.setRadius(radius);

            ...
            
   if(!landmasses.isEmpty()) 
    for(int i = 0; i < landmasses.size(); i++)
         graphicsHandler.drawLandmass(g2, landmasses.get(i), i);
}

Where landmasses is the ArrayList<Landmass> that I'm using to store every Landmass.

To be clear, I never directly call the paintComponent, I only ever use repaint();

If anyone was wondering drawLandmas does:

public void drawLandmass(Graphics2D g, Landmass l, int index) {
        int[] xPoints = new int[l.nodes.size()]; //setup two integer arrays to store the x and y coordinates of each point
        int[] yPoints = new int[l.nodes.size()];
        for(int j = 0; j < l.nodes.size(); j++)//and assign each of the arrays with the points we take from the nodes
        {
            xPoints[j] = l.nodes.get(j).x();
            yPoints[j] = l.nodes.get(j).y();
        }
        fillLandmass(l, xPoints, yPoints,g);
        if(l.getIsSelected()) {
            traceLandmass(l, xPoints, yPoints, g);
            boxLandmass(l, w.zoomHandler, g);
        }
    }
    
    public void fillLandmass(Landmass l, int[] x, int[] y, Graphics2D g) {
        g.setColor(l.getColor());
        g.fillPolygon(x, y, l.nodes.size());
    
        
    }
    
    public void traceLandmass(Landmass l, int[] x, int[] y, Graphics2D g) {
        g.setColor(Color.RED);
        g.drawPolygon(x,y,l.nodes.size());
        for(int j = 0; j < l.nodes.size(); j++)//and assign each of the arrays with the points we take from the nodes
        {
            if(l.getSelectedNode() == j)
                circle(g, l.nodes.get(j), "fill", radius,g.getColor());
            else
                circle(g, l.nodes.get(j), "draw", radius,g.getColor());
        }
    }
    
    public void boxLandmass(Landmass l, ZoomHandler z, Graphics2D g) {
        g.setColor(Color.YELLOW);
        g.drawRect(l.prox[0] - radius, l.prox[1] - radius,
                l.prox[2]-l.prox[0] + 2*radius, 
                l.prox[3]-l.prox[1] + 2*radius);
        g.fillRect(l.prox[0] - z.divZ(3) - radius, l.prox[1] - z.divZ(3) - radius, 2*z.divZ(3), 2*z.divZ(3));
        g.fillRect(l.prox[2] - z.divZ(3) + radius, l.prox[1] - z.divZ(3) - radius, 2*z.divZ(3), 2*z.divZ(3));
        g.fillRect(l.prox[2] - z.divZ(3) + radius, l.prox[3] - z.divZ(3) + radius, 2*z.divZ(3), 2*z.divZ(3));
        g.fillRect(l.prox[0] - z.divZ(3) - radius, l.prox[3] - z.divZ(3) + radius, 2*z.divZ(3), 2*z.divZ(3));
    
        if(l.centreNodeVisible)
            circle(g,l.centreNode,"draw",radius, g.getColor());
    }

I obviously can't just use an if(landmasses.get(i).isEdited()) statement to only draw landmasses which have been edited, and the unedited ones won't be shown.

I tried researching the paintComponent, but I couldn't find anything there that was helpful.

1

There are 1 best solutions below

0
Progman On

As mentioned in the technical details Painting in AWT and Swing, the Graphics object has a "clip rectangle" property, which indicates which area should be redrawn:

  • The Graphics object's clip rectangle is set to the area of the component that is in need of repainting.

It is used/set when you call the repaint() method with the integer arguments to specify the region you need to repaint. So when you call

panel.repaint(10, 20, 30, 40); // rectangle of 30x40 at pos (10, 20)

the paint*() methods get called with a Graphics object, where getClip() will return a Shape/Rectangle with these coordinates (or the coordinates related to that GUI component). You can use this information to (re)draw only the parts in that area. But this also means that you have to call repaint() on the area that has been changed. If you call repaint() without arguments, you will give the Swing/AWT drawing library no information whatsoever what has been "changed" and what is "unchanged", resulting in repainting everything (because that's what you are requesting).

See the following prototype to show the effect of repaint(...); and how the paintComponent() method is called:

public class Testing extends JPanel {
    
    private int paintCounter = 0;
    
    private Color[] colors = new Color[] {
            Color.GREEN,
            Color.RED,
            Color.BLUE,
            Color.YELLOW,
            Color.ORANGE,
            Color.PINK
    };
    @Override
    protected void paintComponent(Graphics g)
    {
        g.setColor(colors[paintCounter++%colors.length]);
        Rectangle r = (Rectangle)g.getClip();
        System.out.println(r);
        if (r.getX() == 0) {
            g.setColor(Color.WHITE);
        }
        g.fillRect((int)r.getX(), (int)r.getY(), (int)r.getWidth(), (int)r.getHeight());
    }
    
    public static void main(String[] args) throws Exception {
        JFrame frame = new JFrame("Testing");
        Testing panel = new Testing();
        SwingUtilities.invokeLater(() -> {
            frame.add(panel, BorderLayout.CENTER);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setPreferredSize(new Dimension(400, 500));
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);         
        });
        
        Thread t = new Thread(() -> {
            Random r = new Random();
            for (int i=0; i<10; i++) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    return;
                }
                System.out.println("Going to repaint step "+i);
                SwingUtilities.invokeLater(() -> {
                    int x = r.nextInt(100)+10;
                    int y = r.nextInt(100)+100;
                    int width = r.nextInt(100)+100;
                    int height = r.nextInt(100)+100;
                    System.out.println("Repaint at: x="+x+", y="+y+", width="+width+", height="+height);
                    frame.repaint(x, y, width, height);
                });
            }
        });
        t.start();
    }    
}

This will create a window like this:

JFrame screenshot

The debug output looks as follow:

java.awt.Rectangle[x=0,y=0,width=390,height=470]
java.awt.Rectangle[x=0,y=0,width=1,height=1]
java.awt.Rectangle[x=0,y=0,width=390,height=470]
Going to repaint step 0
Repaint at: x=60, y=150, width=108, height=191
java.awt.Rectangle[x=56,y=117,width=108,height=191]
Going to repaint step 1
Repaint at: x=58, y=161, width=171, height=112
java.awt.Rectangle[x=54,y=128,width=171,height=112]
Going to repaint step 2
Repaint at: x=91, y=195, width=108, height=153
java.awt.Rectangle[x=87,y=162,width=108,height=153]
Going to repaint step 3
Repaint at: x=96, y=186, width=109, height=117
java.awt.Rectangle[x=92,y=153,width=109,height=117]
Going to repaint step 4
Repaint at: x=81, y=194, width=170, height=128
java.awt.Rectangle[x=77,y=161,width=170,height=128]
Going to repaint step 5
Repaint at: x=11, y=168, width=114, height=101
java.awt.Rectangle[x=7,y=135,width=114,height=101]
Going to repaint step 6
Repaint at: x=96, y=151, width=137, height=199
java.awt.Rectangle[x=92,y=118,width=137,height=199]
Going to repaint step 7
Repaint at: x=99, y=167, width=127, height=130
java.awt.Rectangle[x=95,y=134,width=127,height=130]
Going to repaint step 8
Repaint at: x=23, y=107, width=167, height=118
java.awt.Rectangle[x=19,y=74,width=167,height=118]
Going to repaint step 9
Repaint at: x=42, y=155, width=167, height=103
java.awt.Rectangle[x=38,y=122,width=167,height=103]

As you see in the output, the paintComponent() method is called with a rectangle of the whole JPanel size first, since in the beginning it has to been drawn fully at least once. After that only the specific part should be drawn and the paintComponent() method does that.

However, keep in mind that the drawing system of Swing/AWT will decide really quick that is must redraw everything. GUI actions like focus switch or resize can result in the Swing/AWT system thinking "Well, what I am currently showing is not current anymore, I have to redraw everything" and calls repaint() without any limits.