如何将 BufferedImage 绘制到 JPanel

Posted

技术标签:

【中文标题】如何将 BufferedImage 绘制到 JPanel【英文标题】:How to Draw an BufferedImage to a JPanel 【发布时间】:2015-03-12 01:25:19 【问题描述】:

我正在尝试使用某种绘图方法将精灵图像绘制到我称为 AnimationPanel 的 JPanel 子类中。我创建了一个 Spritesheet 类,它可以生成一个包含工作表中所有精灵的 BufferedImage[]。在实现 Runnable 的 AnimationPanel 类中,我从 AnimationPanel 构造函数中实例化的 spritesheet 中获取 BufferedImage[]。我希望能够遍历这个数组并将每个精灵显示到屏幕上。我该怎么做?这是我的 AnimationPanel 和 Spritesheet 类。

AnimationPanel

package com.kahl.animation;

import javax.swing.JPanel;

public class AnimationPanel extends JPanel implements Runnable 

//Instance Variables
private Spritesheet sheet;
private int currentFrame;
private Thread animationThread;
private BufferedImage image;

public AnimationPanel(Spritesheet aSheet) 
    super();
    sheet = aSheet;
    setPreferredSize(new Dimension(128,128));
    setFocusable(true);
    requestFocus();



public void run() 
    BufferedImage[] frames = sheet.getAllSprites();
    currentFrame = 0;
    while (true) 
        frames[currentFrame].draw(); //some implementation still necessary here
        currentFrame++;
        if (currentFrame >= frames.length) 
            currentFrame = 0;
        
    


public void addNotify() 
    super.addNotify();
    if (animationThread == null) 
        animationThread = new Thread(this);
        animationThread.start();
    



Spritesheet

package com.kahl.animation;

import java.awt.image.BufferedImage;
import java.imageio.ImageIO;
import java.io.IOException;
import java.io.File;

public class Spritesheet 

//Instance Variables
private String path;
private int frameWidth;
private int frameHeight;
private int framesPerRow;
private int frames;
private BufferedImage sheet = null;

//Constructors
public Spritesheet(String aPath,int width,int height,int fpr, int numOfFrames) 

    path = aPath;
    frameWidth = width;
    frameHeight = height;
    framesPerRow = fpr;
    frames = numOfFrames;

    try 
        sheet = ImageIO.read(getClass().getResourceAsStream());
     catch (IOException e) 
        e.printStackTrace();
    



//Methods

public int getHeight() 
    return frameWidth;


public int getWidth() 
    return frameWidth;


public int getFramesPerRow() 
    return framesPerRow;


private BufferedImage getSprite(int x, int y, int h, int w) 
    BufferedImage sprite = sheet.getSubimage(x,y,h,w);


public BufferedImage[] getAllSprites() 
    BufferedImage[] sprites = new BufferedImage[frames];
    int y = 0;
    for (int i = 0; i < frames; i++) 
        x = i * frameWidth;
        currentSprite = sheet.getSprite(x,y,frameHeight,frameWidth);
        sprites.add(currentSprite);
    
    return sprites;




【问题讨论】:

1) 为了尽快获得更好的帮助,请发布MCVE(最小完整可验证示例)或SSCCE(简短、自包含、正确示例)。严格来说,一个 MCVE 只能有一个源文件和一个 public 类。它还需要一个main(string[]) 方法来运行它。 2) 例如,获取图像的一种方法是热链接到在this Q&A 中看到的图像。 唯一的问题是我还没有让它运行。实际运行的部分是我遇到问题的部分。不过,我会在未来牢记这一点。谢谢你的信息。 【参考方案1】:
    我鼓励使用javax.swing.Timer 来控制帧速率,而不是不受控制的循环 一旦计时器“滴答”,您需要增加当前帧,获取要渲染的当前图像并在JPanel 上调用repaint 使用Graphics#drawImage 渲染图像。

看...

Painting in AWT and Swing Performing Custom Painting How to use Swing Timers Graphics#drawImage(Image, int, int, ImageObserver)

更多详情

您的Spritesheet 类存在一系列级联问题,除了它实际上不会编译之外,还有一些问题是您从某些方法返回错误值并依赖于更好计算的值。 ..

我不得不修改你的代码,大部分我都不记得了

public int getHeight() 
    return frameWidth;

public BufferedImage[] getAllSprites() 
    BufferedImage[] sprites = new BufferedImage[frames];
    int y = 0;
    for (int i = 0; i < frames; i++) 
        x = i * frameWidth;
        currentSprite = sheet.getSprite(x,y,frameHeight,frameWidth);
        sprites.add(currentSprite);
    
    return sprites;


作为两个主要示例脱颖而出...

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
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 TestSpriteSheet 

    public static void main(String[] args) 
        new TestSpriteSheet();
    

    public TestSpriteSheet() 
        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 Spritesheet spritesheet;
        private BufferedImage currentFrame;
        private int frame;

        public TestPane() 
            spritesheet = new Spritesheet("/Sheet02.gif", 240, 220);
            Timer timer = new Timer(100, new ActionListener() 
                @Override
                public void actionPerformed(ActionEvent e) 
                    currentFrame = spritesheet.getSprite(frame % spritesheet.getFrameCount());
                    repaint();
                    frame++;
                
            );
            timer.start();
        

        @Override
        public Dimension getPreferredSize() 
            return new Dimension(240, 220);
        

        @Override
        protected void paintComponent(Graphics g) 
            super.paintComponent(g);
            if (currentFrame != null) 
                Graphics2D g2d = (Graphics2D) g.create();
                int x = (getWidth() - currentFrame.getWidth()) / 2;
                int y = (getHeight() - currentFrame.getHeight()) / 2;
                g2d.drawImage(currentFrame, x, y, this);
                g2d.dispose();
            
        

    

    public class Spritesheet 

