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

Swing UI元素背景重影问题:原因排查与解决求助

游戏悬浮计时器UI重影问题排查与修复

问题描述

正在制作一款游戏悬浮层(Overlay),包含带开始/重置、停止快捷键的简易计时器,用自定义进度条展示计时进度和关键节点。但启动计时器、计时跳动或停止时会出现UI重影,调整窗口大小可临时消除,但后续操作会再次触发。

正常显示状态:
计时器正常显示状态

重影状态:
启动计时器后的重影状态

原因分析

  1. 透明组件双缓冲失效:Swing默认双缓冲机制在处理半透明背景(new Color(50,50,50,150))时存在缺陷,父面板背景无法被正确清除,每次重绘都会叠加旧渲染内容,形成重影。
  2. 自定义组件重绘逻辑不完整:虽然CustomProgressBar调用了super.paintComponent(g),但在透明父容器下,Swing的重绘区域更新机制未正确覆盖所有需刷新区域。
  3. 线程调度冲突:使用java.util.Timer配合SwingUtilities.invokeLater虽能保证EDT线程更新UI,但相比Swing原生javax.swing.Timer,高频UI更新场景下易导致重绘队列延迟,加重重影。

解决方案

1. 给悬浮面板启用手动双缓冲并正确绘制背景

重写SmudgeTimerPanel的paintComponent方法,手动绘制透明背景,确保每次重绘前清除旧内容:

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g.create();
    // 绘制透明背景,覆盖整个面板区域
    g2d.setColor(getBackground());
    g2d.fillRect(0, 0, getWidth(), getHeight());
    g2d.dispose();
}

同时在构造函数中开启双缓冲:

setDoubleBuffered(true);

2. 优化自定义进度条重绘逻辑

确保CustomProgressBar每次重绘时完全覆盖自身区域,避免残留旧内容:

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g.create();
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    int width = getWidth();
    int height = getHeight();

    // 先清除当前组件区域
    g2d.setColor(new Color(0,0,0,0));
    g2d.fillRect(0, 0, width, height);

    // 绘制进度条背景
    g2d.setColor(Color.GRAY);
    g2d.fillRect(0, 0, width, height);

    // 绘制进度
    int progressWidth = (int) ((value / (double) maximum) * width);
    g2d.setColor(Color.WHITE);
    g2d.fillRect(0, 0, progressWidth, height);

    // 绘制刻度线
    g2d.setColor(Color.BLACK);
    int demonMark = (int) (((TIMER_SPIRIT - TIMER_DEMON) / (double) maximum) * width);
    int otherMark = (int) ((TIMER_OTHER / (double) maximum) * width);

    g2d.drawLine(demonMark, 0, demonMark, height);
    g2d.drawLine(otherMark, 0, otherMark, height);

    g2d.dispose();
}

3. 替换为Swing原生Timer

使用javax.swing.Timer替代java.util.Timer,天生适配Swing事件调度线程(EDT),避免线程调度导致的重绘延迟:

// 替换resetTimer方法中的Timer部分
timer = new Timer(1000, e -> {
    if (remainingTime > 0) {
        remainingTime--;
        progressBar.setValue(remainingTime);
        updateTimeDisplay();
        updateGhostLabel();
    } else {
        stopTimer();
        ((Timer) e.getSource()).stop();
    }
});
timer.start();

修改stopTimer方法:

public void stopTimer() {
    if (timer != null) {
        timer.stop();
    }
    isRunning = false;
    statusLabel.setBackground(Color.RED);
}

完整修复代码

import lombok.extern.slf4j.Slf4j;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;

@Slf4j
public class SmudgeTimerPanel extends JPanel {

    private static final int TIMER_SPIRIT = 180; // seconds
    private static final int TIMER_DEMON = 60; // seconds
    private static final int TIMER_OTHER = 90; // seconds

    private final CustomProgressBar progressBar;
    private final JLabel timeRemainingLabel;
    private final JLabel statusLabel;
    private final JLabel ghostLabel;

    private Timer timer;
    private int remainingTime;
    private boolean isRunning;

