Reputation: 11
I want to make a quiz game using Java. I used panels to display different "stages" of the quiz (more like different interfaces) and I used cardLayout to navigate through those panels (each panel is in a different file).
I have a panel called SubjectsPanel
in which, basically, there are a few buttons with subjects one can choose from. Pressing one of those buttons leads to another panel, ModePanel
, in which there are two buttons, a "Practice" button and a "Test" one. Each of those buttons, if pressed, will open their respective panels and the quiz will finally start.
The thing is, I want all the questions and options to be imported from a .txt
file. This would work just fine if I use only one .txt
, but I've made a .txt
for each of the subjects that the player can choose from, and based on which button is pressed in the SubjectsPanel
, I want the questions to be imported from its corresponding .txt
.
I've tried to compact the code as much as I could, so I've made a version of the quiz that does not really do what it should do (it actually does not work much and it looks quite bad), but that's not the point. It's to see where the problem might be.
So:
public class App {
public static void main(String[] args) throws Exception {
SwingUtilities.invokeLater(() -> new MainFrame());
}
}
The MainFrame
file:
import javax.swing.*;
import java.awt.*;
class MainFrame extends JFrame {
private CardLayout cardLayout;
private JPanel mainPanel;
private SharedData sharedData;
public MainFrame() {
setTitle("QUIZ");
setSize(1000, 750); // frame dimension
setLocationRelativeTo(null);
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
sharedData = new SharedData();
// CardLayout for switching panels
cardLayout = new CardLayout();
mainPanel = new JPanel(cardLayout);
mainPanel.add(new SubjectsPanel(this, sharedData), "SubjectsPanel");
mainPanel.add(new ModePanel(this, sharedData), "ModePanel");
mainPanel.add(new PracticePanel(this, sharedData), "PracticePanel");
add(mainPanel);
cardLayout.show(mainPanel, "SubjectsPanel");
}
public void switchToPanel(String panelName) {
cardLayout.show(mainPanel, panelName);
}
}
I've thought of making a shared class, that every other class can access, which looks like this:
public class SharedData {
public String subject;
public String getSubjectChosen() {
return this.subject;
}
public void setSubjectChosen(String subject) {
this.subject = subject;
}
}
Then there is SubjectsPanel
:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
class SubjectsPanel extends JPanel {
JButton b1, b2;
public SubjectsPanel(MainFrame frame, SharedData sharedData) {
setLayout(new GridLayout(3, 2));
JButton b1 = new JButton("Anatomy");
JButton b2 = new JButton("Cellular Buiology");
b1.setFont(new Font("Arial", Font.BOLD, 40));
b2.setFont(new Font("Arial", Font.BOLD, 40));
add(b1);
add(b2);
b1.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
sharedData.setSubjectChosen("C:\\Users\\Utilizator\\Documents\\anatomy.txt");
System.out.println("1st bug print: " + sharedData.getSubjectChosen()); // it displays the file path
frame.switchToPanel("ModePanel");
}
});
b2.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
sharedData.setSubjectChosen("C:\\Users\\Utilizator\\Documents\\biocell.txt");
frame.switchToPanel("ModePanel");
}
});
}
}
Then there is ModePanel
for the two modes, test and practice:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
class ModePanel extends JPanel {
public ModePanel(MainFrame frame, SharedData sharedData) {
setLayout(new GridLayout(0, 2));
JButton practiceButton = new JButton("Practice");
JButton testButton = new JButton("Test");
practiceButton.setFont(new Font("Arial", Font.BOLD, 40));
testButton.setFont(new Font("Arial", Font.BOLD, 40));
add(practiceButton);
add(testButton);
practiceButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("2nd bug print: " + sharedData.getSubjectChosen()); // !!!!!it displays the file path
frame.switchToPanel("PracticePanel");
}
});
}
}
The PracticePanel
(the TestPanel
is very similar):
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
public class PracticePanel extends JPanel {
JLabel question;
JCheckBox c[] = new JCheckBox[4];
JButton b1, b2;
int q_curent = 0, n = 3;
QnA q;
PracticePanel(MainFrame frame, SharedData sharedData) {
String subjectPath = sharedData.getSubjectChosen(); // should get the chosen subject
System.out.println(subjectPath); // displays null
this.q = new QnA(subjectPath); // passing the chosen subject to QnA
setLayout(new GridLayout(7, 0));
question = new JLabel();
question.setFont(new Font("Arial", Font.BOLD, 20));
add(question);
for (int i = 0; i < 3; i++) {
c[i] = new JCheckBox();
add(c[i]);
}
b1 = new JButton("Next");
b2 = new JButton("Previous");
b1.setFont(new Font("Arial", Font.BOLD, 18));
b2.setFont(new Font("Arial", Font.BOLD, 18));
b2.setEnabled(false);
add(b1);
add(b2);
set();
// if the "next" button is pressed
b1.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
q_curent++;
b2.setEnabled(true);
set();
}
});
// if the "previous" button is pressed
b2.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
q_curent--;
b1.setText("Next");
b1.setEnabled(true);
set();
if (q_curent == 0) {
b2.setEnabled(false);
}
}
});
}
private void set() {
for (int i = 0; i < n; i++) {
if (q_curent == i) {
question.setText(q.questions[q_curent]); // questions are displayed
for (int j = 0; j < 3; j++) {
c[j].setText(q.options[q_curent][j]); // answer options are displayed
}
}
}
}
}
And lastly, there is the class for importing the questions with their possible answer options:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class QnA {
public String[] questions;
public String[][] options;
public QnA(String subject) {
// importing the questions and their options from the .txt
try {
System.out.println(subject); //displays null
BufferedReader f = new BufferedReader(new FileReader(subject)); //error; the path is null
int nr_questions = 3;
questions = new String[nr_questions];
options = new String[nr_questions][3];
String linie;
int i = 0;
// reading from the file
while ((linie = f.readLine()) != null) {
linie = linie.trim();
if (linie.isEmpty())
continue;
questions[i] = linie;
for (int j = 0; j < 3; j++) {
options[i][j] = f.readLine().trim();
}
i++;
}
// stop reading
f.close();
}
catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
The problem is that when it reaches PracticePanel
, the sharedData
variable is null. I've implemented debug prints and I've kept some (the comments with exclamation marks at the beginning).
It might be something obvious, but I really don't know what I should do. Also, the code may have parts that look weird (for those who are experts) and I might have been smarter about organizing it but I'm still learning.
Upvotes: 0
Views: 102
Reputation: 4141
I hope you figured out how you want to solve this, or otherwise, I hope I can provide you with some assistance in your programming journey.
In fact, I haven't touched java in years, and installed it, and an IDE, just for this question. So I might not fully know what I'm doing :D. At least in regard to libraries and patterns.
The issue is caused by you initializing all screens at the same time:
mainPanel.add(new SubjectsPanel(this, sharedData), "SubjectsPanel");
mainPanel.add(new ModePanel(this, sharedData), "ModePanel");
mainPanel.add(new PracticePanel(this, sharedData), "PracticePanel");
So shared data is accessed immediately in PracticePanel
through the class QnA
.
In fact, the classes are all instantiated, and access it, at (roughly) the same time.
Depends on your UI needs. Sadly, the UI doesn't render anything for me, so I can't judge it. But to me, there are two main options:
If the UI is supposed to be used step by step, I'm sure there is some other control which can be used for that. Important would be, that the screen is only rendered (or whatever code path accesses the value), when the data has been set.
Like I said in the beginning, I'm not an expert in java and its libraries at all. But I could look into it if you can't figure something out.
Whatever "stuff" might be in your case, this is really simple, but also not pretty, but now the program doesn't crash immediately:
PracticePanel(MainFrame frame, SharedData sharedData) {
String subjectPath = sharedData.getSubjectChosen(); // should get the chosen subject
if (subjectPath != null) {
this.q = new QnA(subjectPath); // passing the chosen subject to QnA
}
Again, I don't know how this UI framework works, but now you would need to be notified when the subject is actually set, and re-render / re-execute this evaluation.
Full disclosure, due to my limited java experience, this was assisted by ChatGPT. I've cleaned all of it, and it seems conceptually sound. But please watch or read some tutorials if you're going to use this, especially on the state management bean thing.
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
public class Main {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
AppState state = new AppState();
new MyFrame(state).setVisible(true);
});
}
}
class AppState {
public static final String PROP_PATH = "path";
private String path;
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public String getPath() {
return path;
}
public void setPath(String path) {
String oldPath = this.path;
this.path = path;
pcs.firePropertyChange(PROP_PATH, oldPath, path);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
}
class MyPanel extends JPanel {
private final JLabel labelPath;
public MyPanel(AppState state) {
labelPath = new JLabel(getLabelText(state.getPath()), SwingConstants.CENTER);
setBackground(Color.ORANGE);
setLayout(new java.awt.BorderLayout());
add(labelPath);
state.addPropertyChangeListener(evt -> {
if (evt.getPropertyName().equals(AppState.PROP_PATH)) {
labelPath.setText(getLabelText((String) evt.getNewValue()));
}
});
}
private String getLabelText(String path) {
// Technically this ternary is unnecessary because String.format can deal
// with `null`, but I thought I would keep it to be closer to your problem.
return path == null
? "Path: (None)"
: String.format("Path: (%s)", path);
}
}
class MyFrame extends JFrame {
private static final String PANEL_MY_PANEL = "MyPanel";
public MyFrame(AppState state) {
super("Example");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(500, 300);
CardLayout cardLayout = new CardLayout();
JPanel cardPanel = new JPanel(cardLayout);
cardPanel.add(new MyPanel(state), PANEL_MY_PANEL);
JButton btnSetPath = new JButton("Set Path");
btnSetPath.addActionListener(e -> state.setPath("/my/cool/path"));
JPanel bottomPanel = new JPanel();
bottomPanel.add(btnSetPath);
add(cardPanel, BorderLayout.CENTER);
add(bottomPanel, BorderLayout.SOUTH);
setLocationRelativeTo(null);
}
}
Upvotes: 0