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);
}
}
Related
I want to know how to add a GUI to my program. I have started creating a java program in Blue J and the first class of the program is a class which has been extended by other classes.
Now I have to make a GUI too but from my understanding I can only implement an interface as the GUI extends the class Frame. The problem is I want to create a GUI of my class, it has instance variables too so is there a work around? Could I make my first class an interface without altering the extensions too much?
code:
public class Players /* Class name */
{
private int attack; /* Instance variables* */
private int defence;
private int jump;
public Players(int a, int d, int j) /* Constructor being defined */
{
int total = a + d + j;
if((total) == 100)
{
attack = a;
defence = d;
jump = j;
}
else
{
System.out.println("Make stats add to 100");
}
}
public Players()/* Default contructor if not user defined */
{
attack = 34;
defence = 33;
jump = 33;
}
public void addAttack(int a)
{
attack += a;
}
public void addDefence(int a)
{
defence += a;
}
public void addJump(int a)
{
jump += a;
}
public void getBasicStats()
{
System.out.println(attack + " " + defence + " " + jump);
}
}
This is my first class and my superclass for most of the other classes
I suggest learning how to use Swing. You will have several different classes interacting together. In fact, it is considered good practice to keep separate the code which creates and manages the GUI from the code which performs the underlying logic and data manipulation.
Another suggestion:
Learn JavaFX and download SceneBuilder from Oracle: here
At my university they have stopped teaching Swing and started to teach JavaFX, saying JavaFX has taken over the throne from Swing.
SceneBuilder is very easy to use, drag and drop concept. It creates a FXML file which is used to declare your programs GUI.
How will I declare aan instance variable inside the GUI class?
Like as shown bellow, you could start with something like this, note that your application should be able to hand out your data to other classes, for instance I changed getBasicStats() to return a String, this way you can use your application class anywhere you want, I guess this is why you were confused about where to place the GUI code...
public class PlayersGUI extends JFrame {
private static final long serialVersionUID = 1L;
private Players players; // instance variable of your application
private PlayersGUI() {
players = new Players();
initGUI();
}
private void initGUI() {
setTitle("This the GUI for Players application");
setPreferredSize(new Dimension(640, 560));
setLocation(new Point(360, 240));
JPanel jPanel = new JPanel();
JLabel stat = new JLabel(players.getBasicStats());
JButton attack = new JButton("Attack!");
attack.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
players.addAttack(1);
}
});
JButton hugeAttack = new JButton("HUGE Attack!");
hugeAttack.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
players.addAttack(10);
}
});
JButton defend = new JButton("Defend");
defend.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
players.addDefence(1);
}
});
JButton showStats = new JButton("Show stats");
showStats.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
stat.setText(players.getBasicStats());
}
});
jPanel.add(stat);
jPanel.add(attack);
jPanel.add(hugeAttack);
jPanel.add(defend);
jPanel.add(showStats);
add(jPanel);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
}
public static void main(String... args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
PlayersGUI pgui = new PlayersGUI();
pgui.pack();
pgui.setVisible(true);
}
});
}
}
I would recommend using netbeans to start with. From there you can easily select pre created classes such as Jframes. Much easier to learn. You can create a GUI from there by dragging and dropping buttons and whatever you need.
Here is a youtube tut to create GUI's in netbeans.
https://www.youtube.com/watch?v=LFr06ZKIpSM
If you decide not to go with netbeans, you are gonna have to create swing containers withing your class to make the Interface.
I want to print multiple label according to the number(no string allowed) you wrote in a text field first. I want it to be dynamical. I want it to change every time you type something in the text field.
So far it can read if it's a number or a string and throw exception if the text doesn't match the requirement.
I've try multiple thing to print multiple Jlabel on the screen, but it didn't work so far.
Here's the code: can you help me?
The main window class
public class MainWindow extends JFrame {
private MainPanel mp = new MainPanel();
public MainWindow()
{
this.setVisible(true);
this.setTitle("Calculateur sur 100");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(200, 400);
this.setLocationRelativeTo(null);
this.setContentPane(mp);
}}
The mainPanel class
public class MainPanel extends JPanel implements ActionListener, MouseListener, KeyListener {
private JTextField tI = new JTextField("Pourcentage");
private JOptionPane jop3 = new JOptionPane();
public MainPanel()
{
this.add(tI);
tI.addKeyListener(this);
tI.addMouseListener(this);
}
//Mathematic calculation
private double onHundred(int tot, int now)
{
return (tot / 100) * now;
}
public void keyReleased(KeyEvent e)
{
boolean ok = true;
try
{
int numbs = Integer.parseInt(tI.getText());
}
catch(Exception s)
{
tI.setText("");
jop3.showMessageDialog(null, "Veuillez entrer seulement des chiffres", "Erreur", JOptionPane.ERROR_MESSAGE);
ok = false;
}
if(ok)
{
System.out.print("Supposed to print");
JLabel[] label = new JLabel[Integer.parseInt(tI.getText())];
for(int i = Integer.parseInt(tI.getText()); i <= 0; i--)
{
label[i] = new JLabel(i + " = " + Math.ceil(onHundred(Integer.parseInt(tI.getText()), i)));
label[i].setVisible(true);
this.add(label[i]);
}
}
}
You MainWindow class should look something like this:
public class MainWindow extends JFrame {
private MainPanel mp = new MainPanel();
public static void main(String[] args) {
new MainWindow();
}
public MainWindow() {
setContentPane(mp);
setTitle("Calculateur sur 100");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
}
Note the order: setContentPane then pack then setVisible. pack replaces setSize as it determines the preferred size of the window based on its components.
I modified your MainPanel class:
public class MainPanel extends JPanel {
private JTextField tI = new JTextField("Pourcentage");
JPanel labelPanel = new JPanel();
public MainPanel() {
setLayout(new BorderLayout());
tI.getDocument().addDocumentListener(new MyDocumentListener());
add(tI, BorderLayout.PAGE_START);
add(labelPanel);
}
private int check() {
int numL;
try {
numL = Integer.parseInt(tI.getText());
} catch (NumberFormatException exc) {
return 0;
}
return numL > 100? 100 : numL;
}
private void update(int numL) {
labelPanel.removeAll();
for (int i = 0; i < numL; i++)
labelPanel.add(new JLabel(String.valueOf(i+1)));
JFrame mainWindow = ((JFrame) SwingUtilities.getWindowAncestor(this));
mainWindow.pack();
mainWindow.repaint();
}
class MyDocumentListener implements DocumentListener {
#Override
public void insertUpdate(DocumentEvent e) {
update(check());
}
#Override
public void removeUpdate(DocumentEvent e) {
update(check());
}
#Override
public void changedUpdate(DocumentEvent e) {
}
}
}
Explanation:
The main panel has the text field separately from another panel which updates dynamically to contain the labels.
The text field uses a DocumentListener instead of a KeyListener to listen to changes in its contents. This is the correct approach for many reasons I will not get into here unless really necessary.
Whenever the text changes, a check method verifies that the input is a number. If it's not it returns 0. If it's more than 100 it returns 100. You can change this behavior as you need.
The value from check is passed to update which clears all the previous labels and reconstructs them. (You can do a bit of optimization here if you want by keeping labels in memory but not displaying them. If the cap is 100 as in my example this won't be noticeable.). The main frame then recalculates the space it needs for all the labels and then repaints.
The labels appear next to each other because the default layout for JPanel is FlowLayout. You can change this as needed.
First - you have Integer.parseInt(tI.getText()) a number of times within the same keyReleased function. When you have done the first check to assign it to int numbs, then use numbs from then on, instead of referring back to tI.getText(). Theoretically the user input can change while you are processing your array, which will cause runtime exceptions or undesired results. Hint - declare numbs directly under ok.
Second - after you add controls programmatically, you need to invalidate the control on to which you are adding them, ie your MainPanel. The invalidate directive tells the control that it is not drawn correctly and needs to be repainted (do this at the completion of your loop). Look through the documentation for JPanel for invalidate and paint.
I've hit the infinite loop problem in Swing. Done some research and come across SwingWorker threads but not really sure how to implement them. I've knocked together a simple program that shows the problem. One button starts the infinite loop and I want the other button to stop it but of course due to the Swing single thread problem the other button has frozen. Code below and help appreciated:-
public class Model
{
private int counter;
private boolean go = true;
public void go()
{
counter = 0;
while(go)
{
counter++;
System.out.println(counter);
}
}
public int getCounter()
{
return counter;
}
public void setGo(boolean value)
{
this.go = value;
}
}
public class View extends JFrame
{
private JPanel topPanel, bottomPanel;
private JTextArea messageArea;
private JButton startButton, cancelButton;
private JLabel messageLabel;
private JScrollPane scrollPane;
public View()
{
setSize(250, 220);
setTitle("View");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
topPanel = new JPanel();
bottomPanel = new JPanel();
messageArea = new JTextArea(8, 20);
messageArea.setEditable(false);
scrollPane = new JScrollPane(messageArea);
messageLabel = new JLabel("Message Area");
topPanel.setLayout(new BorderLayout());
topPanel.add(messageLabel, "North");
topPanel.add(scrollPane, "South");
startButton = new JButton("START");
cancelButton = new JButton("CANCEL");
bottomPanel.setLayout(new GridLayout(1, 2));
bottomPanel.add(startButton);
bottomPanel.add(cancelButton);
Container cp = getContentPane();
cp.add(topPanel, BorderLayout.NORTH);
cp.add(bottomPanel, BorderLayout.SOUTH);
}
public JButton getStartButton()
{
return startButton;
}
public JButton getCancelButton()
{
return cancelButton;
}
public void setMessageArea(String message)
{
messageArea.append(message + "\n");
}
}
public class Controller implements ActionListener
{
private Model theModel;
private View theView;
public Controller(Model model, View view)
{
this.theModel = model;
this.theView = view;
view.getStartButton().addActionListener(this);
view.getCancelButton().addActionListener(this);
}
public void actionPerformed(ActionEvent ae)
{
Object buttonClicked = ae.getSource();
if(buttonClicked.equals(theView.getStartButton()))
{
theModel.go();
}
else if(buttonClicked.equals(theView.getCancelButton()))
{
theModel.setGo(false);
}
}
}
public class Main
{
public static void main(String[] args)
{
Model model = new Model();
View view = new View();
Controller controller = new Controller(model, view);
view.setVisible(true);
}
}
You can do it easily without implementing any timer, you just need to add two lines to your actionPerformed method:
public void actionPerformed(ActionEvent ae)
{
Object buttonClicked = ae.getSource();
if(buttonClicked.equals(theView.getStartButton()))
{
theModel.setGo(true); //make it continue if it's just stopped
Thread t = new Thread(new Runnable() { public void run() {theModel.go();}}); //This separate thread will start the new go...
t.start(); //...when you start the thread! go!
}
else if(buttonClicked.equals(theView.getCancelButton()))
{
theModel.setGo(false);
}
}
As your Model.go() is running in a separate thread, the Event Dispatch Thread is free to do its stuff, like drawing the button released again, instead of hanging with the button down.
There's a catch! however, because the thread running Model.go() will run wildly!, it's virtually called as many times per second as your system can.
If you plan to implement some animation or the like, then you will need to:
use a Timer,
or
add some sleep time to the new thread.
Example if you go with threads:
public void go()
{
counter = 0;
while(go)
{
counter++;
System.out.println(counter);
try {
Thread.sleep(1500); //Sleep for 1.5 seconds
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
As you can see I added Thread.sleep(1500) being 1500 the time in milliseconds (1.5 seconds). Thread.sleep can be interrupted for some reasons, so you must catch the InterruptedException.
It's not necessary to go deeper on handling correctly the InterruptedException in this particular case, but if you feel curious about it you can read this nice article.
You are blocking the Event Dispatch Thread (EDT). The thread is responsible to process painting and other UI related requests. Once EDT is blocked the UI will become frozen since it cannot process any events. For more details see The Event Dispatch Thread tutorial.
Consider using timers (How to Use Swing Timers), SwingWorker or an auxiliary background thread. Background thread can communicate with EDT using SwingUtilities.invokeLater(). This mechanism is already implemented in SwingWorker, so it may be easier to go with it. It depends on functionality that is required.
Use a javax.swing.Timer to do the go() work once, (with some optional delay), using start() and stop() in the event handling.
I decided to use a SwingWorker thread and below is the updated Controller class. It does what I need it to do but my question is, is it the correct way and is it clean code? Also, I've tried getting the output of the model.go() method into the view's textarea as per the commented out lines but not been succesful, anyone know how?
public class Controller implements ActionListener
{
private Model theModel;
private View theView;
private SwingWorker<Void, Void> worker;
public Controller(Model model, View view)
{
this.theModel = model;
this.theView = view;
view.getStartButton().addActionListener(this);
view.getCancelButton().addActionListener(this);
}
public void actionPerformed(ActionEvent ae)
{
Object buttonClicked = ae.getSource();
if(buttonClicked.equals(theView.getStartButton()))
{
theModel.setGo(true);
worker = new SwingWorker<Void, Void>()
{
#Override
protected Void doInBackground()
{
// theView.setMessageArea(theModel.getCounterToString());
return theModel.go();
}
#Override
protected void done()
{
// theView.setMessageArea(theModel.getCounterToString());
}
};
worker.execute();
}
else if(buttonClicked.equals(theView.getCancelButton()))
{
theModel.setGo(false);
}
}
}
public class Model
{
public Void go()
{
counter = 0;
while(go)
{
counter++;
System.out.println(counter);
}
return null;
}
I have a JDialog dlg, created by a JFrame frm, that contains a JList list.
When I modify the list (through the ListModel), the list itself is repainted but not the JDialog.
This means that, if I delete a line, the list remains with an empty line while if I add a line, this new line won't be shown (because there is no space in the dialog) until I manually force repainting of dlg (doubleclicking in frm).
Following advices in this post :
How to make repaint for JDialog in Swing?
and in this post:
Force repaint after button click
I tried to call, from my controller class (which is where updates to list are made), the following line:
SwingUtilities.getWindowAncestor(dlg).repaint();
but it didn't work.
I also tried:
dlg.repaint();
No luck either...
Any clue?
Thank you very much.
EDIT:
The organization of my classes is as follows:
a controller class that contains a reference to the main JFrame, frm.
I also extended JDialog into MyDialog, which contains a JList.
When a doubleclick on frm is detected, I show the instance of MyDialog (or create, if it is the first time I show it) and the JList is filled with the data passed to the DefaultListModel. MyDialog is painted so that the list has only the space that it needs.
Now, when a specific event is detected by the controller, I get the specific MyDialog, get the ListModel from JList and update it. Here the JList is indeed updated, but Dialog remains the same.
I use a code like this:
MyDialog dlg = group.getDlg();
if(dlg != null){
DefaultListModel listModel = ((DefaultListModel) dlg.getMyJList().getModel());
listModel.addElement(idStock);
SwingUtilities.getWindowAncestor(dlg).repaint();
}
This doesn't repaint dlg.
I also tried:
SwingUtilities.getWindowAncestor(dlg.getMyJList()).repaint();
but it doesn't work.
I checked with the debugger that the lines are actually executed.
I don't have much more code to show, really.....
I think that you going wrong way, define DefaultListModel that accesible throught all Java methods and Classes, this Model would holds your Objects, then put JList to the JDialog or JOptionPane, for example
import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;
// based on #trashgod code
/** #see http://stackoverflow.com/questions/5759131 */
// http://stackoverflow.com/questions/8667719/jdialog-repaint-after-jlist-modification
public class ListDialog {
private static final int N = 12;
private JDialog dlg = new JDialog();
private DefaultListModel model = new DefaultListModel();
private JList list = new JList(model);
private JScrollPane sp = new JScrollPane(list);
private int count;
public ListDialog() {
list.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
JPanel panel = new JPanel();
panel.add(new JButton(new AbstractAction("Add") {
private static final long serialVersionUID = 1L;
#Override
public void actionPerformed(ActionEvent e) {
append();
if (count <= N) {
list.setVisibleRowCount(count);
dlg.pack();
}
}
}));
panel.add(new JButton(new AbstractAction("Remove") {
private static final long serialVersionUID = 1L;
#Override
public void actionPerformed(ActionEvent e) {
int itemNo = list.getSelectedIndex();
if (itemNo > -1) {
removeActionPerformed(e, itemNo);
}
}
}));
for (int i = 0; i < N - 2; i++) {
this.append();
}
list.setVisibleRowCount(N - 2);
dlg.add(sp, BorderLayout.CENTER);
dlg.add(panel, BorderLayout.SOUTH);
dlg.pack();
dlg.setLocationRelativeTo(null);
dlg.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dlg.setVisible(true);
}
private void removeActionPerformed(ActionEvent e, int itemNo) {
System.out.println("made_list's model: " + list.getModel());
System.out.println("Model from a fresh JList: " + new JList().getModel());
model = (DefaultListModel) list.getModel();
if (model.size() > 0) {
if (itemNo > -1) {
model.remove(itemNo);
}
}
}
private void append() {
model.addElement("String " + String.valueOf(++count));
list.ensureIndexIsVisible(count - 1);
}
public static void main(String[] a_args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
ListDialog pd = new ListDialog();
}
});
}
}
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)