Batch Repainting Swing Components using FormLayout

300 views Asked by At

I'm making a touch screen keyboard. I want the alphabetic keys to change text when I press shift (from lower case to upper case). Here's a snippet of my current implementation:

public void updatedButtons()
{
    switch( m_state )
    {
        case QWERTY:
            for( KeyboardButton button : m_keyboardButtons )
            {
                button.setText( button.getQwertyText().toLowerCase() );
            }
            break;
        case QWERTY_SHIFT:
            for( KeyboardButton button : m_keyboardButtons )
            {
                button.setText( button.getQwertyText().toUpperCase() );
            }
            break;
    }
}

Where KeyboardButton is a simple extension of JButton with the qwertyText String field.

The issue here is that the buttons update in a chaotic manner. I understand why this is happening; when I call setText(), it's calling repaint() for the individual component, and this is happening for each button. My question is, am I able to "batch repaint" these buttons, without undermining Swing's design (I'd prefer not override AbstractButton's setText() method). Thanks.

UPDATE

Turns out it is an issue with FormLayout. Below is simple code that illustrates the problem (note that you will need a JGoodies Form Jar, and might need to modify the breakpoint value).

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

public class JButtonTest
{
    public static List<JButton> buttons = new ArrayList<JButton>();
    public static boolean isCaps = false;
    public static JFrame frame;


    public static void main(String[] args)
    {
        frame = new JFrame();
        frame.setSize( 1000, 100 );

        JPanel panel = new JPanel();
        // Form with 11 83x83 pixel squares, with 5x83 pixel spaces.
        panel.setLayout( new FormLayout("83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, "
                + "83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, 86px",
                                        "83px"));

        int i = 1;
        for (char c = 'a'; c < 'l'; ++c)
        {
            JButton button = new ComplexButton( Character.toString(c) );

            button.addActionListener( new ActionListener()
            {
                @Override
                public void actionPerformed( ActionEvent e )
                {
                    updateButtons();
                }
            });

            panel.add( button, new CellConstraints().xywh( (i*2)-1, 1, 1, 1, CellConstraints.FILL, CellConstraints.FILL) );
            buttons.add( button );
            ++i;
        }

        frame.setContentPane( panel );
        frame.setVisible( true );
    }

    // Enable the commented-out lines in this method to allow concurrent updating.
    public static void updateButtons()
    {
        for (JButton button : buttons)
        {
            if (!isCaps)
                button.setText( button.getText().toUpperCase() );
            else
                button.setText( button.getText().toLowerCase() );
            //RepaintManager.currentManager( button ).markCompletelyClean( button );
        }
        //frame.repaint();
        isCaps = !isCaps;
    }
    protected static class ComplexButton extends JButton
    {
        public ComplexButton( String string )
        {
            super(string);
        }

        @Override
        public void paint( Graphics g )
        {
            int breakpoint = 3000000;
            super.paint( g );
            // Simulate some complex operations.
            for (int i = 0; i < breakpoint; ++i)
            {
                g.setColor( new Color( i%255, (2*i)%255, (3*i)%255 ));
            }
        }
    }
}

Note that, if you change from FormLayout to FlowLayout, it works perfectly fine (though sluggish in nature). It also works fine if you remove the comments on the commented-out code (thanks MadProgrammer).

Also note that, if you put printlns at the beginning and end of the updateButtons() method, the method will end long before the buttons stop updating, and the buttons will not update in unison. This means the coalesced nature of repaint is somehow not preserved with FormLayout.

Regardless, even if it was preserved, sluggish controls are just as bad as chaotically-updating controls. Guess I'll have to attempt to optimize our paint code. Thanks for the support.

1

There are 1 answers

2
MadProgrammer On BEST ANSWER

Painting in Swing is controlled by the RepaintManager which decides when things should get updated. The RepaintManager is optimised to reduce the number paint events it schedules in order to maintain performance.

This means that when it receives a bunch of requests for repaints, it will consolidate them down into as few paint events as it can. This means that when you call setText from within a loop on a bunch of buttons, it's likely that the RepaintManager has reduced this down to as close to one as it can (depending on what you are updating there might be more, but will most likely be less than the number of iterations of the loop)...

For example, it seems to work for me...

Keys

import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestKeyboard {

    public static void main(String[] args) {
        new TestKeyboard();
    }

    public TestKeyboard() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private List<JButton> buttons;

        public TestPane() {
            setFocusable(true);
            setLayout(new GridLayout(0, 4));
            buttons = new ArrayList<>(26);
            for (int index = 0; index < 26; index++) {
                JButton btn = createButton(index);
                buttons.add(btn);
                add(btn);
            }

            addKeyListener(new KeyAdapter() {

                @Override
                public void keyPressed(KeyEvent e) {
                    if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
                        for (JButton btn : buttons) {
                            String text = btn.getText().toUpperCase();
                            btn.setText(text);
                        }
                        revalidate();
                        repaint();
                    }
                }

                @Override
                public void keyReleased(KeyEvent e) {
                    if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
                        for (JButton btn : buttons) {
                            String text = btn.getText().toLowerCase();
                            btn.setText(text);
                        }
                        revalidate();
                        repaint();
                    }
                }               
            });
        }

        protected JButton createButton(int index) {
            JButton btn = new JButton(Character.toString((char) ('a' + index)));
            btn.setMargin(new Insets(4, 4, 4, 4));
            btn.setFocusable(false);
            btn.setFocusPainted(false);
            return btn;
        }

    }

}