Swing调用repaint()致兄弟组件重复显示:原因及解决方法
问题:点击按钮后面板重绘导致按钮重复显示在NORTH区域
我正在实验javax.swing库,目标是实现点击兄弟组件按钮时,面板中的圆形改变颜色。功能大体可用,但存在异常:点击按钮后,按钮会被复制一份并显示在容器的NORTH区域。常规窗口缩放不会触发此问题,mouseClicked()方法仅修改颜色并调用repaint(),怀疑是repaint()导致该问题。查阅API文档可知,repaint()用于重绘当前组件,此处的当前组件应为面板本身,且面板的paintComponent()方法并未添加按钮,想知道这一现象的原因是什么,该如何修复?
代码实现
App类
public class App { public static void main(String[] args) { MyUI ui = new MyUI(); ui.display(); } }
MyUI类
public class MyUI { public void display() { UIUtil.getUIBuilder() .withButton(SOUTH, () -> { JButton button = new JButton("click me!"); button.setFont(new Font(SANS_SERIF, BOLD, 28)); button.setSize(150, 50); button.addMouseListener(MouseListeningPanel.getInstance()); return button; }) .withPanel(CENTER, MouseListeningPanel::getInstance) .withFrameSize(300, 300) .withDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) .visualize(); } }
MouseListeningPanel类
public class MouseListeningPanel extends JPanel implements MouseListener { public static final MouseListeningPanel INSTANCE = new MouseListeningPanel(); private Color startColor = UIUtil.getRandomColor(); private Color endColor = UIUtil.getRandomColor(); private MouseListeningPanel() {} public static MouseListeningPanel getInstance() { return INSTANCE; } @Override protected void paintComponent(Graphics g) { Graphics2D twoDGraphics = (Graphics2D) g; GradientPaint gradient = new GradientPaint(0, 0, startColor, getMiddle(), getMiddle(), endColor); twoDGraphics.setPaint(gradient); twoDGraphics.fillOval(calculateX(), calculateY(), calculateWidth(), calculateWidth()); } private float getMiddle() { return getWidth() / 2f; } private int calculateX() { return getWidth() / 2 - calculateWidth() / 2; } private int calculateY() { return getHeight() / 2 - calculateWidth() / 2; } private int calculateWidth() { return Math.min(getWidth(), getHeight()) / 2; } @Override public void mouseClicked(MouseEvent e) { startColor = UIUtil.getRandomColor(); endColor = UIUtil.getRandomColor(); repaint(); } // dummy implementations for other MouseListener methods // I can't extend JPanel and MouseAdapter at the same time so I have to provide them }
UIUtil类
public class UIUtil { public static UIBuilder getUIBuilder() { return new UIBuilder(); } public static UIBuilder getUIBuilder(Supplier<Container> contentPaneSupplier) { return new UIBuilder(contentPaneSupplier); } public static Color getRandomColor() { int red = Util.randomInt(255); int green = Util.randomInt(255); int blue = Util.randomInt(255); return new Color(red, green, blue); } public static SimpleRectangle getRandomRectangle(int parentWidth, int parentHeight) { return getRandomRectangle(parentWidth, parentHeight, 0.5F); } public static SimpleRectangle getRandomRectangle(int parentWidth, int parentHeight, float recDimensionToParentDimensionRatio) { int x = Util.randomInt(parentWidth); int y = Util.randomInt(parentHeight); int width = Util.randomInt((int) (parentWidth * recDimensionToParentDimensionRatio)); int height = Util.randomInt((int) (parentHeight * recDimensionToParentDimensionRatio)); return new SimpleRectangle(x, y, width, height); } public record SimpleRectangle(int x, int y, int width, int height) {} @NoArgsConstructor public static class UIBuilder { private final JFrame frame; private final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); { frame = new JFrame(); this.withFrameSize(300, 300) .withDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public UIBuilder(@NotNull Supplier<Container> contentPaneSupplier) { Objects.requireNonNull(contentPaneSupplier); frame.setContentPane(contentPaneSupplier.get()); } public UIBuilder withButton(@NotNull String position, @NotNull Supplier<JButton> buttonSupplier) { Stream.of(position, buttonSupplier).forEach(Objects::requireNonNull); JButton button = buttonSupplier.get(); frame.getContentPane().add(position, button); return this; } public UIBuilder withPanel(@NotNull String position, @NotNull Supplier<JPanel> panelSupplier) { Stream.of(position, panelSupplier).forEach(Objects::requireNonNull); JPanel panel = panelSupplier.get(); JScrollPane scrollPane = new JScrollPane(panel); frame.getContentPane().add(position, scrollPane); return this; } public UIBuilder withFrameSize(int width, int height) { checkAgainstScreenSize(width, height); int x = (screenSize.width - width) / 2; int y = (screenSize.height - height) / 2; frame.setBounds(x, y, width, height); return this; } private void checkAgainstScreenSize(int width, int height) { if (screenSize.width < width) { throw new IllegalArgumentException("Frame width cannot be greater than screen width"); } else if (screenSize.height < height) { throw new IllegalArgumentException("Frame height cannot be greater than screen height"); } } public UIBuilder withDefaultCloseOperation(int windowConstant) { frame.setDefaultCloseOperation(windowConstant); return this; } public void visualize() { frame.setVisible(true); } } }
原因分析
- 未调用父类paintComponent方法:Swing中
paintComponent的默认实现负责清除组件原有绘制区域、绘制背景等操作。如果不调用super.paintComponent(g),面板重绘时不会清除之前的内容,按钮的视觉残留会被保留,结合布局管理器的重绘逻辑,错误显示在NORTH区域。 - 组件职责耦合:让
MouseListeningPanel同时充当JPanel和MouseListener,违反了单一职责原则。这种耦合会导致事件处理和组件绘制的逻辑相互干扰,增加异常出现的概率。
修复方案
1. 修复paintComponent方法
在MouseListeningPanel的paintComponent方法第一行添加父类调用,确保每次重绘先清理面板:
@Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 添加这行 Graphics2D twoDGraphics = (Graphics2D) g; GradientPaint gradient = new GradientPaint(0, 0, startColor, getMiddle(), getMiddle(), endColor); twoDGraphics.setPaint(gradient); twoDGraphics.fillOval(calculateX(), calculateY(), calculateWidth(), calculateWidth()); }
2. 分离事件监听器
移除MouseListeningPanel的MouseListener实现,创建独立监听器处理按钮点击事件:
- 修改
MouseListeningPanel,添加公开方法用于更新颜色:
public class MouseListeningPanel extends JPanel { // 保留原有单例和属性 // 移除MouseListener相关实现 public void updateColors() { startColor = UIUtil.getRandomColor(); endColor = UIUtil.getRandomColor(); repaint(); } // 其余代码不变 }
- 修改MyUI中的按钮监听器:
.withButton(SOUTH, () -> { JButton button = new JButton("click me!"); button.setFont(new Font(SANS_SERIF, BOLD, 28)); button.setPreferredSize(new Dimension(150, 50)); // 替换setSize为setPreferredSize MouseListeningPanel panel = MouseListeningPanel.getInstance(); button.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { panel.updateColors(); } }); return button; })
3. 优化组件大小设置
移除button.setSize(150, 50);,改用setPreferredSize,让BorderLayout参考组件的preferred size进行布局,避免手动设置大小导致的布局异常。
内容的提问来源于stack exchange,提问作者Sergey Zolotarev
相关产品推荐
相关产品推荐

