Repainting an instance of a class from an ArrayLis

2019-08-08 16:13发布

问题:

Ok so I am very new to Java Swing and a beginner in Java in general. My current problem is I have designed a "cityscape". I am working on a UFO flying around, but my randomly generated buildings continue to get regenerated. I am wondering if there is a way to save my instance of buildings to an ArrayList as I have attempted, and paint that selection from that list each time paint is called. I tried what I thought of and I believe it just crashed it when run, because it didn't even open a JFrame and instead produced errors upon errors. Here is what I have:

CityScape class (the main class):

import java.awt.*; 
import javax.swing.*;
public class CityScape extends JPanel 
{     
  Buildings a = new Buildings ();
UFO b = new UFO();

  @Override
  public void paint (Graphics g)
  {
    //RememberBuildings.buildingList.get(1).paint(g);
    a.paint(g);
    b.paint(g);
  }
  public void move()
  {
    b.move();
  }


  public static void main(String[] args) throws InterruptedException
  { 
    JFrame frame = new JFrame("Frame"); 
    CityScape jpe = new CityScape();
    frame.add(jpe);
    frame.setSize(800, 750); 
    frame.setBackground(Color.BLACK);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    System.out.println(frame.getContentPane().getSize());
    while (true)
    {
      jpe.move(); //Updates the coordinates
      jpe.repaint(); //Calls the paint method
      Thread.sleep(10); //Pauses for a moment
    }
  }
}

Buildings class (the class that generates the buildings):

import java.awt.*;

public class Buildings
{

  private int maxX = 784;
  private int maxY = 712;
  private int width = (int)(Math.random()*100+100);
  private int height = (int)(Math.random()*350+100);
  private  int rows = Math.round((height)/25);
  private int columns = Math.round(width/25);

  public void addBuilding()
  {
  RememberBuildings.addBuilding();
  }

  public void paint(Graphics g) 
  { 
    Graphics2D g2d = (Graphics2D) g;

    Color transYellow = new Color (255, 255, 0, 59);

    g2d.setColor(Color.BLACK);
    g2d.fillRect(0, 0, maxX, maxY);

    g2d.setColor(Color.WHITE);
    g2d.fillRect(5, 5, 25, 25);

    int a = 0;

    for (int i =10; i<634; i+=(a+10))//buildings
    {

      g2d.setColor(Color.GRAY);
      g2d.drawRect(i, maxY-height, width, height);
      g2d.fillRect(i, maxY-height, width, height);


      rows = Math.round((height)/25);
      columns = Math.round(width/25);

      for (int j = 1; j<=columns; j++)//windows
      {
        for (int k = 1; k<=rows; k++)
        {
          g2d.setColor(Color.BLACK);
          g2d.drawRect(i+5*j+20*(j-1), (maxY-height)+5*k+20*(k-1), 20, 20);
          if (Math.random()<0.7)
          {
            g2d.setColor(Color.YELLOW);
            g2d.fillRect(i+5*j+20*(j-1), (maxY-height)+5*k+20*(k-1), 20, 20);
          }
          else
          {
            g2d.setColor(Color.BLACK);
            g2d.fillRect(i+5*j+20*(j-1), (maxY-height)+5*k+20*(k-1), 20, 20);
            g2d.setColor(transYellow);
            g2d.fillRect(i+5*j+20*(j-1), (maxY-height)+5*k+20*(k-1), 20, 20);
          }
        }
      }
      addBuilding();
      a = width;
      height = (int)(Math.random()*462+100);
      width = (int)(Math.random()*100+100);

    }
  }
}

RememberBuildings class (the point of this is to add an instance to an ArrayList):

import java.util.*;
public class RememberBuildings
{
  public static ArrayList<Buildings> buildingList = new ArrayList<Buildings>();

  public static void addBuilding()
  {
    buildingList.add(new Buildings());
  }
}

And finally my UFO class (creates the UFO flying by):

