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.
/*****************************************************************************
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();
});
}
}
There are two types of coalescing painting requests:
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.
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:
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.