I'm creating a Java application using Swing and the MVC design pattern.
The application is designed as follows:
There is a class QuizPanel that extends JPanel. This panel is the container that holds the main 'Screens' of my application.
Each 'screen' of my application is a separate class that extends JPanel. These JPanel's are added/removed from the QuizPanel as required.
The top level QuizPanel implements my interface Switchable. The Switchable interface consists of a single SwitchView(ViewState state) method. I pass this to each 'screen' instantiated in the QuizPanel top level panel, so they are able to call SwitchView when a button is pressed.
On the login screen, the user enters a pin and student ID, if these match a token in the database, I need to pass the Token object to another screen of my application (a question screen that I haven't implemented yet) or have it available somehow. The token is retrieved from an embedded Derby database.
The only way I can think of doing this is creating a utility class with static Token variable that can be accessed by the other classes (this seems like a nasty way to do it). Am I having trouble with this because the design of my application is flawed? Is there any technique I can use to pass the Token across the different screens of my application?
Main
public static void main(String[] args) {
QuizPanel quizPanel = new QuizPanel();
JFrame frame = new JFrame("Quiz");
frame.setPreferredSize(new Dimension(400, 400));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(quizPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
QuizPanel Class
public class QuizPanel extends JPanel implements Switchable{
private MainMenuPane mainMenuPane;
private RegisterPane registerPane;
private LoginPane loginPane;
public QuizPanel() {
setLayout(new BorderLayout());
registerPane = new RegisterPane();
RegisterController registerController = new RegisterController(registerPane, this);
mainMenuPane = new MainMenuPane();
MainMenuController mainMenuController = new MainMenuController(mainMenuPane, this);
loginPane = new LoginPane();
LoginController loginController = new LoginController(loginPane, this);
switchView(ViewState.MAINMENU_STATE);
}
#Override
public void switchView(ViewState state) {
System.out.println("Changing state: " + state);
switch (state) {
case REGISTER_STATE:
removeAll();
setLayout(new BorderLayout());
add(registerPane, BorderLayout.CENTER);
repaint();
revalidate();
break;
case MAINMENU_STATE:
removeAll();
setLayout(new BorderLayout());
add(mainMenuPane, BorderLayout.CENTER);
repaint();
revalidate();
break;
case LOGIN_STATE:
removeAll();
setLayout(new BorderLayout());
add(loginPane, BorderLayout.CENTER);
repaint();
revalidate();
break;
default:
System.out.println("UNREGISTERED STATE!");
break;
}
}
}
Login Controller
public class LoginController implements ILoginController, ILoginViewObserver {
private ILoginView view;
private LoginModel loginModel;
private Switchable parentView;
public LoginController(ILoginView view, Switchable parentView) {
this.view = view;
this.loginModel = new LoginModel();
this.parentView = parentView;
view.addLoginViewObserver(this);
}
#Override
public ILoginView getLoginView() {
return view;
}
#Override
public void submitButtonWasPressed(Token token) {
Token verifiedToken = loginModel.verifyToken(token);
if (verifiedToken != null) {
System.out.println("Token (" + token.token + ") successfully verified");
// How can I pass the token to the new JPanel the parent view will be displaying?
} else {
System.out.println("Token is invalid");
}
}
#Override
public void cancelButtonWasPressed() {
parentView.switchView(ViewState.MAINMENU_STATE);
}
}
LoginModel Class
public class LoginModel {
private List<Token> tokens;
public LoginModel() {
TokenDao tokenAccessObject = new TokenAccessObject();
tokens = tokenAccessObject.getAllTokens();
}
public Token verifyToken(Token providedToken) {
for (Token token : tokens) {
if (token.studentID == providedToken.studentID){
if (token.token.compareTo(providedToken.token) == 0) {
return token;
}
}
}
return null;
}
}
I think that in this case you can use Singleton pattern. This pattern should be used as rarely as possible, but in your case (common information which must be accessed from different classes of application) you can use it (IMHO).
But in your case you can also use one Swing feature.
Any Swing window has a root pane. And each JComponent that is laid
out in the window has access to this pane.
JComponent has also possibility to store some user data in a map,
called "client properties". Because JRootPane extends JComponent
you can store/retrieve your token is this map.
Here is a simple code:
public class TokenUtils {
private static final String TOKEN_PROPERTY = "token";
public static Token findToken(JComponent component) {
JRootPane root = component.getRootPane();
if (root != null) {
return Token.class.cast(root.getClientProperty(TOKEN_PROPERTY));
}
return null;
}
public static void putToken(JComponent component, Token token) {
JRootPane root = component.getRootPane();
if (root != null) {
root.putClientProperty(TOKEN_PROPERTY, token);
}
}
}
Important: if you use more than one window, you must put the token into the each of them.
A methode to pass the value of the token to the parent JPanel is to add a methode in your interface like setToken(int token) and a global variable in your Quiz panel
QuizPanel:
private int token;
#Override
public void setToken(int token){
this.token = token;
}
Swiched Interface:
public void setToken(int token);
Login:
parentView.setToken(token);
Than you say parentView.setToken(token) in your LoginController. Now the token variable in the QuizPanel will be set.
You could save the token to a file then on the jpanel read that file to get the token
Related
I got a problem with my JPanel. I built a html/css scraper with an interface. That interface has an JTextArea that updates with the steps the scraper completes like "Found HTML" and "Saved Files Succesfully". I want to add these messages to the JTextArea while the code is running. A simple check shows the updates are working with the observerpattern, but the all the messages don't show up until all code is finished.
Example Code from observerable class(triggered like 100 times):
private void addItem(String line, char type, String classOrId) {
String[] lineSplit = line.split(classOrId+"="+type);
lineSplit = lineSplit[1].split(""+type);
lineSplit = lineSplit[0].split(" ");
for (String a : lineSplit) {
if(classOrId == "id"){
if (!usedIds.contains(a)) {
usedIds.add(a);
}
}
else if(classOrId == "class"){
if (!usedClasses.contains(a)) {
usedClasses.add(a);
}
}
consoleText = consoleText + "Class \"" + a + "\" is found.";
setChanged();
notifyObservers();
}
}
Example Code from observer class:
public class ScraperView extends JPanel implements Observer {
Scraper scraper;
public ScraperView(Scraper scraper){
this.scraper = scraper;
scraper.addObserver(this);
this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
refresh();
}
private void refresh() {
System.out.println("TrIGGER");
removeAll();
int removedClasses = scraper.getRemovedClasses();
int totalClasses = scraper.getTotalClasses();
JLabel classesText = new JLabel(" Total Classes: "+ Integer.toString(totalClasses));
JLabel removedClassesText = new JLabel(" Removed Classes: "+ Integer.toString(removedClasses));
this.add(classesText);
this.add(removedClassesText);
this.revalidate();
this.repaint();
}
#Override
public void update(Observable o, Object arg) {
refresh();
}
}
Is there a way to wait until the jPanel is updated? I notice that the code gets triggered every time, but doesn't update..
You should look at the SwingWorker class, it is designed to perform a task in thread while updating the UI at the same time.
You're creating and adding new JLabels every time you update which is a bad idea, you should create the labels once and then update them, you should also ensure that you update them on the UI thread which you can do with SwingUtilities.invokeLater(Runnable)
public class ScraperView extends JPanel implements Observer {
Scraper scraper;
JLabel classesText = new JLabel();
JLabel removedClassesText = new JLabel();
public ScraperView(Scraper scraper){
this.scraper = scraper;
scraper.addObserver(this);
this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
this.add(classesText);
this.add(removedClassesText);
refresh();
}
private void refresh() {
System.out.println("TrIGGER");
int removedClasses = scraper.getRemovedClasses();
int totalClasses = scraper.getTotalClasses();
classesText.setText(" Total Classes: "+ Integer.toString(totalClasses));
removedClassesText.setText(" Removed Classes: "+ Integer.toString(removedClasses));
}
#Override
public void update(Observable o, Object arg) {
SwingUtilities.invokeLater(this::refresh);
}
}
I know that calling overridable methods from constructors is a bad idea. But I also see that it's being done everywhere with Swing, where code like add(new JLabel("Something")); occurs in constructors all the time.
Take NetBeans IDE, for example. It is very picky about overridable calls in constructors. And yet, when it generates Swing code, it puts all those add() method calls into an initializeComponents() method... which is then called from the constructor! A nice way to hide a problem and disable the warning (NetBeans doesn't have a “a private method that calls overridable methods is called from a constructor” warning). But not really a way to solve the problem.
What's going on here? I've been doing it for ages, but always had an uneasy feeling about this. Is there a better way of initializing Swing containers, except for making an additional init() method (and not forgetting to call it every time, which is kind of boring)?
Example
Here is an extremely contrived example of how things can go wrong:
public class MyBasePanel extends JPanel {
public MyBasePanel() {
initializeComponents();
}
private void initializeComponents() {
// layout setup omitted
// overridable call
add(new JLabel("My label"), BorderLayout.CENTER);
}
}
public class MyDerivedPanel extends MyBasePanel {
private final List<JLabel> addedLabels = new ArrayList<>();
#Override
public void add(Component comp, Object constraints) {
super.add(comp);
if (comp instanceof JLabel) {
JLabel label = (JLabel) comp;
addedLabels.add(label); // NPE here
}
}
}
To avoid wiring Swing components together in the constructor, you could simply give the responsibility of the wiring to another object. For instance, you could give wiring duties to a Factory:
public class MyPanelFactory {
public MyBasePanel myBasePanel() {
MyBasePanel myBasePanel = new MyBasePanel();
initMyBasePanel(myBasePanel);
return myBasePanel;
}
public MyDerivedPanel myDerivedPanel() {
MyDerivedPanel myDerivedPanel = new MyDerivedPanel();
initMyBasePanel(myDerivedPanel);
return myDerivedPanel;
}
private void initMyBasePanel(MyBasePanel myBasePanel) {
myBasePanel.add(new JLabel("My label"), BorderLayout.CENTER);
}
}
Or you could go all out and instantiate all your Swing components with a dependency injection container and have the container trigger the wiring. Here's an example with Dagger:
#Module
public class MyPanelModule {
static class MyBasePanel extends JPanel {
private final JLabel myLabel;
MyBasePanel(JLabel myLabel) {
this.myLabel = myLabel;
}
void initComponents() {
this.add(myLabel, BorderLayout.CENTER);
}
}
static class MyDerivedPanel extends MyBasePanel {
private final List<JLabel> addedLabels = new ArrayList<>();
MyDerivedPanel(JLabel myLabel) {
super(myLabel);
}
#Override
public void add(Component comp, Object constraints) {
super.add(comp);
if (comp instanceof JLabel) {
JLabel label = (JLabel) comp;
addedLabels.add(label);
}
}
}
#Provides MyBasePanel myBasePanel(#Named("myLabel") JLabel myLabel) {
MyBasePanel myBasePanel = new MyBasePanel(myLabel);
myBasePanel.initComponents();
return myBasePanel;
}
#Provides MyDerivedPanel myDerivedPanel(#Named("myLabel") JLabel myLabel) {
MyDerivedPanel myDerivedPanel = new MyDerivedPanel(myLabel);
myDerivedPanel.initComponents();
return myDerivedPanel;
}
#Provides #Named("myLabel") JLabel myLabel() {
return new JLabel("My label");
}
}
One of OOP principles is: Prefer composition over inheritance. When I create a Swing GUI I never extend Swing components except I create a new general purpose Swing component (like a JTreeTable, JGraph, JCalendar etc.).
So my code looks like:
public class MyPanel {
private JPanel mainPanel;
public MyPanel() {
init();
}
private void init() {
mainPanel = new JPanel();
}
public Component getComponent() {
return mainPanel;
}
}
public class MyComposedPanel {
private JPanel mainPanel;
public MyComposedPanel() {
init();
}
private void init() {
mainPanel = new JPanel();
mainPanel.add(new MyPanel().getComponent());
}
public Component getComponent() {
return mainPanel;
}
}
This way has one disadvantage: there is no GUI builder which supports it ;)
Coming back after a while and reading the accepted answer, I realized that there is an even simpler way of solving this issue. If the responsibility of calling overridable methods can be moved off to another class, it can also be moved off to a static method, using the factory method pattern:
class MyBasePanel extends JPanel {
public static MyBasePanel create() {
MyBasePanel panel = new MyBasePanel();
panel.initializeComponents();
return panel;
}
protected MyBasePanel() {
}
protected void initializeComponents() {
// layout setup omitted
// overridable call
add(new JLabel("My label"), BorderLayout.CENTER);
}
}
class MyDerivedPanel extends MyBasePanel {
private final List<JLabel> addedLabels = new ArrayList<>();
public static MyDerivedPanel create() {
MyDerivedPanel panel = new MyDerivedPanel();
panel.initializeComponents();
return panel;
}
protected MyDerivedPanel() {
}
#Override
public void add(Component comp, Object constraints) {
super.add(comp);
if (comp instanceof JLabel) {
JLabel label = (JLabel) comp;
addedLabels.add(label); // no more NPE here
}
}
}
Of course, one still has to remember to call initializeComponents when subclassing, but at least not every time an instance is created! Properly documented, this approach can be both simple and reliable.
Netbeans is generating the function private.
private initializeComponents() {...}
Thus the method is not overridable. Only protected and public methods are overridable.
An extra function keeps your code much cleaner for the Netbeans expample.
But in general you can savely use private methods to initialize classes.
Moreover if you have multiple constructors it's practical to use one extra method for initialization.
class Foo {
int x,y;
String bar;
public Foo(x) {
this.x = x;
init();
}
public Foo(y) {
this.y = y;
init();
}
private void init() {
// .. something complicated or much to do
bar = "bla";
}
}
I have a Main class that stores a TabbedComponent(extending JTabbedPane) as a variable. Another class (ToolbarComponent(extending JMenuBar) is also stored as a variable within my main class.
Upon a user event on the Toolbar, it calls the parent class (main), to get the TabbedComponent object and call a method to create a new tab. Which all works fine.
My issue is that when I attempt to click on a ta with my mouse, nothing changes. I'm pretty sure that I don't need a listener on MouseAdapter for something that simple, but will add it if I need it I guess.
Below is are stripped down versions of classes relevant to this issue
public class ExampleClass extends JFrame {
private TabbedBrowserPaneComponent cTabbedBrowserPane;
public ExampleClass() {
super("");
// Set up Components
this.cTabbedBrowserPane = new TabbedBrowserPaneComponent(this);
// Set up behaviour
setSize(500, 300);
setVisible(true);
}
/**
* #return the cTabbedBrowserPane
*/
public TabbedBrowserPaneComponent getTabbedBrowserPane() {
return cTabbedBrowserPane;
}
/**
* #param cTabbedBrowserPane the cTabbedBrowserPane to set
*/
public void setTabbedBrowserPane(TabbedBrowserPaneComponent cTabbedBrowserPane) {
this.cTabbedBrowserPane = cTabbedBrowserPane;
}
}
public class TabbedBrowserPaneComponent extends JTabbedPane {
// Parent class of the component
private JFrame parent = null;
public TabbedBrowserPaneComponent(JFrame parent) {
super();
setParent(parent);
// Add an initial pane
createNewTab();
parent.getContentPane().add(this);
}
public void createNewTab() {
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JScrollPane(), BorderLayout.CENTER);
this.addTab("Tab " + this.getTabCount(), panel);
}
/**
* #return the parent
*/
public JFrame getParent() {
return parent;
}
/**
* #param parent the parent to set
*/
public void setParent(JFrame parent) {
this.parent = parent;
}
}
To create a new tab, ToolBarComponent's listener calls like this
public class CreateNewTabAction extends AbstractAction {
// Parent
private JMenu parent;
public CreateNewTabAction(JMenu parent) {
super();
this.setParent(parent);
// Values for the tab
putValue(Action.NAME, "New Tab");
}
#Override
public void actionPerformed(ActionEvent e) {
ExampleClass.class.cast((parent.getParent().getParent())).getTabbedBrowserPane().createNewTab();
}
/**
* #return the parent
*/
public JMenu getParent() {
return parent;
}
/**
* #param parent the parent to set
*/
public void setParent(JMenu parent) {
this.parent = parent;
}
}
It this something really simply that I am missing?
Your code demonstrates a significant lack of design, sorry (I'm not trying to be mean, but I've spent the better part of 3 years undoing this kind of behavior so it gives me a nasty twitch).
Your problem is you are overriding getParent, which is method of Component used to determine where the component is actually added to. This is causing issues for the internal workings of the system.
There is no need to supply the parent frame to the tab component. If you REALLY need to get access back to the parent frame for some reason, consider using SwingUtilities.getWindowAncestor. If you are just planing to supply functionality for the tab in the frame, create a interface that can establish the contract between the tab and the controller/engine.
Don't get me started on the Action ...
An Example
I'm not sure what is you're actually trying to achieve, but there is absolutely no need to pass around a reference to the browser tab or the main frame. The elements of your program your passing them to just don't need to know that much information about their parents in order to achieve there work, also, you are significantly limiting the flexibility and re-usability of your components.
Below, I use a simple interface\controller that provides a contract between the various views and controls within the example. None of the various elements really need to know much more...
public class ExampleClass {
public static void main(String[] args) {
new ExampleClass();
}
public ExampleClass() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
BrowserPane browserPane = new BrowserPane();
CreateNewTabAction createNewTabAction = new CreateNewTabAction(browserPane);
JMenu mnu = new JMenu("Stuff");
mnu.add(createNewTabAction);
JMenuBar mb = new JMenuBar();
mb.add(mnu);
JToolBar tb = new JToolBar();
tb.add(createNewTabAction);
JFrame frame = new JFrame();
frame.setJMenuBar(mb);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(browserPane);
frame.add(tb, BorderLayout.NORTH);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public interface TabController {
public void createNewTab();
}
public class BrowserPane extends JPanel implements TabController {
private TabbedBrowserPaneComponent cTabbedBrowserPane;
public BrowserPane() {
setLayout(new BorderLayout());
// Set up Components
this.cTabbedBrowserPane = new TabbedBrowserPaneComponent();
add(cTabbedBrowserPane);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
public void createNewTab() {
cTabbedBrowserPane.createNewTab();
}
}
public class TabbedBrowserPaneComponent extends JTabbedPane {
public TabbedBrowserPaneComponent() {
super();
createNewTab();
}
public void createNewTab() {
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JScrollPane(), BorderLayout.CENTER);
this.addTab("Tab " + this.getTabCount(), panel);
}
}
public class CreateNewTabAction extends AbstractAction {
private TabController controller;
public CreateNewTabAction(TabController controller) {
super();
this.controller = controller;
putValue(Action.NAME, "New Tab");
}
#Override
public void actionPerformed(ActionEvent e) {
getController().createNewTab();
}
/**
* #return the parent
*/
public TabController getController() {
return controller;
}
}
}
I have 2 classes.
when I put bold 3 lines in the method addCourses() the dialog does not show combobox in the Panel
but when I remove from addCourses and put those bold lines in the constructor, JComboBox are shown in the Panel.
But data will not show because data items updates to ComboBox will happen after Constructor is created.
How can I solve this problem.
this.mainPanel.add(courseCombo, BorderLayout.NORTH);
this.mainPanel.add(sessionCombo, BorderLayout.CENTER);
this.mainPanel.add(courseButton, BorderLayout.SOUTH);
public class Updator {
CourseListFrame clf = new CourseListFrame();
for(...){
clf.addContentsToBox(displayName, className);
}
clf.addCourses();
}
and second class is
public class CourseListFrame extends JDialog implements ActionListener {
public JPanel mainPanel = new JPanel(new BorderLayout(2, 2));
public JButton courseButton = new JButton(("Submit"));
public JComboBox courseCombo;
public JComboBox sessionCombo;
public Multimap<String, String> map; // = HashMultimap.create();
public static CourseListFrame courseListDialog;
public CourseListFrame() {
super(this.getMainFrame());
this.getContentPane().add(mainPanel);
map = HashMultimap.create();
courseCombo = new JComboBox();
courseCombo.addItem("Select Courses");
courseCombo.addActionListener(this);
sessionCombo = new JComboBox();
}
public void addContentsToBox(String course, String session) {
map.put(course, session);
courseCombo.addItem(course);
}
public void actionPerformed(ActionEvent e) {
JComboBox cb = (JComboBox) e.getSource();
String str = (String) cb.getSelectedItem();
setSessionCombo(str);
}
public void setSessionCombo(String course) {
if (map.containsKey(course)) {
sessionCombo.removeAllItems();
Iterator it = map.get(course).iterator();
while (it.hasNext()) {
sessionCombo.addItem(it.next());
}
}
}
public void addCourses() {
this.mainPanel.add(courseCombo, BorderLayout.NORTH);
this.mainPanel.add(sessionCombo, BorderLayout.CENTER);
this.mainPanel.add(courseButton, BorderLayout.SOUTH);
}
public static void showCourseListDialog() {
if (courseListDialog == null) {
courseListDialog = new CourseListFrame();
}
courseListDialog.pack();
courseListDialog.setVisible(true);
courseListDialog.setSize(260, 180);
}
}
The reason why they arent showing is because you are probably calling the static showCourseListDialog() to show your dialog. This method will test whether your static courseListDialog is null, and if so, create one and set that dialog visible, not the clf that you instantiated.
If in your showCourseListDialog() you call the addCourses() method after instantiating your 'singleton', you should be OK:
public static void showCourseListDialog() {
if (courseListDialog == null) {
courseListDialog = new CourseListFrame();
courseListDialog.addCourses();// <<---- this is key!
}
courseListDialog.pack();
courseListDialog.setVisible(true);
courseListDialog.setSize(260, 180);
}
That said, by having the static courseListDialog, it is apparent that you want that dialog to be a singleton. If that is the case, I would at least make your constructor private. You want to proactively avoid the situation that you are getting into where you can construct multiple instances of a singleton. You still would have a race condition to deal with in your showCourseListDialog, but as you will only be calling this method in the EDT, you should be safe.
Take a look at this and other topics on Singleton development in Java (and dont forget to read the con arguments where it is described as an anti-pattern)
How to set the default icon for all Java Swing windows?
Otherwise i have to set icons for every frame i created.
What's your suggestions?
Simple hackings are also accepted.
many thx
Update: Best if the method you suggested can leave existing frame-creation codes untouched. thx
Create an Abstact class that extends JFrame
In the constructor set your icon.
create child class that extends your new Abstract Class and call super in your constructor
public abstract class MainFrame extends JFrame {
protected MainFrame() {
this.setIconImage(null); // Put your own image instead of null
}
}
public class ChildFrame extends MainFrame {
public ChildFrame() {
super();
}
}
You can also just create object from your new class
public class MainFrame extends JFrame {
public MainFrame() {
this.setIconImage(null); // Put your own image instead of null
}
}
public class Frame {
private MainFrame mainframe = new MainFrame();
public Frame() {
super();
}
}
To make windows icons changes globally without changing old code I am using this code snippet
public static void fixWindowsIcons(final List<Image> iconImages) {
PropertyChangeListener l = new PropertyChangeListener() {
private Window prevActiveWindow;
#Override
public void propertyChange(PropertyChangeEvent evt) {
final Window o = KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getActiveWindow();
if (o != null && prevActiveWindow != o) {
prevActiveWindow = o;
List<Image> windowIcons = o.getIconImages();
if (windowIcons == null || windowIcons.size() == 0) {
o.setIconImages(iconImages);
}
}
}
};
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addPropertyChangeListener("activeWindow", l); //$NON-NLS-1$
}