如何使用getGraphics()覆盖在PaintComponent()外部绘制到JPanel的BufferedImage()

如何使用getGraphics()覆盖在PaintComponent()外部绘制到JPanel的BufferedImage()

问题描述:

我正在开发一个简单的2D游戏。每个勾号,我想检查一个效果队列,它将启动一个特定效果的线程(淡入淡出转换,音频淡入淡出等)。例如,按下菜单屏幕上的“播放”将向该队列添加一个“FadeOut”消息,该消息将被处理,并启动一个线程来绘制一个黑色矩形,并在我的GamePanel上增加一个alpha值。我将重写paintComponent()并将我的Graphics对象发送给我的GameStateManager,GameStateManager将Graphics对象传递给当前状态的draw()。我目前没有一个效果状态(也许我应该)将paintComponent()图形对象路由到,但是我将我的gamepanel传递给了我的效果线程,我可以在其中使用getGraphics()来绘制它。由于游戏循环仍然在渲染游戏,所以直接绘制矩形到GamePanel只会导致闪烁。如何使用getGraphics()覆盖在PaintComponent()外部绘制到JPanel的BufferedImage()

我发现我可以绘制一个带有增加alpha的BufferedImage的黑色矩形,将复合设置为AlphaComposite.Src(这会导致新绘图替换旧绘图),然后通过游戏面板绘制BufferedImage。问题是绘制到游戏面板的BufferedImages不会被每个绘制覆盖,所以淡出很快就会发生,因为这些不同alpha的黑色BufferedImages彼此堆叠在一起。

我写了这个简短的程序来测试复合设置,看看什么是重写。所有绘图都在draw()中完成,这将是我在效果线程中的run()。

import java.awt.AlphaComposite; 
import java.awt.Color; 
import java.awt.Graphics2D; 
import java.awt.image.BufferedImage; 
import javax.swing.JFrame; 
import javax.swing.JPanel; 

public class ScratchPad extends JPanel implements Runnable 
{ 
    private JFrame  oFrame   = null; 
    private Thread  oGameThread = null; 
    private Graphics2D oPanelGraphics = null; 
    private Graphics2D oImageGraphics = null; 
    private BufferedImage oImage   = null; 

public static void main(String args[]) throws Exception 
{ 
    new ScratchPad(); 
} 


public ScratchPad() 
{ 
    createFrame(); 
    initPanel(); 
    addAndShowComponents(); 

    oGameThread = new Thread(this, "Game_Loop"); 
    oGameThread.start(); 
} 


private void addAndShowComponents() 
{ 
    oFrame.add(this); 
    oFrame.setVisible(true); 
} 


private void initPanel() 
{ 
    this.setOpaque(true); 
    this.setBackground(Color.cyan); 
} 


private void createFrame() 
{ 
    oFrame = new JFrame("Fade"); 
    oFrame.setSize(700, 300); 
    oFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
    oFrame.setLocationRelativeTo(null); 
} 


public void run() 
{ 
    oImage = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB); 

    while(true) 
    { 
    try 
    { 
     draw(); 
     Thread.sleep(100); 
    } 
    catch(InterruptedException e) 
    { 

    } 
    } 
} 


private void draw() 
{ 
    oPanelGraphics = (Graphics2D)this.getGraphics(); 
    oImageGraphics = oImage.createGraphics(); 

    oImageGraphics.setComposite(AlphaComposite.Src); 

    oImageGraphics.setColor(new Color(0,0,0,90)); 
    oImageGraphics.fillRect(0, 0, oImage.getWidth(), oImage.getHeight()); 
    oPanelGraphics.drawImage(oImage, 10, 10, null); 

    oImageGraphics.setColor(new Color(0,0,0,60)); 
    oImageGraphics.fillRect(0, 0, oImage.getWidth(), oImage.getHeight()); 
    oPanelGraphics.drawImage(oImage, 220, 10, null); 

    oImageGraphics.setColor(new Color(0,0,0,30)); 
    oImageGraphics.fillRect(0, 0, oImage.getWidth(), oImage.getHeight()); 
    oPanelGraphics.drawImage(oImage, 430, 10, null); 

// Drawing this image over location of first image, should overwrite first 
// after setting composite to 'Src' 

oPanelGraphics.setComposite(AlphaComposite.Src); 
oImageGraphics.setColor(new Color(0,0,0,10)); 
oImageGraphics.fillRect(0, 0, oImage.getWidth(), oImage.getHeight()); 
oPanelGraphics.drawImage(oImage, 10, 10, null); 

oImageGraphics.dispose(); 
oPanelGraphics.dispose(); 
} 
} // end class 

