PAYATHON 2026

Swing 窗口加载后执行 serial port 代码

支付老李

结论

窗口显示不完整,是因为耗时的串口读写、Thread.sleep() 和空循环都在 Swing 的事件派发线程(EDT)上运行。EDT 被占用后无法处理绘制事件,窗口看起来就像“没有加载完成”。

isValid() 不能用来判断窗口是否绘制完成。它表示的是组件当前的布局状态是否有效。

处理方式如下:

  1. 在 EDT 上创建、显示和更新 Swing 组件。
  2. 用 SwingWorker 或独立线程处理串口通信。
  3. 收到通信结果后,切回 EDT 更新界面。
  4. 不要在 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 应只处理界面事件和耗时很短的界面更新。串口读写、等待和轮询都应放到后台线程中。

备注:内容仅供参考。