Swing UI元素背景重影问题:原因排查与解决求助
游戏悬浮计时器UI重影问题排查与修复
问题描述
正在制作一款游戏悬浮层(Overlay),包含带开始/重置、停止快捷键的简易计时器,用自定义进度条展示计时进度和关键节点。但启动计时器、计时跳动或停止时会出现UI重影,调整窗口大小可临时消除,但后续操作会再次触发。
正常显示状态:
重影状态:
原因分析
- 透明组件双缓冲失效:Swing默认双缓冲机制在处理半透明背景(
new Color(50,50,50,150))时存在缺陷,父面板背景无法被正确清除,每次重绘都会叠加旧渲染内容,形成重影。 - 自定义组件重绘逻辑不完整:虽然
CustomProgressBar调用了super.paintComponent(g),但在透明父容器下,Swing的重绘区域更新机制未正确覆盖所有需刷新区域。 - 线程调度冲突:使用
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
相关产品推荐
相关产品推荐