import java.awt.*;
import javax.swing.*;
public class UFO extends JPanel
{
  private int x = 20; //x and y coordinates of the ball
  private int y = 20;
  private int xa = 1;
  public void move() //Increase both the x and y coordinates
  {
    if (x + xa < 0) {
      xa = 1;
    }
    if (x + xa > 784-75) 
    {
      xa = -1;
    }
    x = x + xa; 
  }
  public void paint(Graphics g)
  {
    super.paint(g); //Clears the panel, for a fresh start
    Graphics2D g2d = (Graphics2D) g;
    g2d.setColor(Color.LIGHT_GRAY);
    g2d.fillOval(x,y,75,25); //Draw the ball at the desired point
  }
}

回答1:

So, every time Buildings#paint is called, it regenerates all the builds, which is done randomly.

public void paint(Graphics g) {
    Graphics2D g2d = (Graphics2D) g;

    Color transYellow = new Color(255, 255, 0, 59);

    g2d.setColor(Color.BLACK);
    g2d.fillRect(0, 0, maxX, maxY);

    g2d.setColor(Color.WHITE);
    g2d.fillRect(5, 5, 25, 25);

    int a = 0;

    for (int i = 10; i < 634; i += (a + 10))//buildings
    {

        g2d.setColor(Color.GRAY);
        g2d.drawRect(i, maxY - height, width, height);
        g2d.fillRect(i, maxY - height, width, height);

        rows = Math.round((height) / 25);
        columns = Math.round(width / 25);

        for (int j = 1; j <= columns; j++)//windows
        {
            for (int k = 1; k <= rows; k++) {
                g2d.setColor(Color.BLACK);
                g2d.drawRect(i + 5 * j + 20 * (j - 1), (maxY - height) + 5 * k + 20 * (k - 1), 20, 20);
                if (Math.random() < 0.7) {
                    g2d.setColor(Color.YELLOW);
                    g2d.fillRect(i + 5 * j + 20 * (j - 1), (maxY - height) + 5 * k + 20 * (k - 1), 20, 20);
                } else {
                    g2d.setColor(Color.BLACK);
                    g2d.fillRect(i + 5 * j + 20 * (j - 1), (maxY - height) + 5 * k + 20 * (k - 1), 20, 20);
                    g2d.setColor(transYellow);
                    g2d.fillRect(i + 5 * j + 20 * (j - 1), (maxY - height) + 5 * k + 20 * (k - 1), 20, 20);
                }
            }
        }
        addBuilding();
        a = width;
        height = (int) (Math.random() * 462 + 100);
        width = (int) (Math.random() * 100 + 100);

    }
}

There's two ways you might be able to solve this, which you use will depend on what you want to achieve. You could render the buildings directly to a BufferedImage and simply paint that on each paint cycle or you could cache the information you need in order to re-create the buildings.

The BufferedImage approach is quicker, but can't be animated, so if you want to animate the buildings in some way (make the lights flicker), you will need to build up a series of information which allows you to simply repaint them.

I'm going for the second, as you've asked about painting assets from a ArrayList.

I started by translating your "paint" code into a single concept of a virtual building, which has also has information about it's own lights.

public class Building {

    protected static final Color TRANS_YELLOW = new Color(255, 255, 0, 59);

    private int x, y, width, height;
    private List<Light> lights;

    public Building(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;

        lights = new ArrayList<>(25);
        int rows = Math.round((height) / 25);
        int columns = Math.round(width / 25);

        for (int j = 1; j <= columns; j++)//windows
        {
            for (int k = 1; k <= rows; k++) {
                Color color = null;
                if (Math.random() < 0.7) {
                    color = Color.YELLOW;
                } else {
                    color = TRANS_YELLOW;
                }
                lights.add(new Light(x + 5 * j + 20 * (j - 1), y + 5 * k + 20 * (k - 1), color));
            }
        }
    }

