首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計綜合問答

Java Swing 繪圖機制深度解析:paint() 調用時機、雙重調用原因與空指針避坑

     編輯:程式設計綜合問答

問題現象

自學 Java Swing 時,繼承 `JFrame` 重寫 `paint(Graphics g)` 方法繪製背景圖片。運行發現:

  1. 控制台輸出顯示 `paint()` 被調用了兩次
  2. 第一次調用時 `nowBG` 為 `null`,導致 `NullPointerException`
  3. 去掉 `if (nowBG != null)` 判斷直接繪圖會拋出異常

核心原理:Swing 繪圖機制與組件生命周期

1. paint() 調用時機

`paint()` 不是開發者主動調用,而是由 **Swing 繫統在以下情況自動觸發**:

  • 組件首次顯示:`setVisible(true)` / `pack()`
  • 窗口狀態變化:最小化/最大化/恢復/調整大小
  • 組件被遮擋後重新暴露
  • 調用 `repaint()` 请求重繪
  • 系統判定組件為「髒」狀態需要更新

2. 為什麼調用兩次?

代碼關鍵順序:

public myFrame() {
    // ... 初始化代碼 ...
    this.setVisible(true);  // 第1次觸發 paint() → 此時 nowBG 還未初始化!
    
    StaticValue.init();     // 初始化圖片資源
    for(...) allBG.add(...); // 創建場景
    this.nowBG = allBG.get(0); // nowBG 賦值
    
    this.repaint();         // 第2次觸發 paint() → 此時 nowBG 已就緒
}

關鍵錯誤:`setVisible(true)` **必須放在所有組件初始化完成之後**!

3. 正確的構造器順序

public myFrame() {
    // 1. 基礎屬性設置
    this.setTitle("馬里奧遊戲程序");
    this.setSize(900, 600);
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setResizable(false);
    
    // 2. 資源初始化(在顯示之前!)
    StaticValue.init(); // 載入圖片等靜態資源
    for (int i = 1; i <= 3; i++) {
        this.allBG.add(new Background(i, i == 3));
    }
    this.nowBG = this.allBG.get(0);
    
    // 3. 事件監聽器註冊
    this.addKeyListener(this);
    
    // 4. 最後顯示窗口(此時 paint() 會被調用,nowBG 已就緒)
    this.setVisible(true);
}

雙緩衝繪圖正確實現

原代碼在 `paint()` 中創建 `BufferedImage` 屬於「主動繪圖」模式,但 Swing 推薦「被動繪圖」+ 雙緩衝:

推薦做法:繼承 JPanel 重寫 paintComponent()

public class GamePanel extends JPanel {
    private Background currentBG;
    
    public GamePanel() {
        setDoubleBuffered(true); // 啟用雙緩衝(默認開啟)
        setPreferredSize(new Dimension(900, 600));
    }
    
    public void setBackground(Background bg) {
        this.currentBG = bg;
        repaint(); // 請求重繪,而非直接調用 paint
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g); // 關鍵:清除背景、繼承雙緩衝邏輯
        
        if (currentBG != null) {
            // 直接繪製到組件的 Graphics 上(Swing 已處理雙緩衝)
            g.drawImage(currentBG.getBgImage(), 0, 0, this);
        }
    }
}

// 主窗口使用
public class MyFrame extends JFrame {
    private GamePanel gamePanel;
    
    public MyFrame() {
        gamePanel = new GamePanel();
        add(gamePanel);
        
        // 初始化資源...
        gamePanel.setBackground(firstBG);
        
        pack(); // 自適應大小
        setVisible(true);
    }
}

關鍵最佳實踐總結

錯誤做法正確做法原因
繼承 JFrame 重寫 paint()繼承 JPanel 重寫 paintComponent()JPanel 專為繪圖設計,支持雙緩衝
在 paint() 中創建 BufferedImage直接用傳入的 Graphics 繪製Swing 已在底層實現雙緩衝,手動創建多餘且易出錯
構造器中先 setVisible(true) 再初始化先初始化所有資源,最後 setVisible(true)避免 paint() 在數據未就緒時被調用
去掉 null 判斷直接繪圖保留防禦性判斷 if (bg != null)應對異步初始化、資源加載失敗等異常情況
調用 paint() 手動觸發繪圖調用 repaint() 請求重繪repaint() 會合併請求、在 EDT 執行,性能更好

空指針異常根因與修復

// 原代碼異常點
g2.drawImage(this.nowBG.getBgImage(), 0, 0, this); // nowBG 為 null

// 修復:防禦性編程
@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (currentBG != null && currentBG.getBgImage() != null) {
        g.drawImage(currentBG.getBgImage(), 0, 0, this);
    }
}

總結

Java Swing 繪圖核心原則:

  1. 繼承 JPanel,重寫 paintComponent(),而非 JFrame.paint()
  2. 構造器最後調用 setVisible(true),確保數據就緒
  3. 使用 repaint() 觸發重繪,不要手動調用 paint 方法
  4. 依賴 Swing 內置雙緩衝(`setDoubleBuffered(true)`),不要手動創建 BufferedImage
  5. 防禦性檢查 null,應對異步初始化競態條件

掌握這些原理,能避開 90% 的 Swing 繪圖坑,實現流暢無閃爍的遊戲/動畫界面。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved