I have a tedious problem with my Java JFrame project.
What I want to do (and looking for how to) is add elements to my ListBox from a no-GUI class in REAL TIME, or in other words "asynchronous", with out freezing my app. Is this clear? I tried SwingWorker and Threads but without results. All I can do is update the listbox after all process finish (obviously with my app froze because my process is long).
This is my architecture:
And here is my code (not functional, just for understanding)
EDITTED
View (Generated with NetBeans)
package view;
import com.everis.ingesta.controller.MyController;
import java.awt.event.ActionListener;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
public class MyView extends javax.swing.JFrame {
public MyView(DefaultListModel<String> model) {
setVisible(true);
}
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
btnRun = new javax.swing.JButton();
jscrlLog = new javax.swing.JScrollPane();
jlstLog = new javax.swing.JList();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
btnRun.setText("Run");
jscrlLog.setViewportView(jlstLog);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGap(159, 159, 159)
.addComponent(btnRun)
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jscrlLog, javax.swing.GroupLayout.DEFAULT_SIZE, 376, Short.MAX_VALUE)
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(btnRun)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jscrlLog, javax.swing.GroupLayout.DEFAULT_SIZE, 242, Short.MAX_VALUE)
.addContainerGap())
);
pack();
}// </editor-fold>
public void addButtonListener(ActionListener listener) {
btnRun.addActionListener(listener);
}
public static void main(String args[]) {
/* Set the Nimbus look and feel */
//<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
/* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
* For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
*/
try {
for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (ClassNotFoundException ex) {
java.util.logging.Logger.getLogger(MyView.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
java.util.logging.Logger.getLogger(MyView.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
java.util.logging.Logger.getLogger(MyView.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(MyView.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
//</editor-fold>
//</editor-fold>
//</editor-fold>
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new MyController();
}
});
}
// Variables declaration - do not modify
private javax.swing.JButton btnRun;
private javax.swing.JList jlstLog;
private javax.swing.JScrollPane jscrlLog;
// End of variables declaration
}
Controller
package controller;
import business.MyBusiness;
import util.MyLog;
import view.MyView;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class MyController {
MyLog log;
MyBusiness business;
MyView view;
public MyController(){
log = new MyLog();
business = new MyBusiness(log.getLog());
view = new MyView(log.getLog());
}
public void runProcess() {
view.addButtonListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
business.runProcess();
}}
);
}
}
Business
package business;
import MyLog;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.SwingWorker;
public class MyBusiness {
private int counter = 0;
private DefaultListModel<String> model;
private MyLog log;
public MyBusiness(DefaultListModel<String> model) {
this.model = model;a
}
public void runProcess() {
SwingWorker<Void, String> worker = new SwingWorker<Void, String>() {
@Override
protected Void doInBackground() throws Exception {
for (int i = 0; i < 10; i++) {
publish("log message number " + counter++);
Thread.sleep(2000);
}
return null;
}
@Override
protected void process(List<String> chunks) {
// this is called on the Swing event thread
for (String text : chunks) {
model.addElement("");
}
}
};
worker.execute();
}
}
Log (model)
package util;
import javax.swing.DefaultListModel;
public class MyLog {
private DefaultListModel<String> model;
public MyLog() {
model = new DefaultListModel<String>();
}
public DefaultListModel<String> getLog(){
return model;
}
}
This is an over-simplified example of a long-process generating String values that update GUI using a SwingWorker.
It is over simplified in an attempt to make the basic use of a SwingWorker somewhat easier to follow.
This is a one-file mcve, meaning you can copy-paste the entire code into one file (MyView.java) and execute :
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.SwingWorker;
//gui only, unaware of logic
public class MyView extends JFrame {
private JList<String> loglist;
private JButton log;
public MyView(DefaultListModel<String> model) {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
log = new JButton("Start Process");
add(log, BorderLayout.PAGE_START);
loglist = new JList<>(model);
loglist.setPreferredSize(new Dimension(200,300));
add(loglist, BorderLayout.PAGE_END);
pack();
setVisible(true);
}
void addButtonListener(ActionListener listener) {
log.addActionListener(listener);
}
public static void main(String args[]) {
new MyController();
}
}
//represents the data (and some times logic) used by GUI
class Model {
private DefaultListModel<String> model;
Model() {
model = new DefaultListModel<>();
model.addElement("No logs yet");
}
DefaultListModel<String> getModel(){
return model;
}
}
//"wires" the GUI, model and business process
class MyController {
MyController(){
Model model = new Model();
MyBusiness business = new MyBusiness(model.getModel());
MyView view = new MyView(model.getModel());
view .addButtonListener(e -> business.start());
}
}
//represents long process that needs to update GUI
class MyBusiness extends SwingWorker<Void, String>{
private static final int NUMBER_OF_LOGS = 9;
private int counter = 0;
private DefaultListModel<String> model;
public MyBusiness(DefaultListModel<String> model) {
this.model= model;
}
@Override //simulate long process
protected Void doInBackground() throws Exception {
for(int i = 0; i < NUMBER_OF_LOGS; i++) {
//Successive calls to publish are coalesced into a java.util.List,
//which is by process.
publish("log message number " + counter++);
Thread.sleep(1000);
}
return null;
}
@Override
protected void process(List<String> logsList) {
//process the list received from publish
for(String element : logsList) {
model.addElement(element);
}
}
void start() {
model.clear(); //clear initial model content
super.execute();
}
}
First a few rules and suggestions:
- Your model or here your "business" code should have no knowledge of the GUI, and should not depend on the GUI structure
- Again all Swing code should be called on the event thread, and all long-running code within a background thread
- If you have long running code that changes state, and if this state needs to be reflected in the GUI, this means that you'll need a call-back mechanism of some sort, some way for the model to notify important parties that its state has changed. This can be done using PropertyChangeListeners, or by injecting some type of callback method into the model.
- Make the view dumb. It gets input from user and notifies the controller, and it is updated by the controller. Almost all program "brains" resides within the controller and the model. Exceptions -- some input verification is often done in the view.
- Don't ignore basic Java OOPs rules -- Hide information by keeping fields private, and allowing outside classes to update state only through public methods that you fully control.
- The MCVE structure is a good one to learn and use, even if not asking a question here. Learn to simplify your code and to isolate your problem in order to best solve it.
For example this code can be copied and pasted into a single file within the IDE of choice and then run:
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.swing.*;
public class Mcve1 {
private static void createAndShowGui() {
// create your model/view/controller and hook them together
MyBusiness1 model = new MyBusiness1();
MyView1 myView = new MyView1();
new MyController1(model, myView); // the "hooking" occurs here
// create and start the GUI
JFrame frame = new JFrame("MCVE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(myView);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
// start GUI on Swing thread
SwingUtilities.invokeLater(() -> createAndShowGui());
}
}
@SuppressWarnings("serial")
class MyView1 extends JPanel {
private MyController1 controller;
private DefaultListModel<String> logListModel = new DefaultListModel<>();
private JList<String> logList = new JList<>(logListModel);
public MyView1() {
logList.setFocusable(false);
logList.setPrototypeCellValue("abcdefghijklabcdefghijklabcdefghijklabcdefghijkl");
logList.setVisibleRowCount(15);
add(new JScrollPane(logList, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED));
// my view's buttons just notify the controller that they've been pushed
// that's it
add(new JButton(new AbstractAction("Do stuff") {
{
putValue(MNEMONIC_KEY, KeyEvent.VK_D);
}
@Override
public void actionPerformed(ActionEvent evt) {
if (controller != null) {
controller.doStuff(); // notification done here
}
}
}));
}
public void setController(MyController1 controller) {
this.controller = controller;
}
// public method to allow controller to update state
public void updateList(String newValue) {
logListModel.addElement(newValue);
}
}
class MyController1 {
private MyBusiness1 myBusiness;
private MyView1 myView;
// hook up concerns
public MyController1(MyBusiness1 myBusiness, MyView1 myView) {
this.myBusiness = myBusiness;
this.myView = myView;
myView.setController(this);
}
public void doStuff() {
// long running code called within the worker's doInBackground method
SwingWorker<Void, String> worker = new SwingWorker<Void, String>() {
@Override
protected Void doInBackground() throws Exception {
// pass a call-back method into the method
// so that this worker is notified of changes
myBusiness.longRunningCode(new Consumer<String>() {
// call back code
@Override
public void accept(String text) {
publish(text); // publish to the process method
}
});
return null;
}
@Override
protected void process(List<String> chunks) {
// this is called on the Swing event thread
for (String text : chunks) {
myView.updateList(text);
}
}
};
worker.execute();
}
}
class MyBusiness1 {
private Random random = new Random();
private String text;
public void longRunningCode(Consumer<String> consumer) throws InterruptedException {
consumer.accept("Starting");
// mimic long-running code
int sleepTime = 500 + random.nextInt(2 * 3000);
TimeUnit.MILLISECONDS.sleep(sleepTime);
consumer.accept("This is message for initial process");
// ...
// Doing another long process and then print log
// ...
sleepTime = 500 + random.nextInt(2 * 3000);
TimeUnit.MILLISECONDS.sleep(sleepTime);
consumer.accept("This is not complete. Review");
// ...
// Doing another long process and then print log
// ...
sleepTime = 500 + random.nextInt(2 * 3000);
TimeUnit.MILLISECONDS.sleep(sleepTime);
consumer.accept("Ok this works. Have fun");
}
public String getText() {
return text;
}
}
Another way to do the same thing is to use a Swing-compliant PropertyChangeSupport and PropertyChangeListeners. For example:
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.*;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;
public class Mcve2 {
private static void createAndShowGui() {
MyBusiness2 myBusiness = new MyBusiness2();
MyView2 myView = new MyView2();
new MyController2(myBusiness, myView);
JFrame frame = new JFrame("MCVE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(myView);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}
}
@SuppressWarnings("serial")
class MyView2 extends JPanel {
private MyController2 controller;
private DefaultListModel<String> logListModel = new DefaultListModel<>();
private JList<String> logList = new JList<>(logListModel);
public MyView2() {
logList.setFocusable(false);
logList.setPrototypeCellValue("abcdefghijklabcdefghijklabcdefghijklabcdefghijkl");
logList.setVisibleRowCount(15);
add(new JScrollPane(logList, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED));
add(new JButton(new AbstractAction("Do stuff") {
{
putValue(MNEMONIC_KEY, KeyEvent.VK_D);
}
@Override
public void actionPerformed(ActionEvent evt) {
if (controller != null) {
controller.doStuff();
}
}
}));
}
public void setController(MyController2 controller) {
this.controller = controller;
}
public void updateList(String newValue) {
logListModel.addElement(newValue);
}
}
class MyController2 {
private MyBusiness2 myBusiness;
private MyView2 myView;
public MyController2(MyBusiness2 myBusiness, MyView2 myView) {
this.myBusiness = myBusiness;
this.myView = myView;
myView.setController(this);
myBusiness.addPropertyChangeListener(MyBusiness2.TEXT, new TextListener());
}
public void doStuff() {
new Thread(() -> {
try {
myBusiness.longRunningCode();
} catch (InterruptedException e) {
e.printStackTrace();
}
}) .start();
}
private class TextListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
String newValue = (String) evt.getNewValue();
myView.updateList(newValue);
}
}
}
class MyBusiness2 {
public static final String TEXT = "text";
private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(this);
private Random random = new Random();
private String text;
public void longRunningCode() throws InterruptedException {
setText("Starting");
// mimic long-running code
int sleepTime = 500 + random.nextInt(2 * 3000);
TimeUnit.MILLISECONDS.sleep(sleepTime);
setText("This is message for initial process");
// ...
// Doing another long process and then print log
// ...
sleepTime = 500 + random.nextInt(2 * 3000);
TimeUnit.MILLISECONDS.sleep(sleepTime);
setText("This is not complete. Review");
// ...
// Doing another long process and then print log
// ...
sleepTime = 500 + random.nextInt(2 * 3000);
TimeUnit.MILLISECONDS.sleep(sleepTime);
setText("Ok this works. Have fun");
}
public void setText(String text) {
String oldValue = this.text;
String newValue = text;
this.text = text;
pcSupport.firePropertyChange(TEXT, oldValue, newValue);
}
public String getText() {
return text;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(listener);
}
public void addPropertyChangeListener(String name, PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(name, listener);
}
public void removePropertyChangeListener(String name, PropertyChangeListener listener) {
pcSupport.removePropertyChangeListener(name, listener);
}
}
来源:https://stackoverflow.com/questions/52762220/how-to-update-java-jframe-controls-from-no-gui-class-on-real-time
