This is an animation in which a given number of icons start on the left side of the frame and race across the screen to the right side. Each icon is drawn to its own JPanel that fills a row within a container's single-column GridLayout, and each JPanel races on its own thread. (Threads must be used, even though a Swing Timer might be a better approach.)
A finish line is also supposed to get painted onto the container, but it is not showing up. I have tried setting the Racer's JPanel opacity to false, but this doesn't work. How might I get the other threads to allow the finish line's painting to execute?
The code that isn't working:
gui = new JPanel() {
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawLine(finishLineXPos, 0, finishLineXPos, windowHeight);
}
};
The full code:
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
public class Races2 {
private JFrame frame;
private JPanel gui; // to hold all components
private int finishLineXPos; // x-coordinate of finish line
private Icon racerImg;
private int racerImgWidth;
private int racerImgHeight;
private int numOfRacers;
private ArrayList<Racer> racers;
private Racer winner;
private int windowHeight;
private int windowWidth;
public Races(int num) {
numOfRacers = num;
racerImg = new ImageIcon("races.png");
racerImgWidth = racerImg.getIconWidth();
racerImgHeight = racerImg.getIconHeight();
windowHeight = racerImgHeight * numOfRacers;
windowWidth = racerImgWidth * 20;
finishLineXPos = racerImgWidth * 18; // two icon widths from the right
frame = new JFrame("Off to the Races - by Brienna Herold");
frame.setResizable(false); // prevents window resizing which affects painting
gui = new JPanel() {
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawLine(finishLineXPos, 0, finishLineXPos, windowHeight);
}
};
gui.setLayout(new GridLayout(numOfRacers,1));
gui.setPreferredSize(new Dimension(windowWidth, windowHeight));
// Create and add racers to gui panel
racers = new ArrayList<Racer>();
for (int i = 0; i < numOfRacers; i++) {
Racer racer = new Racer();
gui.add(racer);
racers.add(racer);
}
// Start racers
for (Racer racer : racers) {
Thread racerThread = new Thread(racer);
racerThread.start();
}
frame.add(gui);
frame.pack();
frame.setVisible(true);
}
protected class Racer extends JPanel implements Runnable {
private int lastPosX;
private int posX;
public Racer() {
posX = 0;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
racerImg.paintIcon(this, g, posX, 0);
posX += Math.random() * 20;
}
@Override
public void run() {
// While the race has not been won yet, proceed with race
while (winner == null) {
repaint();
try {
Thread.sleep(100); // slows down racing a bit
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// If racer passes specified x-coordinate, set it as winner
if (posX >= finishLineXPos) {
System.out.println("Winner: " + this.getName());
winner = this;
}
}
}
}
}
Each icon is drawn to its own JPanel that fills a row within a container's single-column GridLayout
First you need to learn how to do custom painting. Start by reading the section from the Swing tutorial on Custom Painting for some basics. A couple of key points:
You need to override the getPreferredSize() method to the component has a preferred size so the layout manager can do its job. If you don't specify the preferred size then the size may be zero so there is nothing to paint.
A painting method is for painting only. You should NEVER modify the state of the component in a painting method since you can't control when Swing will repaint a component. So you need a method like setPositionX(...)
to control the image location where it should be painted. Then in the Thread (or Timer) you invoke this method to change the location and invoke repaint() on the component.
A finish line is also supposed to get painted onto the container, but it is not showing up
Well you add all the Racer components to the top of the race panel so those components will cover the line painted on the panel.
You have been given one approach to use racer.setOpaque(false)
;
Another approach is to override the paint() method. (This is an exception to the general rule to do custom painting in the paintComponent() method). If you read the tutorial link I provided you will see that using this approach will cause the painting of the race panel to be done after all the Racer components have been painted.
frame.setResizable(false); // prevents window resizing which affects painting
It is not necessary for this. The only reason the painting is affected is because you are changing the state of the component in the painting method. See my comment above.
racerImg.paintIcon(this, g, posX, 0);
People generally use the drawImage(...) method to paint an image. There is no reason to create an Icon just to paint the image.
racerImg = new ImageIcon("races.png");
Don't use an ImageIcon to read a file. Using ImageIO.read(...) to read the image. Then just paint the image as described above.
and each JPanel races on its own thread.
That seems like a silly requirement because it brings a different kind of randomness to the race. You already have logic that generates a random distance for the image to move, so why do you need separate Threads for this. Instead you should just have a single Thread and then you iterate through all the Races and invoke the setPositionX()
method suggested above. Then all Races will be repainted at the same time with there own random distance change.
Edit:
Having said all the above your code works fine for me with a single change:
posX = 0;
setOpaque(false);
You just need to make the Racer transparent in it's constructor.
As this appears to be a school assignment I guess you have to follow the rules, but you really should understand the problems with the assignment.
As everybody else in this posting has suggested, I still believe the better approach is to have an ArrayList or Racer objects (not components) that you can paint. I gave you a working example of painting Ball objects in your last question on this topic.