【问题标题】:How To Paint Multiple Objects Of Same Class Onto One JPanel?如何将同一类的多个对象绘制到一个 JPanel 上?
【发布时间】:2013-12-02 13:26:18
【问题描述】:

我是一名大学生,我的作业遇到了问题。通常情况下,我会去实验室时间问助教,但他整个星期都生病了,所以我们没有任何实验室时间,而且这个作业要在星期一到期!

我遇到的具体问题与创建一个 java 应用程序有关,该应用程序显示一个带有按钮的框架,允许用户创建一个开始在屏幕上弹跳并从框架边界反弹的球。

之前作业中的一个练习是创建一个类似的程序,但在运行时会立即显示一个球弹跳。 (我开始工作了)现在,我们必须修改我们的代码并合并允许我们创建多个球的按钮。

起初,我认为这将是一个简单的修改,但现在我对如何实际实例化 Ball 对象感到困惑。我的想法是,我首先必须让 ReboundPanel 和按钮面板出现(这可行),然后每当用户按下按钮时,都会实例化一个新的 Ball 对象并显示在 ReboundPanel 上。 (目前不工作)

感谢大家的帮助!

主程序:

import java.awt.*;

public class Rebound {

public static void main(String[] args) {

    JFrame frame = new JFrame ("Rebound");
    frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
    frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));

    JPanel reboundPanel = new ReboundPanel();
    JPanel buttonPanel = new ButtonPanel();
    frame.getContentPane().add(reboundPanel);
    frame.getContentPane().add(buttonPanel);
    frame.pack();
    frame.setVisible(true);
}
}

应该出现球的面板:

import java.awt.*;

public class ReboundPanel extends JPanel {

private final int WIDTH = 400, HEIGHT = 300;

public ReboundPanel() {

    setPreferredSize (new Dimension(WIDTH, HEIGHT));
    setBackground (Color.black);

}
}

按键面板:

import java.awt.*;

public class ButtonPanel extends JPanel {

private final int WIDTH = 400, HEIGHT = 35;

public ButtonPanel() {

    setPreferredSize (new Dimension(WIDTH, HEIGHT));
    setBackground (Color.GRAY);

    JButton button = new JButton("New ball");
    add(button);

    button.addActionListener(new ActionListener() {

        public void actionPerformed(ActionEvent event) {

            new Ball(); 

        }
    });
}
}

球类:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Ball extends JPanel {

private final int DELAY = 20, IMAGE_SIZE = 35;

private ImageIcon image;
private Timer timer;
private int x, y, moveX, moveY;


public Ball() {

    timer = new Timer(DELAY, new ReboundListener());
    image = new ImageIcon ("/src/pa1/images/earth.gif");
    x = 0;
    y = 40;
    moveX = moveY = 3;
    draw(null);
    timer.start();
}

public void draw(Graphics page) {

    super.paintComponent (page);
    image.paintIcon (new ReboundPanel(), page, x, y);
}

private class ReboundListener implements ActionListener {

    public void actionPerformed (ActionEvent event) {

        x += moveX;
        y += moveY;

        if (x <= 0 || x >= WIDTH-IMAGE_SIZE)
            moveX = moveX * -1;

        if (y <= 0 || x >= WIDTH-IMAGE_SIZE)
            moveY = moveY * -1;

        repaint();
    }
}
}

【问题讨论】:

  • 当按钮被按下时,你是在调用 Ball 的构造函数,但不会存储新对象的地址。您不应该保留对它的引用,以便垃圾收集器不会摆脱它吗?尝试制作一个 Balls 向量,并将每个新 Ball 添加到其中。
  • 不确定你所说的向量是什么意思...我对课程设计感到困惑...什么课程应该做什么?
  • [link]docs.oracle.com/javase/6/docs/api/java/util/Vector.html 向量是对象的动态数组。它没有强加的大小限制(我认为除了你的可用内存)。在您的 ButtonPanel Vector vectorOfBalls = new Vector 中创建它;并使用 vectorOfBalls.add(new Ball()); 添加球
  • 太好了,我如何告诉 ReboundPanel 绘制在 ButtonPanel 中声明的球矢量?
  • 既然在构造函数中调用了 Draw 方法,那么 Balls 不会自己绘制吗?我从来没有设计过像你这样的应用程序,我总是在 JPanel 中包含paint方法并在那里处理所有的绘图,所以我不完全理解你的例子。绘图应该如何在您的应用程序中工作?