//Instance Variables
        private String path;
        private int frameWidth;
        private int frameHeight;
        private BufferedImage sheet = null;
        private BufferedImage[] frameImages;

//Constructors
        public Spritesheet(String aPath, int width, int height) 

            path = aPath;
            frameWidth = width;
            frameHeight = height;

            try 
                sheet = ImageIO.read(getClass().getResourceAsStream(path));
                frameImages = getAllSprites();
             catch (IOException e) 
                e.printStackTrace();
            

        

        public BufferedImage getSprite(int frame) 
            return frameImages[frame];
        

//Methods
        public int getHeight() 
            return frameHeight;
        

        public int getWidth() 
            return frameWidth;
        

        public int getColumnCount() 
            return sheet.getWidth() / getWidth();
        

        public int getRowCount() 
            return sheet.getHeight() / getHeight();
        

        public int getFrameCount() 
            int cols = getColumnCount();
            int rows = getRowCount();
            return cols * rows;
        

        private BufferedImage getSprite(int x, int y, int h, int w) 
            BufferedImage sprite = sheet.getSubimage(x, y, h, w);
            return sprite;
        

        public BufferedImage[] getAllSprites() 
            int cols = getColumnCount();
            int rows = getRowCount();
            int frameCount =  getFrameCount();
            BufferedImage[] sprites = new BufferedImage[frameCount];
            int index = 0;
            System.out.println("cols = " + cols);
            System.out.println("rows = " + rows);
            System.out.println("frameCount = " + frameCount);
            for (int row = 0; row < getRowCount(); row++) 
                for (int col = 0; col < getColumnCount(); col++) 
                    int x = col * getWidth();
                    int y = row * getHeight();
                    System.out.println(index + " " + x + "x" + y);
                    BufferedImage currentSprite = getSprite(x, y, getWidth(), getHeight());
                    sprites[index] = currentSprite;
                    index++;
                
            
            return sprites;

        

    

请记住,动画是随时间变化的错觉。您需要在动画的每一帧之间提供一个延迟,足够长以便用户识别它,但又足够短以使动画看起来流畅。

在上面的例子中,我使用了100 毫秒,只是作为一个任意值。可以使用更像1000 / spritesheet.getFrameCount() 的东西,这将为整个动画留出整整一秒的时间(一秒内的所有帧)。

您可能需要为较长或较短的动画使用不同的值,具体取决于您的需要

【讨论】:

感谢您花时间写下所有这些。我以前从未真正做过这样的事情,所以我的代码有一些问题我并不感到惊讶。 请记住,动画是随时间变化的错觉。除了其他一些事情之外,您的代码缺少“时间”。上面的例子使用了大约 10fps(每个 from 之间有 100 毫秒的延迟),事实上,考虑到只有 6 帧动画,我们可能可以使用更像 166 的帧,这取决于你想要每个动画的周期数第二... 在您的代码中,之前的图像是从屏幕上移除的吗?在使用您的代码一段时间后,我能够在屏幕上显示精灵,但动画只是将它们叠加在一起。 忘记打电话给super.paintComponent 对不起,我昨晚要评论,是的,就是这样。【参考方案2】:

这里有一些用于将图像绘制到 JPanel 的通用代码。调用此方法来绘制 JPanel 组件。

public void paintComponent (Graphics g)
 
     super.paintComponent(g);
     //I would have image be a class variable that gets updated in your run() method
     g.drawImage(image, 0, 0, this); 
 

我还可以将 run() 修改为如下所示:

public void run() 
  BufferedImage[] frames = sheet.getAllSprites();
  currentFrame = 0;
  while (true) 
    image = frames[currentFrame];
    this.repaint(); //explicitly added "this" for clarity, not necessary.
    currentFrame++;
    if (currentFrame >= frames.length) 
        currentFrame = 0;
    
  

关于只重绘组件的一部分,它变得有点复杂

public void run() 
  BufferedImage[] frames = sheet.getAllSprites();
  currentFrame = 0;
  while (true) 
    image = frames[currentFrame];
    Rectangle r = this.getDirtyRect();
    this.repaint(r); 
    currentFrame++;
    if (currentFrame >= frames.length) 
        currentFrame = 0;
    
  


public Rectangle getDirtyRect() 
  int minX=0; //calculate smallest x value affected
  int maxX=0; //calculate largest x value affected
  int minY=0; //calculate smallest y value affected
  int maxY=0; //calculate largest y value affected 
  return new Rectangle(minX,minY,maxX,maxY);

【讨论】:

@MadProgrammer,这可能超出了我的知识范围。你这是什么意思? 您未能满足超类的要求并破坏了绘制方法调用链。该示例可能会导致意外和不良的油漆故障,请参阅 Painting in AWT and Swing 和 Performing Custom Painting 了解更多详情 我认为他的意思是你需要调用 super.paintComponent(g);首先 哦,是的。抱歉,我很快就从记忆中消失了,我同意。 你有一个实例变量保存在你的类中作为“私有 BufferedImage 图像;”理想情况下,您的 run 方法会更新 this 的值,然后调用“repaint();”我会在答案中添加一些关于此的内容。

以上是关于如何将 BufferedImage 绘制到 JPanel的主要内容,如果未能解决你的问题,请参考以下文章

旋转 BufferedImage 时如何产生清晰的绘制结果?

将 BufferedImage 裁剪到某个区域

如何编辑 BufferedImage 中的像素?

JPanel 绘制 BufferedImage

如何将 BufferedImage 放入 ByteBuffer?

如何通过 BufferedImage 将巨大的图像加载到 Java?