I'm making Tetris in Java and would like to have the game play on the left and scoring, buttons, and nextPiece on the right, like so:
You'll notice that the score on the Game Panel is updating, but the score on the Score Panel (on the right) is not.
On the Game Panel, I have global variables for score and level: private int level, totalScore; which are initialized to 0.
and this in my paint component():
g.setColor(Color.RED);
g.drawString("Level: " + level, this.getWidth()/2+110, this.getHeight()/2-200);
g.drawString("Score: " + totalScore, this.getWidth()/2+110, this.getHeight()/2-170);
Then I have this code within the Game Panel which calculates level and scoring:
public void changeLevel () {
int max = (level+1)*100;
if (totalScore >= max) {
System.out.println(max + "reached... next level");
level++;
totalScore = 0;
timer();
}
}
public int tallyScore(int totalLines) {
int score = 0;
switch (totalLines) {
case 1: score = 40 * (level + 1);
break;
case 2: score = 100 * (level + 1);
break;
case 3: score = 300 * (level + 1);
break;
case 4: score = 1200 * (level + 1);
break;
default: break;
}
return score;
}
//loop through all rows starting at bottom (12 rows)
public void checkBottomFull() {
int lines = 0;
for(int row = totalRows-1; row > 0; row--) {
while (isFull(row)) {
lines++;
clearRow(row);
}
}
totalScore += tallyScore(lines);
//check if level needs to be changed based on current score...
changeLevel();
//reset lines after score has been incremented
lines=0;
}
And since I want the Score Panel to display the score, I have these two methods in Game Panel which return the global variables:
public int getScore() {
return totalScore;
}
public int getLevel() {
return level;
}
In my Score Panel paintComponent() I have board.getLevel() and board.getScore() (board class is the Game Panel) so I can feed the Game Panel scores to the Score Panel.
g.setColor(Color.BLACK);
g.drawString("Level: " + board.getLevel(), this.getWidth()/2, this.getHeight()/2-130);
g.drawString("Score: " + board.getScore(), this.getWidth()/2, this.getHeight()/2-100);
Yet as you can see from the picture, these scores are not updating.
Any thoughts?
Thanks!
You will want to separate concerns so that they can be shared. Consider making a class for the logic and data that underlies your GUI, and say you call this class you Model class. Then you can give it a level and a score field and make them "bound properties", meaning that other classes can listen for changes to these fields. I usually do this by giving my Model a SwingPropertyChangeSupport object and give it an addPropertyChangeListener(PropertyChangeListener listener) and a removePropertyChangeListener(PropertyChangeListener listener), and then I notify all registered PropertyChangeListener's of changes by calling PropertyChangeSupport's fire. e.g.,
import java.beans.PropertyChangeListener;
import javax.swing.event.SwingPropertyChangeSupport;
public class Model {
public static final String SCORE = "score";
public static final String LEVEL = "level";
private SwingPropertyChangeSupport pcSupport =
new SwingPropertyChangeSupport(this);
private int score;
private int level;
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(listener);
}
public int getScore() {
return score;
}
public void setScore(int score) {
int oldValue = this.score;
int newValue = score;
this.score = score;
pcSupport.firePropertyChange(SCORE, oldValue, newValue);
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
int oldValue = this.level;
int newValue = level;
this.level = level;
pcSupport.firePropertyChange(LEVEL, oldValue, newValue);
}
}
Then any GUI or view component that wishes to listen to changes in the values may do so. The below class combines "View" with "Control" if you're studying MVC structure:
import java.awt.FlowLayout;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class UseModelGui {
private static void createAndShowGui() {
Panel1 panel1 = new Panel1();
Panel2 panel2 = new Panel2();
Model model = new Model();
panel1.setModel(model);
panel2.setModel(model);
JFrame frame = new JFrame("UseModelGui");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().setLayout(new FlowLayout());
frame.getContentPane().add(panel1);
frame.getContentPane().add(panel2);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
class Panel1 extends JPanel {
private JTextField scoreField = new JTextField(2);
private JTextField levelField = new JTextField(2);
public Panel1() {
scoreField.setFocusable(false);
scoreField.setEditable(false);
levelField.setFocusable(false);
levelField.setEditable(false);
add(new JLabel("score:"));
add(scoreField);
add(new JLabel("Level:"));
add(levelField);
setBorder(BorderFactory.createTitledBorder("Check Values"));
}
public void setModel(Model model) {
model.addPropertyChangeListener(new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent pcEvt) {
if (Model.LEVEL.equals(pcEvt.getPropertyName())) {
String level = pcEvt.getNewValue().toString();
levelField.setText(level);
} else if (Model.SCORE.equals(pcEvt.getPropertyName())) {
String score = pcEvt.getNewValue().toString();
scoreField.setText(score);
}
}
});
}
}
class Panel2 extends JPanel {
private JSpinner scoreSpinner = new JSpinner(new SpinnerNumberModel(0, 0,
20, 1));
private JSpinner levelSpinner = new JSpinner(new SpinnerNumberModel(0, 0,
10, 1));
private Model model;
public Panel2() {
add(new JLabel("score:"));
add(scoreSpinner);
add(new JLabel("Level:"));
add(levelSpinner);
setBorder(BorderFactory.createTitledBorder("Set Values"));
scoreSpinner.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent evt) {
int score = ((Integer) scoreSpinner.getValue()).intValue();
if (model != null) {
model.setScore(score);
}
}
});
levelSpinner.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent evt) {
int level = ((Integer) levelSpinner.getValue()).intValue();
if (model != null) {
model.setLevel(level);
}
}
});
}
public void setModel(Model model) {
this.model = model;
}
}
The beauty of this is that Panel1 has no knowledge of Panel2, and Model has no knowledge of either. All Panel1 knows of is if Model changes. All Panel2 knows is that it is changing the Model's state. All the Model knows is that its state can change, and its values can be listened to.
You're right that in this simple example this is over-kill, but as soon as you start having complex data and state, this makes sense and becomes quite helpful.
You should separate your business logic from your presentation. Please read about: Model View Controller pattern.
Related
I'm working on a simple GUI project to get the foundations of Java Swing.
I created a Rock Paper Scissors game which you play against the computer which i implemented a GUI for
My text based GUI Form for Rock, Paper,Scissors
My problem is that once either my score or the computers score reach a value of 3, i want the text on the frame to change. I've tried to implement the code to check each time the variable increases in the button function but it still does not works, neither does implementing the function in the main, the game doesn't change any text or stop once the scores reach 3.
chooseRock.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
String cc = getCompChoice();
if (cc.equalsIgnoreCase("Paper")) {
compcount++;
if(compcount == 3){
winner.setText("Computer Wins");
}
compChoice.setText("Computers Choice: " + cc);
This code shows the GUI object and the listener for selecting "Rock", the code is the same for both paper and Scissors. Both compchoice and playchoice are declared with the other attributes at the top.
gameScreen(String title){
super(title);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setContentPane(gameBoard);
this.pack();
chooseRock.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
String cc = getCompChoice();
if (cc.equalsIgnoreCase("Paper")) {
compcount++;
compChoice.setText("Computers Choice: " + cc);
compScore.setText("Computer Score: " + compcount);
} else if (cc.equalsIgnoreCase("Scissors")) {
playcount++;
compChoice.setText("Computers Choice: " + cc);
playerScore.setText("Your Score: " + playcount);
} else {
compChoice.setText("Computers Choice: " + cc + " Its a DRAW!");
}
}
});
This is a function I've written to check the scores and display the winner, the 'winner' text is displayed at the top of the panel and has a placeholder.
public void checkScore(){
if(playcount == 3 ){
winner.setText("GAME OVER - PLAYER WINS");
chooseRock.setEnabled(false);
chooseScissors.setEnabled(false);
choosePaper.setEnabled(false);
}else if(compcount == 3 ){
winner.setText("GAME OVER - COMPUTER WINS! BETTER LUCK NEXT TIME");
chooseRock.setEnabled(false);
chooseScissors.setEnabled(false);
choosePaper.setEnabled(false);
}
}
Is there any way to take the variable written inside the listener and use it to change the text field or is there some way conditions like this should be implemented?
You should call checkScore() within the ActionListeners actionPerformed method so that the score is calculated and acted upon with each button press.
Interesting aside:
One way to simplify your listeners is to create an enum, one that checks for win, something like:
public enum RPS {
ROCK("Rock"), PAPER("Paper"), SCISSORS("Scissors");
private String text;
private RPS(String text) {
this.text = text;
}
public String getText() {
return text;
}
#Override
public String toString() {
return text;
}
This way, your check for win can be as simple as adding a comparing method to the enum:
// returns 1 for win, -1 for loss, 0 for tie
public int compare(RPS other) {
int length = values().length;
int delta = (length + ordinal() - other.ordinal()) % length;
return delta != 2 ? delta : -1;
}
}
Each enum value has an ordinal() method that returns its order of declaration, 0 for ROCK, 1 for PAPER, and 2 for SCISSORS. The delta equation simply gets the difference between these ordinal values, adds 3 (the "size" of the enum) and checks the remainder of the calculation. If it is equal to 1, then that enum "wins" the battle, if 0, then its a tie and if 2, then this enum "loses". This can simplify your listeners.
For example, here I create a single listener class for all buttons:
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
#SuppressWarnings("serial")
public class RockPaperScissors extends JPanel {
public static final int MAX_SCORE = 3;
private static final int PANEL_WIDTH = 600;
private int myScore;
private int compScore;
private JLabel statusLabel = new JLabel(" ");
private JLabel myChoiceLabel = new JLabel();
private JLabel compChoiceLabel = new JLabel();
private JLabel myScoreLabel = new JLabel();
private JLabel compScoreLabel = new JLabel();
private List<Action> actionList = new ArrayList<>();
public RockPaperScissors() {
JPanel statusPanel = new JPanel();
statusPanel.setBorder(BorderFactory.createTitledBorder("Status"));
statusPanel.add(statusLabel);
JPanel scorePanel = new JPanel();
scorePanel.setBorder(BorderFactory.createTitledBorder("Score"));
scorePanel.add(new JLabel("My Score:"));
scorePanel.add(myScoreLabel);
scorePanel.add(Box.createHorizontalStrut(15));
scorePanel.add(new JLabel("Comp Score:"));
scorePanel.add(compScoreLabel);
JPanel selectionPanel = new JPanel();
selectionPanel.setBorder(BorderFactory.createTitledBorder("Selections"));
selectionPanel.add(new JLabel("My Choice:"));
selectionPanel.add(myChoiceLabel);
selectionPanel.add(Box.createHorizontalStrut(15));
selectionPanel.add(new JLabel("Comp Choice:"));
selectionPanel.add(compChoiceLabel);
JPanel btnPanel = new JPanel(new GridLayout(1, 0, 3, 0));
for (RPS rps : RPS.values()) {
Action action = new ButtonAction(rps);
actionList.add(action);
JButton button = new JButton(action);
btnPanel.add(button);
}
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
add(statusPanel);
add(scorePanel);
add(selectionPanel);
add(btnPanel);
}
#Override
public Dimension getPreferredSize() {
Dimension superSize = super.getPreferredSize();
int height = superSize.height;
int width = Math.max(superSize.width, PANEL_WIDTH);
return new Dimension(width, height);
}
private class ButtonAction extends AbstractAction {
private RPS rps;
public ButtonAction(RPS rps) {
super(rps.getText());
this.rps = rps;
}
#Override
public void actionPerformed(ActionEvent e) {
int randomValue = (int) (RPS.values().length * Math.random());
RPS compChoice = RPS.values()[randomValue];
myChoiceLabel.setText(rps.getText());
compChoiceLabel.setText(compChoice.getText());
if (rps.compare(compChoice) > 0) {
statusLabel.setText("I Win");
myScore++;
} else if (rps.compare(compChoice) < 0) {
statusLabel.setText("Computer Wins");
compScore++;
} else {
statusLabel.setText("Draw");
}
myScoreLabel.setText(String.valueOf(myScore));
compScoreLabel.setText(String.valueOf(compScore));
if (myScore >= MAX_SCORE) {
statusLabel.setText("I Win the Game");
} else if (compScore >= MAX_SCORE) {
statusLabel.setText("Computer Wins the Game");
}
if (myScore >= MAX_SCORE || compScore >= MAX_SCORE) {
for (Action action : actionList) {
action.setEnabled(false);
}
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}
private static void createAndShowGui() {
RockPaperScissors mainPanel = new RockPaperScissors();
JFrame frame = new JFrame("RockPaperScissors");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
}
enum RPS {
ROCK("Rock"), PAPER("Paper"), SCISSORS("Scissors");
private String text;
private RPS(String text) {
this.text = text;
}
public String getText() {
return text;
}
#Override
public String toString() {
return text;
}
public int compare(RPS other) {
int length = values().length;
int delta = (length + ordinal() - other.ordinal()) % length;
return delta != 2 ? delta : -1;
}
}
I'm trying to build a simple rolling dice game for my assignment. There will be 2 players. In my code I try to use method getSource() so that when a player roll JButton is enabled, the other player's button will be disabled but when I run the game both player's buttons are still enabled. Here are my code:
Die.java
import java.util.*;
public class Die
{
private final int MAX = 6;
private int die1;
Random rand = new Random();
//Constructor
public Die()
{
die1 = 1;
}// end Constructor
public int Roll()
{
die1 = rand.nextInt(MAX)+1;
return die1;
}
}
DisplaySixNumbersPanel.java
import java.awt.*;
import javax.swing.*;
public class DisplaySixNumbersPanel
{
public static void main(String[ ] args)
{
JFrame w1 = new JFrame("Six Numbers Game");
w1.setLayout(new GridLayout(1,2));
SixNumbersPanel2 player1 =new SixNumbersPanel2();
SixNumbersPanel2 player2 =new SixNumbersPanel2();
w1.add(player1);
w1.add(player2);
w1.setSize(540, 350);
w1.setVisible(true);
w1.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
} //end main
} //end class
SixNumbersPanel2.java
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.*;
public class SixNumbersPanel2 extends JPanel implements ActionListener
{
private JTextField text1;
private boolean flag = false;
private boolean checkflag[] = {false,false,false,false,false,false,false};
private JLabel label;
private JLabel label1;
public JButton roll;
private JButton restartButton;
private JTextArea display;
private JTextField[] fields = new JTextField[7];
private Die dice = new Die();
private int key;
private int count;
private int count1;
private Player player1 = new Player();
private Player player2 = new Player();
private int check;
private int[] list = new int [7];
//private SixNumbersPanel2 player2panel;
public SixNumbersPanel2()
{
label= new JLabel ("A number between 1 to 6 wil appear");
label1 = new JLabel("once in each of the following textfields");
roll = new JButton("Roll dice");
restartButton = new JButton("Restart Game");
text1 = new JTextField(3);
display = new JTextArea(10,15);
add(label);
add(label1);
for(int i=1;i<7;i++)
{
fields[i] = new JTextField(3);
add(fields[i]);
}
add(roll);
add(text1);
add(display);
display.setEditable(false);
restartButton.setVisible(false);
add(restartButton);
restartButton.addActionListener(this);
roll.addActionListener(this);
}
public void restart()
{
display.setText("");
text1.setText("");
for(int i=1; i<7; i++)
{
fields[i].setText("");
}
count=0;
count1=0;
Arrays.fill(checkflag,false);
Arrays.fill(list,0);
flag = false;
restartButton.setVisible(false);
rollenable();
}
public void actionPerformed(ActionEvent event)
{
if(event.getSource() == roll)
{
player2.rolldisable();
}
key=dice.Roll();
count++;
String toScreen= "Number of rolls: "+count;
display.setText(toScreen);
text1.setText(""+key);
check = player1.Check(list,key);
if(check < 0)
{
count1++;
for(int i=1;i<7;i++)
{
if(key == i)
{
list[i] = key;
checkflag[i]=true;
fields[i].setText(""+key);
if(checkflag[i] == true)
{
flag = true;
for(int a=1; a<7; a++)
{
if(checkflag[a]==false)
{
flag=false;
}
}
}
}
if(flag == true)
{
display.setText("Congratulation, you have \ntaken "+count+" rolls to get one of \neach number between 1 and 6");
rolldisable();
restartButton.setVisible(true);
}
}
}
if(event.getSource() == restartButton)
{
restart();
}
}
public void rollenable()
{
roll.setEnabled(true);
}
public void rolldisable()
{
roll.setEnabled(false);
}
public JButton getSubButton()
{
return this.roll;
}
}
Player.java
import java.util.*;
import javax.swing.*;
public class Player
{
private final int MAX = 100;
private Die play = new Die();
private boolean[] results = {false,false,false,false,false,false,false};
private int count;
private int key;
private boolean check = false;
//private JButton roll;
public Player()
{
count=0;
key=0;
}
public void Play()
{
for(int i=0;i<MAX;i++)
{
key=play.Roll();
System.out.print("\nNumber rolled: "+key);
count++;
for(int a=1;a<7;a++)
{
if(a==key)
{
results[a]=true;
System.out.print("\nSo far, you have rolled ");
if(results[a]==true)
{
check=true;
for(int k=1;k<7;k++)
{
if(results[k] ==true)
{
System.out.print(" "+k+" ");
}
if(results[k] == false)
check=false;
}
}
}
}
if(check==true)
{
System.out.print("\nCongratulations, you have taken "+count+" rolls to get one of each number between 1 and 6");
break;
}
}
}
public int Check(int []check,int key)
{
int check1= Arrays.binarySearch(check,key);
return check1;
}
//public JButton getButton()
//{
// JButton button = null;
//SixNumbersPanel2 roll = new SixNumbersPanel2();
//return roll.roll = button;
//}
public void rolldisable()
{
SixNumbersPanel2 dis = new SixNumbersPanel2();
dis.getSubButton().setEnabled(false);
}
}
Also with the restartButton JButton, it doesn't restart the whole game but only for the player who clicks it. Any suggestion as how to make it restart the whole GUI would be awesome.
All helps would be appreciated.
This...
public void rolldisable()
{
SixNumbersPanel2 dis = new SixNumbersPanel2();
dis.getSubButton().setEnabled(false);
}
is wrong, basically you're creating a new instance of SixNumbersPanel2 and are trying to change it's state, but it's not actually displayed on the screen nor does it have anything to do with what is displayed on the screen.
This solution is not a simple one. What you need is some kind of model/controller which can be shared between the two instances of SixNumbersPanel2 and which generate appropriate state events to which they can respond.
You will need to devise some way of identifying which player is which so each SixNumbersPanel2 knows which player it represents and which player is currently active
Updated...
In order to implement the function changes needed, you need some kind of "central" hub, which is managing the core functionality and which can generate notification/events when the state changes in some meaningful way (like the next players turn)
In broad terms, these are covered by:
Model-View-Controller
Observer Patten
First, we need to start with some kind of token, to differentiate the players...
enum Player {
ONE, TWO;
}
Next, we need some kind of "model", which is used to manage the data and the core functionality
public interface SixNumbersModel {
public Player getCurrentTurn();
public Player nextTurn();
public int roll();
public boolean hasWon(Player player);
public Set<Integer> getPlayerResults(Player player);
public int getTurnsCount(Player player);
public void addChangeListener(ChangeListener listener);
public void removeChangeListener(ChangeListener listener);
}
nb: You could actually have a "player model" which managed the results, but I'm been lazy
The reason I've started with a interface is users of the model should not care about how the model is implemented (implementations might seperate the management of each players results into individual models, but users of the SixNumbersModel won't care)
public class DefaultSizeNumbersModel implements SixNumbersModel {
private List<ChangeListener> changeListeners;
private Die die = new Die();
private Player turn;
private Map<Player, Set<Integer>> results;
private Map<Player, Integer> turns;
public DefaultSizeNumbersModel() {
changeListeners = new ArrayList<>(2);
results = new HashMap<>();
results.put(Player.ONE, new HashSet<>(6));
results.put(Player.TWO, new HashSet<>(6));
turns = new HashMap<>(2);
turns.put(Player.ONE, 0);
turns.put(Player.TWO, 0);
setCurrentTurn(Player.ONE);
}
#Override
public Player getCurrentTurn() {
return turn;
}
protected void setCurrentTurn(Player player) {
turn = player;
}
#Override
public Player nextTurn() {
switch (getCurrentTurn()) {
case ONE:
setCurrentTurn(Player.TWO);
break;
case TWO:
setCurrentTurn(Player.ONE);
break;
default:
setCurrentTurn(Player.ONE);
break;
}
fireStateChanged();
return getCurrentTurn();
}
#Override
public int roll() {
incrementTurnCount(getCurrentTurn());
int result = die.Roll();
Set<Integer> playerResults = results.get(getCurrentTurn());
playerResults.add(result);
return result;
}
#Override
public boolean hasWon(Player player) {
Set<Integer> playerResults = results.get(getCurrentTurn());
return playerResults.size() == 5; // 0...5
}
#Override
public Set<Integer> getPlayerResults(Player player) {
Set<Integer> actualResults = results.get(player);
Set<Integer> copy = new HashSet<>(actualResults);
return copy;
}
#Override
public int getTurnsCount(Player player) {
return turns.get(player);
}
protected void setTurnsCount(Player player, int count) {
turns.put(player, count);
}
protected void incrementTurnCount(Player player) {
int count = getTurnsCount(player);
count++;
setTurnsCount(player, count);
}
#Override
public void addChangeListener(ChangeListener listener) {
changeListeners.add(listener);
}
#Override
public void removeChangeListener(ChangeListener listener) {
changeListeners.remove(listener);
}
protected void fireStateChanged() {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : changeListeners) {
listener.stateChanged(evt);
}
}
}
Then we need some kind of "view"...
public class SixNumbersPanel extends JPanel {
private Player player;
private SixNumbersModel model;
private JButton roll;
private JTextArea ta;
public SixNumbersPanel(Player player, SixNumbersModel model) {
this.player = player;
this.model = model;
model.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
roll.setEnabled(player == model.getCurrentTurn());
}
});
roll = new JButton("Roll");
ta = new JTextArea(5, 10);
roll.setEnabled(player == model.getCurrentTurn());
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
add(new JLabel(player.name()), gbc);
add(roll, gbc);
gbc.weightx = 1;
gbc.weighty = 1;
gbc.fill = GridBagConstraints.BOTH;
add(new JScrollPane(ta), gbc);
roll.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
int result = model.roll();
ta.append(Integer.toString(result) + "\n");
if (model.hasWon(player)) {
JOptionPane.showMessageDialog(SixNumbersPanel.this, player + " has won");
}
model.nextTurn();
}
});
}
}
Okay, massively basic, but, it simply has a button and a text area. It registers interest to the model to be notified when the state changes and makes sure that the button is only enabled when the player it represents is the current player.
Runnable example...
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class Test {
public static void main(String[] args) {
new Test();
}
enum Player {
ONE, TWO;
}
public interface SixNumbersModel {
public Player getCurrentTurn();
public Player nextTurn();
public int roll();
public boolean hasWon(Player player);
public Set<Integer> getPlayerResults(Player player);
public int getTurnsCount(Player player);
public void addChangeListener(ChangeListener listener);
public void removeChangeListener(ChangeListener listener);
}
public class DefaultSizeNumbersModel implements SixNumbersModel {
private List<ChangeListener> changeListeners;
private Die die = new Die();
private Player turn;
private Map<Player, Set<Integer>> results;
private Map<Player, Integer> turns;
public DefaultSizeNumbersModel() {
changeListeners = new ArrayList<>(2);
results = new HashMap<>();
results.put(Player.ONE, new HashSet<>(6));
results.put(Player.TWO, new HashSet<>(6));
turns = new HashMap<>(2);
turns.put(Player.ONE, 0);
turns.put(Player.TWO, 0);
setCurrentTurn(Player.ONE);
}
#Override
public Player getCurrentTurn() {
return turn;
}
protected void setCurrentTurn(Player player) {
turn = player;
}
#Override
public Player nextTurn() {
switch (getCurrentTurn()) {
case ONE:
setCurrentTurn(Player.TWO);
break;
case TWO:
setCurrentTurn(Player.ONE);
break;
default:
setCurrentTurn(Player.ONE);
break;
}
fireStateChanged();
return getCurrentTurn();
}
#Override
public int roll() {
incrementTurnCount(getCurrentTurn());
int result = die.Roll();
Set<Integer> playerResults = results.get(getCurrentTurn());
playerResults.add(result);
return result;
}
#Override
public boolean hasWon(Player player) {
Set<Integer> playerResults = results.get(getCurrentTurn());
return playerResults.size() == 5; // 0...5
}
#Override
public Set<Integer> getPlayerResults(Player player) {
Set<Integer> actualResults = results.get(player);
Set<Integer> copy = new HashSet<>(actualResults);
return copy;
}
#Override
public int getTurnsCount(Player player) {
return turns.get(player);
}
protected void setTurnsCount(Player player, int count) {
turns.put(player, count);
}
protected void incrementTurnCount(Player player) {
int count = getTurnsCount(player);
count++;
setTurnsCount(player, count);
}
#Override
public void addChangeListener(ChangeListener listener) {
changeListeners.add(listener);
}
#Override
public void removeChangeListener(ChangeListener listener) {
changeListeners.remove(listener);
}
protected void fireStateChanged() {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : changeListeners) {
listener.stateChanged(evt);
}
}
}
public class Die {
private final int MAX = 6;
private int die1;
Random rand = new Random();
//Constructor
public Die() {
die1 = 1;
}// end Constructor
public int Roll() {
die1 = rand.nextInt(MAX) + 1;
return die1;
}
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("Roll Six");
frame.setLayout(new GridLayout(2, 0));
SixNumbersModel model = new DefaultSizeNumbersModel();
SixNumbersPanel onePane = new SixNumbersPanel(Player.ONE, model);
SixNumbersPanel twoPane = new SixNumbersPanel(Player.TWO, model);
frame.add(onePane);
frame.add(twoPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class SixNumbersPanel extends JPanel {
private Player player;
private SixNumbersModel model;
private JButton roll;
private JTextArea ta;
public SixNumbersPanel(Player player, SixNumbersModel model) {
this.player = player;
this.model = model;
model.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
roll.setEnabled(player == model.getCurrentTurn());
}
});
roll = new JButton("Roll");
ta = new JTextArea(5, 10);
roll.setEnabled(player == model.getCurrentTurn());
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
add(new JLabel(player.name()), gbc);
add(roll, gbc);
gbc.weightx = 1;
gbc.weighty = 1;
gbc.fill = GridBagConstraints.BOTH;
add(new JScrollPane(ta), gbc);
roll.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
int result = model.roll();
ta.append(Integer.toString(result) + "\n");
if (model.hasWon(player)) {
JOptionPane.showMessageDialog(SixNumbersPanel.this, player + " has won");
}
model.nextTurn();
}
});
}
}
}
This is homework. I included relevant code at the bottom.
Problem:
In an attempted to allow the user to resize the grid, the grid is now being drawn severely overpopuated.
Screen Shots:
"Overpopulation" -
http://i.imgur.com/zshAC6n.png
"Desired Population" -
http://i.imgur.com/5Rf6P42.png
Background:
It's a version of Conway's Game of Life. In class we completed 3 classes: LifeState which handles the game logic, LifePanel which is a JPanel that contains the game, and a driver that created a JFrame and added the LifePanel. The assignment was to develop it into a full GUI application with various requirements. My solution was to extend JFrame and do most of my work in that class.
Initializing the LifePanel outside of the actionlistener yields normal population, but intializing the LifePanel in the actionlistener "overpopulates" the grid.
Question: Why is the overpopulation occurring?
LifePanel class
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Random;
public class LifePanel extends JPanel implements MouseListener
{
private int row;
private int col;
private int scale;
private LifeState life;
boolean state;
boolean wrap;
int delay;
Timer timer;
public LifePanel(int r, int c, int s, int d)
{
row = r;
col = c;
scale = s;
delay = d;
life = new LifeState(row,col);
Random rnd = new Random();
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
life.setCell(i,j,rnd.nextBoolean());
timer = new Timer(delay, new UpdateListener());
setPreferredSize( new Dimension(scale*row, scale*col));
addMouseListener(this);
timer.start();
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
if(life.getCell(i,j))
g.fillRect(scale*i,scale*j,scale,scale);
}
public int getRow() {
return row;
}
public void setRow(int row) {
this.row = row;
}
public int getCol() {
return col;
}
public void setCol(int col) {
this.col = col;
}
public int getScale() {
return scale;
}
public void setScale(int scale) {
this.scale = scale;
}
public int getDelay() {
return delay;
}
public void setDelay(int delay) {
this.delay = delay;
timer.setDelay(delay);
}
public void pauseGame(){
timer.stop();
}
public void playGame(){
timer.restart();
}
public void setInitState(boolean set){
state = set;
if(state){
timer.stop();
}
}
public void setWrap(boolean set){
wrap = set;
if(wrap){
//implement allow wrap
}
}
#Override
public void mouseClicked(MouseEvent e) {
if(state){
int x=e.getX();
int y=e.getY();
boolean isFilled;
isFilled = life.getCell(x,y);
//Test pop-up
JOptionPane.showMessageDialog(this, x+","+y+"\n"+life.getCell(x,y));
if(isFilled){
life.setCell(x,y,false);
}else{
life.setCell(x,y,true);
}
repaint();
}
}
#Override
public void mousePressed(MouseEvent e) {}
#Override
public void mouseReleased(MouseEvent e) {}
#Override
public void mouseEntered(MouseEvent e) {}
#Override
public void mouseExited(MouseEvent e) {}
private class UpdateListener implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
life.iterate();
repaint();
}
}
}
LifeFrame class
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class LifeFrame extends JFrame implements ActionListener{
JMenuBar menuBar;
JMenu mainMenu, helpMenu;
JMenuItem restartItem, quitItem, helpItem;
JButton stopButton, playButton, pauseButton, startButton;
CardLayout cardLayout = new MyCardLayout();
CardLayout cardLayout2 = new MyCardLayout();
SetupPanel setupPanel; //panel for input
LifePanel gamePanel; //game panel
JPanel controls = new JPanel(); //controls for game
JPanel controls2 = new JPanel(); //controls for input panel
JPanel cardPanel = new JPanel(cardLayout);
JPanel cardPanel2 = new JPanel(cardLayout2);
int gridRow=480;
int gridCol=480;
int scale=1;
int delay=2;
boolean setState = false;
boolean setWrap = false;
public LifeFrame() {
setTitle("Game of Life");
setLayout(new BorderLayout());
//Add the Panels
setupPanel = new SetupPanel();
gamePanel = new LifePanel(gridRow,gridCol,scale,delay);
cardPanel.add(setupPanel, "1");
cardPanel.add(gamePanel, "2");
add(cardPanel, BorderLayout.NORTH);
cardPanel2.add(controls2, "1");
cardPanel2.add(controls, "2");
add(cardPanel2, BorderLayout.SOUTH);
//init menu
menuBar = new JMenuBar();
//button listener setup
stopButton = new JButton("Stop");
pauseButton = new JButton("Pause");
playButton = new JButton("Play");
startButton = new JButton("Start");
stopButton.addActionListener(this);
pauseButton.addActionListener(this);
playButton.addActionListener(this);
startButton.addActionListener(this);
//menu listener setup
restartItem = new JMenuItem("Restart", KeyEvent.VK_R);
quitItem = new JMenuItem("Quit", KeyEvent.VK_Q);
helpItem = new JMenuItem("Help", KeyEvent.VK_H);
restartItem.addActionListener(this);
quitItem.addActionListener(this);
helpItem.addActionListener(this);
//add buttons
controls.add(stopButton);
controls.add(pauseButton);
controls.add(playButton);
controls2.add(startButton);
//build the menus
mainMenu = new JMenu("Menu");
mainMenu.setMnemonic(KeyEvent.VK_M);
helpMenu = new JMenu("Help");
helpMenu.setMnemonic(KeyEvent.VK_H);
menuBar.add(mainMenu);
menuBar.add(helpMenu);
setJMenuBar(menuBar);
//add JMenuItems
restartItem.getAccessibleContext().setAccessibleDescription("Return to setup screen");
mainMenu.add(restartItem);
mainMenu.add(quitItem);
helpMenu.add(helpItem);
this.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
pack();
setLocationRelativeTo(null);
setVisible(true);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
#Override
public void actionPerformed(ActionEvent e) {
try{
gridRow = setupPanel.getRowSize();
gridCol = setupPanel.getColSize();
scale = setupPanel.getScale();
delay = setupPanel.getDelay();
setWrap = setupPanel.getSetWrap();
setState = setupPanel.getSetState();
}catch (NumberFormatException n){
JOptionPane.showMessageDialog(LifeFrame.this, "Make sure the fields contain only digits and are completed!");
return;
}
if(e.getSource() == pauseButton){
gamePanel.pauseGame();
}else if(e.getSource() == playButton){
gamePanel.playGame();
}else if(e.getSource() == quitItem){
System.exit(0);
}else if(e.getSource() == restartItem || e.getSource() == stopButton){
cardLayout.show(cardPanel, "1");
cardLayout2.show(cardPanel2, "1");
pack();
setLocationRelativeTo(null);
}else if(e.getSource() == helpItem){
String helpText = "Help\nPlease make sure every field is completed and contains only digits\nCurrent Stats:\nGrid Size: "+gamePanel.getRow()+" by "+gamePanel.getCol()+"\nScale: "+ gamePanel.getScale() +"\nDelay: "+gamePanel.getDelay()+"\nManual Initial State: "+setState+"\nEnable Wrapping: "+setWrap;
JOptionPane.showMessageDialog(LifeFrame.this, helpText);
}else if(e.getSource() == startButton){
gamePanel = new LifePanel(gridRow,gridCol,scale,delay);
cardPanel.add(gamePanel, "2");
/*
* Alternate solution, throws array index out of bounds due to array usage in the LifePanel, but properly
* populates the grid.
*
gamePanel.setRow(gridRow);
gamePanel.setCol(gridCol);
gamePanel.setScale(scale);
gamePanel.setDelay(delay);
*/
if(setWrap){
gamePanel.setWrap(true);
gamePanel.playGame();
}else if(setState){
gamePanel.setInitState(true);
}else{
gamePanel.setWrap(false);
gamePanel.setInitState(false);
gamePanel.playGame();
}
gamePanel.repaint();
cardLayout.show(cardPanel, "2");
cardLayout2.show(cardPanel2, "2");
pack();
setLocationRelativeTo(null);
}
}
public static class MyCardLayout extends CardLayout {
#Override
public Dimension preferredLayoutSize(Container parent) {
Component current = findCurrentComponent(parent);
if (current != null) {
Insets insets = parent.getInsets();
Dimension pref = current.getPreferredSize();
pref.width += insets.left + insets.right;
pref.height += insets.top + insets.bottom;
return pref;
}
return super.preferredLayoutSize(parent);
}
public Component findCurrentComponent(Container parent) {
for (Component comp : parent.getComponents()) {
if (comp.isVisible()) {
return comp;
}
}
return null;
}
}
}
Thanks for reading all this, and in advance for any help/advice you offer.
EDIT: Added screen shots and refined question.
Based on how you initialize LifePanel
Random rnd = new Random();
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
life.setCell(i,j,rnd.nextBoolean());
what you call "overpopulation" is the expected state. The above code will set about 1/2 of the cells to "alive" (or "occupied"), which is what your "overpopulated" state looks like.
The "desired population" screenshot contains many "life" artifacts such as "beehives", "gliders", "traffic lights", etc, and was either manually constructed or is the result of running several iterations on an initially 50% random population. With a 50% occupied population the first generation will result in wholesale clearing ("death") of many, many cells due to the proximity rules.
Most crucially, consider that, when starting up, your program does not paint the initial configuration. At least one iteration occurs before the first repaint() call.
I don't think your code is broken at all, just your expectation for what the initial population looks like.
I'm working on this piece of code which can be found at
http://pastebin.com/7bCFtUHL
Basically, I want to add a clear method (button) which clears the sudoku after having it solved.
I've tried making a loop that goes through every cell and puts it to null but I'm not completely sure how to connect it exactly. Nor am I sure in which class I'd have to create it so it can be connected to the GUI where I have the other button.
EDIT:
This is the clear method I currently got
public void clearCells(){
for (int y = 0; y < 9; y++) {
for (int x = 0; x < 9; x++) {
cells[y][x] = null;
cells[y][x].setText("");
}
}
}
Now I need to attach that to the JButton in another class, how would that be possible?
My clear button looks like this
JButton clear = new JButton("Clear");
clear.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
//Code
}
}
);
What code would I need to add in the actionPerformed method to connect it with my clearCells method?
Again, I would put the "meat" of the clear method in the model itself. The general form of a solution would be to do this:
clear.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
myModel.clearCells();
}
});
Where the Model class would have a public void clearCells() method that iterates through the cells and clears them.
Edit 1
Note: yeah I did look at your pastebin code link and one big problem I see is that your SwingSudokuBoard class extends the SudokuBoard class, and this is misuse of inheritance where you should be using composition instead. The SwingSudokuBoard class should hold an instance of a SudokuBoard object and call methods on it.
Edit 2
You ask:
I'm not sure that I can completely understand you. You want me to have the clear method in the same class as I got the button, but then I cant call the cells. I added x.clearCells(); while x being what? My main class like, SwingSudokuBoard.clearCells(); ? Eitherway, if I add what you say the program complaints that it want the clearCells method and cells to be static. But if I put them to static, I get a NullPointerException.
I think that you need to use the Model-View-Control (MVC) pattern or an abbreviated version of it, perhaps one where you combine the view with the control since your program is small. I suggest that you have a separate model class, here this would likely be the SudokuBoard class, and then a view class, here probably the SwingSudokuBoard class. Your view's control methods (the ActionListeners) would call the model's clearCells() method. And don't use static anything here.
Edit 3
You ask:
I assume something along with these lines. Model: SudokuBoard; View: SwingSudokuBoard; Control: SwingSudoKiller. How would that go about? I'd have the actionListener posted above in the control. How would the other classes look like? Since I assume the clear method lays in the Model which you want to be in SudokuBoard but it cant connect with the cells there.
I'm not a professional, nor have I received formal programming training, so theory is one of my weak points, but my interpretation of MVC is that the view listens to the model and updates itself when the model notifies it of changes and that the control listens to the view and responds to view changes by notifying the model. This precise pattern has variations and does not need to be followed exactly to the letter, but the key in all of this is to separate out in your code the separate concerns as much as possible so that "coupling" (the number of direct connections between classes) is low or "loose" and "cohesion" (code that deals with the same concerns) is high or "tight".
In your program, again I'd combine the view and control by using anonymous inner listeners just as you're doing. I'd have the view/control, which is the SwingSudokuBoard class, hold an instance of the SudokuBoard class as a class field, and have the view/control's anonymous listeners call methods on the SudokuBoard field. When I've done this sort of thing before, I've given the model support for being observed by giving it a SwingPropertyChangeSupport object as well as public addPropertyChangeListener(...) and removePropertyChangeListener(...) methods. Then the view could respond easily to changes in the model.
You state:
Since I assume the clear method lays in the Model which you want to be in SudokuBoard but it cant connect with the cells there.
I'm not sure what you mean by this. The model holds the cells. Perhaps you don't mean the logical cells held by the model but rather the displayed cells held by the view. The view would add a listener to the model, and when notified of changes to the model, would ask the model for its data and use that to update the visualized cells.
Edit 4
For example:
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;
public class OverlySimpleModelView {
private static void createAndShowGui() {
Model model = new Model();
ViewControl viewControl = new ViewControl(model);
JFrame frame = new JFrame("OverlySimpleModelView");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(viewControl.getMainComponent());
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
class ViewControl {
private JPanel mainPanel = new JPanel();
private JTextField number1Field = new JTextField(5);
private JTextField number2Field = new JTextField(5);
private JTextField productField = new JTextField(5);
private Model model;
public ViewControl(Model model) {
this.model = model;
model.addPropertyChangeListener(new MyPropChngListener());
productField.setEditable(false);
productField.setFocusable(false);
mainPanel.add(number1Field);
mainPanel.add(new JLabel(" * "));
mainPanel.add(number2Field);
mainPanel.add(new JLabel(" = "));
mainPanel.add(productField);
CalculateAction calculateAction = new CalculateAction("Calculate", KeyEvent.VK_C);
mainPanel.add(new JButton(calculateAction));
number1Field.addActionListener(calculateAction);
number2Field.addActionListener(calculateAction);
mainPanel.add(new JButton(new ClearAction("Clear", KeyEvent.VK_L)));
}
public JComponent getMainComponent() {
return mainPanel;
}
private class MyPropChngListener implements PropertyChangeListener {
#Override
public void propertyChange(PropertyChangeEvent evt) {
number1Field.setText(String.valueOf(model.getNumber1()));
number2Field.setText(String.valueOf(model.getNumber2()));
productField.setText(String.valueOf(model.calculateProduct()));
}
}
private class CalculateAction extends AbstractAction {
public CalculateAction(String text, int keyCode) {
super(text);
putValue(MNEMONIC_KEY, keyCode);
}
#Override
public void actionPerformed(ActionEvent evt) {
try {
double number1 = Double.parseDouble(number1Field.getText());
double number2 = Double.parseDouble(number2Field.getText());
model.setNumber1(number1);
model.setNumber2(number2);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
private class ClearAction extends AbstractAction {
public ClearAction(String text, int keyCode) {
super(text);
putValue(MNEMONIC_KEY, keyCode); // to allow buttons a mnemonic letter
}
#Override
public void actionPerformed(ActionEvent evt) {
model.clear();
}
}
}
class Model {
public static final String NUMBERS_CHANGED = "numbers changed";
private double number1 = 0.0;
private double number2 = 0.0;
private SwingPropertyChangeSupport propChngSupport =
new SwingPropertyChangeSupport(this);
public double getNumber1() {
return number1;
}
public double getNumber2() {
return number2;
}
public void clear() {
setNumber1(0.0);
setNumber2(0.0);
}
// make number1 field a "bound" property, one that notifies listeners if it is changed.
public void setNumber1(double number1) {
Double oldValue = this.number1;
Double newValue = number1;
this.number1 = number1;
propChngSupport.firePropertyChange(NUMBERS_CHANGED, oldValue , newValue);
}
// ditto for the number2 field
public void setNumber2(double number2) {
Double oldValue = this.number2;
Double newValue = number2;
this.number2 = number2;
propChngSupport.firePropertyChange(NUMBERS_CHANGED, oldValue , newValue);
}
public double calculateProduct() {
return number1 * number2;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propChngSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
propChngSupport.removePropertyChangeListener(listener);
}
}
Or maybe better since it uses an array of numbers:
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;
public class OverlySimpleModelView {
private static void createAndShowGui() {
Model model = new Model(5);
ViewControl viewControl = new ViewControl(model);
JFrame frame = new JFrame("OverlySimpleModelView");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(viewControl.getMainComponent());
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
class ViewControl {
private JPanel mainPanel = new JPanel();
private JTextField[] numberFields;
private JTextField productField = new JTextField(5);
private Model model;
public ViewControl(Model model) {
this.model = model;
model.addPropertyChangeListener(new MyPropChngListener());
productField.setEditable(false);
productField.setFocusable(false);
CalculateAction calculateAction = new CalculateAction("Calculate", KeyEvent.VK_C);
numberFields = new JTextField[model.getNumberFieldsLength()];
for (int i = 0; i < numberFields.length; i++) {
numberFields[i] = new JTextField("0.0", 5);
mainPanel.add(numberFields[i]);
numberFields[i].addActionListener(calculateAction);
if (i < numberFields.length - 1) {
mainPanel.add(new JLabel(" + "));
} else {
mainPanel.add(new JLabel(" = "));
}
}
mainPanel.add(productField);
mainPanel.add(new JButton(calculateAction));
mainPanel.add(new JButton(new ClearAction("Clear", KeyEvent.VK_L)));
}
public JComponent getMainComponent() {
return mainPanel;
}
private class MyPropChngListener implements PropertyChangeListener {
#Override
public void propertyChange(PropertyChangeEvent evt) {
for (int i = 0; i < numberFields.length; i++) {
numberFields[i].setText(String.valueOf(model.getNumber(i)));
}
productField.setText(String.valueOf(model.calculateSum()));
}
}
private class CalculateAction extends AbstractAction {
public CalculateAction(String text, int keyCode) {
super(text);
putValue(MNEMONIC_KEY, keyCode);
}
#Override
public void actionPerformed(ActionEvent evt) {
try {
double[] numbers = new double[numberFields.length];
for (int i = 0; i < numbers.length; i++) {
numbers[i] = Double.parseDouble(numberFields[i].getText());
}
model.setNumbers(numbers);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
private class ClearAction extends AbstractAction {
public ClearAction(String text, int keyCode) {
super(text);
putValue(MNEMONIC_KEY, keyCode); // to allow buttons a mnemonic letter
}
#Override
public void actionPerformed(ActionEvent evt) {
model.clear();
}
}
}
class Model {
public static final String NUMBERS_CHANGED = "numbers changed";
private double[] numbers;
private SwingPropertyChangeSupport propChngSupport =
new SwingPropertyChangeSupport(this);
public Model(int length) {
numbers = new double[length];
}
public void setNumbers(double[] numbers) {
double[] oldValue = this.numbers;
double[] newValue = numbers;
this.numbers = numbers;
propChngSupport.firePropertyChange(NUMBERS_CHANGED, oldValue , newValue);
}
public double calculateSum() {
double sum = 0.0;
for (double number : numbers) {
sum += number;
}
return sum;
}
public double getNumber(int i) {
return numbers[i];
}
public int getNumberFieldsLength() {
return numbers.length;
}
public void clear() {
double[] newNumbers = new double[numbers.length];
setNumbers(newNumbers);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propChngSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
propChngSupport.removePropertyChangeListener(listener);
}
}
When there are more than 50 components in my JScrollPane, every time the user decides to go to the next page, the first 10 components in the JScrollPane are cleared.
This functionality works fine and is desirable, but when the components are removed, the viewport/scroll is changed. The view of the scrollpane jumps back each time the components are cleared.
What I'd like is to keep the viewport at exactly the same place, while removing the first components from the JScrollPane.
At the moment I have a workaround, but it's not elegant, and although it finds where the user last was, it's choppy, and jumps the scrollpane which doesn't look right:
if (middlePanel.getComponents().length > 50)
{
Component currentScroll = scrollPane.getViewport().getView();
for (int counter = 0; counter < memeAnchors.size(); counter++)
{
middlePanel.remove(counter);
}
scrollPane.setViewportView(currentScroll);
scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getValue() - 800);
}
Is what I want to do even possible?
Thanks in advance everyone,
OK, I lied about not responding.
One way to do it is as noted below in my SSCCE. Note use of Scrollable. Explanation is in comments:
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
#SuppressWarnings("serial")
public class Sscce extends JPanel {
private static final int INITIAL_ROW_COUNT = 100;
public static final int INCREMENTAL_ADD_ROW_COUNT = 10;
private static final int MIN_VERT_PERCENT = 4;
private static final int TIMER_DELAY = 250;
private ViewportViewPanel viewportViewPanel = new ViewportViewPanel(
new GridLayout(0, 1));
private JScrollPane scrollPane = new JScrollPane(viewportViewPanel);
private BoundedRangeModel vertModel;
private Timer vertChangeTimer;
private int viewportViewPanelIndex = 0;
public Component firstViewedComp;
public Sscce() {
setLayout(new BorderLayout());
add(scrollPane);
for (viewportViewPanelIndex = 0; viewportViewPanelIndex < INITIAL_ROW_COUNT; viewportViewPanelIndex++) {
viewportViewPanel.add(new ViewablePanel(viewportViewPanelIndex));
}
vertModel = scrollPane.getVerticalScrollBar().getModel();
vertModel.addChangeListener(new VertModelChangeListener());
}
private class VertModelChangeListener implements ChangeListener {
#Override
public void stateChanged(ChangeEvent cEvt) {
// if timer is running, get out of here
if (vertChangeTimer != null && vertChangeTimer.isRunning()) {
return;
}
// if haven't set firstViewedComp back to null (done in Timer) get out of here
if (firstViewedComp != null) {
return;
}
// check to see if near bottom
int diff = vertModel.getMaximum() - vertModel.getValue()
- vertModel.getExtent();
int normalizedDiff = (100 * diff) / vertModel.getMaximum();
// if not near bottom, get out of here
if (normalizedDiff >= MIN_VERT_PERCENT) {
return;
}
// create and start timer
vertChangeTimer = new Timer(TIMER_DELAY, new VertChangeTimerListener());
vertChangeTimer.setRepeats(false);
vertChangeTimer.start();
// get viewport and its rectangle
JViewport viewport = scrollPane.getViewport();
Rectangle viewRect = viewport.getViewRect();
// find first component that is inside of viewport's rectangle
Component[] components = viewportViewPanel.getComponents();
for (Component component : components) {
if (viewRect.contains(component.getBounds())) {
if (firstViewedComp == null) {
firstViewedComp = component; // first component found
break;
}
}
}
// delete 10 components at start
// add 10 components add end
for (int i = 0; i < INCREMENTAL_ADD_ROW_COUNT; i++) {
viewportViewPanel.remove(components[i]);
viewportViewPanel.add(new ViewablePanel(viewportViewPanelIndex));
viewportViewPanelIndex++;
}
// redo laying out components and repainting the container
viewportViewPanel.revalidate();
viewportViewPanel.repaint();
// scroll back to first viewed component, but give a little delay to allow
// layout out above to complete. So queue it on the event queue via invokeLater
SwingUtilities.invokeLater(new Runnable() {
public void run() {
viewportViewPanel.scrollRectToVisible(firstViewedComp
.getBounds());
}
});
}
}
// the timer listener. it just nulls out the first viewed component
private class VertChangeTimerListener implements ActionListener {
#Override
public void actionPerformed(ActionEvent e) {
firstViewedComp = null;
}
}
private static void createAndShowGui() {
Sscce mainPanel = new Sscce();
JFrame frame = new JFrame("Sscce");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
// JPanel that is held by the JScrollPane's JViewport and that holds the smaller
// JPanels. Note that it implements Scrollable
#SuppressWarnings("serial")
class ViewportViewPanel extends JPanel implements Scrollable {
private static final int BLOCK = 8;
public ViewportViewPanel(LayoutManager layout) {
super(layout);
}
#Override
public Dimension getPreferredScrollableViewportSize() {
int scrollWidth = ViewablePanel.PREF_W;
int scrollHeight = ViewablePanel.PREF_H * BLOCK;
return new Dimension(scrollWidth, scrollHeight);
}
#Override
public int getScrollableBlockIncrement(Rectangle visibleRectangle,
int orientation, int direction) {
if (orientation == SwingConstants.VERTICAL) {
return ViewablePanel.PREF_H * (3 * BLOCK) / 4;
}
return 0;
}
#Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
#Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
#Override
public int getScrollableUnitIncrement(Rectangle visibleRect,
int orientation, int direction) {
if (orientation == SwingConstants.VERTICAL) {
return ViewablePanel.PREF_H;
}
return 1;
}
}
// small JPanel. Many of these are held in a single GridLayout column
// by the JPanel above.
#SuppressWarnings("serial")
class ViewablePanel extends JPanel {
public static final int PREF_W = 400;
public static final int PREF_H = 50;
private int index;
public ViewablePanel(int index) {
this.setIndex(index);
String title = "index " + index;
setBorder(BorderFactory.createTitledBorder(title));
}
#Override
public Dimension getPreferredSize() {
return new Dimension(PREF_W, PREF_H);
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}