Search code examples
javaswingrepaint

Repaint() requests coalescing into one is a myth?


Everywhere I look it is said that there is no need to worry about multiple repaint() requests, as they are getting coalesced and postponed to happen simultaneously in one optimized batch. But this simple SSCE shows that it is not true. Every button repaints separately in seemingly random order. enter image description here

/*****************************************************************************
SSCE1.java

*****************************************************************************/
package com.example;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

/**
 * <p>No paint coalescing!</p>
 */
final class SSCE1
{
    private static class MainWin
    {
        private static final int NUMBTN = 400;
        private final JButton[]         buttons;
        private final JButton           reset_button;
        private final JButton           quit_button;
        private final JPanel            tools;
        private final JPanel            main;
        private final JPanel            root;
        private final JFrame            frame;
        private final ActionListener    reset_handler;
        private final ActionListener    quit_handler;
        private final ActionListener    btn_handler;
        MainWin()
        {
            this.buttons = new JButton[NUMBTN];
            this.reset_button = new JButton("Reset");
            this.quit_button = new JButton("Quit");
            this.tools = new JPanel(new BorderLayout(5,5));
            this.main = new JPanel(new FlowLayout(FlowLayout.LEFT,5,5));
            this.root = new JPanel(new BorderLayout(5,5));
            this.frame = new JFrame("Paint coalescing is a myth!");
            this.reset_handler = (ev)->{ for( int i=0; i<NUMBTN; ++i ) buttons[i].setEnabled(true); };
            this.quit_handler = (ev)->{ Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(new WindowEvent(frame,WindowEvent.WINDOW_CLOSING)); };
            this.btn_handler = (ev)->{ for( int i=0; i<NUMBTN; ++i ) buttons[i].setEnabled(false); };

            root.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
            reset_button.addActionListener(reset_handler);
            quit_button.addActionListener(quit_handler);
            for( int i=0; i<NUMBTN; ++i ) {
                buttons[i] = new JButton(Integer.toString(i+1));
                buttons[i].addActionListener(btn_handler);
                main.add(buttons[i]);
            }
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            frame.setUndecorated(true);
            frame.setBounds(GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds());

            tools.add(reset_button,BorderLayout.NORTH);
            tools.add(quit_button,BorderLayout.SOUTH);
            root.add(tools,BorderLayout.WEST);
            root.add(main,BorderLayout.CENTER);
            frame.getContentPane().add(root,BorderLayout.CENTER);
        }
        private void open()
        {
            frame.setVisible(true);
        }
    }
    public static void main( String[] args )
    {
        SwingUtilities.invokeLater(()->{
            MainWin win = new MainWin();
            win.open();
        });
    }
}

So the question is: how do I turn on repaint coalescing? Of course, I can do something like this SSCE #2 - create my own buttons, and then everything works fine. But what about standard controls?

/*****************************************************************************
SSCE2.java

*****************************************************************************/
package com.example;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

/**
 * <p>We must do something about it.</p>
 */
