Can't Use .addKeyListener(this) for a static JPanel, but need the JPanel to stay static - Java

1.1k Views Asked by At

I am trying to make a simple program where an oval follows your mouse cursor, and if you enter "r", "g", or "b" on the keyboard, the oval changes color accordingly.

However, I cannot get my KeyListener to work. Here is my issue. I have a static JPanel, because I need it to be accessible in all functions and methods. However, Java does not let you do this with a static JPanel. I need the JPanel to be static so I can set the color in the keyPressed(KeyEvent e) function.

I understand the basics of Java fairly well, and am grasping some more complicated concepts. Please try to explain if there is any complex code. Thank you!

Here is the code in Drivers.java, the main class.

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import javax.swing.*;

public class Drivers implements KeyListener 
{

    // panel.red = panel.red - 3;
    // panel.green = panel.green - 3;
    // panel.blue = panel.blue - 3;

    public static JFrame frame = new JFrame();
    public static ShapesPanel panel = new ShapesPanel().addKeyListener(this);
    // Notice the error we get with the addKeyListener(this);

    public static void main(String[] args)
    {
        // Creates new pointer info
        PointerInfo info;
        // Creates a point (for mouse tracking)
        Point point;
        JLabel label = new JLabel();
        panel.add(label);
        // Set window size
        panel.setPreferredSize(new Dimension(300, 350));
        // Set panel inside frame
        frame.setContentPane(panel);
        // Transparent 16 x 16 pixel cursor image.
        BufferedImage cursorImg = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
        // Create a new blank cursor.
        Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
        cursorImg, new Point(0, 0), "blank cursor");
        // Set the blank cursor to the JFrame.
        frame.getContentPane().setCursor(blankCursor);
        // Compile everything into the frame
        frame.pack();
        // Set frame to close on red exit button
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // Get screen size
        Dimension sSize = Toolkit.getDefaultToolkit().getScreenSize();
        // Position frame
        frame.setLocation(sSize.width / 2 - frame.getWidth(), sSize.height / 2 - frame.getHeight());
        // Make frame visible
        frame.setVisible(true);
        // Set name of frame
        frame.setTitle("Graphical User Interface");
        // While loop to draw oval
        while(true)
        {
            // Repaint the panel 
            panel.repaint();
            // Get mouse info (for tracking)
            info = MouseInfo.getPointerInfo();
            // Set mouse location data to point
            point = info.getLocation();
            // Create variables to store coordinates of oval from mouse point location
            int x = (int) point.getX();
            int y = (int) point.getY();
            // Assign those coordinate variables to oval
            panel.x = x;
            panel.y = y;
//          System.out.println("X: " + x);
//          System.out.println("Y: " + y);
//          System.out.println("X: " + point.getX());
//          System.out.println("Y: " + point.getY());
            // Try-catch to sleep, to reduce some memory
            try
            {
                Thread.sleep(10);
            }
            catch(InterruptedException e)
            {

            }
        }
    }
    // If key is pressed
    public void keyPressed(KeyEvent e) 
    {
        // If key is R, change color and print that key has been pressed
        if (e.getKeyCode() == KeyEvent.VK_R) 
        {
            System.out.println("R");
            panel.red = 255;
            panel.green = 0;
            panel.blue = 0;
        }
        // If key is G, change color and print that key has been pressed
        if (e.getKeyCode() == KeyEvent.VK_G) 
        {
             System.out.println("G");
             panel.red = 0;
             panel.green = 255;
             panel.blue = 0;
        }
        // If key is B, change color and print that key has been pressed
        if (e.getKeyCode() == KeyEvent.VK_B) 
        {
            System.out.println("B");
            panel.red = 0;
            panel.green = 0;
            panel.blue = 255;
        }
    }
    // Doesn't do anything.. yet
    @Override
    public void keyReleased(KeyEvent e) 
    {

    }

    @Override
    public void keyTyped(KeyEvent e) 
    {

    }
}

And, here is the code in ShapesPanel.java:

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

public class ShapesPanel extends JPanel 
{
    // Create x and y variables, and set as default to zero
    public int x = 0, y = 0;
    // Create RGB variables, used for changing color
    public int red = 0, green = 0, blue = 0;
    private static final long serialVersionUID = 1L;

    // Create new paintComponent function, using an override
    @Override
    public void paintComponent(Graphics g)
    {
        // Create new Graphics2D g2 version
        Graphics2D g2 = (Graphics2D) g;
        // Reset screen, so there are no trails
        g2.clearRect(0, 0, getWidth(), getHeight());
        // Set background, currently unfunctional
//      g2.setBackground(new Color(235, 150, 30));
        // Set color palette, using RGB variables
        g2.setColor(new Color(red, green, blue));
        // Use color palette to draw oval, using x and y variables
        g2.fillOval(x, y, 100, 100);
    }

}
1

There are 1 best solutions below

1
On BEST ANSWER

I have a static JPanel, because I need it to be accessible in all functions and methods.

This is not a good reason for making a field static.

However, Java does not let you do this with a static JPanel.

This is not true at all. You can add KeyListeners or any other similar construct just as easily to static and non-static fields. Your problem has nothing to do with restrictions on use of static fields. It's all because you're trying to use this in a static context where this doesn't exist.