    public void paint(Graphics2D g2d) {
        g2d.setColor(Color.GRAY);
        g2d.drawRect(x, y, width, height);
        g2d.fillRect(x, y, width, height);
        for (Light light : lights) {
            light.paint(g2d);
        }
    }

    public class Light {

        private int x, y;
        private Color color;

        public Light(int x, int y, Color color) {
            this.x = x;
            this.y = y;
            this.color = color;
        }

        public void paint(Graphics2D g2d) {
            g2d.setColor(Color.BLACK);
            g2d.fillRect(x, y, 20, 20);
            g2d.setColor(color);
            g2d.fillRect(x, y, 20, 20);
        }
    }

}

This allows you to generate the primary parameters for the Building and simple cache the results and when needed, simply paint it.

For example...

public class Buildings {

    private int maxX = 784;
    private int maxY = 712;

    private List<Building> buildings;

    public Buildings() {
        buildings = new ArrayList<>(25);
        for (int i = 10; i < 634; i += 10)//buildings
        {
            int width = (int) (Math.random() * 100 + 100);
            int height = (int) (Math.random() * 350 + 100);
            int x = i;
            int y = maxY - height;

            buildings.add(new Building(x, y, width, height));
        }
    }

    public void paint(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
        for (Building building : buildings) {
            building.paint(g2d);
        }
    }
}

I also changed your UFO class so it no longer extends from JPanel, as it just doesn't need to and is probably the primary cause of confusion with your painting.

I then updated your paint method in your CityScape to use paintComponent instead...

public class CityScape extends JPanel {

    Buildings a = new Buildings();
    UFO b = new UFO();

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        a.paint(g);
        b.paint(g);
    }

As a runnable example...

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class CityScape extends JPanel {

    Buildings a = new Buildings();
    UFO b = new UFO();

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g); //To change body of generated methods, choose Tools | Templates.
        a.paint(g);
        b.paint(g);
    }

    public void move() {
        b.move();
    }

    public static void main(String[] args) throws InterruptedException {
        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("Frame");
                CityScape jpe = new CityScape();
                frame.add(jpe);
                frame.setSize(800, 750);
                frame.setBackground(Color.BLACK);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                System.out.println(frame.getContentPane().getSize());

                Timer timer = new Timer(10, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        jpe.move(); //Updates the coordinates
                        jpe.repaint(); //Calls the paint method
                    }
                });
                timer.start();
            }
        });
    }

    public class Buildings {

        private int maxX = 784;
        private int maxY = 712;

        private List<Building> buildings;

        public Buildings() {
            buildings = new ArrayList<>(25);
            for (int i = 10; i < 634; i += 10)//buildings
            {
                int width = (int) (Math.random() * 100 + 100);
                int height = (int) (Math.random() * 350 + 100);
                int x = i;
                int y = maxY - height;

                buildings.add(new Building(x, y, width, height));
            }
        }

        public void paint(Graphics g) {
            Graphics2D g2d = (Graphics2D) g;
            for (Building building : buildings) {
                building.paint(g2d);
            }
        }
    }

    public static class Building {

        protected static final Color TRANS_YELLOW = new Color(255, 255, 0, 59);

        private int x, y, width, height;
        private List<Light> lights;

        public Building(int x, int y, int width, int height) {
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;

            lights = new ArrayList<>(25);
            int rows = Math.round((height) / 25);
            int columns = Math.round(width / 25);

            for (int j = 1; j <= columns; j++)//windows
            {
                for (int k = 1; k <= rows; k++) {
                    Color color = null;
                    if (Math.random() < 0.7) {
                        color = Color.YELLOW;
                    } else {
                        color = TRANS_YELLOW;
                    }
                    lights.add(new Light(x + 5 * j + 20 * (j - 1), y + 5 * k + 20 * (k - 1), color));
                }
            }
        }

        public void paint(Graphics2D g2d) {
            g2d.setColor(Color.GRAY);
            g2d.drawRect(x, y, width, height);
            g2d.fillRect(x, y, width, height);
            for (Light light : lights) {
                light.paint(g2d);
            }
        }

        public class Light {

            private int x, y;
            private Color color;

            public Light(int x, int y, Color color) {
                this.x = x;
                this.y = y;
                this.color = color;
            }

            public void paint(Graphics2D g2d) {
                g2d.setColor(Color.BLACK);
                g2d.fillRect(x, y, 20, 20);
                g2d.setColor(color);
                g2d.fillRect(x, y, 20, 20);
            }
        }

    }

    public class UFO {

        private int x = 20; //x and y coordinates of the ball
        private int y = 20;
        private int xa = 1;

        public void move() //Increase both the x and y coordinates
        {
            if (x + xa < 0) {
                xa = 1;
            }
            if (x + xa > 784 - 75) {
                xa = -1;
            }
            x = x + xa;
        }

        public void paint(Graphics g) {
            Graphics2D g2d = (Graphics2D) g;
            g2d.setColor(Color.LIGHT_GRAY);
            g2d.fillOval(x, y, 75, 25); //Draw the ball at the desired point
        }
    }
}