标签: java swing graphics


【解决方案1】:

想想你的应用程序元素之间的关系,并让它帮助形成你的类和对象设计。

说明:

应用程序将有一个窗口,其中包含一个按钮和一个容纳 0 个或多个球的容器区域。 单击按钮时,应将一个新球添加到容器中。 一个球应该以固定的速度在其容器周围移动并从容器的边界反弹。

设计:

这个描述告诉了我们很多关于如何构建我们的代码的信息。 我们有一些名词:(应用程序)、窗口、按钮、容器、边界和球。 我们有一些动词:(have、contains、hold、add)、move[ball]、bounce[ball]、click[button]。

名词暗示可能要实现的类。以及可能在相关类中实现的方法处的动词。

让我们创建一个类来表示窗口并将其称为 Rebound、一个表示容器的类称为 BallPanel 和一个表示球的类称为 Ball。在这种情况下,窗口和应用程序可以被认为是相同的。事实证明,该按钮可以巧妙地实现,而无需为其创建单独的类。而且边界很简单,可以用整数表示。

上面我只是解释了一种方法来帮助澄清问题,下面我将提供一种可能的实现。这些旨在提供一些指导性提示以帮助您理解。您可以通过多种方式分析此问题或实施解决方案,希望对您有所帮助。

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.List;

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

public class Rebound extends JFrame {
    /* Milliseconds between each time balls move */
    static final int MOVE_DELAY = 20;

    /* The JButton for adding a new ball. An AbstractAction
     * provides a neat way to specify the label and on-click
     * code for the button inline */
    JButton addBallButton = new JButton(new AbstractAction("Add ball") {
        public void actionPerformed(ActionEvent e) {
            ballContainer.addBall();
        }
    });

    /* The Panel for holding the balls. It will need to
     * keep tracks of each ball, so we'll make it a subclass
     * of JPanel with extra code for the ball management (see
     * the definition, after the end of the Rebound class) */
    BallPanel ballContainer = new BallPanel();

    public Rebound() {
        super("Rebound");
        setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));

        /* There was no neat way to specify the button size
         * when we declared it, so let's do that now */
        addBallButton.setPreferredSize(new Dimension(400, 35));

        /* Add the components to this window */
        getContentPane().add(addBallButton);
        getContentPane().add(ballContainer);

        pack();

        /* Create a timer that will send an ActionEvent
         * to our BallPanel every MOVE_DELAY milliseconds */
        new Timer(MOVE_DELAY, ballContainer).start();
    }

    /* The entry point for our program */
    public static void main(String[] args) {
        /* We use this utility to ensure that code
         * relating to Swing components is executed
         * on the correct thread (the Swing event 
         * dispatcher thread) */
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new Rebound().setVisible(true);
            }
        });
    }
}

/* Our subclass of JPanel that also manages a list of
 * balls. It implements ActionListener so that it can
 * act on the Timer event we set up in the Rebound class */