    public SmudgeTimerPanel() {
        setLayout(new GridBagLayout());
        setBackground(new Color(50, 50, 50, 150)); // Transparent gray background
        setDoubleBuffered(true); // 启用双缓冲

        progressBar = new CustomProgressBar(0, TIMER_SPIRIT);

        timeRemainingLabel = new JLabel("3:00");
        timeRemainingLabel.setForeground(Color.WHITE);
        timeRemainingLabel.setOpaque(false); // 设置为非不透明
        timeRemainingLabel.setFont(timeRemainingLabel.getFont().deriveFont(24f));

        statusLabel = new JLabel();
        statusLabel.setOpaque(true);
        statusLabel.setBackground(Color.RED);
        statusLabel.setPreferredSize(new Dimension(10, 10));

        ghostLabel = new JLabel("None");
        ghostLabel.setForeground(Color.WHITE);
        ghostLabel.setOpaque(false); // 设置为非不透明
        ghostLabel.setFont(ghostLabel.getFont().deriveFont(24f));

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.insets = new Insets(10, 10, 10, 10);
        add(progressBar, gbc);

        gbc.gridy = 1;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.anchor = GridBagConstraints.CENTER;

        JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
        statusPanel.setOpaque(false);
        statusPanel.add(statusLabel);
        statusPanel.add(timeRemainingLabel);
        add(statusPanel, gbc);

        gbc.gridy = 2;
        gbc.gridx = 0;
        gbc.fill = GridBagConstraints.NONE;
        gbc.anchor = GridBagConstraints.CENTER;
        add(ghostLabel, gbc);

        gbc.gridy = 3;
        gbc.fill = GridBagConstraints.HORIZONTAL;

        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
        buttonPanel.setOpaque(false);

        JButton startResetButton = new JButton("Start/Reset Timer");
        startResetButton.addActionListener(e -> resetTimer(TIMER_SPIRIT));

        JButton stopButton = new JButton("Stop Timer");
        stopButton.addActionListener(e -> stopTimer());

        buttonPanel.add(startResetButton);
        buttonPanel.add(stopButton);
        add(buttonPanel, gbc);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g.create();
        // 手动绘制透明背景,确保覆盖整个面板
        g2d.setColor(getBackground());
        g2d.fillRect(0, 0, getWidth(), getHeight());
        g2d.dispose();
    }

    public void resetTimer(int timerDuration) {
        if (timer != null) {
            timer.stop();
        }

        remainingTime = timerDuration;
        isRunning = true;
        updateGhostLabel();
        updateTimeDisplay();
        progressBar.setMaximum(timerDuration);
        progressBar.setValue(timerDuration);
        statusLabel.setBackground(Color.GREEN);

        // 使用Swing Timer替代java.util.Timer
        timer = new Timer(1000, e -> {
            if (remainingTime > 0) {
                remainingTime--;
                progressBar.setValue(remainingTime);
                updateTimeDisplay();
                updateGhostLabel();
            } else {
                stopTimer();
                ((Timer) e.getSource()).stop();
            }
        });
        timer.start();
    }

    public void stopTimer() {
        if (timer != null) {
            timer.stop();
        }
        isRunning = false;
        statusLabel.setBackground(Color.RED);
    }

    public void startOrResetTimer(String ghostType) {
        resetTimer(TIMER_SPIRIT);
    }

    private void updateTimeDisplay() {
        int minutes = remainingTime / 60;
        int seconds = remainingTime % 60;
        timeRemainingLabel.setText(String.format("%d:%02d", minutes, seconds));
    }

    private void updateGhostLabel() {
        if (remainingTime > TIMER_OTHER) {
            ghostLabel.setText("Spirit");
        } else if (remainingTime > TIMER_DEMON) {
            ghostLabel.setText("All Others");
        } else {
            ghostLabel.setText("Demon");
        }
    }

    public boolean isTimerRunning() {
        return isRunning;
    }

    @Slf4j
    private static class CustomProgressBar extends JComponent {
        private int value;
        private int maximum;

        public CustomProgressBar(int min, int max) {
            this.value = min;
            this.maximum = max;
            setPreferredSize(new Dimension(300, 20));
            setMinimumSize(new Dimension(300, 20));
            setOpaque(false); // 设置为非不透明
        }

        public void setValue(int value) {
            this.value = Math.min(value, maximum);
            repaint();
        }

        public void setMaximum(int maximum) {
            this.maximum = maximum;
            repaint();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            int width = getWidth();
            int height = getHeight();

            // 先清除当前区域
            g2d.setColor(new Color(0, 0, 0, 0));
            g2d.fillRect(0, 0, width, height);

            // 绘制进度条背景
            g2d.setColor(Color.GRAY);
            g2d.fillRect(0, 0, width, height);

            // 绘制进度
            int progressWidth = (int) ((value / (double) maximum) * width);
            g2d.setColor(Color.WHITE);
            g2d.fillRect(0, 0, progressWidth, height);

            // 绘制刻度线
            g2d.setColor(Color.BLACK);
            int demonMark = (int) (((TIMER_SPIRIT - TIMER_DEMON) / (double) maximum) * width);
            int otherMark = (int) ((TIMER_OTHER / (double) maximum) * width);

            g2d.drawLine(demonMark, 0, demonMark, height);
            g2d.drawLine(otherMark, 0, otherMark, height);

            g2d.dispose();
        }
    }

    // Test the SmudgeTimerPanel in its own JFrame
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Smudge Timer Test");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(400, 300);

            SmudgeTimerPanel timerPanel = new SmudgeTimerPanel();
            frame.add(timerPanel);
            frame.setVisible(true);
        });
    }
}

额外注意事项

  • 所有非不透明组件需设置setOpaque(false),避免Swing默认背景绘制干扰透明效果。
  • 悬浮层窗口建议设置setUndecorated(true)和setAlwaysOnTop(true),同时确保窗口背景透明处理正确(如Java 7+的Window.setOpacity)。

内容的提问来源于stack exchange,提问作者Jordan Renaud

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 03:42:02