Damian
Damian

Reputation: 5561

java swing: in paintComponent method how to know what to repaint?

My component is bigger than the screen and parts of it are not shown (I will use scrollbars).
When I receive a call in paintComponent(g) how do I know what area should I paint?

Upvotes: 3

Views: 2090

Answers (3)

Marco13
Marco13

Reputation: 54611

You can find out the area that actually has to be painted by querying the clip bounds of the Graphics object.

The JavaDoc seems to be a bit out-dated for this method: It says, that it may return a null clip. However, this is obviously never the case (and other Swing classes also rely on the clip never being null!).

The follwing MCVE illustrates the difference between using a the clip or painting the whole component:

ClipDemo

It contains a JPanel with a size of 800x800 in a scroll pane. The panel paints a set of rectangles, and prints how many rectangles have been painted.

One can use the "Use clip bounds" checkbox to enable and disable using the clip. When the clip is used, only the visible area of the panel is repainted, and the number of rectangles is much lower. (Note that the test whether a rectangle has to be painted or not is rather simple here: It only performs an intersection test of the rectangle with the visible region. For a real application, one would directly use the clip bounds to find out which rectangles have to be painted).

This example also shows some of the tricky internals of scroll panes: When the blinking is switched off, and the scroll bars are moved, one can see that - although the whole visible area changes - only a tiny area actually has to be repainted (namely the area that has become visible due to the scrolling). The other part is simply moved as-it-is, by blitting the previous contents. This behavior can be modified with JViewport.html#setScrollMode.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class PaintRegionTest
{
    public static void main(String[] args) throws Exception
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final PaintRegionPanel paintRegionPanel = new PaintRegionPanel();
        paintRegionPanel.setPreferredSize(new Dimension(800, 800));

        final Timer timer = new Timer(1000, new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                paintRegionPanel.changeColor();
            }
        });
        timer.setInitialDelay(1000);
        timer.start();

        JScrollPane scrollPane = new JScrollPane(paintRegionPanel);
        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(scrollPane, BorderLayout.CENTER);

        JPanel controlPanel = new JPanel(new FlowLayout());

        final JCheckBox blinkCheckbox = new JCheckBox("Blink", true);
        blinkCheckbox.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (blinkCheckbox.isSelected())
                {
                    timer.start();
                }
                else
                {
                    timer.stop();
                }
            }
        });
        controlPanel.add(blinkCheckbox);


        final JCheckBox useClipCheckbox = new JCheckBox("Use clip bounds");
        useClipCheckbox.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                paintRegionPanel.setUseClipBounds(
                    useClipCheckbox.isSelected());
            }
        });
        controlPanel.add(useClipCheckbox);

        frame.getContentPane().add(controlPanel, BorderLayout.SOUTH);

        frame.setPreferredSize(new Dimension(400, 400));
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

class PaintRegionPanel extends JPanel
{
    private Color color = Color.BLACK;
    private boolean useClipBounds = false;

    void setUseClipBounds(boolean useClipBounds)
    {
        this.useClipBounds = useClipBounds; 
    }

    void changeColor()
    {
        if (color == Color.BLACK)
        {
            color = Color.RED;
        }
        else
        {
            color = Color.BLACK;
        }
        repaint();
    }

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

        g.setColor(color);

        Rectangle clipBounds = g.getClipBounds();
        Rectangle ownBounds = new Rectangle(0,0,getWidth(),getHeight());

        System.out.println("clipBounds: " + clipBounds);
        System.out.println(" ownBounds: " + ownBounds);

        Rectangle paintedRegion = null;
        if (useClipBounds)
        {
            System.out.println("Using clipBounds");
            paintedRegion = clipBounds;
        }
        else
        {
            System.out.println("Using ownBounds");
            paintedRegion = ownBounds;
        }

        int counter = 0;

