How to update Java Jframe controls from no-gui class on real time

こ雲淡風輕ζ 提交于 2019-12-02 10:17:27
c0der

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);
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!