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