final class SSCE2
{
    private static interface Handler
    {
        void    handleClicked( FakeBtn src );
    }
    private static class FakeBtn extends JLabel
    {
        private static final long serialVersionUID = -1372036387522045748L;
        private static final Color  back = new Color(0x66,0x77,0x88);
        private static final Color  backdown = back.darker();
        private static final Color  backhover = back.brighter();
        private static final Color  backoff = Color.DARK_GRAY;
        private static class MouseHandler implements MouseMotionListener, MouseListener
        {
            @Override public void mouseClicked( MouseEvent ev )
            {
                FakeBtn btn = (FakeBtn)ev.getSource();
                btn.setDownState(false);
                if( btn.m_click_handler!=null )
                    btn.m_click_handler.handleClicked(btn);
            }
            @Override public void mousePressed( MouseEvent ev )
            {
                FakeBtn btn = (FakeBtn)ev.getSource();
                if( btn.m_state_active )
                    btn.setDownState(true);
            }
            @Override public void mouseReleased( MouseEvent ev )
            {
                FakeBtn btn = (FakeBtn)ev.getSource();
                btn.setDownState(false);
            }
            @Override public void mouseEntered( MouseEvent ev )
            {
                FakeBtn btn = (FakeBtn)ev.getSource();
                btn.setHoverState(true);
            }
            @Override public void mouseExited( MouseEvent ev )
            {
                FakeBtn btn = (FakeBtn)ev.getSource();
                btn.setHoverState(false);
            }
            @Override public void mouseDragged( MouseEvent ev )
            {
            }
            @Override public void mouseMoved( MouseEvent ev )
            {
            }
        }
        private static MouseHandler mhandler = new MouseHandler();
        private boolean m_state_active;
        private boolean m_state_down;
        private boolean m_state_hover;
        private Handler m_click_handler;
        FakeBtn( String label )
        {
            super(label);
            this.m_state_active = true;
            this.m_state_down = false;
            this.m_state_hover = false;
            this.m_click_handler = null;
            addMouseListener(mhandler);
            addMouseMotionListener(mhandler);
        }
        private void setActiveState( boolean flag )
        {
            m_state_active = flag;
            getParent().repaint(getX(),getY(),getWidth(),getHeight());
        }
        private void setDownState( boolean flag )
        {
            m_state_down = flag;
            getParent().repaint(getX(),getY(),getWidth(),getHeight());
        }
        private void setHoverState( boolean flag )
        {
            m_state_hover = flag;
            setCursor( m_state_active && m_state_hover ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : Cursor.getDefaultCursor() );
            getParent().repaint(getX(),getY(),getWidth(),getHeight());
        }
        @Override protected void paintBorder( Graphics g )
        {
            g.setColor(m_state_active?m_state_hover?Color.RED:Color.WHITE:Color.BLACK);
            g.drawRect(0,0,getWidth()-1,getHeight()-1);
        }
        @Override public Dimension getPreferredSize()
        {
            Dimension ret = super.getPreferredSize();
            ret.width = 80;
            ret.height = 40;
            return ret;
        }
        @Override protected void paintComponent( Graphics g )
        {
            g.setColor(m_state_active?m_state_down?backdown:m_state_hover?backhover:back:backoff);
            g.fillRect(0,0,getWidth(),getHeight());
            int w = getWidth();
            int h = getHeight();
            String text = getText();
            FontMetrics fm = g.getFontMetrics();
            int tw = fm.stringWidth(text);
            int th = fm.getHeight();
            int base = fm.getMaxAscent();
            g.setColor(m_state_active?Color.WHITE:Color.GRAY);
            g.drawString(text,(w-tw)/2,(h-th)/2+base);
        }
        void setActive( boolean state )
        {
            setActiveState(state);
        }
        void setHandler( Handler h )
        {
            m_click_handler = h;
        }
    }
    private static class MainWin
    {
        private static final int NUMBTN = 400;
        private final FakeBtn[]         buttons;
        private final FakeBtn           reset_button;
        private final FakeBtn           quit_button;
        private final JPanel            tools;
        private final JPanel            main;
        private final JPanel            root;
        private final JFrame            frame;
        private final Handler    reset_handler;
        private final Handler    quit_handler;
        private final Handler    btn_handler;
        MainWin()
        {
            this.buttons = new FakeBtn[NUMBTN];
            this.reset_button = new FakeBtn("Reset");
            this.quit_button = new FakeBtn("Quit");
            this.tools = new JPanel(new BorderLayout());
            this.main = new JPanel(new FlowLayout(FlowLayout.LEFT));
            this.root = new JPanel(new BorderLayout());
            this.frame = new JFrame("Paint coalescing is a myth!");
            this.reset_handler = (src)->{ for( int i=0; i<NUMBTN; ++i ) buttons[i].setActive(true); };
            this.quit_handler = (src)->{ Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(new WindowEvent(frame,WindowEvent.WINDOW_CLOSING)); };
            this.btn_handler = (src)->{ for( int i=0; i<NUMBTN; ++i ) buttons[i].setActive(false); };

            root.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
            reset_button.setHandler(reset_handler);
            quit_button.setHandler(quit_handler);
            for( int i=0; i<NUMBTN; ++i ) {
                buttons[i] = new FakeBtn(Integer.toString(i+1));
                buttons[i].setHandler(btn_handler);
                main.add(buttons[i]);
            }
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            frame.setUndecorated(true);
            frame.setBounds(GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds());

            tools.add(reset_button,BorderLayout.NORTH);
            tools.add(quit_button,BorderLayout.SOUTH);
            root.add(tools,BorderLayout.WEST);
            root.add(main,BorderLayout.CENTER);
            frame.getContentPane().add(root,BorderLayout.CENTER);
        }
        private void open()
        {
            frame.setVisible(true);
        }
    }
    public static void main( String[] args )
    {
        SwingUtilities.invokeLater(()->{
            MainWin win = new MainWin();
            win.open();
        });
    }
}

