How can I switch between JPanels in a CardLayout using MVC? - java

I'm making a Maths Game application and recently began implementing MVC.
I have the following structure:
auiAs2
MigJPanel: extends JPanel
ScreenInterface.java: contains global variables, fonts, and difficulty enum
MathsGame.java: extends JFrame
auiAs2.view
DiffView.java: extends MigJPanel implements ScreenInterface
GameView.java: extends MigJPanel implements ScreenInterface
EndGameView.java: extends MigJPanel implements ScreenInterface
auiAs2.controller
DiffControl.java
GameControl.java
EndGameControl.java
auiAs2.model
Model.java: implements ScreenInterface
My MathsGame.java contains a JPanel set to CardLayout which instances of DiffView, GameView, and EndGameView are added to. When I run my program, the diffView 'card' is shown to the user.
If the user clicks "New Game", an ActionListener in DiffControl.java gets the selected difficulty.
public class DiffControl {
private DiffView diffView;
private Model model;
public DiffControl(DiffView diffView, Model model) {
this.diffView = diffView;
this.model = model;
this.diffView.addNewGameListener(new NewGameListener());
}
class NewGameListener implements ActionListener {
String selectedDiff;
#Override
public void actionPerformed(ActionEvent e) {
selectedDiff = diffView.getSelectedDiff();
//MathsGame.setLayCard(panContainer, "New Game"));
}
}
}
This is where I get stuck. Where should I switch between panels in my CardLayout JPanel layCard? (MathsGame.java is shown below with irrelevant code removed. The entire code for relevant classes is linked above if required)
public class MathsGame extends JFrame {
private JPanel panContainer = new JPanel();
private CardLayout layCard = new CardLayout();
public MathsGame() {
panContainer.setLayout(layCard);
setContentPane(panContainer);
setSize(new Dimension(WIDTH, HEIGHT));
setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
setTitle(TITLE);
DiffView panDiffView = new DiffView();
panContainer.add(panDiffView, "Choose Difficulty");
GameView panGameView = new GameView();
panContainer.add(panGameView, "New Game");
EndGameView panEndGameView = new EndGameView();
panContainer.add(panEndGameView, "End Game");
Model model = new Model();
DiffControl diffControl = new DiffControl(panDiffView, model);
//GameControl gameControl = new GameControl(panGameView, model);
//EndGameControl EndGameControl = new EndGameControl(panEndGameView, model);
layCard.show(panContainer, "Choose Difficulty");
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(MathsGame::new);
}
}
So my question is:
Where would be the best place to put code related to switching between Views in my CardLayout container?

The model will have a state field, perhaps an enum, that would reflect the view. You could make this a bound property using a SwingPropertyChangeSupport object. Then the view could listen to the state of this property and swap the Card based on its state. In fact the toString() of each enum constant could be used to add card views to the CardLayout-using container.
Edit
You ask:
Could you elaborate on the usage of SwingPropertyChangeSupport?
You can find examples of this here, here, and especially here.
I've attempted using addPropertyChangeListener in the model. And how would the View control the card change when the CardLayout is in MathsGame.java?
The view would be notified of the state change of the model, and then when this happens, the view would call its code to swap cards.
I've had problems with doing that inside View for "calling a static method from a non-static context".
This is a completely different unrelated issue, a basic core Java issue, that I'm sure with a little work, you'll be able to solve. In brief -- don't try to call instance code in a static way. Always call it on a proper reference, not on the class.

Related

Add listener to JPanel

