3

我是新来的。希望您能提供帮助。

问题: 在 JFrame 上显示动画时出现问题。似乎我想念/不太了解 Java 的图形是如何工作的。

全球理念: 假设我想做一个游戏/电影/剪辑。为此,我需要这个(不)简单的动画开始工作。

这个问题的一个例子: 我有类 Screen,它有屏幕的东西——JFrame 的声明,设置它的配置(大小,关闭操作等),然后创建类 Box 的对象,以显示在框架上。请检查这个类的图像/图表(希望我写对了):ClassesDiagram

现在,Box 类扩展了 JPanel。我从 JPanel 继承方法 Paint() 并覆盖它,绘制框。

在 Screen 类中,创建两个 Box 后,我将它们添加到 JFrame 中。接下来,开始一个循环 while(true),并通过使线程休眠该数量,每 200 毫秒更新一次盒子的位置。(在这种情况下,只是简单的 x++ 或 y++ 取决于哪个框,框 1 或框 2)。

主要问题 1):程序运行,并显示 JFrame,但在 JFrame 上显示最后添加的对象/组件-Box。它没有显示另一个。为什么?

我找到了一个主题,如何将多个组件添加到 JFrame?,并尝试了投票最多的帖子给出的提示,由jjnguy 2010 年 11 月 15 日 17:02 提供。出于某种奇怪的原因,不是第一个,也不是第二个技巧对我有用。

主要问题2):据我了解,我需要布局管理器。如果我只想在框架上的特定 X,Y 处绘制(),为什么需要它?

找到其他帖子(找不到了)+Oracle关于布局的指南,建议我需要考虑使用 setLayout(null); 我试图这样做,但是又出现了一个问题。

主要问题 3): JFrame 出现,它只显示 1 个框(绿色的不会出现,不管你会做什么。不知道为什么),当它移动时 - 它从另一侧被删除。这里:盒子运动

提前感谢您的任何帮助、提示和解释!希望帖子清晰,有条理,好看。


public class Screen {

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

private JFrame window;

public Screen() {
    window = new JFrame("Multiply components panel");
    //window.setLayout(null);
    window.setSize(200, 200);

    window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

    Box b1 = new Box(10,10, "box1");
    //b1.setBounds(10, 10, 50, 50);

    Box b2 = new Box(100,100, "box2");
    //b2.setBounds(100, 100, 50, 50);


    window.add(b1);
    //window.add(b2);


    window.setVisible(true);

    while (true){

        b1.update();
        b2.update();

        try {
            Thread.sleep(200);
        } catch (Exception e) {
            // TODO: handle exception
        }
    }

}
}

public class Box extends JPanel{

int x, y, w, h;
private String name;

public Box(int x, int y, String name) {
    this.x = x;
    this.y = y;
    this.w = 100;
    this.h = 100;
    this.name=name;
}


public void paint(Graphics g){
    System.out.println(this.name.equalsIgnoreCase("box1"));
    if(this.name.equalsIgnoreCase("box1")){
        g.setColor(Color.BLACK);
        g.fillRect(x, y, w, h);
    }else{
        g.setColor(Color.GREEN);
        g.fillRect(x, y, w, h);
    }


}


public void update() {
    if(this.name.equalsIgnoreCase("box1"))
        x++;
    else
        y++;
    //this.setBounds(x, y, w, h);
    System.out.println("Current "+name+": X: "+x+", Y: "+y+", W: "+w+", H: "+h);
    repaint();
}

}
4

2 回答 2

3

主要问题 1):程序运行,并显示 JFrame,但在 JFrame 上只显示最后添加的对象/组件-Box。它没有显示另一个。为什么?

你这样做 window.add(b1); window.add(b2);了,默认情况下JFrameBorderLayout正在替换最后添加的框add(..)

主要问题2):据我了解,我需要布局管理器。如果我只想在框架上的特定 X,Y 处绘制(),为什么需要它?

如果您JPanel在游戏中使用 s 作为对象,那么这将是唯一一次使用setLayout(null),因为我们希望完全控制JPanels 的放置。

主要问题 3):JFrame 出现,它只显示 1 个框(绿色的不会出现,不管你会做什么。不知道为什么),当它移动时 - 它从另一侧被删除。这里:盒子运动

因为你你这样做g.fillRect(x,y,w,h),它应该是g.fillRect(0,0,w,h)

其他问题:

1)我认为你遇到的一个主要问题是:

public class Box extends JPanel{

    ...

    public void paint(Graphics g){

       ...
    }

}

您应该重写paintComponentof JPanel,并记住super.paintComponent(g)在重写方法中作为第一次调用调用。

3)您应该覆盖getPreferredSizeJPanel返回与图像或内容相匹配的正确尺寸JPanel

