javaswingjpanel

Force JPanel to repaint when a sub-component resizes


I'm designing a Java Swing game, where a JPanel displays game-related stuff, and a menu where some buttons are not always visible is overlaid onto that panel.

Problem: when a button is hidden, the background panel is not properly redrawn.

Simplified code:

public class MyPanel extends JPanel { // This is where I draw game-related stuff in the background

    public MyPanel() {
        JPanel toolPanel = new JPanel();
        this.add(toolPanel, BorderLayout.SOUTH);
    }
}

public class ToolPanel extends JPanel { // This panel holds the buttons overlaid onto MyPanel

        Color transparent = new Color(255, 255, 255, 0);
        toolPanel.setForeground(transparent);
        toolPanel.setBackground(transparent);

        JButton mainButton = new JButton("main button");
        toolPanel.add(mainButton);

        JButton subButton = new JButton("sub-button");
        toolPanel.add(subButton);
        subButton.setVisible(false);

        mainButton.addActionListener((e) -> {
            subButton.setVisible(!subButton.isVisible());
            System.out.println("Setting sub-button to " + subButton.isVisible());
            // repaint();
            // invalidate(); // None of these can force the parent JPanel to repaint...
        });

}

What I have after init: normal state

What I get when I show the second button: background of subpanel is not painted correctly

And when I set the second button to invisible (I'm expecting to go back to the first image): main button is OK, button panel background is KO

Edit: minimal reproducible example:


public class SSCCE {

    public static void main(String[] args) {

        JFrame f = new JFrame();
        JPanel drawingPanel = new JPanel() {
            @Override
            public void paintComponent(Graphics g) { // Default black and gray lines
                int w = g.getClipBounds().width;
                int h = g.getClipBounds().height;
                for (int i = 0; i < 10; i++) {
                    g.setColor(Color.black);
                    g.fillRect(i * w / 10, 0, w / 20 + 1, h);
                    g.setColor(Color.gray);
                    g.fillRect((int) ((i + 0.5) * w / 10), 0, w / 20 + 1, h);
                }
            }
        };
        drawingPanel.add(new ButtonPanel());
        drawingPanel.setPreferredSize(new Dimension(600, 400));
        f.setContentPane(drawingPanel);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setVisible(true);
        f.pack();

    }

    private static class ButtonPanel extends JPanel {

        // mainButton is always visible.
        JButton mainButton;

        // secondary button is visible only when the mouse hovers the ButtonPanel or the mainButton or the secondary button.
        JButton secondaryButton;

        public ButtonPanel() {
            super();
            mainButton = new JButton("main button alone");
            secondaryButton = new JButton("secondary button");
            add(mainButton);
            add(secondaryButton);
            secondaryButton.setVisible(false);
            setBackground(Color.red);

            MouseAdapter listener = new MouseAdapter() {
                @Override
                public void mouseEntered(MouseEvent e) {
                    secondaryButton.setVisible(true);
                    mainButton.setText("main button not alone");
                    invalidate();
                    revalidate();
                    repaint();
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    secondaryButton.setVisible(false);
                    mainButton.setText("main button alone");
                    invalidate();
                    revalidate();
                    repaint();
                }
            };

            this.addMouseListener(listener);
            mainButton.addMouseListener(listener);
            secondaryButton.addMouseListener(listener);
        }
    }
}


Solution

  • There is no need for:

    //invalidate();
    //revalidate();
    //repaint();
    

    in any of your listener code. When you change a property of a Swing component the component will automatically invoke revalidate() and repaint().

    Your issue is that you are trying to use the clipping width/height in your painting code. Custom painting should always be done relative to the component. Just do the painting normally and Swing will handle only repainting the clipped area.

    So your code should be:

    public void paintComponent(Graphics g) { // Default black and gray lines
        //int w = g.getClipBounds().width;
        //int h = g.getClipBounds().height;
        int w = getWidth();
        int h = getHeight();