回答2:

  • Avoid overriding paint, use paintComponent instead. Always call the super paint method before you do any custom painting to ensure that the paint chain is maintained. See Painting in AWT and Swing and Performing Custom Painting for more details
  • Beware, Swing is not thread safe and it's unwise to update any component (or any variable that a component may rely on) from outside the context of the Event Dispatching Thread. A simple solution might be to use a Swing Timer instead of a while (true) loop and Thread.sleep. See How to use Swing Timers for more details.
  • You should also only create and modify UI components from within the context of the event dispatching thread, see Initial Threads for more details
  • If you have a problem with your code not working, you should consider providing a runnable example which demonstrates your problem. This is not a code dump, but an example of what you are doing which highlights the problem you are having. This will result in less confusion and better responses. Providing code which is not runnable and is missing classes makes it difficult to know why it's not working and how to fix it.


回答3:

A few things here:

  1. To address the paintComponent note and view an example, check out this other thread: Concerns about the function of JPanel: paintcomponent()
  2. There seems to be a bit of a disconnect between the logic you've got going and the object-oriented programming logic that I think will help sort things out (for general info on OOP: https://en.wikipedia.org/wiki/Object-oriented_programming):

What You've Got:

The Structure you've got going is as follows:

  • CityScape :: here's where you've extended JPanel and setup the main function
  • UFO :: an object class that represents 1 UFO
  • Building :: a class that has methods for drawing randomized buildings and calling methods in RememberBuildings
  • RememberBuildings :: I think this is intended to track buildings that have been drawn

The issue here is that your Building class's paint method continually draws multiple newly randomized buildings instead of a set building that retains its structure.

My Suggestion:

There are plenty of solutions to this issue and different ways to implement each solution, but my recommendation is to remodel your Building class in an OOP fashion, meaning that it would represent 1 single building (truer to the name of the class). This would contain a constructor that initializes all of the randomized dimensions of that single building once and draws that single building on the jpanel. Then you would need to keep an array or list of some sort in the cityscape that contains buildings that are part of the cityscape, eliminating the need for a "RememberBuildings" class. so roughly:

CityScape extends JPanel:
    variables:
        Building[] buildings;    //might be useful to use an arraylist/stack/queue instead of an array depending on implementation
        UFO craft;

    constructor:
        setup new Building objects and add to list buildings
        initialize craft to new UFO

    paintComponent:
        calls the paint methods for each building & the ufo craft

Building:
    variables:
        int x, y; // position of building
        int height, width; // of this building

    constructor:
        initializes x, y // probably needs to be inputed from CityScape with this setup
        calc height and width randomly // stored in this.height/width

    paint:
        paints single building based on it's variables

//side-note, you'll probably need getters for the x/y/width to build each building from CityScape

Everything else should be much the same.

Good Luck !