【问题标题】:How to overwrite a BufferedImage drawn to JPanel outside PaintComponent() with getGraphics()如何使用 getGraphics() 覆盖在 PaintComponent() 外部绘制到 JPanel 的 BufferedImage
【发布时间】:2018-03-18 09:06:34
【问题描述】:

我正在开发一个简单的 2D 游戏。每个滴答声,我都想检查一个效果队列,该队列将为特定效果启动一个线程(淡入淡出过渡,音频淡入淡出等)。例如,在菜单屏幕上按“播放”将向此队列添加“淡出”消息,该消息将被处理并启动一个线程以在我的 GamePanel 上绘制一个具有递增 alpha 值的黑色矩形。

我正在重写paintComponent() 并将我的Graphics 对象发送到我的GameStateManager,它会将Graphics 对象传递给当前状态的draw()。我目前没有将paintComponent() 图形对象路由到的效果状态(也许我应该),但我确实将我的游戏面板传递给我的效果线程,在那里我可以使用getGraphics() 来绘制它。直接在 GamePanel 上绘制一个矩形只会导致闪烁,因为游戏循环仍在渲染游戏。

我发现我可以在 BufferedImage 中绘制一个增加 alpha 的黑色矩形,将合成设置为 AlphaComposite.Src(这会导致新绘制替换旧绘制),然后在游戏面板上绘制 BufferedImage。问题是绘制到游戏面板的 BufferedImage 不会在每次绘制时都被覆盖,因此淡出发生得非常快,因为这些不同 alpha 的黑色 BufferedImage 只是相互堆叠。

我编写了这个简短的程序来测试复合设置并查看被覆盖的内容。所有的绘图都是在 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”上设置合成会导致 BufferedImage 的任何 alpha 消失,从而导致在先前存在的图像上绘制完全不透明的黑色图像。即使将颜色设置为黑色以外的颜色也没有效果。

另外有趣的是将 BufferedImage 的合成设置为:

oImageGraphics.setComposite(AlphaComposite.SrcIn);

不显示任何内容。关于在 Java2D 中合成图形的 Oracle 文档为“SrcIn”声明了这一点:

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

所以,我希望这具有与 AlphaComposite.Src 相同的行为。

也许有人可以解释一下这些复合材料发生了什么,以及我如何才能达到我想要的效果。

【问题讨论】:

  • 您不能覆盖面板中的 onDraw(Graphics g2d) 而不是执行此 draw() 方法吗?
  • @MarcosVasconcelos 是的,这就是我目前正在做的事情。但是,我在游戏中处理效果(过渡、屏幕和音频淡入淡出)的想法是让动作(单击菜单上的“播放”,或角色在某些图块上行走)将消息添加到队列中以进行检查每个游戏打勾。这些效果将触发一个线程并在那里进行处理,并且游戏循环可以继续进行。我目前没有将我的 paintComponent() 提供的 Graphics 对象路由到我的 Effects 类。这就是我在效果线程中使用 getGraphics 的原因
  • 根据您的代码,您根本没有覆盖paintComponent,而是使用getGraphics,这是不推荐的,因为您试图在摆动定义的油漆周期。您也不应该处置不是您创建的Graphics 上下文。我也看不出BufferedImage 的意义。对于动画在 Swing 中的工作方式,您似乎也没有任何概念
  • 根据我的理解(很少),您真正需要的是一个输入(没有应用效果)和输出,应用了效果。这意味着在每个绘制周期中,您都拥有原始的、未更改的图像,然后根据它们的属性和使用时间来应用效果

标签: java swing graphics alpha java-2d


【解决方案1】:

你“似乎”试图做的事情有很多问题

  • 不要在组件上调用getGraphics。这可以返回 null 并且只返回在 Swing 绘制周期中最后绘制的内容的快照。您在其上绘制的任何内容都将在下一个绘制周期中删除
  • 您也不应该处理不是您创建的 Graphics 上下文,这样做可能会影响 Swing 绘制的其他组件
  • 绘画是复合的,这意味着一遍又一遍地绘画到相同的Graphics上下文(或BufferedImage),将继续在之前绘画的顶部应用这些更改
  • 您似乎也不知道动画应该如何工作。与其尝试在单个通道中绘制您的淡入淡出效果,结果无法应用到屏幕上,您需要在每个循环中应用一个阶段,并允许在下一个通道运行之前将其更新到屏幕上。

以下是我所说的一个非常基本的示例。它需要一个“基础”图像(这可能是游戏的“基础”状态,但我使用的是静态图像)和顶部的绘画效果。

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 的上下文中调用了 ActionListers actionPerformed 方法,从而可以安全地更新 UI。它还提供了一种简单的同步方法,因为在调用actionPerformed 方法时无法绘制 UI

【讨论】:

  • 感谢您的回复。那么,您有一个一直在运行的计时器来检查效果列表吗?然后将当前背景绘制到 BufferedImage 上,然后在 after 上绘制效果,然后传回重新绘制到屏幕的主面板?我可以在我已有的游戏循环中使用这样的计时器吗?似乎可以使用其中之一。
  • 基本上 - 这里的基础图像可以通过动态创建,但想法是,每个绘画过程都从头开始
  • 所以,我总是必须从 paintComponent() 开始并以适当的方式路由该图形对象。
  • 如果你想使用 Swing,是的,你可以使用 BufferStrategy,它可以让你完全访问绘图过程
  • 为什么不鼓励使用 getGraphics?我认为只有在面板不可见时它才有可能返回 null。
猜你喜欢
  • 1970-01-01
  • 2015-03-12
  • 1970-01-01
  • 2022-01-21
  • 1970-01-01
  • 2014-10-02
  • 2021-04-14
  • 1970-01-01
  • 2019-07-28
相关资源
最近更新 更多