有趣的是设定在“oPanelGraphics”复合引起的任何Alpha使用BufferedImage走开,导致完全不透明的黑色图像上画出来,这是以前没有的图像。即使将颜色设置为黑色以外的其他颜色也没有效果。

什么也有趣的是,对于BufferedImage的设定合成到:

oImageGraphics.setComposite(AlphaComposite.SrcIn); 

导致什么可显示。关于在Java2D中合成图形的Oracle文档声明了'SrcIn':

“如果源和目标中的像素重叠,则只会渲染重叠区域中的源像素。”

因此,我希望这与AlphaComposite.Src获得的行为相同。

也许有人在那里可以揭示这些复合材料正在发生什么,以及如何实现我想要的效果。

+0

无法在面板中重写onDraw(Graphics g2d)而不是执行此draw()方法吗? –

+0

@MarcosVasconcelos是的,那就是我目前正在做的。然而,我在游戏中处理效果的想法(转换,屏幕和音频淡入和淡出)是有动作的(点击菜单上的“播放”或角色在某个图块上行走)将消息添加到被检查的队列每场比赛打勾。这些效果会触发一个线程并在那里进行处理,并且游戏循环可以继续。我目前没有将我的paintComponent()提供的Graphics对象路由到我的Effects类。这就是为什么我在效果线程中使用getGraphics – WhitJackman

+0

根据你的代码,你根本不重写'paintComponent',而是使用'getGraphics',这是不推荐的,因为你试图更新UI之外的UI Swing定义的绘画循环的上下文。你也不应该处理你没有创建的'Graphics'上下文。我也没有看到'BufferedImage'的重点。你也似乎没有任何关于动画假设如何在Swing中工作的概念 – MadProgrammer

有你“似乎”有什么问题的数量要尝试做

  • 不要叫getGraphics一个组件上。这可以返回null,并且只返回在Swing绘制循环中上次绘制内容的快照。任何你画它会在下油漆周期
  • 被删除你也永远不应该丢弃你没有创建,这样做可能会影响由摇摆
  • 绘画是以复利画其他组件Graphics背景下,这意味着一次又一次地绘制到相同的Graphics上下文(或BufferedImage),将继续将这些更改应用于之前绘制的顶部
  • 您似乎也没有关于动画应如何工作的概念。与其试图一次性绘制淡入淡出效果,而不能将结果应用于屏幕,您需要在每个循环上应用一个阶段,并允许在下一次运行之前将其更新到屏幕。

以下是我正在谈论的一个非常基本的例子。它需要一个“基础”图像(这可能是游戏的“基础”状态,但我已经使用了静态图像)以及顶部的涂料效果。

Fade to black

import java.awt.AlphaComposite; 
import java.awt.Color; 
import java.awt.Dimension; 
import java.awt.EventQueue; 
import java.awt.Graphics; 
import java.awt.Graphics2D; 
import java.awt.Image; 
import java.awt.event.ActionEvent; 
import java.awt.event.ActionListener; 
import java.awt.event.MouseAdapter; 
import java.awt.event.MouseEvent; 
import java.awt.image.BufferedImage; 
import java.io.File; 
import java.io.IOException; 
import java.util.ArrayList; 
import java.util.Iterator; 
import java.util.List; 
import javax.imageio.ImageIO; 
import javax.swing.JFrame; 
import javax.swing.JPanel; 
import javax.swing.Timer; 
import javax.swing.UIManager; 
import javax.swing.UnsupportedLookAndFeelException; 

public class Test { 

    public static void main(String[] args) { 
     new Test(); 
    } 

