Repaint() 请求合并成一个是一个神话?

Repaint() requests coalescing into one is a myth?

我看到的每个地方都说无需担心多个 repaint() 请求,因为它们正在合并并推迟在一个优化的批次中同时发生。但是这个简单的 SSCE 表明事实并非如此。每个按钮都以看似随机的顺序分别重新绘制。

/*****************************************************************************
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();
        });
    }
}

所以问题是:如何打开重绘合并? 当然,我可以做类似 SSCE #2 的事情——创建我自己的按钮,然后一切正常。但是标准控件呢?

/*****************************************************************************
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();
        });
    }
}

合并绘画请求有两种类型:

  1. 如果您重新绘制多个组件,那么将计算由多个组件定义的区域,并为 Graphics 对象设置裁剪区域以最小化正在绘制的区域。您的 SSCE1 代码就是一个例子。

  2. 当您多次重新绘制同一个组件时。通常,当您同时设置组件的多个属性时会发生这种情况,例如前景、背景、边框、字体等。或者如果您在循环中多次重置相同的 属性,例如,组件的文本。

这是在不断重置相同 属性 时合并重绘请求的示例:

即使标签的文本更改了 10 次,也只生成了一个 paintComponent()

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() );
    }
}

编辑:

我在您的原始代码中做了以下更改:

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

执行此操作时,您会注意到:

  1. 首次显示屏幕时,按钮以相反的顺序绘制。这是正常的,也是 Swing 支持基于 ZOrder 的绘画的方式。
  2. 当您单击一个按钮时,按钮将被禁用并以随机顺序绘制(如您所见)。注意我查看了 RepaintManager 源代码。在高层次上,我可以看到它使用 Map 来保存需要绘制的组件。这可以解释绘画的随机顺序,但不能解释为什么它们仍然不是同时绘制的。它们都应该被绘制到缓冲区(因此顺序无关紧要)然后缓冲区被绘制。

然后我做了以下更改:

//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();};

父面板上的 repaint() 似乎按预期一次绘制了所有组件。这是有道理的,因为现在只有一个组件要绘制。它的子组件将按照正常的绘制逻辑进行绘制。

请注意,如果您在代码中保留 System.out.println(...) 语句,那么当您单击“重置”按钮时,您真的可以看到随机绘制按钮的效果。

how do I turn on repaint coalescing?

似乎简单的答案是在更改每个单独按钮的状态后重新绘制()父面板。