4)不要要求setSize使用JFrame正确Layoutmanager和/或覆盖getPreferredSize必要的容器,而不是在将其设置为可见之前简单地pack()调用JFrame

5) 正如@MadProgrammer 所说,阅读了 Swing 中的并发,但基本上所有 Swing 组件都应该通过SwingUtilities.inokeXXX块在 EDT 上创建/操作。

6)这样做绝对是不好的:

while (true){

    b1.update();
    b2.update();

    try {
        Thread.sleep(200);
    } catch (Exception e) {
        // TODO: handle exception
    }
}

因为您不仅要创建一个连续循环,还要调用Thread.sleep您创建 GUI 的线程,因此似乎会冻结。看看How to use Swing Timers,也可以看看这个类似的问题。关于上述主题的答案。

这是实现了上述修复的代码:

在此处输入图像描述

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class Screen {

    private JFrame window;

    public static void main(String[] args) {

        //creat UI on EDT
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Screen();
            }
        });
    }

    public Screen() {
        window = new JFrame("Multiply components panel") {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(600, 400);
            }
        };

        window.setLayout(null);//only reason this is warrented is because its a gmae using JPanels as game objects thus we need full control over its placing
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//changed from DISPOSE to EXIT so Timers  will be exited too

        final Box b1 = new Box(10, 10, 50, 50, "box1");

        final Box b2 = new Box(100, 100, 50, 50, "box2");

        window.add(b1);
        window.add(b2);

        window.pack();
        window.setVisible(true);

        Timer timer = new Timer(200, new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                b1.update();
                b2.update();
            }
        });
        timer.setInitialDelay(0);
        timer.start();

    }
}

class Box extends JPanel {

    int x, y, w, h;
    private String name;

    public Box(int x, int y, int w, int h, String name) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.name = name;
        setBounds(x, y, w, h);//let the Box class handle setting the bounds more elegant OOP
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        System.out.println(this.name.equalsIgnoreCase("box1"));
        if (this.name.equalsIgnoreCase("box1")) {
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, w, h);
        } else {
            g.setColor(Color.GREEN);
            g.fillRect(0, 0, w, h);
        }
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(w, h);
    }

    public void update() {
        if (this.name.equalsIgnoreCase("box1")) {
            x++;
        } else {
            y++;
        }
        this.setBounds(x, y, w, h);//set the new bounds so box may be updated
        System.out.println("Current " + name + ": X: " + x + ", Y: " + y + ", W: " + w + ", H: " + h);
        revalidate();
        repaint();
    }
}

最后看看我的教程/代码片段Game Development Loop, Logic and Collision detection Java Swing 2D

它拥有启动简单 2D 游戏所需的一切,例如游戏循环、逻辑、像素碰撞检测、动画(即在多个精灵之间交换以创建精灵集的动画)等等,但根本区别在于它使用对象作为游戏实体,即一个类将保存要绘制的对象所需的所有信息,IMO 这就是游戏应该如何完成的,事情可能会变得图形密集,并且有许多 JPanel 想知道屏幕肯定会降低整体 FPS

于 2013-06-28T21:47:09.227 回答
1

我意识到这是一个老问题,但它已经被浏览了 1000 多次。

创建 Swing GUI 时,最好使用模型/视图/控制器(MVC) 模式。这种模式使我们能够分离我们的关注点并一次专注于 GUI 的一个部分。如果有问题,那么我们很清楚问题出在代码的什么地方。

这是我创建的 GUI。

多组件 GUI

所以,让我们从模型开始。这是Box课程。

public class Box {
    
    private final Color color;
    
    private final Point boxMotion;
    
    private final Rectangle rectangle;
    
    private final String name;

    public Box(String name, Color color, Rectangle rectangle, 
            Point boxMotion) {
        this.name = name;
        this.color = color;
        this.rectangle = rectangle;
        this.boxMotion = boxMotion;
    }
    
    public void update(int width, int height) {
        this.rectangle.x += boxMotion.x;
        this.rectangle.y += boxMotion.y;
        int boxWidth = rectangle.x + rectangle.width;
        int boxHeight = rectangle.y + rectangle.height;
        
        if (rectangle.x < 0) {
            rectangle.x = -rectangle.x;
            boxMotion.x = -boxMotion.x;
        }
        
        if (boxWidth > width) {
            rectangle.x = width - rectangle.width - boxMotion.x;
            boxMotion.x = -boxMotion.x;
        }
        
        if (rectangle.y < 0) {
            rectangle.y = -rectangle.y;
            boxMotion.y = -boxMotion.y;
        }
        
        if (boxHeight > height) {
            rectangle.y = height - rectangle.height - boxMotion.y;
            boxMotion.y = -boxMotion.y;
        }
    }

    public Color getColor() {
        return color;
    }

    public Rectangle getRectangle() {
        return rectangle;
    }