I have a custom class CustomField that extends JPanel. As I often have to reuse the same pattern, my custom class is made of 2 JLabels and 2 JComboBox.
It's quite simple; the first JComboBox has ON/OFF choices and the second JComboBox is only visible if the first is set to "ON". I can manage this part.
The part that I however don't know who to design it well is that CustomField instances are in another class that is the main JFrame and in this JFrame, some parts will be visible only if the JComboBox from the CustomField class is set to "ON". I thought about using a MouseAdapter, but I don't know it is good practice.
Here is my CustomField class:
public class CustomField extends JPanel {
/**
*
*/
private static final long serialVersionUID = 1L;
JLabel text, portText;
JComboBox<String> testCB, option;
public CustomField(String text, String opt, String tst) {
this.text = new JLabel(text);
String[] onOffOpt= {"OFF", "ON"};
this.option = new JComboBox<String>(onOffOpt);
this.option.setSelectedItem(opt);
this.option.addItemListener(new ItemListener(){
#Override
public void itemStateChanged(ItemEvent ie) {
portText.setVisible(option.getSelectedIndex() == 1);
testCB.setVisible(option.getSelectedIndex() == 1);
}
});
this.portText = new JLabel("Test:");
String[] testChoices = {"Test", "Test2"};
this.testCB = new JComboBox<String>(testChoices);
this.testCB.setSelectedItem(tst);
this.setLayout(new FlowLayout());
add(this.text);
add(this.option);
add(this.portText);
add(this.testCB);
}
}
And here is the main JFrame:
public class Main {
CustomField cf = new CustomField("test", "ON, "Test2");
public static void main(String s[]) {
JFrame frame = new JFrame("Application");
JPanel panel = new JPanel();
panel.setLayout(new FlowLayout());
panel.add(cf);
JLabel labelTest = new JLabel("Label that should be visible or not");
panel.add(labelTest);
frame.add(panel);
frame.setSize(300, 300);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
Basically, I want that the labelTest visibily changes according to the CustomField settings. In the way that it is made, I can not put the labelTest in the CustomField class.
Is there a clean way to do what I want? Should I redesign the actual thing and put all the fields in the same class?
Thanks!
First, you want to expose the combobox's state with a method in CustomField:
public boolean isOn() {
return testCB.getSelectedIndex() == 1;
}
You can get an idea for how listening for state is done by looking at the method signatures in the documentation for various Swing components, which use the standard JavaBean listener pattern: You’ll want to add three public methods, and one protected method:
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
public void removeChangeListener(ChangeListener listener) {
listenerList.remove(ChangeListener.class, listener);
}
public ChangeListener[] getChangeListeners() {
return listenerList.getListeners(ChangeListener.class);
}
protected void fireChangeListeners() {
ChangeEvent event = new ChangeEvent(this);
for (ChangeListener listener : getChangeListeners()) {
listener.stateChanged(event);
}
}
(The listenerList field is inherited from JComponent.)
Now, you can simply add a call to fireChangeListeners(); whenever you detect that the user has changed the value of the On/Off combobox—that is, you’ll want to call it in your ItemListener.
As you can probably guess, your Main class can now call cf.addChangeListener, and inside that listener adjust the visibility of your label based on the value returned by cf.isOn().
You can learn a lot more by reading these.

Using a single JFrame across multiple classes

I am working on a video game in Java that so far has a main menu class and a class for the game. As I have it set up right now, each class uses its own JFrame, which means that when the user clicks "start game", the main menu JFrame closes and the games JFrame opens. Obviously this is not ideal and I would like to have both classes use the same JFrame, however I really don't know how to go about this and internet searches have not been helpful.
The class for my main menu is:
public class Frame extends javax.swing.JFrame {
...
}
I have it set up right now so that my Game class imports my Frame class, but when I try to make the JFrame display elements from my game nothing comes up. So my question is:
How do I use one single JFrame across multiple classes?
Any help is much appreciated!
Rather then needing to pass a reference of the main frame to each of the child panels, which might expose parts of the program you don't want them to have access to (as an example), you should use something like a CardLayout and use the main frame as the main display hub, switching out the panels as you need to
Check out How to use CardLayout for more examples
Rather than having each class be its own frame, you can have one frame, with several classes manipulating it. I would probably set something up like this:
public class MainFrame extends JFrame {
public MainFrame() {
super("Cool Game!");
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
}
public class Game {
private final MainFrame mainFrame;
public Game(final MainFrame mainFrame) {
this.mainFrame = mainFrame;
mainFrame.setContentPane(createGamePanel());
}
private JPanel createGamePanel() {
//...
}
}
public class MainMenu {
private final MainFrame mainFrame;
public MainMenu(final MainFrame mainFrame) {
this.mainFrame = mainFrame;
}
public void showMainMenu() {
mainFrame.setContentPane(createMainMenuPanel());
}
private JPanel createMainMenuPanel() {
//...
}
}
You should use just one frame at overall game. And there should be many JPanels for different contents.
Deciding content and switching should be like this :
switch( currentState ) {
case introduction:
setContentPane(new IntroductionPanel());
break;
case insideGame:
setContentPane( new GamePanel() );
...
...
...
}
Use cardLayout to switch panels
http://docs.oracle.com/javase/tutorial/uiswing/layout/card.html

Assign function to Jbutton

I have a few classes to create a very simple GUI (JFrame) with a few objects like a JTabbedPanel and a JTree. And one of the classes that creates a ribbon in the JTabbedPanel creates a JButton, that should have a function that updates the JTree.
I do have some getter, but I have no idea how to use the JButton to update something on an object that gets created in the main method (the GUI object, from where I would be able to get to the JTree).
How do I update something on an object that gets created in the main method in the actual class of the JButton?
I might have to change the structure of my project.
public class Gui extends JFrame{
private Ribbon ribbon;
private Status status;
public Panel panel;
public Gui(){
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setTitle("INIT TITLE");
setExtendedState(Frame.MAXIMIZED_BOTH);
setMinimumSize(new Dimension(600,400));
setVisible(true);
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
} catch (Exception e) {
e.printStackTrace();
}
ribbon = new Ribbon();
add(ribbon, BorderLayout.NORTH);
status = new Status();
add(status, BorderLayout.SOUTH);
panel = new Panel();
add(panel, JSplitPane.HORIZONTAL_SPLIT);
//panel.setLeftComponent(panel.getTree());
//panel.openProject();
setVisible(true);
panel.loadProject();
}
The Ribbon doesnt do a lot:
public class Ribbon extends JTabbedPane {
public Ribbon(){
addTab("Home", null, new RibbonHome());
addTab("Import", null, new RibbonImport());
addTab("Options", null, new RibbonOptions());
}
}
But the Ribbon-Tab creates some buttons:
public class RibbonHome extends JPanel{
private JButton b1, b2, b3;
public RibbonHome(){
b1 = new JButton("test1");
b2 = new JButton("test2");
b3 = new JButton("test3");
add(b1);
add(b2);
add(b3);
b1.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent arg0) {
//UPDATE the JTree
}
});
}
}
button.addActionListener( main );
main implements ActionListener and provides the method actionPerformed()
A code sample of what you are doing would make things easier, but anyway.
Probably you want to make the component you are planning to update a class member. This way, you can initialize it where it is initialized now, but still reach it from your ActionListener.
Again, this is a bit a guess on how you structured things, so if you can post a code sample, I am sure you can get a much more detailed answer.
EDIT after code sample:
You could give RibbonHome an additional private member of type Gui. I usually call it parent, so it becomes: private Gui parent;
In the constructor of RibbonHome, you add Gui as a parameter, this way, RibbonHome will always have access to its parent.
Also add it to the constructor of Ribbon, because you will want to pass it through there.
As a last step, you will always have to construct Ribbon with Gui as a parameter, so in Gui it will become: ribbon = new Ribbon(this);
As soon as you have done all of this, you can reach the JTree by the getter of you JTree on parent.
There are better solutions, since this will have as a drawback that you will never be able to construct a new Ribbon from another component than a Gui, but if you want that, you can work with interfaces.
The most obvious answer is of course to pass the reference of your Gui class to your Ribbon class, which in turn can pass it to the RibbonHome class.
public Gui{
//...
ribbon = new Ribbon( this );
//...
}
public Ribbon ( Gui gui ){
//...
addTab("Home", null, new RibbonHome( gui ));
//...
}
public RibbonHome( Gui gui){
//...
b1.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent arg0) {
//UPDATE the JTree
}
});
//...
}
However, this ensures a very tight coupling between your different classes. I do not know what exactly you want to update on that JTree but the more fundamental solution might be to share a model between your different views. Your JTree would update itself when the model changes, and the button just updates the model.

