You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 06:49:52