Note that your compiler error could go away with something as simple as:

public static ShapesPanel panel = new ShapesPanel().addKeyListener(new Drivers());

I need the JPanel to be static so I can set the color in the keyPressed(KeyEvent e) function.

This again is not a good reason for the field to be static. A Swing listener has direct access to the listened to component any time and at all times via the XxxEvent parameter's getSource() method. For instance, if you used a KeyListener, then its method's KeyEvent parameter has a getSource() method which will return the component (here your drawing JPanel) that is being listened to. If you need references to other components or objects, then pass them into the listener via a constructor setter parameter.


  1. Your main problem is that you're trying to use this in a static context, and this doesn't exist within this context.
  2. First thing is to not make your panel field static. You state that you know Java pretty well, but then give a wrong reason for making it static. Instead make it an instance field and pass the instance where needed.
  3. You've got many other issues with this code including:
    • You've a huge static main method. This method should be much smaller and its job should be to create the key objects of your program, set them running and that's it.
    • You've a Swing program that has a while (true) and a Thread.sleep(...) within code that on later iterations (when you've structured your program better and have started all Swing code on the event thread) risk being called on the Swing event thread. You will want to get rid of these guys and consider using a Swing Timer instead.
    • Your paintComponent method does not call the super's method, breaking the Swing painting chain.
    • You would be much better off not using a KeyListener but instead using Key Bindings.
    • Your while (true) block isn't even needed. Don't poll for the mouse's position but instead use a MouseListener and/or MouseMotionListener for this.

For example:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.*;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;

public class KeyBindingTest {
   // start gui
   private static void createAndShowGui() {
      KeyBindingPanel mainPanel = new KeyBindingPanel();

      JFrame frame = new JFrame("Key Binding Example");
      frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
      frame.getContentPane().add(mainPanel);
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   // start all in a thread safe manner
   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

class KeyBindingPanel extends JPanel {
   private static final long serialVersionUID = 1L;
   private static final int PREF_W = 600;
   private static final int PREF_H = PREF_W;
   private static final Color BACKGROUND = Color.WHITE;
   private Color ovalColor = Color.blue;
   private int ovalX = PREF_W / 2;
   private int ovalY = PREF_H / 2;
   private int ovalWidth = 100;

   public KeyBindingPanel() {
      setName("Key Binding Eg");
      setBackground(BACKGROUND);

      final Map<Color, Integer> colorKeyMap = new HashMap<>();
      colorKeyMap.put(Color.BLUE, KeyEvent.VK_B);
      colorKeyMap.put(Color.RED, KeyEvent.VK_R);
      colorKeyMap.put(Color.GREEN, KeyEvent.VK_G);

      // set Key Bindings
      int condition = WHEN_IN_FOCUSED_WINDOW;
      InputMap inputMap = getInputMap(condition);
      ActionMap actionMap = getActionMap();

      for (final Color color : colorKeyMap.keySet()) {
         int keyCode = colorKeyMap.get(color);
         KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, 0);
         inputMap.put(keyStroke, keyStroke.toString());
         actionMap.put(keyStroke.toString(), new ColorAction(color));
      }

      MyMouse myMouse = new MyMouse();
      addMouseMotionListener(myMouse);
   }

   public void setOvalColor(Color color) {
      ovalColor = color;
      repaint();
   }

   public void setOvalPosition(Point p) {
      ovalX = p.x;
      ovalY = p.y;
      repaint();
   }

   @Override
   protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g;
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setColor(ovalColor);
      int x = ovalX - ovalWidth / 2;
      int y = ovalY - ovalWidth / 2;
      g2.fillOval(x, y, ovalWidth, ovalWidth);
   }

   @Override // make panel bigger
   public Dimension getPreferredSize() {
      if (isPreferredSizeSet()) {
         return super.getPreferredSize();
      }
      return new Dimension(PREF_W, PREF_H);
   }   
}

class ColorAction extends AbstractAction {
   private static final long serialVersionUID = 1L;
   private Color color;

   public ColorAction(Color color) {
      this.color = color;
   }

   @Override
   public void actionPerformed(ActionEvent e) {
      // get reference to bound component
      KeyBindingPanel panel = (KeyBindingPanel) e.getSource();
      panel.setOvalColor(color);
   }
}

class MyMouse extends MouseAdapter {
   @Override
   public void mouseMoved(MouseEvent e) {
      // get reference to listened-to component
      KeyBindingPanel panel = (KeyBindingPanel) e.getSource();
      panel.setOvalPosition(e.getPoint());
   }
}

You may ask, why use a Map<Color, Integer> when creating the Key Bindings?

Doing this allows me to use the for loop to avoid code repetition. Often more concise code is easier to understand and to debug. It also makes it easier to enhance the program later. For instance, if later I want to add Color.CYAN and associate it with the c char, all I have to do is add another entry to my Map:

colorKeyMap.put(Color.CYAN, KeyEvent.VK_C);

and boom it's done. If I needed more than a 1-1 association, then I'd consider using an Enum or separate class to hold the related attributes together.