Controlling another class from a different class

I am having a bit of problem regarding Swing. I have a JFrame called FrameMain. Inside it is a JPanel called panelChoices.
When FrameMain is called/created, it fills up the panelChoices object with a number of PanelEntries objects, which is a JPanel with a number of JButtons in it (it is a different class that I wrote).
What I want to do is when I click one of the buttons inside the PanelEntries object, I want to destroy/remove FrameMain, along with the rest of it components (including the PanelEntries object that contains the JButton).
I've tried using super but it returns the JPanel (the PanelEntries object) that holds the JButton and not FrameMain that holds them all together. How can I achieve this?
EDIT: It seems that I am not clear enough, so here's a bit more information from my work. I don't have the actual code right now because I am on a different machine but I hope this will help elaborate my question.
public class FrameMain() {
private JFrame frameMain;
private JPanel panelChoices;
public FrameMain(args) {
createGUI();
loadData();
}
private void createGUI() {
JFrame frameMain = new JFrame();
JPanel panelChoices = new JPanel(new GridLayout(1,1));
frameMain.add(panel);
// removed formatting and other design codes since they are not important.
pack();
}
private void loadData() {
boolean available;
for (int i = 1; i <= 10; i++) {
// do some if/else and give value to boolean available
PanelEntries panel = new PanelEntries(i, available);
frameMain.add(panel);
// more code here to handle data.
}
}
}
public class PanelEntries() extends JPanel {
public PanelEntries(int num, boolean avb) {
JButton button = new JButton("Button Number " + num);
button.setEnabled(avb);
add(button);
// add action listener to created button so that it calls 'nextScreen()' when clicked.
// more code
pack();
}
private void nextScreen() {
// destroy/dispose MainFrame here.
// See Notes.
AnotherFrame anotherFrame = new AnotherFrame();
}
}
Notes:
All classes are inside their own .java file.
I need to know how to dispose FrameMain from the button inside the PanelEntries object, not just disposing a JFrame.
As per the given information,
If you want to exit the application, its not a big deal use System.exit(0); :)
If you mean to dispose the frame, jframe.dispose();
If you want to remove a componet / all components you can use .remove(Component) / .removeAll() etc
If this did not help, please re-write your question with more information.

