lorau
lorau

Reputation: 11

How can I make sure that a String variable is set before the panel switch?

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

Answers (1)

Elias
Elias

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 Problem

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.

Solution

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:

"Flow-like" UI

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.

Only do the "stuff" when it's actually set

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.

An example

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

Related Questions