I have recently been making a game and came across a problem I could not solve. My problem is with removing the content pane of a JFrame and setting as something else. While this works, the KeyListener in the class of the content pane does not work unless I change the primary window on the computer to something else then back to the JFrame.
I replicated the problem in a smaller amount of code than what is was originally:
import java.awt.*;
import java.awt.event.*;
import javax.awt.swing.*;
public class TheFrame extends JFrame{
private JButton play;
private FirstPanel fp;
private SecondPanel sp;
public TheFrame(){
setSize(800, 600);
setLocationRelativeTo(null);
setResizable(false);
setDefaultCloseOperation(EXIT_ON_CLOSE);
fp = new FirstPanel();
setContentPane(fp);
setVisible(true);
}
public static void main(String args[]){
TheFrame tf = new TheFrame();
}
class FirstPanel() extends JPanel{
private boolean test = false;
public FirstPanel(){
play = new JButton("play");
play.addActionListener(new PlayListener());
add(play);
}
public void paintComponent(Graphics g){
if(test == true){
sp = new SecondPanel();
removeAll();
revalidate();
setContentPane(sp);
}
}
class PlayListener implements ActionListener{
public void actionPerformed(ActionEvent e){
test = true;
repaint();
}
}
}
}
Here is also the code for the class SecondPanel:
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.*;
public class SecondPanel extends JPanel implements KeyListener{
private int draw = 0;
public SecondPanel(){
addKeyListener(this);
}
public void paintComponent(Graphics g){
super.paintComponent(g);
g.drawString("press f to draw circles", 90, 40);
if(draw > 0){
for(int i = 0; i < draw; i++){
g.drawOval((i*100)+100, (i*100)+100, 100, 100);
}
}
}
public void keyTyped(KeyEvent e){
if(e.getKeyChar() == 'f' || e.getKeyChar() == 'F'){
draw++;
repaint();
}
}
}
So before anything else, this...
public void paintComponent(Graphics g){
if(test == true){
sp = new SecondPanel();
removeAll();
revalidate();
setContentPane(sp);
}
}
This incredibly bad! First, you are breaking the paint chain (not calling super.paintComponent) and secondly, you are changing the state of the component from within the paint cycle, this will trigger a new repaint request and will call your paintComponent again and again and again and again ....
Painting is for painting the current state of the component, nothing more. NEVER change the state of any component from within ANY paint method EVER
Instead of trying to use remove/add, consider using a CardLayout instead, see How to Use CardLayout. This will allow you to switch between the first and second panels based on your needs, from a centralised control point.
KeyListener is a fickle mistress, it wants all the attention, all of the time. It will only raise key events if the component it is registered to is focusable AND has focus. A better solution is to use the key bindings API, which has been designed to overcome this limitation and provide you with a level of control over the level of focus required to trigger the associated actions.
See How to Use Key Bindings for more details
To swap content of a container, be it a JFrame's contentPane or any JPanel, consider using a CardLayout since this tool was built specifically for this job.
Note that this code:
sp = new SecondPanel();
removeAll();
revalidate();
setContentPane(sp);
should never be found inside of a paintComponent method. This method is not under our direct control, and should be for painting and painting only. Also by not calling the super's method, you have broken the painting chain.
Also, instead of KeyListeners, use Key Bindings, and your functionality should work.
For instance, please have a look at the similar CardLayout code I created today for another similar question.
Related
I'm practising to draw a shape on a JPanel by clicking on a Jbutton, but I cannot. It's been five hours that I'm surfing the web, but I cannot find the way to do it.
This is what I want to do: if I click on "Rectangle" button a rectangle appears under the buttons and if I click on "Circle" button a circle appears.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
class Shape extends JFrame {
JButton rec, circle;
static String botSelected;
Shape (){
frameSet ();
}
void frameSet(){
JFrame frame = new JFrame();
frame.setVisible(true);
frame.setSize(600,300);
rec = new JButton ("Rectangle");
circle = new JButton("Circle");
JPanel panel = new JPanel();
frame.add(panel);
panel.add(rec);
panel.add(circle);
Click clk = new Click();
rec.addActionListener(clk);
circle.addActionListener(clk);
}
public void paint (Graphics g){
super.paint(g);
if (botSelected.equals("Rectangle"))
g.fillRect(50,50,50,50);
else if (botSelected.equals("Circle"))
g.fillOval(50,50,50,50);
}
public static void main (String [] arg){
Shape s = new Shape();
}
}
class Click implements ActionListener{
public void actionPerformed (ActionEvent e){
Shape.botSelected = e.getActionCommand();
}
}
The first thing I would do is have a read through Painting in Swing and Performing custom painting to better understand how the painting process works.
Next you need to understand that JFrame is a bad choice for painting to. Why? Because it's multilayered.
A JFrame contains a JRootPane, which contains a JLayeredPane the contentPane, glassPane and the JMenuBar and in your example, it also contains a JPanel.
With the (default) exception of the glassPane, all these components are opaque.
While it's possible to have something drawn in the paint method show it, if any of the other components paint themselves, it will be wiped clean - this is because Swing components can actually be painted independently of each other, with having to have the parent paint itself first.
A better solution is to start by extending from JPanel and override its paintComponent method.
For simplicity, I'd also encourage you to implement the ActionListener against this class as well, it will allow the actionPerformed method to access the properties of the component and, in your case, call repaint to trigger a paint cycle when you want to update the UI.
Here is a derived example from your code.
As #MadProgrammer said, don't extend JFrame.
In the following example, here are the major changes :
give a non-null value to botSelected, or the first calls to paintComponent will give you a NullPointerException
the class now extends JPanel, and overrides paintComponent for custom painting
the ActionListener is an anonymous class, because you don't need a separate class, and it has direct access to the fields from Shape
botSelected is no longer static (see above point)
.
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
class Shape extends JPanel {
JButton rec, circle;
String botSelected = "";// don't let it be null, it would make paintComponent crash on startup
Shape() {
frameSet();
}
void frameSet() {
JFrame frame = new JFrame();
frame.setVisible(true);
frame.setSize(600, 300);
rec = new JButton("Rectangle");
circle = new JButton("Circle");
frame.add(this);
this.add(rec);
this.add(circle);
// anonymous class, has access to fields from the outer class Shape
ActionListener clk = new ActionListener() {
public void actionPerformed(final ActionEvent e) {
botSelected = e.getActionCommand();
repaint();
}
};
rec.addActionListener(clk);
circle.addActionListener(clk);
}
//custom painting of the JPanel
#Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
if (botSelected.equals("Rectangle")) {
g.fillRect(50, 50, 50, 50);
} else if (botSelected.equals("Circle")) {
g.fillOval(50, 50, 50, 50);
}
}
public static void main(final String[] arg) {
Shape s = new Shape();
}
}
When instantiating my modified JPanel class, I pass in a file through the constructor. The file (XML) gets read and the data gets used by me later in the paint method. However, right after I parse the data, I call both repaint() and revalidate(), but my GUI looks exactly the same. I call these methods both in my main class which extends JFrame and my panel class which extends JPanel.
When I choose an xml file from my JFileChooser, the drawPanel class gets instantiated with its other constructor, taking in the file and parsing it, then calling repaint and revalidate. I omitted most of the code to save your time.
Here's the main class' code:
public class myCode extends JFrame {
public myCode() {
super("Roadway Simulator");
setSize(800, 600);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(null);
drawie = new drawPanel();
drawie.setSize(new Dimension(width - 200, height));
drawie.setMinimumSize(new Dimension(width - 200, height));
drawie.setMaximumSize(new Dimension(width - 200, height));
drawie.setLocation(0, 0);
add(drawie);
setVisible(true);
try{Thread.sleep(500);revalidate();repaint();}catch(InterruptedException eeee){}
}
public static void main(String[] args){new myCode();}
}
Here's my drawPanel class:
class drawPanel extends JPanel {
boolean drawRoad = false;
public drawPanel() {
super();
}
public drawPanel(Document doc){
super();
//the change in my paint method
drawRoad = true;
revalidate();
repaint();
}
public paint(Graphics g){
super.paint(g);
if(drawRoad){
g.setColor(Color.BLACK);
g.fillRect(0,0,600,600);
}
}
}
My code is the same as above, just with a lot more detail. Why isn't my JFrame repainting?
Here:
try{Thread.sleep(500);revalidate();repaint();}catch(InterruptedException eeee){}
Understand what Thread.sleep(...) does to a Swing GUI when called on the Swing event thread -- it puts the current thread which happens to be the Swing event thread, the one responsible for all drawing and user interaction, to sleep. In other words, you put your entire application completely to sleep.
Solution -- don't call this ever on the event thread.
As an aside, there's no cost to putting each method call on its own line, and no reason for that long line that you've posted as it serves no purpose other than to confuse.
try{Thread.sleep(500);revalidate();repaint();}catch(InterruptedException eeee){} is most likely blocking the Event Dispatching Thread, preventing from processing the Event Queue and making it look like your program has hung.
See Concurrency in Swing for more details...
It is also not recommended to override paint of Swing components and instead use paintComponent, see Performing Custom Painting for more details
In your case, I would recommend using a javax.swing.Timer instead of Thread.sleep, see How to use Swing Timers for more details
Updated
I don't see any where in your code that would change drawRoad from false to true, so your paint method is painting...nothing...so I guess you frame is painting exactly the way you told it to...
You may also like to take a look at Initial Threads and you might like to have a read through Code Conventions for the Java TM Programming Language, it will make it easier for people to read your code and for you to read others
Updated
Given that you example is incomplete and won't compile, when I rebuild it, this will work...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
import org.w3c.dom.Document;
public class TestDraw extends JFrame {
public TestDraw() {
super("Roadway Simulator");
setSize(800, 600);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
DrawPanel drawie = new DrawPanel(null);
add(drawie);
setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
new TestDraw();
}
});
}
class DrawPanel extends JPanel {
boolean drawRoad = false;
public DrawPanel() {
super();
}
public DrawPanel(Document doc) {
super();
drawRoad = true;
revalidate();
repaint();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(600, 600);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (drawRoad) {
g.setColor(Color.BLACK);
g.fillRect(0, 0, getWidth(), getHeight());
}
}
}
}
If I change DrawPanel drawie = new DrawPanel(null); to DrawPanel drawie = new DrawPanel(); it still paints, but doesn't perform your custom painting.
The other problem is, as has already been highlighted, is the use of null layouts
Avoid using null layouts, pixel perfect layouts are an illusion within modern ui design. There are too many factors which affect the individual size of components, none of which you can control. Swing was designed to work with layout managers at the core, discarding these will lead to no end of issues and problems that you will spend more and more time trying to rectify.
Have a look at Why is it frowned upon to use a null layout in SWING? for more details...
Now, having said that, when you add drawie, you never give it a size, Swing is smart enough not to paint 0x0 sized components...
I am trying to write a simple 2d animation engine in Java for visualizing later programming projects. However, I am having problems with the window refresh. On running, the frame will sometimes display a blank panel instead of the desired image. This begins with a few frames at a time at apparently random intervals, worsening as the program continues to run until the actual image only occasionally blinks into view. The code for processing each frame is run, but nothing in the frame is actually displayed. I believe the problem may come from my computer more than my code (certainly not from bad specs though), but am not sure. Help much appreciated.
Three classes. Code here:
package animator;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.applet.AudioClip;
public class APanel extends JPanel
{
public APanel(int l, int h){
setPreferredSize(new Dimension(l,h));
setLocation(80, 80);
setVisible(true);
setFocusable(true);
}
public Graphics renderFrame(Graphics g){
return g;
}
public void paintComponent(Graphics g) {
requestFocusInWindow();
renderFrame(g);
}
}
package animator;
import java.awt.*;
public class Animator extends APanel
//extending the APanel class allows you to code for different animations
//while leaving the basic functional animator untouched
{
public static final int SCREEN_X = 700;
public static final int SCREEN_Y = 700;
int frameNum;
public Animator() {
super(SCREEN_X, SCREEN_Y);
frameNum = 0;
}
public Graphics renderFrame(Graphics g) {
frameNum++;
g.drawString(""+frameNum,5,12);
return g;
}
}
package animator;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.Timer;
public class Runner {
int framerate = 30;
Animator a = new Animator();
JFrame j = new JFrame();
public Runner(){
j.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
j.add(a);
start();
j.setSize(a.getPreferredSize());
j.setVisible(true);
}
public void start() {
Timer t = new Timer(1000/framerate, new ActionListener() {
public void actionPerformed(ActionEvent e){
j.getComponent(0).paint(j.getComponent(0).getGraphics());
//The following line of code keeps the window locked to a preferred size
// j.setSize(j.getComponent(0).getPreferredSize());
}
});
t.start();
}
public static void main(String[] args){
Runner r = new Runner();
}
}
There are some serious mistakes in your code which could be the cause or a factor of your problem...
j.setSize(a.getPreferredSize()); is irrelevant, simply use JFrame#pack, you get better results as it takes into account the frame decorations
j.setSize(j.getComponent(0).getPreferredSize()); use JFrame#setResizable and pass it false instead...
NEVER do j.getComponent(0).paint(j.getComponent(0).getGraphics()); this! You are not responsible for the painting of components within Swing, that's the decision of the RepaintManager. Just call j.repaint()
super(SCREEN_X, SCREEN_Y);...just override the getPreferredSize method and return the size you want.
setLocation(80, 80); irrelevant, as the component is under the control of a layout manager
setVisible(true); (inside APanel)...Swing components are already visible by default, with the exception of windows and JInternalFrames
And finally...
public void paintComponent(Graphics g) {
requestFocusInWindow();
renderFrame(g);
}
There is never any need for this method to be made public, you NEVER want someone to be able to call it, this will break the paint chain and could have serious ramifications in the ability for the component to paint itself properly.
You MUST call super.paintComponent before performing any custom painting, otherwise you could end up with all sorts of wonderful paint artifacts and issues
Never modify the state of a component from within a paint method...while it "might" not be an immediate issue calling requestFocusInWindow(); within your paintComponent could have side effects you don't know about...
Instead, it should look more like...
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
renderFrame(g);
}
In this program, I want to draw a series of lines that interact to form a web. Each time the timer ticks, a line is drawn. Therefore, I cannot have the super.paintComponent(g) call in the paintComponent() because I need the previous lines to appear. However, I would like to set the background colour and as far as I've found, the setBackground() method can be called only if the super call is first made. I am not sure if the fillRect method would work either because it would draw a rectangle over the old line each time. I tried having the setBackground() method in the constructor, and it did not work either.
Here is my code:
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
public class prettyWebPanel extends JPanel implements ActionListener {
Timer time = new Timer(100,this);
private Color colour1 = Color.black;
private Color colour2 = Color.white;
JButton start = new JButton("Start");
int j = 0;
public prettyWebPanel() {
setPreferredSize(new Dimension (550,550));
this.add(start);
start.addActionListener(this);
setBackground(colour1);
}
public void paintComponent(Graphics g) {
setBackground(colour1);
setForeground(colour2);
if (j<490) g.drawLine(20, 20+j, 20+j, 500);
}
public void actionPerformed(ActionEvent e) {
if (e.getSource() == start) time.start();
else if (e.getSource() == time) {
j+=10;
repaint();
}
}
}
because I need the previous lines to appear.
Then you need to do incremental painting. See Custom Painting Approaches for two common ways to do this:
Keep a List of objects to paint and repaint them every time
Do the painting onto a BufferedImage.
Im currently trying to make my first game and i am having trouble getting the repaint() method to work. I have checked my keyListeners and have confirmed that they are working a ok! The ship that i have created moves but only if i forcibly resize the window by dragging on the sides. If anyone has any tips i would be very greatful!
If you need any more information feel free to ask!
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class Main extends Canvas implements KeyListener{
public static Ship playerShip = new Ship(150,450,"F5S4-Recovered.png");
public int numberOfEnemies = 0;
public static void createFrame(){
Window frame1 = new Window();
final JPanel pane = new JPanel(){
public void paintComponent(Graphics g){
super.paintComponent(g);
g.setColor(Color.BLACK);
g.fillRect(0, 0, 1000, 1000);
g.drawImage(playerShip.image, playerShip.xPos1, playerShip.yPos1, this);
}
};
frame1.add(pane);
}
public void keyTyped(KeyEvent e){System.out.println("KeyTyped");}
public void keyPressed(KeyEvent e){
switch(e.getKeyCode())
{
case KeyEvent.VK_LEFT :
playerShip.xPos1-=2;
break;
case KeyEvent.VK_RIGHT:
playerShip.xPos1+=2;
break;
}
repaint();
}
public void keyReleased(KeyEvent e){}
public static void main(String args[]){
createFrame();
}
}
Window class ----------------------------------------------------------
import javax.swing.*;
public class Window extends JFrame{
public Window()
{
setTitle("Space Game");
setSize(800,800);
setDefaultCloseOperation(EXIT_ON_CLOSE);
addKeyListener(new Main());
setVisible(true);
}
}
Your call to repaint() is repainting the class Canvas, but the painting is done on the JPanel pane. The resizing causes an automatic repaint of the panel. So to fix you want to pane.repaint(), but you can't do that unless you put the panel as a class member, so you can access it from the listener method. Right now, it's currently locally scoped in the createFrame() method.
Also, you should probably add the listener to the panel instead, and not even extend Canvas, since you're not even using it
Other Notes:
Look into using Key Bindings instead of low-level KeyListener
Swing apps should be run from the Event Dispatch Thread (EDT). You can do so by simply wrapping the code in your main in a SwingUtilities.invokeLate(..). See more at Initial Threads
Again, I'll just add, Don't extends Canvas