class BallPanel extends JPanel implements ActionListener {
    /* An automatically expanding list structure that can
     * contain 0 or more Ball objects. We'll create a Ball
     * class to manage the position, movement and draw code
     * for each ball. */
    List<Ball> balls = new ArrayList<Ball>();
    /* Let's add some code that will be run
     * when the panel is resized (which will happen
     * if its window is resized.) We need to make sure
     * that each Ball is told about the new bounds
     * of the component, so it knows that the place
     * where it should bounce has changed */
    public BallPanel() {
        super();
        setPreferredSize(new Dimension(400,300));
        addComponentListener(new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                if (BallPanel.this == e.getComponent()) {
                    for (Ball ball : balls) {
                        ball.setBounds(getWidth(), getHeight());
                    }
                }
            }
        });
    }

    /* This method is part of the JPanel class we are subclassing.
     * Here we change the implementation of the method, ensuring
     * we call the original implementation so that we are only
     * adding to what it does. */
    public void paintComponent(Graphics g) {
        /* Call the original implementation of this method */
        super.paintComponent(g);

        /* Lets draw a black border around the bounds of the component
         * to make it clear where the balls should rebound from */
        g.drawRect(0,0,getWidth(),getHeight());

        /* Now lets draw all the balls we currently have stored in
         * our list. */
        for (Ball ball : balls) {
            ball.draw(g);
        }
    }
    /* This method will add a new Ball into our list. Remember
     * from earlier that we call this when our button is clicked. */
    public void addBall() { 
        balls.add(new Ball(this,10,10,getWidth(),getHeight())); 
    }
    /* This method will receive the event from Timer we set up in
     * the Rebound class. We want it to cause all the ball to
     * move to their next position. */
    public void actionPerformed(ActionEvent e) {
        for(Ball ball : balls) {
            ball.move();
        }
        /* Request that Swing repaints this JPanel. This should
         * cause the paintComponent() method we implemented
         * above to be called soon after. */
        repaint();
    }
}
/* This is our class for keeping track of an individual ball
 * and it's position, movement and how it is drawn. */
class Ball {
    /* Let's say all balls will have the same diameter of 35.
     * The static modifier says that this is a value
     * that is shared by all instances of Ball. */ 
    static final int SIZE = 35;
    /* Let's say all balls will have a speed in both the X and Y
     * axes of 3. The static modifier says that this is a value
     * that is shared by all instances of Ball. */ 
    static final int SPEED = 3;
    /* Each ball needs to know its position, which we will store
     * as x and y coordinates in 2D space */
    int x, y; 
    /* Each ball needs to know the bounds in which it lives, so
     * it knows when to bounce. We'll be assuming the minimum
     * bound is 0,0 in 2D space. The maximum bound will be
     * maxX,mayY in 2D space. We could have made these static
     * and shared by all balls, but that means we would have
     * to remember to change them to not be static if in the
     * future we wanted Ball to be used on more than one JPanel.
     * If we didn't remember, then we'd see some buggy behaviour. */
    int maxX, maxY;
    /* Each ball needs to know its current speed in the X and Y 
     * directions. We can use positive and negative values to
     * keep track of the direction of the ball's movement. */
    int speedX = SPEED, speedY = SPEED;
    /* Each ball needs to know which panel it is being drawn to
     * (this is needed by ImageIcon#drawImage()). */
    JPanel panel;
    public Ball(JPanel panel, int x, int y, int maxX, int maxY) { 
        this.x = x; this.y = y;
        this.maxX = maxX; this.maxY = maxY;
        this.panel = panel;
    }
    public void setBounds(int maxX, int maxY) {
        this.maxX = maxX; this.maxY = maxY;
    }
    /* This method updates the position of this ball, using
     * the current speed and bounds to work out what the new
     * position should be.
     * This should be called by our BallPanel#actionPerformed()
     * method in response to the Timer we set up in the Rebound
     * class. */
    public void move() {
        x += speedX;
        y += speedY;
        // Approx bounce, okay for small speed
        if (x<0) { speedX=-speedX; x=0; }
        if (y<0) { speedY=-speedY; y=0; }
        if (x+SIZE>maxX) { speedX=-speedX; x=maxX-SIZE; }
        if (y+SIZE>maxY) { speedY=-speedY; y=maxY-SIZE; }
    }
    /* This method is responsible for drawing this ball on
     * the provided graphics context (which should come from
     * the JPanel associated with the ball). We also have
     * the panel, should we need it (ImageIcon#drawImage() needs 
     * this, but Graphics#drawOval() does not.)
     */
    public void draw(Graphics g) {
        //image.paintIcon(panel, g, x, y); - commented out because I don't have an ImageIcon
        g.drawOval(x, y, SIZE, SIZE);
    }
}