Solution

  • There are two types of coalescing painting requests:

    1. if you repaint multiple components then the area defined by the multiple components will be calculated and a clip region set for the Graphics object to minimize the are being painted. Your SSCE1 code is an example of this.

    2. When you repaint the same component multiple times. Typically this would happen when you set multiple properties of the component at the same time, for example, the foreground, background, border, font etc. Or if you reset the same property multiple time in a loop, for example, the text of the component.

    Here is an example of coalescing repaint requests when the same property is continually reset:

    Even though the text of the label is changed 10 times, only a single paintComponent() is generated:

    import java.awt.*;
    import javax.swing.*;
    import java.awt.event.*;
    
    public class SSCCE extends JPanel
    {
        public SSCCE()
        {
                JLabel label = new JLabel("label 0")
                {
                @Override
                public void setText(String text)
                {
                    super.setText(text);
                    System.out.println("new text: " + text);
                }
    
    
                @Override
                protected void paintComponent(Graphics g)
                {
                    super.paintComponent(g);
                    System.out.println("paintComponent");
                }
            };
            add(label);
    
            JButton button = new JButton("Click Me");
            button.addActionListener((e) -> { for( int i=0; i < 10; ++i ) label.setText("label " + i); });
            add(button);
        }
    
        private static void createAndShowGUI()
        {
            System.setProperty("sun.awt.noerasebackground", "false");
            JFrame frame = new JFrame("SSCCE");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new SSCCE());
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.setSize(300, 200);
            frame.setLocationByPlatform( true );
            frame.setVisible( true );
        }
    
        public static void main(String[] args) throws Exception
        {
            java.awt.EventQueue.invokeLater( () -> createAndShowGUI() );
        }
    }
    

    Edit:

    In your original code I made the following change:

    buttons[i] = new JButton(Integer.toString(i+1))
    {
        @Override
        protected void paintComponent(Graphics g)
        {
            super.paintComponent(g);
            System.out.println("painting: " + getText());
        }
    };
    

    When you do this you will notice that:

    1. When the screen is first displayed the buttons are painted in reverse order. This is normal and is how Swing supports ZOrder based painting.
    2. When you click on a button, the buttons are disabled and are painted in random order (as you noticed). Note I looked at the RepaintManager source code. At a high level I can see that it uses a Map to hold the components that need to be painted. This would explain the random order for painting, but it doesn't explain why they are still not all painted at the same time. They should all be painted to a buffer (so the order should not matter) and then the buffer painted.

    I then made the following change:

    //this.btn_handler = (ev)->{ for( int i=0; i<NUMBTN; ++i ) buttons[i].setEnabled(false); };
    this.btn_handler = (ev)->{ for( int i=0; i<NUMBTN; ++i ) buttons[i].setEnabled(false); ((JButton)ev.getSource()).getParent().repaint();};
    

    The repaint() on the parent panel appears to paint all the components at one time as expected. This sort of makes sense as now there is only a single component to be painted. Its child components will be be painted as per normal painting logic.

    Note, if you leave the System.out.println(...) statement in the code then you can really see the effect of the random painting of the buttons when you click on the "Reset" button.

    how do I turn on repaint coalescing?

    Seems like the simple answer is to repaint() the parent panel after changing the state of each individual button.