我将JavaFX与Gluon Mobile一起使用。用于创建移动应用程序的非常好的框架。但是我有一个JavaFX折线图,它在出现错误后会冻结。
Exception in thread "JavaFX Application Thread"
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1042)
at javafx.scene.chart.LineChart.layoutPlotChildren(LineChart.java:468)
at javafx.scene.chart.XYChart.layoutChartChildren(XYChart.java:731)
at javafx.scene.chart.Chart$1.layoutChildren(Chart.java:94)
at javafx.scene.Parent.layout(Parent.java:1087)
at javafx.scene.Parent.layout(Parent.java:1093)
at javafx.scene.Parent.layout(Parent.java:1093)
at javafx.scene.Parent.layout(Parent.java:1093)
at javafx.scene.Scene.doLayoutPass(Scene.java:552)
at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2397)
at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:398)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:397)
at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:424)
at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:518)
at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:498)
at com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:491)
at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:319)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
at com.sun.glass.ui.gtk.GtkApplication.lambda$null$5(GtkApplication.java:139)
at java.lang.Thread.run(Thread.java:748)
折线图如下所示。
但是过了一会儿,我可以看到点逐渐消失后的点,然后那里什么也没有。就像Java FX折线图停止更新一样。我已经发表了一些评论,指出了中断的地方以及真正发生的事情。
我正在使用折线图进行实时日志记录。这是Java代码。
/*
* Get the time format in HH:mm:ss
*/
LocalDateTime now = LocalDateTime.now();
String time = dtf.format(now);
if (countMeasurements < MEASUREMENTS) {
time_output.getData().add(new XYChart.Data<String, Number>(time, output));
time_input.getData().add(new XYChart.Data<String, Number>(time, input));
countMeasurements++;
} else {
/*
* Now insert
*/
time_output.getData().add(new XYChart.Data<String, Number>(time, output)); // <-- No update after a while
time_input.getData().add(new XYChart.Data<String, Number>(time, input)); // <-- No update after a while
/*
* Delete the first object
*/
time_output.getData().remove(0); // <--- This works
time_input.getData().remove(0); // <-- This works
}
编辑:
这是我收到错误时的外观。点删除越来越多。您看到线消失了吗?
整个代码
package com.gluonapplication.thread;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import com.gluonhq.charm.glisten.mvc.View;
import de.re.easymodbus.modbusclient.ModbusClient;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
public class ModbusConnection extends Thread{
/*
* Static
*/
private static boolean running;
private static boolean start;
/*
* Fields from Scene Builder
*/
private TextField statusTextField;
private TextField ipAddressTextField;
private ComboBox<String> startSignalComboBox;
private TextField predictHorizonTextField;
private TextField controlHorizonTextField;
private TextField sampleTimeTextField;
private TextField referencePointTextField;
private TextField portTextField;
private TextField slopeTextField;
private TextField offsetTextField;
private LineChart<String, Number> lineChart_primary;
private Series<String, Number> time_output;
private LineChart<String, Number> lineChart_third;
private Series<String, Number> time_input;
/*
* Modbus
*/
private ModbusClient modbusClient;
private int MEASUREMENTS = 20;
DateTimeFormatter dtf;
int[] writeRegisters = new int[11];
private int countMeasurements;
@SuppressWarnings("unchecked")
public ModbusConnection(View primaryView, View secondaryView, View thirdView) {
/*
* We start this thread as default
*/
start = true;
/*
* Initial modbus start
*/
writeRegisters[6] = 0;
/*
* For secondaryView
*/
statusTextField = (TextField) secondaryView.lookup("#statusTextField");
ipAddressTextField = (TextField) secondaryView.lookup("#ipAddressTextField");
startSignalComboBox = (ComboBox<String>) secondaryView.lookup("#startSignalComboBox");
predictHorizonTextField = (TextField) secondaryView.lookup("#predictHorizonTextField");
controlHorizonTextField = (TextField) secondaryView.lookup("#controlHorizonTextField");
sampleTimeTextField = (TextField) secondaryView.lookup("#sampleTimeTextField");
referencePointTextField = (TextField) secondaryView.lookup("#referencePointTextField");
portTextField = (TextField) secondaryView.lookup("#portTextField");
slopeTextField = (TextField) secondaryView.lookup("#slopeTextField");
offsetTextField = (TextField) secondaryView.lookup("#offsetTextField");
/*
* For primaryView
*/
lineChart_primary = (LineChart<String, Number>) primaryView.lookup("#lineChart");
/*
* Declare the data object inside the chart
*/
time_output = new Series<String, Number>();
time_output.setName("Output");
lineChart_primary.getData().add(time_output);
/*
* For thirdView
*/
lineChart_third = (LineChart<String, Number>) thirdView.lookup("#lineChart");
/*
* Declare the data object inside the chart
*/
time_input = new Series<String, Number>();
time_input.setName("Input");
lineChart_third.getData().add(time_input);
/*
* This will prevent so we don't get NullPointerException
*/
modbusClient = null;
/*
* For time
*/
dtf = DateTimeFormatter.ofPattern("HH:mm:ss");
/*
* Reset
*/
countMeasurements = 0;
}
@Override
public void run() {
while (start) {
while (running == true || writeRegisters[6] == 1) {
/*
* Connect to Modbus server
*/
if(modbusClient == null) {
modbusClient = new ModbusClient(ipAddressTextField.getText(),Integer.parseInt(portTextField.getText()));
try {
modbusClient.Connect();
} catch (Exception e) {
statusTextField.setText("Cannot connect");
}
}else if(modbusClient.isConnected() == false){
modbusClient = new ModbusClient(ipAddressTextField.getText(),Integer.parseInt(portTextField.getText()));
try {
modbusClient.Connect();
} catch (Exception e) {
statusTextField.setText("Cannot connect");
}
}
/*
* Write registers at address 0
*/
try {
/*
* What start signal should we use.
*/
int mode = startSignalComboBox.getSelectionModel().getSelectedIndex();
switch (mode) {
case 0:
writeRegisters[0] = 255; // PWM 100%
break;
case 1:
writeRegisters[0] = 230; // PWM 90%
break;
case 2:
writeRegisters[0] = 204; // PWM 80%
break;
case 3:
writeRegisters[0] = 179; // PWM 70%
break;
case 4:
writeRegisters[0] = 153; // PWM 60%
break;
case 5:
writeRegisters[0] = 128; // PWM 50%
break;
case 6:
writeRegisters[0] = 102; // PWM 40%
break;
case 7:
writeRegisters[0] = 77; // PWM 30%
break;
case 8:
writeRegisters[0] = 51; // PWM 20%
break;
case 9:
writeRegisters[0] = 26; // PWM 10%
break;
default:
writeRegisters[0] = 255; // PWM 100%
break;
}
/*
* Get the prediction horizon as int
*/
writeRegisters[1] = Integer.parseInt(predictHorizonTextField.getText());
/*
* Get the control horizon as int
*/
writeRegisters[2] = Integer.parseInt(controlHorizonTextField.getText());
/*
* Get the sample time in as int
*/
writeRegisters[3] = Integer.parseInt(sampleTimeTextField.getText());
/*
* Get the reference point in two ints
*/
writeRegisters[4] = (int) Float.parseFloat(referencePointTextField.getText());
writeRegisters[5] = (int) ((Float.parseFloat(referencePointTextField.getText()) - ((float) writeRegisters[4])) * 10000);
/*
* Get if the system is running
*/
writeRegisters[6] = running ? 1 : 0;
/*
* Get the slope
*/
writeRegisters[7] = (int) Float.parseFloat(slopeTextField.getText());
writeRegisters[8] = (int) ((Float.parseFloat(slopeTextField.getText()) - ((float) writeRegisters[7])) * 10000);
/*
* Get the offset
*/
writeRegisters[9] = (int) Float.parseFloat(offsetTextField.getText());
writeRegisters[10] = (int) ((Float.parseFloat(offsetTextField.getText()) - ((float) writeRegisters[9])) * 10000);
/*
* Write 11 elements from address 0
*/
modbusClient.WriteMultipleRegisters(0, writeRegisters);
} catch (Exception e) {
statusTextField.setText("Cannot write");
}
/*
* Read 3 registers at the beginning from address 12
*/
try {
int[] registersRead = modbusClient.ReadHoldingRegisters(12, 3); // two first are output (float) and the last is the input (int)
/*
* Get the output value
*/
System.out.println("registersRead[0] = " + registersRead[0] + " registersRead[1] = " + registersRead[1] );
double output = ((double) registersRead[0]) + ((double) registersRead[1]) / 10000.0;
System.out.println("Output : " + output);
/*
* Get the input value
*/
int input = registersRead[2];
/*
* Get the time format in HH:mm:ss
*/
LocalDateTime now = LocalDateTime.now();
String time = dtf.format(now);
System.out.println("Time : " + time);
if (countMeasurements < MEASUREMENTS) {
time_output.getData().add(new XYChart.Data<String, Number>(time, output));
time_input.getData().add(new XYChart.Data<String, Number>(time, input));
countMeasurements++;
} else {
/*
* Now insert
*/
System.out.println("Add time_output");
time_output.getData().add(new XYChart.Data<String, Number>(time, output));
System.out.println("Add time_input");
time_input.getData().add(new XYChart.Data<String, Number>(time, input));
/*
* Delete the first object
*/
System.out.println("Delete");
time_output.getData().remove(0);
time_input.getData().remove(0);
}
} catch (Exception e) {
statusTextField.setText("Cannot read");
}
try {
Thread.sleep((long) (1000 * Double.parseDouble(sampleTimeTextField.getText())));
} catch (Exception e) {
statusTextField.setText("Cannot delay");
}
statusTextField.setText("Running");
}
/*
* This is because we don't want update so fast
*/
try {
Thread.sleep(100);
} catch (InterruptedException e) {
statusTextField.setText("Cannot delay");
}
/*
* Disconnect if we have been using modbusClient object before
*/
if(modbusClient != null) {
try {
if(modbusClient.isConnected()) {
modbusClient.Disconnect();
statusTextField.setText("Stopped");
}
} catch (Exception e) {
statusTextField.setText("Cannot disconnect");
}
}
}
}
public static boolean isRunning() {
return running;
}
public static void setRunning(boolean running) {
ModbusConnection.running = running;
}
public static boolean isStart() {
return start;
}
public static void setStart(boolean start) {
ModbusConnection.start = start;
}
}
答案 0 :(得分:1)
如评论中所述,您正在创建一个后台任务:
public class ModbusConnection extends Thread {
@Override
public void run() {
while (start) {
...
}
}
}
尽管这对于您的 background 任务(即您的Modbus连接和通讯)来说是完全可以的,但您永远不要在其上执行与UI相关的任务。
Chart
是一个JavaFX节点,当您向其系列中的一个添加新数据点时,如下所示:
public class ModbusConnection extends Thread {
@Override
public void run() {
while (start) {
...
time_output.getData().add(new XYChart.Data<String, Number>(time, output));
...
}
}
}
触发布局遍历以渲染相关节点,但是只能在UI线程中 (JavaFX Application线程)完成。
因此,作为初始解决方案,请修改代码以执行以下操作:
public class ModbusConnection extends Thread {
@Override
public void run() {
while (start) {
...
Platform.runLater(() ->
time_output.getData().add(new XYChart.Data<String, Number>(time, output)));
...
}
}
}
请注意,正如以上评论中提到的@Slaw一样,Platform::runLater的JavaDoc表示:
public static void runLater(Runnable runnable)
将来在某些未指定的时间在JavaFX Application Thread上运行指定的Runnable。可以从任何线程调用此方法,该方法会将Runnable张贴到事件队列中,然后立即返回给调用者。
这看起来正是我们在这种情况下需要的。
但是如果您继续阅读:
注意:应用程序应避免将太多未决Runnable泛洪到JavaFX。否则,应用程序可能无法响应。鼓励应用程序将多个操作分批处理到更少的runLater调用中。此外,应在可能的情况下在后台线程上执行长时间运行的操作,从而释放JavaFX Application Thread进行GUI操作。
因此,在第二步中,您应该尝试批处理来自后台任务的所有调用,例如:
public class ModbusConnection extends Thread {
@Override
public void run() {
while (start) {
...
Platform.runLater(() -> {
time_output.getData().add(new XYChart.Data<String, Number>(time, output));
time_input.getData().add(new XYChart.Data<String, Number>(time, input));
if (time_output.getData().size() > MEASUREMENTS) {
time_output.getData().remove(0);
time_input.getData().remove(0);
}
});
...
}
}
}