为JLabel设置ZOrder导致JFrame中组件闪烁的问题排查
Java Swing 2D游戏闪烁问题解决
问题根源
你的代码里导致闪烁的核心问题不是setComponentZOrder,而是以下几个关键错误:
- 主线程阻塞:用
while(close == false)循环在主线程跑游戏逻辑,完全阻塞了Swing的事件调度线程(EDT),组件重绘、事件处理都被打乱,必然出现闪烁。 - 组件无限堆积:加载动画时每次都新建
JLabel并添加到窗口,旧的组件没有移除,容器里的组件越来越多,重绘时需要处理大量元素,导致卡顿和闪烁。 - 频繁无意义重绘:循环里反复调用
revalidate()和repaint(),加重了EDT的负担。 - Null布局的低效:手动设置
setBounds的null布局,Swing无法对组件重绘做优化,也是闪烁的诱因之一。
修复方案
1. 用Swing Timer替代无限循环
Swing Timer是专门用于EDT中执行定时任务的工具,不会阻塞线程,能保证UI更新在正确的线程中进行。
2. 复用组件而非重复创建
动画元素只创建一次,每次更新时替换ImageIcon即可,避免组件堆积。
3. 启用全局双缓冲
给JFrame的内容面板启用双缓冲,减少闪烁。
4. 优化ZOrder使用
确保层级设置只在组件初始化时做一次,不要频繁修改。
5. 避免手动调用revalidate/repaint
组件属性变更后Swing会自动触发重绘,除非必要不要手动调用。
修改后的完整代码
import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.image.BufferedImage; import java.io.*; import javax.imageio.*; import javax.swing.*; import java.util.*; public class World implements KeyListener { protected JFrame console = new JFrame("Tower of Fiction"); protected ArrayList<JLabel> displayObjects = new ArrayList<>(); // 复用的加载动画标签 private JLabel loadingLabel; private int ticker2 = 21; private Timer loadingTimer; private boolean close = false; private boolean screen2 = false; private int menuOption = 0; public World() throws IOException, FontFormatException { console.setSize(1920, 1080); console.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); console.setLayout(null); console.addKeyListener(this); // 启用全局双缓冲 console.getContentPane().setDoubleBuffered(true); // 初始化菜单文本 drawText("Start Game", "BLKCHCRY.TTF", 100f, 201, 193, 59, 750, 440, 1); drawText("Settings", "BLKCHCRY.TTF", 100f, 201, 193, 59, 828, 605, 1); displayObjects.get(1).setVisible(false); drawText("Quit Game", "BLKCHCRY.TTF", 100f, 201, 193, 59, 762, 775, 1); displayObjects.get(2).setVisible(false); // 背景图放在最底层(order=0) drawImage("Animations and Images/Images/PNG/Fictionville Home Screen (First Time).png", 0, 0, 0); // 初始化加载动画标签(先隐藏) loadingLabel = new JLabel(); loadingLabel.setBounds(0, 0, 1920, 1080); console.add(loadingLabel); console.setComponentZOrder(loadingLabel, 1); // 加载动画放在最上层 loadingLabel.setVisible(false); console.setVisible(true); } // 绘制图片(复用逻辑) public void drawImage(String imagePath, int xCord, int yCord, int order) throws IOException { BufferedImage image = ImageIO.read(new File(imagePath)); JLabel asset = new JLabel(new ImageIcon(image)); Dimension size = asset.getPreferredSize(); asset.setBounds(xCord, yCord, size.width, size.height); console.add(asset); console.setComponentZOrder(asset, order); displayObjects.add(asset); } // 绘制文本 public void drawText(String text, String fontPath, float size, int R, int G, int B, int xCord, int yCord, int order) throws FontFormatException, IOException { JLabel label = new JLabel(text); Font fontStyle = Font.createFont(Font.TRUETYPE_FONT, getClass().getResourceAsStream(fontPath)).deriveFont(size); GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(fontStyle); label.setForeground(new Color(R, G, B)); label.setFont(fontStyle); Dimension sizeOfWords = label.getPreferredSize(); label.setBounds(xCord, yCord, sizeOfWords.width, sizeOfWords.height); console.add(label); console.setComponentZOrder(label, order); displayObjects.add(label); } // 启动加载动画 private void startLoadingAnimation() { ticker2 = 21; loadingLabel.setVisible(true); // 用Swing Timer定时更新动画 loadingTimer = new Timer(100, e -> { try { BufferedImage frame = ImageIO.read(new File( "Animations and Images/Animations/GIF/Load Screen Animation/Loading Screen" + ticker2 + ".png")); loadingLabel.setIcon(new ImageIcon(frame)); ticker2--; if (ticker2 == 0) { loadingTimer.stop(); loadingLabel.setVisible(false); screen2 = false; } } catch (IOException ex) { ex.printStackTrace(); } }); loadingTimer.start(); } @Override public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if (!screen2) { if (keyCode == KeyEvent.VK_DOWN) { displayObjects.get(menuOption).setVisible(false); menuOption = menuOption == 2 ? 0 : menuOption + 1; displayObjects.get(menuOption).setVisible(true); } else if (keyCode == KeyEvent.VK_UP) { displayObjects.get(menuOption).setVisible(false); menuOption = menuOption == 0 ? 2 : menuOption - 1; displayObjects.get(menuOption).setVisible(true); } if (menuOption == 2 && keyCode == KeyEvent.VK_ENTER) { close = true; console.dispose(); } } if (menuOption == 0 && (keyCode == KeyEvent.VK_SPACE || keyCode == KeyEvent.VK_ENTER)) { if (!screen2) { screen2 = true; startLoadingAnimation(); } } } @Override public void keyReleased(KeyEvent e) {} @Override public void keyTyped(KeyEvent e) {} public static void main(String[] args) { // 确保UI初始化在EDT中 SwingUtilities.invokeLater(() -> { try { new World(); } catch (IOException | FontFormatException ex) { ex.printStackTrace(); } }); } }
额外注意事项
- 所有Swing组件的创建和更新都要放在EDT中,用
SwingUtilities.invokeLater()包裹初始化代码是标准做法。 - 尽量避免使用null布局,复杂UI可以用
OverlayLayout或者CardLayout来管理层级和页面切换,比手动设置setBounds高效得多。 - 如果后续游戏逻辑复杂,建议自定义
JPanel并重写paintComponent()方法来绘制所有游戏元素,这是Swing开发游戏的标准模式,比用JLabel组件更灵活高效。
内容的提问来源于stack exchange,提问作者asdadada
相关产品推荐
相关产品推荐

