2

I am developing an application that simulates Cellular Automatons. It happens that I need to draw really fast(each 100ms) a grid of 80x80 squares (6400 squares).

My first approach was using JLabels, but it was really slow. Now I am using Graphics2D and works great, but after drawing about 50 times, it starts getting slow, and goes getting slower as the turns advance.

I need to call repaint() after each turn in order to 'repaint' the squares, but I am guessing that what was drawn before is still in the memory, is that it? How can I discard what was draw so it will not ocupy the buffer ou memory?

A last thing, I saw this post but I can not see the difference between mine and the code provided there: post about repaint

Here's an image to help you understand what it's all about: Application running

And here is my code:

private void drawMatriz(int[][] array, DrawSquare square, int size, AppController contr) {
    Color[] configColors = contr.getArrayOfCollors();
    int posX = 0;
    int posY = 0;
    for (int i = 0; i < array.length; i++) {
        for (int j = 0; j < array[0].length; j++) {
            square.addSquare(posX, posY, size, size, configColors[array[i][j]]);
            posX += size;
        }
        posX = 0;
        posY += size;
    }
    repaint();
}

public AppRun(AppController controller) {
    [...]
    squares = new DrawSquare();
    squares.setBorder(new LineBorder(new Color(0, 0, 0)));
    squares.setBounds(209, 11, 640, 640);
    getContentPane().add(squares);
    squares.setPreferredSize(new Dimension(500, 500));
    squares.setLayout(null);

    drawMatriz(controller.getVector(), squares, (squares.getBounds().width / controller.getVector().length),
            controller);

}

class DrawSquare extends JPanel {
    private static final long serialVersionUID = 1L;
    private static final int PREF_W = 400;
    private static final int PREF_H = PREF_W;
    private List<Rectangle> squares = new ArrayList<Rectangle>();
    private List<Color> colors = new ArrayList<Color>();

    public void addSquare(int x, int y, int width, int height, Color color) {
        Rectangle rect = new Rectangle(x, y, width, height);
        squares.add(rect);
        colors.add(color);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(PREF_W, PREF_H);
    }

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

        for (int i = 0; i < squares.size(); i++) {
            g2.setColor(colors.get(i));
            g2.fill(squares.get(i));
            g2.setColor(Color.BLACK);
            g2.draw(squares.get(i));
        }
    }
}
Community
  • 1
  • 1
Madson Paulo
  • 57
  • 1
  • 9
  • The answer will depend on how dynamic the UI is, for example, instead of drawing all the squares in `paintComponent`, you could use a backing buffer to render the squares and simply paint this. You'd need to do some clever coding to update the image when you wanted to add/remove squares, but based on what you seem to be trying to do, this would seem to be a better idea – MadProgrammer Mar 10 '17 at 21:37
  • I remember I did this same program once, you could have 2 matrix of booleans, and each square being a `JPanel`, then having a bigger `JPanel` with `GridLayout`. The 1st boolean matrix represents your current map state and the 2nd the new one. Every iteration you compare both of them and set the 2nd as the current state. Depending of the current state you paint it of one color or another. Your current approach doesn't seem good, you're using `null` layout and you have a for-loop in the `paintComponent`... – Frakcool Mar 10 '17 at 21:40

2 Answers2

2

I don't see any problem with your paintComponent method.

But you seem to never reset the contents of the ArrayLists in your DrawSquare class, for example by calling clear() on them. Hence, after the first call of drawMatriz(...) the ArrayLists have 6400 entries, after the second call they have 12800 entries, after 50 calls 320000 entries, ...

Thomas Fritsch
  • 9,639
  • 33
  • 37
  • 49
  • I was having dinner and I thought exactly the same thing, array getting too big, but I was thinking on another part of the code that is not there. In fact that was the problem, you were right! See that: DrawSquare array size on cycle 57 is: 371200 And now: DrawSquare array size on cycle 115 is: 6400 The speed is now fixed, thank you very much :) – Madson Paulo Mar 10 '17 at 21:27