【讨论】:

  • 嗨 Fd!哇,这么大的帮助!我非常感谢 cmets 帮助我理解。我还有其他问题,您可以给我发电子邮件吗? wangagat [at] gmail 。 com 谢谢!
【解决方案2】:

我让 ReboundPanel 负责告诉球移动和绘画——它有一个计时器和一个包含所有球的 ArrayList。我标记了对 Rebound.java 和 ButtonPanel.java 的更改。另外两个变化太大,没有任何意义。

回弹.java

import java.awt.*;
import javax.swing.*;

public class Rebound {

public static void main(String[] args) {

    JFrame frame = new JFrame ("Rebound");
    frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
    frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));

    ReboundPanel reboundPanel = new ReboundPanel(); /****/
    JPanel buttonPanel = new ButtonPanel(reboundPanel); /****/
    frame.getContentPane().add(reboundPanel);
    frame.getContentPane().add(buttonPanel);
    frame.pack();
    frame.setVisible(true);
}
}

ReboundPanel.java

import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.ArrayList;

public class ReboundPanel extends JPanel {

private final int WIDTH = 400, HEIGHT = 300;
private final int DELAY = 20;
private ArrayList<Ball> balls;
private Timer timer;

public ReboundPanel() {

    balls = new ArrayList<Ball>();
    timer = new Timer(DELAY, new ReboundListener());
    setPreferredSize (new Dimension(WIDTH, HEIGHT));
    setBackground (Color.black);
    timer.start();

}

public void addBall(Ball b) {

    balls.add(b);
}

protected void paintComponent(Graphics g) {

    super.paintComponent(g);

    for (Ball b : balls) {

        b.paint(g);
    }
}

private class ReboundListener implements ActionListener {

    public void actionPerformed (ActionEvent event) {

        for (Ball b : balls) {

            b.move(getWidth(), getHeight());
        }
        repaint();
    }
}
}

ButtonPanel.java

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class ButtonPanel extends JPanel {

private final int WIDTH = 400, HEIGHT = 35;

public ButtonPanel(final ReboundPanel panel) { /****/

    setPreferredSize (new Dimension(WIDTH, HEIGHT));
    setBackground (Color.GRAY);

    JButton button = new JButton("New ball");
    add(button);

    button.addActionListener(new ActionListener() {

        public void actionPerformed(ActionEvent event) {

            panel.addBall(new Ball()); /****/

        }
    });
}
}

球.java

import javax.swing.*;
import java.awt.*;

public class Ball {

private final int IMAGE_SIZE = 15;
private int x, y, moveX, moveY;
private ImageIcon image;

public Ball() {

    x = 0;
    y = 40;
    moveX = moveY = 3;
    image = new ImageIcon("/src/pa1/images/earth.gif");
}

public void move(int width, int height) {

    x += moveX;
    y += moveY;

    if (x <= 0 || x >= width - IMAGE_SIZE)
        moveX = moveX * -1;

    if (y <= 0 || y >= height - IMAGE_SIZE)
        moveY = moveY * -1;
}
public void paint(Graphics g) {

    image.paintIcon(null, g, x, y);
}
}

【讨论】:

  • 我认为使用常规绘制方法处理 ReboundPanel 中的绘图可能有助于解决闪烁问题。当然,它需要访问 Ball 对象。
  • 嗨,汤姆,非常感谢您的帮助!我复制粘贴了所有代码以查看它是如何工作的,虽然我没有收到任何 JVM 错误,但当我单击按钮时屏幕上没有出现任何球。它对你有用吗?
  • @Wangagat 是的,它在我的电脑上工作(但闪烁)。我计划使用 broncoAbierto 的建议对其进行改进,也许它也可以在您的计算机上运行。
  • @Wangagat 修复了闪烁问题。希望它现在可以在您的计算机上运行(如果没有,请确保 .gif 位于正确的路径中)。
猜你喜欢
  • 1970-01-01
  • 2021-07-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多