Difficulty removing all components from a Jpanel

G'day all,
I am coding a main menu for a project. The menu displays properly. I have also set up ActionListeners for the three buttons on the menu.
What I wish to do is reuse the JPanel for a new set of radio buttons when the user chooses "Start a New Game".
However, coding ActionPerformed to remove the existing components from the JPanel has me stumped. I know removeAll is somehow important, but unfortunately NetBeans informs me I cannot call it on my mainMenu JPanel object within ActionPerformed. So i have commented it out in my code below, but left it in so you can see what I am trying to do.
Your thoughts or hints are appreciated.
Here is my main code:
public class Main {
public static void main(String[] args) {
MainMenu menu = new MainMenu();
menu.pack();
menu.setVisible(true);
}
}
Here is my mainMenu code:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class MainMenu extends JFrame implements ActionListener {
JButton startNewGame = new JButton("Start a New Game");
JButton loadOldGame = new JButton("Load an Old Game");
JButton seeInstructions = new JButton("Instructions");
public MainMenu() {
super("RPG Main Menu");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel mainMenu = new JPanel();
mainMenu.setLayout(new FlowLayout());
startNewGame.setMnemonic('n');
loadOldGame.setMnemonic('l');
seeInstructions.setMnemonic('i');
startNewGame.addActionListener(this);
loadOldGame.addActionListener(this);
seeInstructions.addActionListener(this);
mainMenu.add(startNewGame);
mainMenu.add(loadOldGame);
mainMenu.add(seeInstructions);
setContentPane(mainMenu);
}
public void actionPerformed(ActionEvent evt) {
Object source = evt.getSource();
if (source == startNewGame) {
// StartNewGame code goes here
// mainMenu.removeAll();
}
if (source == loadOldGame) {
// LoadOldGame code goes here
}
if (source == seeInstructions) {
// Quit code goes here
}
}
}
Consider using a CardLayout instead, which manages two or more components (usually JPanel instances) that share the same display space. That way you don't have to fiddle with adding and removing components at runtime.
You need mainMenu to be a member variable:
public class MainMenu extends JFrame implements ActionListener {
JButton startNewGame = new JButton("Start a New Game");
JButton loadOldGame = new JButton("Load an Old Game");
JButton seeInstructions = new JButton("Instructions");
JPanel mainMenu = new JPanel();
Why do you feel the need to re-use this object?
You don't have a reference to mainMenu actionPerformed use. If you declare mainMenu with the buttons. It would work.
The problem is that the actionPerformed method is trying to call the JPanel mainMenu which is out of scope, i.e. the mainMenu variable is not visible from the actionPerformed method.
One way to get around this is to have the JPanel mainMenu declaration in the class itself and make it an instance field which is accessible to all instance methods of the class.
For example:
public class MainMenu extends JFrame implements ActionListener
{
...
JPanel mainMenu;
public MainMenu()
{
...
mainMenu = new JPanel();
...
}
public void actionPerformed(ActionEvent e)
{
...
mainMenu.removeAll();
}
}
Avoid attempting to "reuse" stuff. Computers are quite capable of tidying up. Concentrate on making you code clear.
So instead of attempting to tidy up the panel, simply replace it with a new one.
Generally a better way to write listeners is as anonymous inner classes. Code within these will have access to final variables in the enclosing scope and to members of the enclosing class. So, if you make mainMenu final and you ActionListeners anonymous inner classes, your code should at least compile.
Also don't attempt to "reuse" classes. Try to make each class do one sensible thing, and avoid inheritance (of implementation). There is almost never any need to extend JFrame, so don't do that. Create an ActionListener for each action, rather than attempting to determine the event source.
Also note, you should always use Swing components on the AWT Event Dispatch Thread. Change the main method to add boilerplate something like:
public static void main(final String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() { public void run() {
runEDT();
}});
}

Categories

Resources