    public Test() { 
     EventQueue.invokeLater(new Runnable() { 
      @Override 
      public void run() { 
       try { 
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 
       } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) { 
        ex.printStackTrace(); 
       } 

       JFrame frame = new JFrame("Testing"); 
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
       frame.add(new TestPane()); 
       frame.pack(); 
       frame.setLocationRelativeTo(null); 
       frame.setVisible(true); 
      } 
     }); 
    } 

    public class TestPane extends JPanel { 

     private Engine engine; 
     private Image frame; 

     public TestPane() { 
      engine = new Engine(); 
      engine.setEngineListener(new EngineListener() { 
       @Override 
       public void updateDidOccur(Image img) { 
        frame = img; 
        repaint(); 
       } 
      }); 
      engine.start(); 

      addMouseListener(new MouseAdapter() { 
       @Override 
       public void mouseClicked(MouseEvent e) { 
        engine.addEffect(new FadeOutEffect(Color.BLACK)); 
       } 
      }); 
     } 

     @Override 
     public Dimension getPreferredSize() { 
      return engine.getSize(); 
     } 

     @Override 
     protected void paintComponent(Graphics g) { 
      super.paintComponent(g); 
      if (frame != null) { 
       Graphics2D g2d = (Graphics2D) g.create(); 
       g2d.drawImage(frame, 0, 0, null); 
       g2d.dispose(); 
      } 
     } 

    } 

    public interface EngineListener { 

     public void updateDidOccur(Image img); 
    } 

    public class Engine { 

     // This is the "base" image, without effects 
     private BufferedImage base; 
     private Timer timer; 
     private EngineListener listener; 

     private List<Effect> effects = new ArrayList<Effect>(25); 

     public Engine() { 
      try { 
       base = ImageIO.read(new File("/Volumes/Big Fat Extension/Dropbox/MegaTokyo/megatokyo_omnibus_1_3_cover_by_fredrin-d4oupef 50%.jpg")); 
      } catch (IOException ex) { 
       ex.printStackTrace(); 
      } 

      timer = new Timer(10, new ActionListener() { 
       @Override 
       public void actionPerformed(ActionEvent e) { 
        int width = base.getWidth(); 
        int height = base.getHeight(); 
        BufferedImage frame = new BufferedImage(width, height, base.getType()); 
        Graphics2D g2d = frame.createGraphics(); 
        g2d.drawImage(base, 0, 0, null); 
        Iterator<Effect> it = effects.iterator(); 
        while (it.hasNext()) { 
         Effect effect = it.next(); 
         if (!effect.applyEffect(g2d, width, height)) { 
          it.remove(); 
         } 
        } 
        g2d.dispose(); 
        if (listener != null) { 
         listener.updateDidOccur(frame); 
        } 
       } 
      }); 
     } 

     public void start() { 
      timer.start(); 
     } 

     public void stop() { 
      timer.stop(); 
     } 

     public void addEffect(Effect effect) { 
      effects.add(effect); 
     } 

     public void setEngineListener(EngineListener listener) { 
      this.listener = listener; 
     } 

     public Dimension getSize() { 
      return base == null ? new Dimension(200, 200) : new Dimension(base.getWidth(), base.getHeight()); 
     } 

    } 

    public interface Effect { 
     public boolean applyEffect(Graphics2D context, int width, int height); 
    } 

    public class FadeOutEffect implements Effect { 

     private int tick = 0; 
     private Color fadeToColor; 

     public FadeOutEffect(Color fadeToColor) { 
      this.fadeToColor = fadeToColor; 
     } 

     @Override 
     public boolean applyEffect(Graphics2D context, int width, int height) { 
      tick++; 
      float alpha = (float) tick/100.0f; 
      if (alpha > 1.0) { 
       return false; 
      } 
      Graphics2D g2d = (Graphics2D) context.create(); 
      g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); 
      g2d.setColor(fadeToColor); 
      g2d.fillRect(0, 0, width, height); 
      g2d.dispose();   
      return true; 
     } 

    } 

} 

记住,每效应或变化应相同的“主循环”中被应用,这意味着你不应该有多个线程,实际上,因为Swing不是线程安全的,你如果可能的话应该避免有任何额外的线程此示例使用Swing Timer充当“主循环”,因为在EDT的上下文中调用方法,从而更新UI的安全性。它还提供了一个简单的同步方法,因为在调用actionPerformed方法时无法绘制UI

+0

感谢您的答复。所以,你有一个运行时间检查效果列表的计时器?然后将当前背景拖到BufferedImage上,然后在后面画出效果,然后传回重新绘制到屏幕的主面板?我可以使用这样的计时器吗?我已经使用了gameloop了吗?似乎可以使用任何一个或另一个。 – WhitJackman

+0

基本上 - 这里的基础图像可以通过动态创建,但是想法是,每个paint pass从头开始 – MadProgrammer

+0

因此,我总是必须从paintComponent()开始,并以适当的方式路由该图形对象。 – WhitJackman