Java Swing启动屏JLabel在JProgressBar上重复渲染问题排查
问题描述
我开发了一款简易启动屏应用,用于启动各类程序,该应用是一个仅包含4个元素的JFrame:
- 背景图片
- Logo图片
- ProgressBar
- 显示当前状态的文本消息
SplashScreen类代码
package com.example; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.plaf.basic.BasicProgressBarUI; import java.awt.*; import java.awt.image.BufferedImage; import java.io.InputStream; import java.util.TimerTask; public class SplashScreen extends JFrame { private final JPanel contentPane = new JPanel(); private final JLabel jLabel = new JLabel(""); private final JProgressBar jProgressBar = new JProgressBar(SwingConstants.HORIZONTAL, 0, 100); private String message; private int dots = 0; public SplashScreen() { setIconImage(Toolkit.getDefaultToolkit().getImage(SplashScreen.class.getResource("/favicon.png"))); Dimension dimension = new Dimension(600, 350); this.setSize(dimension); setLocationRelativeTo(null); setUndecorated(true); getRootPane().setWindowDecorationStyle(JRootPane.NONE); contentPane.setLayout(null); setContentPane(contentPane); jProgressBar.setBackground(new Color(255, 255, 255, 50)); jProgressBar.setStringPainted(true); jProgressBar.setBorderPainted(false); jProgressBar.setValue(0); jProgressBar.setBounds(50, 270, 500, 30); jProgressBar.setForeground(Color.WHITE); jProgressBar.setUI(new BasicProgressBarUI() { protected Color getSelectionBackground() { return new Color(0x66,0x10,0xf2); } protected Color getSelectionForeground() { return new Color(0x6f,0x42,0xc1); } }); contentPane.add(jProgressBar); jLabel.setForeground(Color.WHITE); jLabel.setFont(new Font("Arial", Font.PLAIN, 20)); jLabel.setBounds(50, 200, 500, 30); jLabel.setVerticalAlignment(SwingConstants.TOP); jLabel.setLabelFor(jProgressBar); contentPane.add(jLabel); JImage logo = new JImage("images/logo.png"); logo.setBounds(141,50,318, 118); contentPane.add(logo); JImage background = new JImage("images/background.jpg"); background.setSize(dimension); background.setLocation(0, 0); contentPane.add(background); contentPane.validate(); java.util.Timer timer = new java.util.Timer(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { dots = (dots + 1) % 4; jLabel.setText(getMessage()); contentPane.validate(); } }, 0, 300); } public void setMessage(String message) { this.message = message; jLabel.setText(getMessage()); contentPane.validate(); } public void setProgress(int progress) { jProgressBar.setValue(progress); contentPane.validate(); } private String getMessage() { return this.message + new String(new char[this.dots]).replace("\0", "."); } public static class JImage extends JLabel { private ImageIcon image; public JImage(String path){ super(); setIcon(path); } public void setIcon(String path) { image = load(path); super.setIcon(image); } @Override public void setIcon(Icon icon) { super.setIcon(icon); if (icon instanceof ImageIcon) image = ((ImageIcon) icon); else image = null; } @Override public void paint(Graphics g){ g.drawImage(image == null ? null : image.getImage(), 0, 0, this.getWidth(), this.getHeight(), null); } private ImageIcon load(String path) { BufferedImage image = null; try { ClassLoader loader = Thread.currentThread().getContextClassLoader(); InputStream stream = loader.getResourceAsStream(path); image = ImageIO.read(stream); stream.close(); } catch (Exception e) { System.out.println("E/ Failed to load image"); e.printStackTrace(); } return new ImageIcon(image); } } }
主类代码
public static void main(String[] args) throws IOException, InterruptedException { SplashScreen window = new SplashScreen(); window.setVisible(true); Runtime runtime = Runtime.getRuntime(); int percent = 0; for (State state: cycle) { window.setMessage(state.message); runtime.exec(start(state.title, state.path, state.command)); TimeUnit.MILLISECONDS.sleep(state.timeout); percent += state.progress; window.setProgress(percent); } TimeUnit.MILLISECONDS.sleep(2000); System.exit(0); } private static String start(String title, String path, String command){ return String.format("cmd /c start \"%s\" /d \"%s\" /min cmd.exe /k \"%s\"", title, path, command); }
异常现象
运行时启动屏先在正确位置渲染JLabel,随后会将其重新渲染到JProgressBar上;移动ProgressBar时,文本会跟随其位置;移除JLabel后,ProgressBar开始自身重复渲染。
问题根源与解决方案
1. 自定义JImage的绘制逻辑破坏Swing渲染链
你重写了JImage的paint方法但未调用super.paint(g),这会导致Swing组件的绘制流程断裂,父组件的绘制逻辑无法正常执行,进而引发组件渲染错乱。
修复方式:
要么删除自定义的paint方法(JLabel本身已支持图标绘制,该重写完全多余),要么在方法开头调用父类绘制方法:
@Override public void paint(Graphics g){ super.paint(g); // 保证Swing绘制链完整 g.drawImage(image == null ? null : image.getImage(), 0, 0, this.getWidth(), this.getHeight(), null); }
2. 组件添加顺序错误
背景图片被最后添加到面板,本应作为底层背景,却因为绘制异常干扰了上层组件。正确的顺序应该是先添加背景,再添加其他组件。
修复方式:
// 先添加背景 contentPane.add(background); // 再依次添加logo、进度条、文本标签 contentPane.add(logo); contentPane.add(jProgressBar); contentPane.add(jLabel);
3. 使用非EDT线程更新UI
java.util.Timer的任务运行在非事件调度线程(EDT)中,而Swing的UI更新必须在EDT中执行,线程不安全的更新会导致渲染异常。
修复方式:
替换为Swing专用的javax.swing.Timer:
// 替换原java.util.Timer Timer timer = new Timer(300, e -> { dots = (dots + 1) % 4; jLabel.setText(getMessage()); }); timer.start();
4. 滥用validate()调用
频繁调用contentPane.validate()会强制组件重新布局,干扰正常渲染流程。更新文本或进度条时,组件自身的setText/setValue方法会自动触发重绘,无需额外调用validate()。
修复方式:
移除setMessage和setProgress方法中的contentPane.validate()调用:
public void setMessage(String message) { this.message = message; jLabel.setText(getMessage()); } public void setProgress(int progress) { jProgressBar.setValue(progress); }
5. 避免使用null布局
null布局完全依赖手动设置组件位置和大小,极易出现布局错乱。建议使用BorderLayout、GridBagLayout等布局管理器来管理组件位置,减少手动操作bounds的需求。
内容的提问来源于stack exchange,提问作者galalem

