This question already has answers here:
Repaint without clearing
(2 answers)
Closed 3 years ago.
I am implementing a simple Canvas where items can be drawn like a person would in real life with a paper and a pencil, without clearing the entire page every time an object is drawn.
What I have so far...
A Canvas to implement the drawing:
public class Canvas extends JPanel {
private final Random random = new Random();
public Canvas() {
setOpaque(false); // I thought setting this flag makes the drawn pixels be preserved...
}
#Override
public Dimension getPreferredSize() {
return new Dimension(640, 480);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawOval(random.nextInt(getWidth()), random.nextInt(getHeight()), 5, 5);
}
}
The Window as an actual window:
public class Window extends JFrame {
public Window(Canvas canvas) {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
add(canvas);
pack();
setVisible(true);
}
}
And the Controller with an entry-point to the application. Also starts a timer so the repaint on Canvas is called every second to force drawing another circle.
public class Controller {
public static void main(String[] args) {
Canvas canvas = new Canvas();
SwingUtilities.invokeLater(() -> new Window(canvas));
new Timer(1000, e -> canvas.repaint()).start();
}
}
The problem is that whenever a new circle is drawn, the previous one is cleared. Seems like there is still some process filling the JPanel or maybe the entire JFrame with white color.
Painting in Swing is destructive. It is an expected requirement that each time a component is painted, it is painted from scratch, again.
You need to define a model which maintains the information needed in order to restore the state from scratch.
Your paint routines would then iterate this model and draw the elements each time.
This has the benefit of allowing you to modify the model, removing or inserting elements, which would allow you to update what is been painted simply.
Alternatively, you could use a "buffer" (ie a BufferedImage) on to which all you painting is done, you would then simply paint the image to the component each time the component is painted.
This, however, means that you can't undo or layer the paintings, it's drawn directly to the image. It also makes resizing the drawing image area more difficult, as you need to make these updates manually, where as the "model" based implementation is far more adaptable
Consider calling the alternate constructor of repaint(...)
repaint(long tm, int x, int y, int width, int height)
This allows you to set a specified area to be repainted.
Also you can just store what you drew in a list and then reprint the drawing to the canvas after repaint is called.
Related
I am trying to create a 3D game engine. I am using the triangle rasterizing technique so I have a list of lines (that make up triangles) in my CanvasPanel class and each time that paintComponent() is called it goes through all the lines and displays them on the screen. The problem is in my main loop, since I want to remove all lines in order to update the scene but when I call it it's being called while the paintComponent() method is still running so I get a ConcurrentModificationException. I tried having a loop going that checks if it has finished drawing and only then clear the lines list but for some reason it works only when adding a print statement, otherwise it stops working after a few seconds. What should I do to make sure the timing is right?
Relevant code in Main:
import javax.swing.*;
import java.awt.*;
public class Main
{
public static int width = 1280, height = 720;
public static void main(String[] args)
{
JFrame window = new JFrame("3D Game");
CanvasPanel canvas = new CanvasPanel(width, height);
window.setLayout(new FlowLayout());
window.getContentPane().add(canvas);
window.pack();
window.setVisible(true);
window.setResizable(false);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Some irrelevant calculations
while (true) // Main loop
{
// Some irrelevant calculations
for (float[][] triangle: cubeMesh)
{
// Here I'm adding lines to the lines list.
}
canvas.repaint(); // update canvas
// My attempt at making sure that clearing the lines list happens only after drawing
while (true)
{
if (!canvas.isDrawing()}
{
// Only works if I print something
break;
}
}
canvas.clearLines(); // here is the problematic line
}
}
}
Relevant code in CanvasPanel:
public class CanvasPanel extends JPanel
{
BufferedImage canvas;
JLabel label;
private ArrayList<Line> lines;
private boolean drawing;
public CanvasPanel(int width, int height)
{
canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
label = new JLabel(new ImageIcon(canvas));
lines = new ArrayList<>();
add(label);
drawing = false;
}
public boolean isDrawing()
{
return drawing;
}
public void clearLines()
{
lines.clear();
}
#Override
protected void paintComponent(Graphics g)
{
drawing = true;
super.paintComponent(g);
g.setColor(Color.black);
g.fillRect(0, 0, getWidth(), getHeight());
for (Line line: lines)
{
g.setColor(line.color);
g.drawLine(line.x1, line.y1, line.x2, line.y2);
}
drawing = false;
}
public void addLine(int x1, int y1, int x2, int y2, Color color)
{
lines.add(new Line(x1, y1, x2, y2, color));
}
First, go read Performing Custom Painting and Painting in AWT and Swing to get a better understanding of how painting works in Swing.
Swing is lazy and makes use of a passive rendering workflow. This means that painting is done when the system decides it needs to be done. You can "request" a paint pass via the RepaintManager (normal via calling repaint on the component)
What this does is, if the RepaintManager agrees, places a "paint" event on the Event Dispatching Queue, which is, at some point in the future picked up by the Event Dispatching Thread and a paint pass is executed (this may be consolidated so that multiple components are painted in one pass).
There are two problems you're facing:
Painting can occur for any number of reasons, many of which you don't control and you're notified by
There's no reliable way to determine if or when a paint pass will/has occurred.
Now, add in the issue that Swing is not thread safe and you run into a mountain of other issues.
See Concurrency in Swing for more details.
So, what's the answer?
Well, you could use Worker Threads and SwingWorker or a Swing Timer to synchronise the updates depending on your needs, but honestly, I don't think making use of Swing's paint process is what you really need.
Another consideration would be use a backing buffer of your own. That is, use two or more BufferedImages onto which you draw the "state" and then pass these to the canvas to be drawn at some point in the future.
However, I would suggest taking control of the painting process directly yourself. Instead, have a look at BufferStrategy and BufferCapabilities (and the JavaDocs because it has a nice example).
This will provide you with direct control over the painting process so you can decide when and how it should be painted.
I have a little issue with my GUI in NetBeans. I draw images (dots) when a user clics in a JPanel at the mouse clic location. This part works just fine. I store each image locations in two different ArrayList that contains the X location and Y location. Now what I want to do is to delete the latest image drawn in the Panel after a button is clicked. So what I did is remove the last index of both ArrayList, and then call repaint() to draw all the images from the locations in both X and Y ArrayList (code below).
What is weird is that I need to resize the GUI (put it in full screen or just change its' size) in order for the drawn images to show up again in the JPanel otherwise, the panel remains empty.
Here's the parts of code that are affected :
public void paint(Graphics g) {
super.paint(g);
for(int i=0;i<=listePointsX.size()-1;i++) {
try{
BufferedImage icon = ImageIO.read(getClass().getResourceAsStream("/myimage.png"));
Graphics graphe = jPanel1.getGraphics();
graphe.setColor(Color.BLACK);
graphe.drawImage(icon, this.listePointsX.get(i),this.listePointsY.get(i), rootPane);
}catch(Exception e1){
}
}
private void jButtonUndoActionPerformed(java.awt.event.ActionEvent evt) {
if(listePointsX.size()>0){
int lastObject= listePointsX.size();
listePointsX.remove(lastObject-1);
listePointsY.remove(lastObject-1);
jPanel1.repaint();
}
else{
}
}
Any idea what I need to do to some kind of "refresh" the whole thing? Am I doing something wrong? Tried searching about that but did not find anyting...
Graphics graphe = jPanel1.getGraphics(); is NOT how painting should work, instead, you should have overriden the panel's paintComponent method and painted the points within in.
See Painting in AWT and Swing and Performing Custom Painting for more details about how painting works in Swing
Instead, your panel should be doing ALL the work, managing the points in the ArrayList and painting them. You parent component "might" have the ability to add or remove points if that meets your design requirements, but the core responsibility remains with the panel.
Avoid performing any long running or block operations within the paint methods, they should run as fast as possible. Since the image never changes, you should simply load once (either when the class is constructed or when you first need the image) and keep using the same reference.
Alright it worked just fine now. I had to do it the way you told me up here. I created a new class that extends jPanel (below). Then in my main Form, had to create an object of this class. Whenever a user makes a click, it will call this Drawing class object and add an item to the ArrayList (this object manages everything regarding created points... It looks like this :
public class MyDrawingClass extends JPanel {
ArrayList<Integer> arrayListPointX = new ArrayList<>();
ArrayList<Integer> arrayListPointY = new ArrayList<>();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
try{
BufferedImage icon = ImageIO.read(getClass().getResourceAsStream("/images/dot.png"));
g.setColor(Color.BLACK);
if(arrayListPointX.size()<=0){
...
}
else{
for(int i=0;i<listePointsX.size();i++){
g.setColor(Color.BLACK);
g.drawImage(icon, listePointsX.get(i), listePointsY.get(i), rootPane);
}
}
}catch(Exception e){
...
}
}
So if I want to "undo", let's say my object of "MyDrawingClass" is called "draw". I can do : draw.arrayListPointX.remove(draw.arrayListPointX.size()-1); and call repaint(); to display remaining points.
Thanks for your tips appreciate it ! :)
I've been trying to implement double buffering in my game library, but it is dramatically slower than without it.
This is my class that implements it. Basically, it draws onto an image, and then draws the image onto the JPanel's graphics (pg).
public abstract class DrawingPanel extends GameLoop {
private BufferedImage image;
private Graphics2D ig;
private Graphics pg;
private int backgroundRGB;
public DrawingPanel(final int fps, final int ups, final JPanel panel, final Color background) {
super(fps, ups);
initialize(panel, background);
}
/**
* Creates a panel with 60 fps and 120 ups
*
* #param panel
* #param background
*/
public DrawingPanel(final JPanel panel, final Color background) {
this(60, 120, panel, background);
}
public DrawingPanel(final JPanel panel) {
this(panel, Color.WHITE);
}
public DrawingPanel(final int ups, final JPanel panel, final Color background) {
super(ups);
initialize(panel, background);
}
private void initialize(final JPanel panel, final Color background) {
image = GraphicsUtils.createImage(panel.getWidth(), panel.getHeight(), Transparency.OPAQUE);
ig = (Graphics2D) image.getGraphics();
pg = panel.getGraphics();
GraphicsUtils.prettyGraphics(ig);
backgroundRGB = background.getRGB();
panel.addMouseListener(this);
panel.addMouseMotionListener(this);
panel.addKeyListener(this);
panel.addMouseWheelListener(this);
panel.setFocusable(true);
panel.requestFocusInWindow();
}
#Override
public void draw() {
// set background
Arrays.fill(((DataBufferInt) image.getRaster().getDataBuffer()).getData(), backgroundRGB);
// draw on the buffer
draw(ig);
// draw our buffer to the screen
pg.drawImage(image, 0, 0, null);
}
public abstract void draw(Graphics2D g);
}
The results of this:
This means that 25% - 6% = 19% of the time is being spent in the draw method alone!
I thought it might be the fact that I was using Array.fill, but it turned out not to be a useless "premature optimization;" if I replaced that line with
ig.setColor(backgroundColor);
ig.fillRect(0, 0, image.getWidth(), image.getHeight());
The time it takes is even longer: 26 - 5% = 21%. Is there any way I can speed this method up?
By the way, the GraphicsUtils.createImage creates a compatible image from my GraphicsConfiguration.
This entire library is on github, if you want to look at the entire code.
This is NOT how painting works in Swing -> pg = panel.getGraphics(); this is a horribly bad idea. You are fighting between the passive rendering engine of Swing and your attempt to paint to a "snap shot" of the component.
Swing components are, when you use them properly, double buffered internally, but, you need to be working with the API.
Start by having a look at Performing Custom Painting and Painting in AWT and Swing for more details about how painting works in Swing
Basically, what's happening is, you are using the results from getGraphics to paint to the panel, the RepaintManager is coming along and decides it needs to update your component and repaints it, which produces a blank panel, repeat really fast. This is what's causing your flickering. In Swing, you do not control the paint process, you can only ask to be notified when a paint cycle occurs (overriding paintComponent for example) and make requests to the painting system that a repaint should occur, but it's up to the paint system to actually decide WHEN that paint cycle occurs.
Instead, start by overriding the panel's paintComponent method and perform ALL your custom painting within it. Use repaint to request that the component be updated.
If you "really" need a active painting process, then have a look at BufferStrategy and BufferStrategy and BufferCapabilities, which provides you the means to directly control when the output is pushed to the screen device.
I have a program where the base panel is just drawing the background (trees, water, etc), and i have a player and other objects moving around the screen. I don't want to call repaint() on the whole thing because it slows me down because it repaints the whole thing. When I try to add a new panel on top that will be repainted a lot and handle moving objects, nothing happens in my code. This is what I have in the constructor for the first
public GamePanel()
{ //some code
top = TopPanel();
top.setSize(this.getSize());
add(top);
//some more code
}
and then in the class for the toppanel
public TopPanel()
{
}
public void paintComponent(Graphics g)
{
i.drawItem(//);
player.draw(//fields);
}
And no matter what I do, I can't get anything to show up on the panel when i run it.
My general approach when rendering a complex but static 'background' with other things painted on top is to draw the background to a BufferedImage and simply redraw the image before painting the dynamic parts.
I'm just getting into graphics in Java and I have a problem. I created a JFrame window (NetBeans Designer) with a JPanel panel and I drew some graphics on it. Then I added a JButton that changed a variable, which would change the X position of a square on JPanel.
On button press this code would execute:
drawObject.setX(150);
drawObject.repaint();
drawObject is an instance of this class:
public class sola extends JPanel {
private int x = 10;
#Override
public void paintComponent(Graphics g){
super.paintComponents(g);
super.setBackground(Color.WHITE);
g.setColor(Color.ORANGE);
g.fill3DRect(x, 160, 100, 50, true);
}
public void setX(int xX){
x = xX;
}
}
Now, when I press the JButton, the rectangle does move to the new position, however it is still visible in the old position. Only when i resize the window does it refresh and the old rectangle disappears. How can i solve this problem, so that when i press the button, the rectangle is only visible in the new position?
It's
super.paintComponent(g);
not
super.paintComponents(g); // note the s at the edn
Big difference between the two! The first one tells your JPanel to do all the housekeeping functions normally performed by the paintComponent method, including repainting the background (key for your project). The second, the one your calling doesn't do any of the above functionality. So my advice is to get rid of the trailing s in your super call.
You can use the following methods from JComponent: ( http://download.oracle.com/javase/6/docs/api/javax/swing/JComponent.html )
void repaint(long tm, int x, int y, int width, int height)
Adds the specified region to the dirty region list if the component is showing.
void repaint(Rectangle r)
Adds the specified region to the dirty region list if the component is showing.
You can call those before redraw()
You could use repaint() method to do tis.
If you use the paintComponent() on the panel. You should IMHO take care of the painting in the whole panel. There is no code in your example which takes care about deleting the old painted rectangles.
What i recommend is creating an own Component for your rectangles. (You could extend from Component) then you can override the paintComponent method of these classes as you did in your panel. Because the Panel should act as a container component. Not as drawing the rectangles itsself.
Know add instances of these components to a normal JPanel. This should then update as expected.