    public String getName() {
        return name;
    }
    
}

该类Box是一个普通的 Java 类。它具有典型的 getter 方法。没有 setter 类,因为构造函数设置了所有内部字段。

我们使用 ajava.awt.Rectangle和 ajava.awt.Point来分别保存盒子的大小和盒子运动的方向。这使我们不必定义六个int字段。

通过保存Color每个盒子的运动方向和方向,我们可以有许多不同颜色和不同方向的盒子。从技术上讲,运动方向是一个 delta X 和 delta Y 实例,但我不打算创建一个单独的类来说明这一点。Point课已经够好了。

update方法是最难得到正确的。该update方法控制盒子的运动,并检查盒子是否碰到了绘图的边缘JPanel。该方法包含在Box类中,因为它作用于每个框。该方法将由控制器执行。

MVC 模式并不意味着模型、视图和控制器代码驻留在不同的类中,尽管这是最理想的。只要代码的执行由模型、视图和控制器分开,代码就可以放在最有意义的地方。

接下来,我们来看看Boxes类。

public class Boxes {
    
    private final int drawingPanelWidth;
    private final int drawingPanelHeight;
    
    private final List<Box> boxes;
    
    public Boxes() {
        this.drawingPanelWidth = 600;
        this.drawingPanelHeight = 400;
        this.boxes = new ArrayList<>();
        addBoxesFactory();
    }
    
    private void addBoxesFactory() {
        Rectangle rectangle = new Rectangle(10, 10, 50, 50);
        Point point = new Point(3, 3);
        this.boxes.add(new Box("box1", Color.BLACK, rectangle, point));
        
        rectangle = new Rectangle(100, 100, 50, 50);
        point = new Point(-3, -3);
        this.boxes.add(new Box("box2", Color.GREEN, rectangle, point));
    }

    public int getDrawingPanelWidth() {
        return drawingPanelWidth;
    }

    public int getDrawingPanelHeight() {
        return drawingPanelHeight;
    }

    public List<Box> getBoxes() {
        return boxes;
    }
    
}

该类Boxes是一个普通的 Java 类。它具有典型的 getter 方法。没有 setter 类,因为构造函数设置了所有内部字段。

我们使用 aList来保存Box我们想要显示的尽可能多的实例。在这个类中,我们定义了两个Box实例。我们可以定义任意数量的Box实例。其余代码将处理我们定义的任意数量的框。

JPanel我们在这个类中定义了绘图的宽度和高度。我们这样做是因为绘图JPanel需要这些尺寸来调整自身的大小,并且控制器需要这些尺寸来确定Box实例何时到达宽度或高度边缘。

接下来,我们来看看主视图类,BoxMotionGUI类。

public class BoxMotionGUI implements Runnable {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new BoxMotionGUI());
    }
    
    private Boxes boxes;
    
    private DrawingPanel drawingPanel;
    
    public BoxMotionGUI() {
        this.boxes = new Boxes();
    }

    @Override
    public void run() {
        JFrame frame = new JFrame("Multiple Components GUI");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        drawingPanel = new DrawingPanel(boxes);
        frame.add(drawingPanel, BorderLayout.CENTER);
        
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
        
        Timer timer = new Timer(20, new AbstractAction() {

            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                for (Box box : boxes.getBoxes()) {
                    box.update(boxes.getDrawingPanelWidth(), 
                            boxes.getDrawingPanelHeight());
                    drawingPanel.repaint();
                }
            }
        });
        
        timer.setInitialDelay(0);
        timer.start();
    }

}

该类BoxMotionGUI以对SwingUtilities invokeLater方法的调用开始。此方法确保 Swing 应用程序将在Event Dispatch Thread上创建 Swing 组件。一个编写良好的 Swing 程序使用并发来创建一个永远不会“冻结”的用户界面。

构造函数创建Boxes该类的一个实例。通常,您创建模型并将模型传递给视图。视图从模型中读取,但不以任何方式更新模型。控制器类将更新模型并更新/重绘视图。

run 方法包含所有用于创建JFrame. 必须按此顺序调用这些方法。这个顺序是我用于大多数 Swing 应用程序的顺序。

java.swing.Timer代码组成了控制器。我把这段代码放在 view 方法中,因为它很短。如果actionPerformed代码涉及更多,则需要一个单独的控制器类。

actionPerformed方法更新模型并重新绘制视图。

接下来,我们来看看DrawingPanel类。

public class DrawingPanel extends JPanel {

    private static final long serialVersionUID = 1L;
    
    private final Boxes boxes;
    
