How does JComponent.paintImmediately() work in Java Swing?
Asked Answered
A

1

11

My understanding: Unlike most of the components/operations in Swing call to JComponent.repaint() is thread-safe i.e. though a repaint request is made from another thread (i.e. not from EDT), the actual painting happens in EDT only. Below code snippet demonstrates this.

public class PaintingDemo {

    public static void main(String[] args) {
        final JFrame frame = new JFrame();
        final JPanel p = new MyPanel();
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                frame.add(p, BorderLayout.CENTER);
                frame.setSize(200, 200);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setVisible(true);
            }
        });
        new Thread("MyThread") {
            public void run() {
                while (true) {
                // Below statements are important to show the difference
                    p.repaint();
                    p.paintImmediately(p.getBounds());
                    try {
                        Thread.sleep(1000);
                    } catch(Exception e) {}
                }
            }
        }.start();
    }

}

class MyPanel extends JPanel {
    @Override
    public void paint(Graphics g) {
        System.out.println("paint() called in "+ Thread.currentThread().getName());
        super.paint(g);
    }
}

From the output it is known that painting is done in EDT when repaint() is invoked irrespective of from which thread it is called - so no issues. But, in the case of paintImmediately() - painting happens in the same thread from which it is called.

Consider a case where EDT is changing the state of a component and another thread (from which paintImmediately() is invoked) is painting the same component.

My Question: In case of paintImmediately(), how is synchronization between Event Dispatcher Thread (EDT) and other threads handled?

Agonic answered 25/12, 2012 at 14:52 Comment(0)
A
7

To my understanding, when you call paintImmediately, you call the following code:

        Component c = this;
        Component parent;

        if(!isShowing()) {
            return;
        }

        JComponent paintingOigin = SwingUtilities.getPaintingOrigin(this);
        if (paintingOigin != null) {
            Rectangle rectangle = SwingUtilities.convertRectangle(
                    c, new Rectangle(x, y, w, h), paintingOigin);
            paintingOigin.paintImmediately(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
            return;
        }

        while(!c.isOpaque()) {
            parent = c.getParent();
            if(parent != null) {
                x += c.getX();
                y += c.getY();
                c = parent;
            } else {
                break;
            }

            if(!(c instanceof JComponent)) {
                break;
            }
    }
    if(c instanceof JComponent) {
        ((JComponent)c)._paintImmediately(x,y,w,h);
    } else {
        c.repaint(x,y,w,h);
    }

So, unless this is not a JComponent, you end up calling _paintImmediately() which ends up calling paint(Graphics) as suggests the stack trace below (captured from a piece of code I will post at the end of this post):

Thread [pool-1-thread-1] (Suspended)    
    TestPaint$1.paint(Graphics) line: 23    
    TestPaint$1(JComponent).paintToOffscreen(Graphics, int, int, int, int, int, int) line: 5221 
    RepaintManager$PaintManager.paintDoubleBuffered(JComponent, Image, Graphics, int, int, int, int) line: 1482 
    RepaintManager$PaintManager.paint(JComponent, JComponent, Graphics, int, int, int, int) line: 1413  
    RepaintManager.paint(JComponent, JComponent, Graphics, int, int, int, int) line: 1206   
    TestPaint$1(JComponent)._paintImmediately(int, int, int, int) line: 5169    
    TestPaint$1(JComponent).paintImmediately(int, int, int, int) line: 4980 
    TestPaint$1(JComponent).paintImmediately(Rectangle) line: 4992  
    TestPaint$3.run() line: 50  
    ThreadPoolExecutor.runWorker(ThreadPoolExecutor$Worker) line: 1110  
    ThreadPoolExecutor$Worker.run() line: 603   
    Thread.run() line: 722  

But if you try to also call repaint() simultaneously (from another Thread), you see that both run at the same time (I tried stepping in the code with debuger and painting never stopped occuring in the other Thread) it seems that at the Java code level, there is not much synchronization (at least I could not spot anything). So if you end up modifying the component state in the EDT, I believe that results are quite unpredictable and you should avoid such situation by all means.

Just to illustrate my point, I tried modifying the state of a variable within the paint method, add a sleep to increase the risk of collisions between the 2 Threads (EDT and the other) and it obvisouly seems that there is no synchronization between the two Threads (the System.err.println() outputted null from time to time).

Now I would wonder why you need to perform a paintImmediately. Unless you are blocking the EDT, there is not so many valid reasons to perform such thing.

Below is the code I used to test those things (pretty close the one posted in the question). The code is only meant to try to understand what is going on, not to show how to perform proper painting nor any good Swing practice.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
import java.util.concurrent.Executors;

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

public class TestPaint {

    protected void initUI() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setTitle(TestPaint.class.getSimpleName());
        final Random rand = new Random();
        final JPanel comp = new JPanel() {
            private String value;

            @Override
            public void paint(Graphics g) {
                value = "hello";
                super.paint(g);
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                g.setColor(new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256)));
                g.fillRect(0, 0, getWidth(), getHeight());
                if (SwingUtilities.isEventDispatchThread()) {
                    System.err.println("Painting in the EDT " + getValue());
                } else {
                    System.err.println("Not painting in EDT " + getValue());
                }
                value = null;
            }

            public String getValue() {
                return value;
            }
        };
        frame.add(comp);
        frame.setSize(400, 400);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        Timer t = new Timer(1, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                comp.repaint();
            }
        });
        t.start();
        Executors.newSingleThreadExecutor().execute(new Runnable() {

            @Override
            public void run() {
                while (true) {
                    comp.paintImmediately(comp.getBounds());
                }
            }
        });
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new TestPaint().initUI();
            }
        });
    }

}
Ambidexterity answered 25/12, 2012 at 22:0 Comment(3)
+1 you nailed it. I just wonder why JComponent docs reference paintImmediately() and repaint(..) where as source is different. I must ask though in your snippet we would never be overriding paint(..) of a JComponent so why didnt you use paintComponent (it still seems to reproduce results)? And another question why will increasing the sleep(int milis) not increase collisions? And also why do you call paintImmediately from a thread other than EDT, (because I see once called on EDT no null will occur)?Inexistent
Thanks @Guillaume for trying it out yourself and sharing your research. I agree that there are not many reasons to use paintImmediately() but I was just trying to understand "how it works?" Because the javadoc says... "This method is useful if one needs to update the display while the current event is being dispatched." Also, this method is public and there is absolutely no warnings mentioned anywhere about its caveats; at least the javadoc is misleading. Now I understand paintImmediately() needs to be called within EDT and its not thread-safe unlike repaint().Agonic
@DavidKroukamp Absolutely no good reason to override paint rather than paintComponent, I just thought that in the attempt to see how paintImmediately eventually calls paint adding the additional call to paintComponent in the stack call was unnecessary and the observed results would remain the same. So it was more to just remove any "noise" from the information, and as I stated before my snippet it does not illustrate proper way to use Swing. Cheers ;-)Ambidexterity

© 2022 - 2024 — McMap. All rights reserved.