Swing 窗口加载后执行 serial port 代码
结论
窗口显示不完整,是因为耗时的串口读写、Thread.sleep() 和空循环都在 Swing 的事件派发线程(EDT)上运行。EDT 被占用后无法处理绘制事件,窗口看起来就像“没有加载完成”。
isValid() 不能用来判断窗口是否绘制完成。它表示的是组件当前的布局状态是否有效。
处理方式如下:
- 在 EDT 上创建、显示和更新 Swing 组件。
- 用
SwingWorker或独立线程处理串口通信。 - 收到通信结果后,切回 EDT 更新界面。
- 不要在 EDT 上调用
Thread.sleep(),也不要执行轮询循环或忙等待。
为什么 invokeLater() 后串口代码没有执行
EventQueue.invokeLater() 是异步调用:
EventQueue.invokeLater(() -> {
form_objects.cardPayment.setVisible(true);
});
这段代码只是将 setVisible(true) 放入 EDT 的事件队列,随后立即返回。如果后面的代码仍然检查:
form_objects.cardPayment.isValid()
检查发生时,EDT 可能还没来得及执行 setVisible(true)。此时条件可能为 false,串口代码也就不会执行。
即使把串口代码一起放进 invokeLater():
EventQueue.invokeLater(() -> {
form_objects.cardPayment.setVisible(true);
runSerialPortCode();
});
问题也没有解决,因为 runSerialPortCode() 仍会占用 EDT。只有等这个回调执行完毕,EDT 才能继续处理窗口绘制事件。
使用 SwingWorker
显示窗口后,可以立即启动 SwingWorker。串口操作在后台线程中执行,窗口绘制和界面状态更新仍由 EDT 处理。
EventQueue.invokeLater(() -> {
form_objects.cardPayment.setVisible(true);
SwingWorker<Void, String> worker = new SwingWorker<Void, String>() {
private boolean closeWindow;
@Override
protected Void doInBackground() throws Exception {
openPort.sendMessage(global_variables.port, "@PL\r");
String readMessage =
openPort.readMessage(global_variables.port);
String[] checkBit = readMessage.split(",");
if (checkBit.length < 3
|| !checkBit[0].equalsIgnoreCase("@PL")
|| !checkBit[2].trim().equals("0")) {
publish("Terminal offline");
return null;
}
publish("Swipe Card");
openPort.sendMessage(
global_variables.port,
"@PU," + amount + ",,,,1\r"
);
while (!isCancelled()) {
openPort.sendMessage(
global_variables.port,
"@SR,1,131\r"
);
readMessage =
openPort.readMessage(global_variables.port);
if ("Timeout".equalsIgnoreCase(readMessage)) {
publish("Enter Pin");
continue;
}
if (readMessage != null && !readMessage.isEmpty()) {
break;
}
}
if (isCancelled()) {
return null;
}
String[] sr = readMessage.split(",", -1);
if (sr.length <= 5 || !"1".equals(sr[1])) {
publish("Invalid terminal response");
return null;
}
if (sr[5].isEmpty()) {
publish("Payment Cancelled");
closeWindow = true;
} else if ("T".equals(sr[5])) {
publish("Accepted");
// 运行在后台线程,不会冻结 Swing 界面
Thread.sleep(10_000);
closeWindow = true;
} else if ("F".equals(sr[5])) {
publish("Payment Declined");
}
return null;
}
@Override
protected void process(java.util.List<String> messages) {
// process() 在 EDT 上执行,可以安全更新 Swing 组件
String latestMessage = messages.get(messages.size() - 1);
card_payment.card_text.setText(latestMessage);
}
@Override
protected void done() {
// done() 同样在 EDT 上执行
try {
get(); // 取得并处理 doInBackground() 中的异常
if (closeWindow) {
form_objects.cardPayment.dispose();
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
card_payment.card_text.setText("Operation interrupted");
} catch (java.util.concurrent.ExecutionException ex) {
card_payment.card_text.setText("Serial port error");
ex.getCause().printStackTrace();
}
}
};
worker.execute();
});
不需要专门等待窗口“加载完成”。worker.execute() 只会安排后台任务,当前 EDT 回调很快就能结束。之后,EDT 会继续绘制窗口。
如果必须等到窗口首次打开
如果业务要求在窗口触发 windowOpened 事件后再开始通信,可以添加 WindowListener。监听器中仍然只能启动后台任务,不能直接执行串口操作。
form_objects.cardPayment.addWindowListener(
new java.awt.event.WindowAdapter() {
@Override
public void windowOpened(
java.awt.event.WindowEvent event) {
startSerialWorker();
}
}
);
EventQueue.invokeLater(() -> {
form_objects.cardPayment.setVisible(true);
});
startSerialWorker() 负责创建并执行 SwingWorker:
private void startSerialWorker() {
SwingWorker<Void, String> worker =
new SwingWorker<Void, String>() {
@Override
protected Void doInBackground() throws Exception {
// 在这里执行串口读写
return null;
}
@Override
protected void process(java.util.List<String> values) {
// 在这里更新 Swing 组件
}
};
worker.execute();
}
windowOpened 表示窗口已经进入打开流程,但不能严格证明屏幕上的每个像素都已绘制完成。通常也不需要等到这个程度。只要耗时任务不占用 EDT,窗口就能正常完成绘制。
串口读取还需要注意什么
Thread.sleep() 可以暂时放在 SwingWorker.doInBackground() 中,但不适合用作可靠的串口同步机制。设备的响应时间会变化,固定等待一秒或两秒可能不够,也可能带来多余的延迟。
如果串口库支持,可以优先采用以下方式:
- 使用带超时的阻塞读取;
- 监听串口数据到达事件;
- 根据协议终止符判断消息是否接收完整;
- 设置最大重试次数或总超时时间。
InputStream.available() 只表示当前无需阻塞即可读取多少字节,不能保证一条完整的协议消息已经到达。因此,不能看到一次 available() > 0 就认为响应已经接收完整。
原代码还应避免这种写法:
while (ptime + 10000 >=
Calendar.getInstance().getTimeInMillis()) {
// busy wait
}
这是忙等待,会持续占用 CPU。如果它在 EDT 上运行,整个界面都会冻结。放到后台线程后,可以暂时改用:
Thread.sleep(10_000);
如果需求只是十秒后关闭窗口,也可以在 EDT 上使用 Swing 定时器:
javax.swing.Timer timer = new javax.swing.Timer(10_000, event -> {
form_objects.cardPayment.dispose();
});
timer.setRepeats(false);
timer.start();
EDT 应只处理界面事件和耗时很短的界面更新。串口读写、等待和轮询都应放到后台线程中。
备注:内容仅供参考。