1

Rather than trying to run through the list of elements each time paintComponent is called, which is time consuming, simply to update a couple of cells, consider using a backing buffer, which you can draw straight to the Graphics context.

This means that when you want to update a cell, you update a single cell on the backing buffer and repaint this, which is generally more efficient.

This example also examines the Graphics context's clipping bounds and only draws as much of the image that is actually presented within those bounds, this further increases the efficiency.

The example renders a total of 1, 000, 000 cells (1000 x 1000) at 10 pixels in size, which generates a backing buffer of 10, 000x10, 000, so we're not talking about small amount of values.

The example allows you to randomly update cells (because of they can be generated off the screen, I limited it to the first 50x50 cells so you can see them update, but realistically it will work across the range)

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

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

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                int cols = 1000;
                int rows = 1000;
                DrawSquare squares = new DrawSquare(cols, rows);

                JButton btn = new JButton("Random");
                btn.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        int col = (int) (Math.random() * 50);
                        int row = (int) (Math.random() * 50);
                        System.out.println(col + "x" + row);
                        squares.addSquare(col, row, Color.RED);
                        squares.repaint();
                    }
                });

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new JScrollPane(squares));
                frame.add(btn, BorderLayout.SOUTH);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });

    }

    class DrawSquare extends JPanel implements Scrollable {

        private static final long serialVersionUID = 1L;
        private List<Rectangle> squares = new ArrayList<Rectangle>();
        private List<Color> colors = new ArrayList<Color>();

        private BufferedImage img;
        private int cellSize;
        private int cols, rows;

        public DrawSquare(int cols, int rows) {
            this.cols = cols;
            this.rows = rows;
            cellSize = 10;
            img = new BufferedImage(cols * cellSize, rows * cellSize, BufferedImage.TYPE_INT_RGB);
            System.out.println(cellSize);
            for (int i = 0; i < cols; i++) {
                for (int j = 0; j < rows; j++) {
                    addSquare(i, j, Color.WHITE);
                }
            }
        }

        public void addSquare(int col, int row, Color color) {
            Graphics2D g2d = img.createGraphics();

            int x = col * cellSize;
            int y = row * cellSize;

            Rectangle rect = new Rectangle(x, y, cellSize, cellSize);
            g2d.setColor(color);
            g2d.fill(rect);
            g2d.setColor(Color.BLACK);
            g2d.draw(rect);
            g2d.dispose();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(cols * cellSize, rows * cellSize);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g.create();
            Rectangle clip = g2.getClipBounds();

            int width = clip.x + clip.width > img.getWidth() ? img.getWidth() - clip.x : clip.width;
            int height = clip.y + clip.height > img.getHeight()? img.getHeight() - clip.y : clip.height;

            img.getSubimage(clip.x, clip.y, width, height);
            g2.drawImage(img.getSubimage(clip.x, clip.y, width, height), clip.x, clip.y, this);
            g2.dispose();
        }

        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return new Dimension(500, 500);
        }

        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 128;
        }

        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 128;
        }

        @Override
        public boolean getScrollableTracksViewportWidth() {
            return false;
        }

        @Override
        public boolean getScrollableTracksViewportHeight() {
            return false;
        }
    }

}

I placed into a JScrollPane because otherwise it wouldn't render on the screen or if I reduced the size of the cells, would render as completely blank (the cells become too small)

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • I think that for my need, backing buffer is not needed. I tried your approach here and is really nice. I will need to implement Scrollable in my JPanel as well, because now I will try to insert zoom in and out option. Just to you understand how is the usage, see this: https://www.youtube.com/watch?v=65oSje4yowU&feature=youtu.be Thank you for the time in changing my code to fit your back buffering example! – Madson Paulo Mar 11 '17 at 13:07
  • I was wrong. Actually, if I don't draw anything, I reduce 50% of the run time. I guess doing your suggested way I can draw reducing about 30% of something. Will try here more seriously now – Madson Paulo Mar 21 '17 at 02:58