        // This loop performs a a simple test see whether the objects 
        // have to be painted. In a real application, one would 
        // probably use the clip information to ONLY create the
        // rectangles that actually have to be painted:
        for (int x = 0; x < getWidth(); x += 20)
        {
            for (int y = 0; y < getHeight(); y += 20)
            {
                Rectangle r = new Rectangle(x + 5, y + 5, 10, 10);
                if (r.intersects(paintedRegion))
                {
                    g.fill(r);
                    counter++;
                }
            }
        }
        System.out.println("Painted "+counter+" rectangles ");
    }

}

An aside: For many application cases, such an "optimization" should hardly be necessary. The painted elements are intersected against the clip anyhow, so one will probably not gain much performance. When "preparing" the elements to be painted is computationally expensive, one can consider this as one option. (In the example, "preparing" refers to creating the Rectangle instance, but there may be more complicated patterns). But in these cases, there may also be more elegant and easier solutions than manually checking the clip bounds.

Upvotes: 1

Grief
Grief

Reputation: 2040

All answers are wrong. So I decided to answer the question despide the fact that the question is two years old.

I believe that the correct answer is calling g.getClipBounds() inside of paintComponent(Graphics g) method. It will return the rectangle in the control's coordinate system of the area which is invalidated and must be redrawn.

Upvotes: 0

David Kroukamp
David Kroukamp

Reputation: 36423

I'm not sure if this is what you mean, but the problem is you will have to call repaint() on the JScrollPane each time you get a call in paintComponent(Graphics g) of the JPanel or else updates on the JPanel will not be visible in the JScrollPane.

Also I see you want to use JScrollBar (or maybe you confused the terminology)? I'd recommend a JScrollPane

I made a small example which is a JPanel with a grid that will change its colour every 2 seconds (Red to black and vice versa). The JPanel/Grid is larger then the JScrollPane; regardless we have to call repaint() on the JScrollPane instance or else the grid wont change colour:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class Test {

    public static void main(String[] args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new Test().createAndShowUI();
            }
        });
    }

    private void createAndShowUI() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        initComponents(frame);
        frame.setPreferredSize(new Dimension(400, 400));
        frame.pack();
        frame.setVisible(true);
    }

    private void initComponents(JFrame frame) {
        JScrollPane jsp = new JScrollPane();
        jsp.setViewportView(new Panel(800, 800, jsp));
        frame.getContentPane().add(jsp);
    }
}

class Panel extends JPanel {

    private int across, down;
    private Panel.Tile[][] tiles;
    private Color color = Color.black;
    private final JScrollPane jScrollPane;

    public Panel(int width, int height, JScrollPane jScrollPane) {
        this.setPreferredSize(new Dimension(width, height));
        this.jScrollPane = jScrollPane;
        createTiles();
        changePanelColorTimer();//just something to do to check if its repaints fine
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (int i = 0; i < across; i++) {
            for (int j = 0; j < down; j++) {
                g.setColor(color);
                for (int k = 0; k < 5; k++) {
                    g.drawRect(tiles[i][j].x + k, tiles[i][j].y + k, tiles[i][j].side - k * 2, tiles[i][j].side - 2 * k);
                }
            }
        }
        updateScrollPane();//refresh the pane after every paint
    }

    //calls repaint on the scrollPane instance
    private void updateScrollPane() {
        jScrollPane.repaint();
    }

    private void createTiles() {
        across = 13;
        down = 9;
        tiles = new Panel.Tile[across][down];

        for (int i = 0; i < across; i++) {
            for (int j = 0; j < down; j++) {
                tiles[i][j] = new Panel.Tile((i * 50), (j * 50), 50);
            }
        }
    }

    //change the color of the grid lines from black to red and vice versa every 2s
    private void changePanelColorTimer() {
        Timer timer = new Timer(2000, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (color == Color.black) {
                    color = Color.red;
                } else {
                    color = Color.black;
                }
            }
        });
        timer.setInitialDelay(2000);
        timer.start();
    }

    private class Tile {

        int x, y, side;

        public Tile(int inX, int inY, int inSide) {
            x = inX;
            y = inY;
            side = inSide;
        }
    }
}

In the Panel class if we comment the line updateScrollPane(); in paintComponent(Graphics g) we wont see the grid change colour.

Upvotes: 1

Related Questions