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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 02:02:07