问题
I need to create a GUI to show real-time data coming from the Serial port. I'm reading serial port data from a separate thread and I need to update GUI from there. My current implementation is like this.
class Gui extends JFrame {
private JLabel lbl = new JLabel();
....
void updateLabel(String text) {
lbl.setText(text);
}
}
class CommPortReceiver extends Thread {
private Gui gui = new Gui();
void run() {
gui.setVisible(true);
....
while (true) {
if (dataAvailable) {
....
gui.updateLabel(data);
sleep(10);
}
}
}
}
I'm receiving about 10 values per second, I hope Swing can handle that. My problem is the JLabel is not updating real-time and it misses some data as it shows the latest one. How can I fix this?
回答1:
You could implement a thread-safe model, that encapsulates the data the view needs. The model should be updated by the information from the serial port (represented by the Worker
class).
The view should listen to model changes and update.
The following code implements Model-View-Controller pattern. It is a one-file SSCCE : it can be copy-pasted into ViewUpdatedByThread.java
and run.
The view is just that. It listens to changes in Model using Observer
interface.
The Model encapsulates the information that the view needs (in this case just a double value). It allows thread-safe update of the value, and notifies the observers (the view) when information changes.
The Worker
class uses a thread to change the information in Model
.
The Controller
orchestrates the various members : initialize them, and links view to model:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.util.Collections;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class ViewUpdatedByThread {
public static void main(String[] args) {
new Controller();
}
}
//Controller of the MVC pattern."wires" model and view (and in this case also worker)
class Controller{
public Controller() {
Model model = new Model();
View view = new View(model);
model.registerObserver(view); //register view as an observer to model
Worker worker = new Worker(model);
view.getStopBtn().addActionListener(e -> worker.cancel());
}
}
//view of the MVC pattern. Implements observer to respond to model changes
class View implements Observer{
private final Model model;
private final DataPane pane;
private final JButton stopBtn;
public View(Model model) {
this.model = model;
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pane = new DataPane();
frame.add(pane, BorderLayout.CENTER);
stopBtn = new JButton("Stop");
frame.add(stopBtn, BorderLayout.SOUTH);
frame.pack();
frame.setVisible(true);
}
JButton getStopBtn() { return stopBtn; }
@Override
public void onObservableChanged() { //update text in response to change in model
pane.setText(String.format("%.2f",model.getValue()));
}
class DataPane extends JPanel {
private final JLabel label;
DataPane() {
setPreferredSize(new Dimension(200, 100));
setLayout(new GridBagLayout());
label = new JLabel(" ");
add(label);
}
void setText(String text){ label.setText(text); }
}
}
//Model of the MVC pattern. Holds the information view needs
//Notifies observers (in this case View) when model changes
class Model { //you can make it generic Model<T>
//the value that needs to be updated
private Double value = 0.;
// thread safe set for observers
private final Set<Observer> mObservers = Collections.newSetFromMap(
new ConcurrentHashMap<Observer, Boolean>(0));
Model() {}
//set all elements to value
void changeValue(Double value){
this.value = value;
notifyObservers();
}
synchronized Double getValue() { return value; }
synchronized void setValue(Double value) { this.value = value; }
//-- handle observers
// add new Observer - it will be notified when Observable changes
public void registerObserver(Observer observer) {
if (observer != null) {
mObservers.add(observer);
}
}
//remove an Observer
public void unregisterObserver(Observer observer) {
if (observer != null) {
mObservers.remove(observer);
}
}
//notifies registered observers
private void notifyObservers() {
for (Observer observer : mObservers) {
observer.onObservableChanged();
}
}
}
//Interface implemented by View and used by Model
interface Observer {
void onObservableChanged();
}
//Encapsulates thread that does some work on model
class Worker implements Runnable{
private final Model model;
private boolean cancel = false;
private final Random rnd = new Random();
public Worker(Model model) {
this.model = model;
new Thread(this).start();
}
@Override
public void run() {
while(! cancel){
model.changeValue(rnd.nextDouble()* 100); //generate random value
try {
TimeUnit.MILLISECONDS.sleep(300); //pause
} catch (InterruptedException ex) { ex.printStackTrace(); }
}
}
void cancel() { cancel = true; }
}
回答2:
The issue is you can NEVER touch anything owned by the EDT/GUI thread via another thread. This problem is true for all UI systems from Java Swing to Android and iOS platforms. Java Swing has a SwingWorker class to resolve this very issue.
You can find a simple example here: https://docs.oracle.com/javase/tutorial/uiswing/concurrency/simple.html
So in this loop you have:
while (true) {
if (dataAvailable) {
....
gui.updateLabel(data);
sleep(10);
}
}
You will find this has already been answered here: How do I use SwingWorker in Java?
回答3:
class Gui extends JFrame {
private JLabel lbl = new JLabel();
....
void updateLabel(String text) {
SwingUtilities.invokeLater(new Runnable() {lbl.setText(text); });
lbl.repaint();
}
}
class CommPortReceiver extends Thread {
private Gui gui = new Gui();
void run() {
gui.setVisible(true);
....
while (true) {
if (dataAvailable) {
....
gui.updateLabel(data);
sleep(10);
}
}
}
}
来源:https://stackoverflow.com/questions/58526874/updating-jlabel-text-from-another-thread-in-real-time