Related
I'm new here but I did some research before posting. My goal is to create a simple tower defense game using a couple of interesting ideas and moreover to train my development skills using javax.swing and java.awt. As far as I know, developers are mostly lazy guys and they do everything to make their life more simple.
There is map with a grid and for map loading my game uses a boolean matrix and a loading method to locate terrain on panels. I thought it will be quite simple solution. Because the matrix is 12 x 12, I would like to create it with some other application rather than entering a line of 144 numbers.
Here comes an idea to first create a map editor application and later do maps for levels in it. When I have such a tool I could make that map visually and then save its boolean matrix to a file, which later can be read by loading method and recreated in game. Next step is to make graphics and also panels that would react properly on user's actions. On the left there is a panel with buttons - after user clicks one of them, the field currentColor changes.
This field is used by method that implements actionListener and makes color change of the panel that is declared in its constructor. I wanted to change color of certain panel when its clicked. I use colors because its easier for now to make it working, later I want to replace color with a texture - obviously, I know I have to use a paintComponent method, but I assume that will work for it too, right? Also would be nice if the panel border changes color when I move my cursor over it and changes it back to normal when mouse is somewhere else.
The point here is that I'm having some trouble to make panels interactive. First problem is that panels are created in for loop and that makes it difficult to refer to a certain panel while mouse is over it. Another one comes with that I would like to change appearance of that panel after I click on it.
As far as I know, MouseListeners should do the work, but how to actually write it to have an effect on screen? I found some post about that, but for me it doesn't work. Here's the link: highlighting panels in java
My code:
import javax.swing.*;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class Editor extends JFrame
{
private JButton towers = new JButton(new ImageIcon("pu.gif"));
private JButton road = new JButton(new ImageIcon("pu.gif"));
private JButton start = new JButton(new ImageIcon("pu.gif"));
private JButton finish = new JButton(new ImageIcon("pu.gif"));
private String mapTitle = "testmap";
private Color currentColor;
private int width = Toolkit.getDefaultToolkit().getScreenSize().width;
private int height = Toolkit.getDefaultToolkit().getScreenSize().height;
private String currentMapType = "Standard";
private static final int currentHeight = 12;
private static final int currentWidth = 12;
private JPanel[][] currentMapPanel;
private int[][] currentMapField;
//Toolbar - a panel with buttons
private JPanel panel = new JPanel(new GridLayout(10,3));
//Container for map - a panel with map
private Dimension containerSize = new Dimension(height, height);
static JPanel container = new JPanel(new GridLayout(currentHeight, currentWidth), true);
//Separator
private JSplitPane separator = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panel, container);
public Editor()
{
initComponents();
}
public void initComponents()
{
this.setTitle(mapTitle + ".map" + " - " + "Game Map Editor");
this.setSize(800, 600);
int frameWidth = this.getSize().width;
int frameHeight = this.getSize().height;
this.setLocation((width - frameWidth) / 2, (height - frameHeight) / 2);
this.setIconImage(Toolkit.getDefaultToolkit().getImage("pu.gif"));
towers.addActionListener(e -> {
currentColor = Color.CYAN;
System.out.println(currentColor);
});
road.addActionListener(e -> {
currentColor = Color.GRAY;
System.out.println(currentColor);
});
start.addActionListener(e -> {
currentColor = Color.LIGHT_GRAY;
System.out.println(currentColor);
});
finish.addActionListener(e -> {
currentColor = Color.BLACK;
System.out.println(currentColor);
});
new Map(currentMapType, currentWidth, currentHeight, false);
panel.add(towers);
panel.add(road);
panel.add(start);
panel.add(finish);
this.getContentPane().add(separator);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
}
/**
* Class that allows to load the graphic map and to view it in JFrame
*/
public class Map
{
public Map(String mapType, int rows, int columns, boolean load)
{
if (!load)
{
currentMapPanel = mapPanel(rows, columns);
currentMapField = new MapGenerator().mapFieldEmpty(rows, columns);
mapLoader(currentMapField, currentMapPanel);
}
else
{
currentMapPanel = mapPanel(rows, columns);
currentMapField = new MapGenerator().mapFieldGenerator(rows, columns);
mapLoader(currentMapField, currentMapPanel);
}
}
private JPanel[][] mapPanel(int rows, int columns)
{
JPanel[][] mapPanel = new JPanel[rows][columns];
for (int i = 0; i < rows - 1; i++)
{
for (int j = 0; j < columns - 1; j++)
{
mapPanel[i][j] = new JPanel(true);
mapPanel[i][j].setPreferredSize(new Dimension(height/12, height/12));
mapPanel[i][j].setBorder(new LineBorder(Color.BLACK));
mapPanel[i][j].addMouseListener(new MouseAdapter() {
#Override
public void mouseEntered(MouseEvent e) {
super.mouseEntered(e);
JPanel parent = (JPanel) e.getSource();
new colorListener(parent, Color.LIGHT_GRAY);
parent.revalidate();
}
#Override
public void mouseExited(MouseEvent e)
{
super.mouseExited(e);
JPanel parent = (JPanel) e.getSource();
new colorListener(parent, Color.GREEN);
parent.revalidate();
}
#Override
public void mouseClicked(MouseEvent e)
{
super.mouseClicked(e);
JPanel parent = (JPanel) e.getSource();
new colorListener(parent, currentColor);
parent.revalidate();
}
});
}
}
return mapPanel;
}
private void mapLoader(int[][] mapField, JPanel[][] mapPanel)
{
for (int i = 0; i < mapField.length - 1; i++)
{
for (int j = 0; j < mapField.length - 1; j++)
{
if (mapField[i][j] == 0)
{
mapPanel[i][j].setBackground(Color.GREEN);
container.add(mapPanel[i][j]);
}
else if (mapField[i][j] == 1)
{
mapPanel[i][j].setBackground(Color.GRAY);
container.add(mapPanel[i][j]);
}
else if (mapField[i][j] == 2)
{
mapPanel[i][j].setBackground(Color.LIGHT_GRAY);
container.add(mapPanel[i][j]);
}
else if (mapField[i][j] == 3)
{
mapPanel[i][j].setBackground(Color.BLACK);
container.add(mapPanel[i][j]);
}
else
{
System.out.println("An error occurred...");
}
}
}
}
private JPanel mapContainer(int rows, int columns)
{
container = new JPanel();
container.setLayout(createLayout(rows, columns));
container.setPreferredSize(containerSize);
container.setBounds(height/4, height/4, containerSize.width, containerSize.height);
return container;
}
private GridLayout createLayout(int rows, int columns){
GridLayout layout = new GridLayout(rows, columns);
return layout;
}
}
private class colorListener implements ActionListener
{
public colorListener(JPanel p, Color c)
{
this.panel = p;
this.color = c;
}
#Override
public void actionPerformed(ActionEvent e) {
panel.setBackground(color);
}
JPanel panel;
Color color;
}
public static void main(String[] args) {
new Editor().setVisible(true);
}
}
The question is broad and the answer complicated.
Essentially, you want to do some research into concepts such as "separation of responsibilities" and "decoupling code".
The idea is that you break down you functionality requirements so that your objects are doing a single, specialised job. You also "decouple" the code so that changing the implementation of one part won't adversely affect other parts of the program. This is commonly achieved through the use of interfaces.
You will also want to investigate the concept of "model-view-controller", where by the "data" or "state" is modelled in one or more classes, but is wholly independent of the UI. The UI is then free to "render" the model in what ever way it feels is appropriate.
In this way, the "view" (interacting with the controller) can change the state (or react to the change in state) of the model, making it easier to mange (no seriously, it does)
Code Review ...
This...
static JPanel container = new JPanel(new GridLayout(currentHeight, currentWidth), true);
is dangerous and a bad idea. It voids the concept of encapsulation and allows any one to create new instance of container at any time, without notification, which will disconnect it from what the program was previously using. In fact, you actually do this.
static is not your friend. Used correctly, it's useful, but used in this way, it's just a bad idea and should be avoid.
You should instead favour "dependency injection", where the "elements" that any one object relies on are passed to it.
I would avoid things like...
this.setSize(800, 600);
int frameWidth = this.getSize().width;
int frameHeight = this.getSize().height;
this.setLocation((width - frameWidth) / 2, (height - frameHeight) / 2);
Windows are complicated components, which also contain window decorations which wrap about the content. This means that the available space to the content is window size - window decorations. Instead. You should rely on the layout manager API to provide appropriate sizing hints and pack the frame.
On most modern OSs you have "other" system elements, which, again, reduces the amount of available space on the screen (docks, task bars, other funky stuff). Instead, you can use setLocationRelativeTo(null) to centre the window more reliably on the screen.
Instead of setIconImage, you should be using Window#setIconImages(List), which allows you to pass a number of images which can be used by the API to represent the application in different places that require different resolution images.
Not sure what ...
new Map(currentMapType, currentWidth, currentHeight, false);
but it's not really helping.
If you find yourself just creating an instance of class without actually maintaining a reference to it, then it's probably a good sign of a bad design.
Your Map class raises a bunch of questions which aren't easily answered. It kind of makes me worried that the Map class is modifying the state of the parent class and screams "dependency injection" instead.
This...
mapPanel[i][j].setPreferredSize(new Dimension(height / 12, height / 12));
is best avoided. You should prefer overriding getPreferredSize and it should simply return a "desired" size, which could then be used by things like GridLayout to layout the component more effectively.
This then leads into the "separation of responsibility". This section suggestions you should have a "tile" class, which would be self managed and responsible for a single element from the model.
There are a number of things wrong with your mouse event handling...
#Override
public void mouseEntered(MouseEvent e) {
super.mouseEntered(e);
JPanel parent = (JPanel) e.getSource();
new colorListener(parent, Color.LIGHT_GRAY);
parent.revalidate();
}
You shouldn't be calling super.mouseXxx(e) on of the jobs of those methods is to call the delegate MouseListeners, so, mess right there.
You can more easily use e.getComponent() to get a reference to the component which generated the event, but if panel was a self contained unit of work (ie Tile) and the MouseListener an anonymous or inner class, you'd be able to forego the cast altogether.
new colorListener(parent, Color.LIGHT_GRAY); scares me as it's setting up a bunch of strongly references objects which can't be easily dereferenced, nor am I clear on there intent.
parent.revalidate(); isn't doing what you seem to think it's doing.
revalidate generates a new layout pass, what you seem to want is repaint.
These...
container.setPreferredSize(containerSize);
container.setBounds(height / 4, height / 4, containerSize.width, containerSize.height);
are just bad ideas. Let the content of the container, along with the layout manager deal with.
So, the short answer is, you have a lot of research left to do, things like:
OO design patterns
OO good practices, including "separation of responsibilities", "code decoupling" and in a more general sense, "dependency injection"
Model-View-Controller, coding to interface instead of implementation
just to name a few
I'm very new to coding(2 months) and i attempted to make Tic-Tac-Toe in java. I'm in a little over my head but i managed to create it using swing. My main problem is in the button1 class. I was going to use the getText() method but ended up not needing it or so i thought. I tried deleting it but as it turns out my tictactoe buttons don't switch letters without it. The compiler told me it overrides AbstractButton's getText() method but i don't see why that should matter since i never actually used it i thought. I'm thinking it's maybe a scope issue handled by it being overwritten somehow but i'm not sure. I was trying to use the text variable to update the button with setText() and that doesn't seem to work like i thought it should. I also don't understand why the 3 by 3 gridlayout seems to work properly most of the time but sometimes the number of buttons added is wrong.
So in summation the program works(mostly) but i'm not fully understanding how the button1 class is working.
TicTacToe.java
import java.awt.*;
import java.util.*;
import javax.swing.*;
public class TicTacToe extends JFrame {
public static void main(String[] args) {
JFrame window = new JFrame("Tic-Tac-Toe");
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
window.setSize(600, 600);
window.setLayout(new GridLayout(3, 3));
ArrayList<button1> buttonArrayList = new ArrayList<>(9);
for (int i = 0; i < 9; i++) {
button1 newbutton = new button1();
buttonArrayList.add(newbutton);
window.add(buttonArrayList.get(i));
}
}
}
button1.java
import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.JButton;
public class button1 extends JButton {
int value = 0;
String text = "";
public button1() {
class ButtonAction extends AbstractAction {
public ButtonAction() {}
#Override
public void actionPerformed(ActionEvent Switcher) {
System.out.println(text + " " + value);
value++;//value is a relic from earlier attempts that i just felt like keeping.
if (text.equals("O")) {
text = "X";
} else if (text.equals("X")) {
text = "";
} else if (text.equals("")) {
text = "O";
}
}
}
this.setAction(new ButtonAction());
this.setText(text);
this.setFont(new Font("Arial",Font.PLAIN,120));
}
public String getText()// <----culprit
{
return text;
}
}
A JButton class has a methods defined for it, including setText() (which will set the displayed text on the button) and getText() (which will return the current text that is displayed on the button).
You created a class button1 (note: classes should start with Capital Letters).
You added an Action to the button1 class, which means that when the action is activated, something happens. Note that in that actionPerformed method, you should call setText(text) to update the displayed value.
You have also defined a getText() method that overrides the getText() method defined in JButton. This approach is fine if it is a conscious design decision. As it is, I think you should remove the getText() method from the button1 class, and allow the standard JButton class to handle the update. Right now, you are attempting to keep an instance variable text with the value, but it is possible for that instance variable to not be in alignment with the actual displayed value of the button (consider another class calling .setText() on the button).
EDIT: It is true that this referring to the JButton in the ButtonAction is not available. However, the Action itself contains the button that was pressed.
#Override
public void actionPerformed(ActionEvent e)
{
JButton btn = (JButton)e.getSource();
// if desired, String cur = btn.getText() may be called to find the
// current setting; get and process if needed
btn.setText(WHAT_EVER_TEXT);
}
Unless it is a specific requirement to process the current text, however (allowing selecting an O to an X to a blank), I would implement something to keep track of the current turn. This code is something I was experimenting with, and has good and bad points to it (as it is illustrative):
static class TurnController
{
// whose turn it is; start with X
private Player whoseTurn = Player.X;
// the instance variable
private static final TurnController instance = new TurnController();
private TurnController()
{
}
public static Player currentTurn()
{
return instance.whoseTurn;
}
public static Player nextTurn()
{
switch (instance.whoseTurn) {
case X:
instance.whoseTurn = Player.O;
break;
case O:
instance.whoseTurn = Player.X;
break;
}
return instance.whoseTurn;
}
public static String getMarkerAndAdvance()
{
String marker = currentTurn().toString();
nextTurn();
return marker;
}
enum Player
{
X,
O,
;
}
}
Using this TurnController, the actionPerformed becomes:
#Override
public void actionPerformed(ActionEvent e)
{
JButton btn = (JButton)e.getSource();
btn.setText(TurnController.getMarkerAndAdvance());
}
and the Button1 class may have the String text instance variable removed.
What you have tried is Try to make a Custom Button Class and its EventHandler just by extending AbstractAction namee button1 as we See in Your Question.
You have Override the method actionPerformed(ActionEvent Switcher) which actually belongs to Class AbstractAction by your own definition (What should Performed on Action Event of Every Button).
class ButtonAction extends AbstractAction {
public ButtonAction() {}
#Override
public void actionPerformed(ActionEvent Switcher) { // Your Definition For actionPerformed..
System.out.println(text + " " + value);
value++;//value is a relic from earlier attempts that i just felt like keeping.
if (text.equals("O")) {
text = "X";
} else if (text.equals("X")) {
text = "";
} else if (text.equals("")) {
text = "O";
}
}
}
this.setAction(new ButtonAction()); // add ActionListener to each Button.
this.setText(text); // Setting Text to each Button
this.setFont(new Font("Arial",Font.PLAIN,120)); //add Font to each Button.
}
Now In this Code.
ArrayList buttonArrayList = new ArrayList<>();
for (int i = 0; i < 9; i++) {
button1 newbutton = new button1(); // Creating 9 new buttons.
buttonArrayList.add(newbutton); // add each button into the ArrayList.
window.add(buttonArrayList.get(i)); // each Button to the the AWT Window.
}
Above Code will generate 9 Button and add it to Your AWT Window. each button have actionPerformed() method which contains the overrided Definition.
Now Each button will performed action as per the definition you give to actionPerformed() Method.
Thank You.
Not sure how well I will explain this; I'm quite new to programming...
So I'm trying to make a desktop application that draws musical notes of a given type on some sheet music when the user selects the button corresponding to that type of note. Currently, if the user selects the "Whole Note" button, the user can then start clicking on the screen and the note will be drawn where the click occurred. It will also make a "ding" sound and write some info about that note to a text file.
That's all well and good, but unfortunately when the user selects a new button, say the "Quarter Note" button, for each mouse click there will be two notes drawn (one whole, one quarter), two dings, and two packets of info written to the file. I have no idea how to make this work! Currently, I'm trying to use threads, such that each button creates a new thread and the thread currently in use is interrupted when a new button is pressed, but that doesn't resolve the issue.
Initially, an empty linked list of threads ("NoteThreads") is constructed. Also, a private class known as SheetMusicPane (given the variable name "smp") is constructed in order to draw the sheet music. The buttons are added in the main constructor (public CompositionStudio), whereas the method containing the mouse listener (see what follows) is contained in the SheetMusicPane private class. Not sure whether that is part of the problem.
I have a button action listener:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (!threads.isEmpty()) {
NoteThread oldThread = threads.remove();
oldThread.interrupt();
}
NoteThread newThread = new NoteThread(e.getActionCommand());
threads.add(newThread);
newThread.run();
}
});
that produces a thread:
private class NoteThread extends Thread {
private String note;
public NoteThread(String note) {
this.note = note;
}
public void run() {
smp.getShape(smp.getGraphics(), note);
}
}
that when, on running, calls this method with graphics and a mouse listener:
public void getShape(final Graphics g, final String note) {
this.addMouseListener(new MouseListener() {
public void mousePressed(MouseEvent e) {
}
public void mouseReleased(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mouseClicked(MouseEvent e) {
Point p = MouseInfo.getPointerInfo().getLocation();
addShape(g, p.x, p.y, note);
int pitch = 12;
piano.playNote(pitch);
advance(1.0, piano);
try { addToFile(pitch, note);}
catch(FileNotFoundException fnfe) {}
catch(IOException ioe) {}
}
});
}
The above method is responsible for drawing the note ("addShape()"), making the ding sound, and writing to the file.
Thanks in advance for any help you can give!
what you're trying to do does not require multithreading. This is the approach that I'd take:
set up a set of toggle buttons or radio buttons to select the note to paint. this way, only one note will be selected at a time. add action listeners to those that store in an adequately scoped variable what note is selected, or infer that every time a note should be drawn. this way, you don't even add action listeners to the buttons. in any case, don't spawn new threads.
in your mouse listener, find out what note to draw, and do that - only one note.
if you can, stay away from multithreading, especially as a beginner. also, I think you confuse adding and running listeners here. each call to getShape() adds a new listener, meaning they accumulate over time, which might be the cause of your problems.
PS: welcome to stackoverflow! your question contained the important information and I could infer that you tried solving the problem yourself. It's pleasant to answer such questions!
One solution would be to simply fetch all the listeners (which should be 1) and remove them before adding the new listener:
public void getShape(final Graphics g, final String note) {
MouseListener[] listeners = this.getMouseListeners();
for (MouseListener ml : listeners) {
this.removeMouseListener(ml);
}
this.addMouseListener(new MouseListener()...);
}
An alternative, since you have a finite number of buttons, would be to create a finite set of listeners, eg:
private MouseListener wholeNote = new MouseListener()...;
private MouseListener quarterNote = new MouseListener()...;
Create a reference to the "current" listener (private MouseListener current;), and have a means of deciding which listener to use whenever getShape is called (a series of if conditions on the note String would work, although I would prefer some refactoring to use an enum personally). Then you could do something along the lines of:
private MouseListener wholeNote = new MouseListener()...;
private MouseListener quarterNote = new MouseListener()...;
private MouseListener current;
...
public void getShape(final Graphics g, final String note) {
if (current != null) {
this.removeMouseListener(current);
}
if (...) { // note is Whole Note
current = wholeNote;
} else if (...) { // note is Quarter Note
current = quarterNote;
} // etc.
this.addMouseListener(current);
}
Another alternative would be to change your listener so that you only ever need the one, but clicking a button changes a variable which the listener has access to. For example:
// In the listener
public void mouseClicked(MouseEvent e) {
Point p = MouseInfo.getPointerInfo().getLocation();
addShape(g, p.x, p.y, currentNote);
int pitch = 12;
piano.playNote(pitch);
advance(1.0, piano);
try { addToFile(pitch, currentNote);}
catch(FileNotFoundException fnfe) {}
catch(IOException ioe) {}
}
// In the same class
protected String currentNote;
...
public void getShape(final Graphics g, final String note) {
currentNote = note;
}
I'm a student programming a frogger game, when the frog collides with an object or reaches the end zone either the score is incremented or lives decremented and the frog returned to the start position. this section works and the decrement and increment work when outputting them to the console, I try to pass the variable to the other jpanel and display it there but it doesnt update and display the variable in the textField.
Game Panel
public GamePanel() {
super();
setFocusable(true);
addKeyListener(new KeyList());
System.out.println("GAME PANE FOCUS:" + this.isFocusOwner());
scores.setVisible(true);
lives = p.STARTLIVES;
scores.setCurrentLives(lives);
txtTest.setText("Hello");
txtTest.setVisible(true);
add(scores,new AbsoluteConstraints(0,550,50,800));
Boolean displayable = scores.isDisplayable();
System.out.println("Displayable" + displayable);
scores.setEnabled(false);
scores.revalidate();
scores.repaint();
scores.setVisible(true);
System.out.println("Displayable" + displayable);
car1.start();
car2.start();
car3.start();
car4.start();
Log1.start();
Log2.start();
Log3.start();
Log4.start();
Log5.start();
Log6.start();
Log7.start();
Log8.start();
//check for collisions
}
final public void winZone(Rectangle object){
if(myFrog.frogArea().intersects(object)){
currentScore = currentScore + 100;
System.out.println("current Score " + currentScore);
p.setScore(currentScore);
scores.myScore().setText("hello");
myFrog.lostLife();
}
scores panel
public class jplScores extends JPanel {
Properties p = new Properties();
int currentLives;
int i;
/** Creates new form jplScores */
public jplScores() {
initComponents();
}
public void setCurrentLives(int Lives){
currentLives = Lives;
}
public String getCurrentLives(){
String L = Integer.toString(currentLives);
return L;
}
public JTextField myScore(){
return txtScore;
}
Currently it will display the jpanel from the frame that they are both in but i have tried to make it so its a panel within a panel but i cant get the panel to display from within the game panel.
Any help would be great thanks
public FroggerGame() {
initComponents();
setFocusable(true);
//repaint();
// p.setHeight(550);
// p.setWidth(800);
// p.setLives(3);
// p.setScore(0);
PANELHEIGHT = p.getHeight();
PANELWIDTH = p.getWidth();
welcomePanel();
/*
Toolkit tool = Toolkit.getDefaultToolkit();
imgBackground = tool.getImage(imageBackground);
background = new ImageIcon(imgBackground);
//setDefaultCloseOperation(EXIT_ON_CLOSE);
*/
jps.myScore().addPropertyChangeListener(new java.beans.PropertyChangeListener() {
public void propertyChange(java.beans.PropertyChangeEvent evt) {
// txtScorePropertyChange(evt);
jps.myScore().setText(Integer.toString(gp.currentScore()));
System.out.println(Integer.toString(gp.currentScore()));
jps.getScore(gp.currentScore());
System.out.println(" main score " + gp.currentScore());
}
});
}
....
private void btnEnterActionPerformed(ActionEvent evt) {
welcomePanel.setVisible(false);
getContentPane().setLayout(new AbsoluteLayout());
getContentPane().add(gp,new AbsoluteConstraints(0,0,800,550));
getContentPane().add(jps,new AbsoluteConstraints(0,550,800,100));
//gp.setSize(800, 550);
// gp.setBounds(0, 0, 800, 550);
gp.setVisible(true);
gp.requestFocusInWindow();
jps.setVisible(true);
gp is the game panel and jps is the score panel.
This really has little to do with "panels" or Swing GUI coding and all to do with the basic OOPS issue of passing information from one class to another. One way to solve this is to give your Scores panel a public method, say
public void changeScore(int value) {
// in here add the new value to the currently
// displayed value and display the new value.
}
Then the main class, the one with a Scores panel reference, you can call this method, passing in 1 if score is to increase or -1 if it is to decrease.
I think of this as the "push" solution, where one class pushes information into the other class. Another way to solve this is via listeners where the Scores class listens to critical properties of the other class and then changes its own score when an appropriate event occurs, and this often involves using PropertyChangeListeners or other more Swing-specific listener classes. This is sometimes a nicer solution, but I consider it a slightly more advanced concept.
Full disclaimer: I'm a CS student, and this question is related to a recently assigned Java program for Object-Oriented Programming. Although we've done some console stuff, this is the first time we've worked with a GUI and Swing or Awt. We were given some code that created a window with some text and a button that rotated through different colors for the text. We were then asked to modify the program to create radio buttons for the colors instead—this was also intended to give us practice researching an API. I've already handed in my assignment and received permission from my instructor to post my code here.
What's the best way to implement button actions in Java? After some fiddling around, I created the buttons like this:
class HelloComponent3 extends JComponent
implements MouseMotionListener, ActionListener
{
int messageX = 75, messageY= 175;
String theMessage;
String redString = "red", blueString = "blue", greenString = "green";
String magentaString = "magenta", blackString = "black", resetString = "reset";
JButton resetButton;
JRadioButton redButton, blueButton, greenButton, magentaButton, blackButton;
ButtonGroup colorButtons;
public HelloComponent3(String message) {
theMessage = message;
//intialize the reset button
resetButton = new JButton("Reset");
resetButton.setActionCommand(resetString);
resetButton.addActionListener(this);
//intialize our radio buttons with actions and labels
redButton = new JRadioButton("Red");
redButton.setActionCommand(redString);
...
And added action listeners...
redButton.addActionListener(this);
blueButton.addActionListener(this);
...
A stub was already created for the actionPerformed method to give us an idea on how to use it, but since there was only a single button in the template, it wasn't clear how to implement multiple buttons. I tried switching on a String, but quickly realized that, since a String isn't a primitive type, I couldn't use it for a switch statement. I could have improvised with an if-else chain, but this is what I came up with instead. It seems far from elegant, and there must be a better way. If there is, what is it? Is there a way to switch on a string? Or choose an action in a more scaleable fashion?
public void actionPerformed(ActionEvent e){
if (e.getActionCommand().equals(resetString)) {
messageX = 75; messageY = 175;
setForeground(Color.black);
blackButton.setSelected(true);
repaint();
return;
}
if ( e.getActionCommand().equals(redString) ) {
setForeground(Color.red);
repaint();
return;
}
if ( e.getActionCommand().equals(blueString) ) {
setForeground(Color.blue);
repaint();
return;
}
if ( e.getActionCommand().equals(greenString) ) {
setForeground(Color.green);
repaint();
return;
}
if ( e.getActionCommand().equals(magentaString) ) {
setForeground(Color.magenta);
repaint();
return;
}
if ( e.getActionCommand().equals(blackString) ) {
setForeground(Color.black);
repaint();
return;
}
}
Instead of writing this:
resetButton.addActionListener(this);
You could also write this:
resetButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
resetButtonActionPerformed(evt);
}
});
And instead of writing one big actionPerformed() for all actions, you can (and then have to) write this:
public void resetButtonActionPerformed(ActionEvent evt) {
messageX = 75; messageY = 175;
setForeground(Color.black);
blackButton.setSelected(true);
repaint();
}
I don't know if this is the most elegant solution, but at least you no longer have that big if construct.
Two alternate approaches:
Create a new class that implements the Action interface and has a Color field and an actionPerformed method that sets the color
Mantain a HashMap from command names to Color instances and look up the command name in the map
One decent enough approach is to declare an enum whose elements match your strings and switch on valueOf(str) (the linked example shows how to do this with a fair amount of safety).
The reason to avoid anonymous inner classes is probably because the class hasn't had that construct (yet), even though that might be the best solution.
As suggested already, you can use anonymous inner classes to implement the ActionListener interface. As an alternative, you don't have to use anonymous inner classes, but you can use a simple nested class instead:
resetButton = new JButton(new ResetAction());
redButton = new JButton(new ColorAction("Red", Color.red));
and then...
private class ResetAction extends AbstractAction {
public ResetAction() {
super("Reset");
}
public void actionPerformed(ActionEvent e) {
messageX = 75; messageY = 175;
setForeground(Color.black);
blackButton.setSelected(true);
repaint();
}
}
private class ResetAction extends AbstractAction {
private Color color;
public ColorAction(String title, Color color) {
super(title);
this.color = color;
}
public void actionPerformed(ActionEvent e) {
setForeground(color);
repaint();
}
}
For why this approach - or any approach involving inner classes - is better than implementing ActionListener in the outer class see "Design Patterns":
"Favor 'object composition' over 'class inheritance'." (Gang of Four 1995:20)
Choosing between anonymous inner classes and these named inner classes is a largely a matter of style, but I think this version is easier to understand, and clearer when there are lots of actions.
Ergh. Don't implement masses of unrelated interfaces in one mega class. Instead, use anoymous inner classes. They are a bit verbose, but are what you want. Use one for each event, then you wont need big if-else chain. I suggest keeping enough code within the inner class to decode the event and call methods that make sense to the target objects. Further, you can parameterise your inner classes. You will probably find you don't need to keep references to the actual widgets around.
In your example you seem to be using a JComponent as a JPanel. There's not much difference, but use JPanel for collecting a block of widgets. Further there is unlikely any need to subclass it, so don't.
So for instance:
addColorButton("Green" , Color.GREEN );
addColorButton("Red" , Color.RED );
addColorButton("Yellow", Color.YELLOW);
addColorButton("Blue" , Color.BLUE );
...
private void addColorButton(String label, Color color) {
JRadioButton button = new JRadioButton(label);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
target.setForeground(color);
target.repaint();
}
});
colorGroup.add(button);
panel.add(button);
}