    public DrawingPanel(Boxes boxes) {
        this.boxes = boxes;
        this.setPreferredSize(new Dimension(
                boxes.getDrawingPanelWidth(), 
                boxes.getDrawingPanelHeight()));
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        Graphics2D g2d = (Graphics2D) g;
        for (Box box : boxes.getBoxes()) {
            g2d.setColor(box.getColor());
            Rectangle rectangle = box.getRectangle();
            g2d.fillRect(rectangle.x, rectangle.y, 
                    rectangle.width, rectangle.height);
        }
    }
    
}

该类DrawingPanel绘制Box实例。我们扩展 a 是JPanel因为我们想要覆盖该paintComponent方法。您应该扩展 Swing 组件或任何 Java 类的唯一原因是您想要覆盖一个或多个类方法。

paintComponent方法绘制。它不会尝试做任何其他事情。由于每次重新绘制 GUI 时都会调用此方法,因此您希望尽可能减少处理量。除了绘画之外的所有其他处理都在其他地方进行。

最后,这是完整的可运行代码。

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class BoxMotionGUI implements Runnable {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new BoxMotionGUI());
    }
    
    private Boxes boxes;
    
    private DrawingPanel drawingPanel;
    
    public BoxMotionGUI() {
        this.boxes = new Boxes();
    }

    @Override
    public void run() {
        JFrame frame = new JFrame("Multiple Components GUI");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        drawingPanel = new DrawingPanel(boxes);
        frame.add(drawingPanel, BorderLayout.CENTER);
        
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
        
        Timer timer = new Timer(20, new AbstractAction() {

            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                for (Box box : boxes.getBoxes()) {
                    box.update(boxes.getDrawingPanelWidth(), 
                            boxes.getDrawingPanelHeight());
                    drawingPanel.repaint();
                }
            }
        });
        
        timer.setInitialDelay(0);
        timer.start();
    }
    
    public class DrawingPanel extends JPanel {

        private static final long serialVersionUID = 1L;
        
        private final Boxes boxes;
        
        public DrawingPanel(Boxes boxes) {
            this.boxes = boxes;
            this.setPreferredSize(new Dimension(
                    boxes.getDrawingPanelWidth(), 
                    boxes.getDrawingPanelHeight()));
        }
        
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            
            Graphics2D g2d = (Graphics2D) g;
            for (Box box : boxes.getBoxes()) {
                g2d.setColor(box.getColor());
                Rectangle rectangle = box.getRectangle();
                g2d.fillRect(rectangle.x, rectangle.y, 
                        rectangle.width, rectangle.height);
            }
        }
        
    }
    
    public class Boxes {
        
        private final int drawingPanelWidth;
        private final int drawingPanelHeight;
        
        private final List<Box> boxes;
        
        public Boxes() {
            this.drawingPanelWidth = 600;
            this.drawingPanelHeight = 400;
            this.boxes = new ArrayList<>();
            addBoxesFactory();
        }
        
        private void addBoxesFactory() {
            Rectangle rectangle = new Rectangle(10, 10, 50, 50);
            Point point = new Point(3, 3);
            this.boxes.add(new Box("box1", Color.BLACK, rectangle, point));
            
            rectangle = new Rectangle(100, 100, 50, 50);
            point = new Point(-3, -3);
            this.boxes.add(new Box("box2", Color.GREEN, rectangle, point));
        }

        public int getDrawingPanelWidth() {
            return drawingPanelWidth;
        }

        public int getDrawingPanelHeight() {
            return drawingPanelHeight;
        }

        public List<Box> getBoxes() {
            return boxes;
        }
        
    }
    
    public class Box {
        
        private final Color color;
        
        private final Point boxMotion;
        
        private final Rectangle rectangle;
        
        private final String name;

        public Box(String name, Color color, Rectangle rectangle, 
                Point boxMotion) {
            this.name = name;
            this.color = color;
            this.rectangle = rectangle;
            this.boxMotion = boxMotion;
        }
        
        public void update(int width, int height) {
            this.rectangle.x += boxMotion.x;
            this.rectangle.y += boxMotion.y;
            int boxWidth = rectangle.x + rectangle.width;
            int boxHeight = rectangle.y + rectangle.height;
            
            if (rectangle.x < 0) {
                rectangle.x = -rectangle.x;
                boxMotion.x = -boxMotion.x;
            }
            
            if (boxWidth > width) {
                rectangle.x = width - rectangle.width - boxMotion.x;
                boxMotion.x = -boxMotion.x;
            }
            
            if (rectangle.y < 0) {
                rectangle.y = -rectangle.y;
                boxMotion.y = -boxMotion.y;
            }
            
            if (boxHeight > height) {
                rectangle.y = height - rectangle.height - boxMotion.y;
                boxMotion.y = -boxMotion.y;
            }
        }

        public Color getColor() {
            return color;
        }

        public Rectangle getRectangle() {
            return rectangle;
        }

        public String getName() {
            return name;
        }
        
    }

}
于 2020-12-22T12:37:54.273 回答