You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在读取SerialPort串口数据时为InputStream显示JProgressBar

问题

我正在开发一款通过SerialPort与设备通信的Swing应用。有时设备发送的数据量较大,接收过程需耗时5-10秒,因此希望通过JProgressBar向用户展示进度,让用户知晓程序正在运行。

我尝试使用JProgressBar和ProgressMonitor实现,但仅能弹出进度窗口,进度始终停留在0%。

以下是我用于读取串口的方法:

public static void readingDataSB9(SerialPort comPort) {

    comPort.addDataListener(new SerialPortDataListener() {
        @Override
        public int getListeningEvents() {
            return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;
        }

        @Override
        public void serialEvent(SerialPortEvent serialPortEvent) {
            try {

                if (serialPortEvent.getEventType() != SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
                    return;
                }

                //保存计数数据的字符串数组
                List<String> countData = new ArrayList<>();
                //保存图像OCR识别后的文本
                List<String> ocrText = new ArrayList<>();
                //保存设备传来的图像
                List<ImageIcon> serialImage = new ArrayList<>();

                //尝试解码非UTF-8字符串,仅测试用
                CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
                decoder.onMalformedInput(CodingErrorAction.IGNORE);

                //代表序列号开头和换行的二进制字符串
                String startSn = "110000010011010001101100100011011000100011010000111000010001010";
                String newLine = "01100000110110010001101100010001101000011100001000101";

                int x = 0;
                String s1 = "";

                //获取串口输入流并转为缓冲流,支持重置和重复读取
                InputStream in = comPort.getInputStream();
                InputStream bufferdInputStream = new BufferedInputStream(in);
                bufferdInputStream.mark(1000000000);
                
                //尝试展示数据接收进度
                showProgress(bufferdInputStream, MainWindow.frame);

                //第一次读取输入流获取计数数据
                Scanner sc = new Scanner(bufferdInputStream);

                while (sc.hasNextLine()) {
                    countData.add(sc.next());
                    //遇到结束标记则退出循环
                    if (countData.contains("\u001Bm\u001B3")) {
                        break;
                    }
                }

                System.out.println(countData);

                //重置输入流,重新读取用于生成序列号图像的字节
                bufferdInputStream.reset();

                while (((x = bufferdInputStream.read()) != 109)) {
                    //将字节转为二进制字符串,用于生成图像
                    s1 += String.format("%8s", Integer.toBinaryString(x & 0xFF)).replace(' ', '0');
                }

                //按序列号开头字符串分割二进制数据
                String[] binarySerialNumberArr = s1.split(startSn);

                //将二进制字符串写入主窗口UI,便于保存到数据库
                MainWindow.jt_serialBinary.setText(String.join(", ", binarySerialNumberArr));


                //将每个二进制字符串元素转为图像
                for (int i = 1; i < binarySerialNumberArr.length; i++) {

                    //创建空图像获取字体高度
                    BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
                    Graphics2D g2d = img.createGraphics();
                    Font font = new Font("Arial", Font.PLAIN, 2);
                    g2d.setFont(font);
                    int height = g2d.getFontMetrics().getHeight();
                    g2d.dispose();

                    //创建实际序列号图像
                    img = new BufferedImage(384, 40, BufferedImage.TYPE_INT_RGB);
                    g2d = img.createGraphics();

                    g2d.setFont(font);
                    g2d.setColor(Color.WHITE);
                    int fontSize = 1;

                    for (String s : binarySerialNumberArr[i].split(newLine)) {
                        //绘制二进制字符串,0为白色,1为黑色
                        g2d.drawString(s, 0, height);
                        height += fontSize;
                    }
                    //保存图像到文件
                    File file = new File("images\\Text" + i + ".png");
                    ImageIO.write(img, "png", file);
                    g2d.dispose();

                    //使用Tesseract OCR识别图像文本
                    String ocrString = SerialOcr(file);
                    //修正OCR识别错误并加入列表
                    ocrText.add(trainOcr(ocrString));
                    //转为ImageIcon加入列表
                    serialImage.add(makeIcon(img));

                    System.out.println(ocrString);
                }

                //将OCR文本加入主窗口UI
                for (int i = 0; i < ocrText.size(); i++) {
                    MainWindow.model_ocrText.add(i, ocrText.get(i));
                }
                //将图像加入主窗口UI
                for (int i = 0; i < serialImage.size(); i++) {
                    MainWindow.model_serialImage.add(i, serialImage.get(i));
                }

                //测试:过滤非UTF-8字符串
                ArrayList<String> validData = new ArrayList<>();

                for (int i = 0; i < countData.size(); i++) {
                    decoder.decode(java.nio.ByteBuffer.wrap(countData.get(i).getBytes()));
                    validData.add(countData.get(i));
                }

                System.out.println(validData);


                //判断数据对应的货币,构建UI表格
                for (int i = 0; i < countData.size(); i++) {
                    switch (countData.get(i)) {
                        case "RSD" -> insertRSD(countData, MainWindow.jt_denom);
                        case "USD" -> insertUSD(countData, MainWindow.jt_denom);
                        case "EUR" -> insertEUR(countData, MainWindow.jt_denom);
                        default ->
                            //不支持的货币弹出错误提示
                                JOptionPane.showMessageDialog(null, "Odabrana valuta nije podržana", "Greška!", JOptionPane.ERROR_MESSAGE);
                    }
                }

                //计算表格中的总计数据
                ButtonListeners.tableTotalAmountRows(MainWindow.jt_denom);
                ButtonListeners.tableTotalAmountColumns(MainWindow.jt_denom);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });

}

以下是showProgress方法:

public static void showProgress(InputStream bufferedInputStream, JFrame frame) throws IOException {
    JOptionPane pane = new JOptionPane();
    pane.setMessage("Receiving data...");
    JProgressBar jProgressBar = new JProgressBar(1, bufferedInputStream.available());
    jProgressBar.setValue(bufferedInputStream.read());
    pane.add(jProgressBar,1);
    JDialog dialog = pane.createDialog(frame, "Information message");
    dialog.setVisible(true);
    dialog.dispose();
}

若有更好的进度显示方案,或能在接收数据时保持提示窗口显示的方法,欢迎提出建议。


问题分析

当前代码存在几个核心问题导致进度条失效:

  1. showProgress逻辑错误:
    • bufferedInputStream.available()仅返回当前流中已就绪的字节数,并非设备要发送的总数据量,用它作为进度条最大值完全不准确。
    • bufferedInputStream.read()会读取一个字节,破坏了后续业务代码的读取位置,导致数据缺失。
    • dialog.setVisible(true)会阻塞当前线程,直到对话框关闭,此时数据还未开始读取,对话框一关闭就被销毁,无法实时更新进度。
  2. Swing线程规则违反:串口回调serialEvent运行在非事件调度线程(EDT)中,直接更新Swing组件会导致UI卡顿或线程安全问题,进度条更新必须在EDT中执行。
  3. 数据读取方式不合理:重复读取输入流的mark/reset方式不可靠,且无法实时统计已接收字节数来更新进度。

解决方案

使用SwingWorker处理后台任务,安全更新UI,并优化数据读取逻辑:

1. 重构串口读取逻辑,用SwingWorker封装

public static void readingDataSB9(SerialPort comPort, JFrame parentFrame) {
    comPort.addDataListener(new SerialPortDataListener() {
        @Override
        public int getListeningEvents() {
            return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;
        }

        @Override
        public void serialEvent(SerialPortEvent serialPortEvent) {
            if (serialPortEvent.getEventType() != SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
                return;
            }

            // 启动SwingWorker执行后台任务
            new SwingWorker<Void, Integer>() {
                private JDialog progressDialog;
                private JProgressBar progressBar;
                private List<String> countData = new ArrayList<>();
                private List<String> ocrText = new ArrayList<>();
                private List<ImageIcon> serialImage = new ArrayList<>();
                private String s1 = "";

                @Override
                protected void doInBackground() throws Exception {
                    InputStream in = comPort.getInputStream();
                    BufferedInputStream bufferedInputStream = new BufferedInputStream(in);
                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    long receivedBytes = 0;

                    // 先将所有数据读取到字节数组,便于重复处理
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesRead);
                        receivedBytes += bytesRead;
                        // 发布进度:-1表示不确定模式,若能获取总数据量则改为百分比
                        publish(-1);
                        Thread.sleep(10); // 避免进度更新过于频繁
                    }
                    byte[] allData = baos.toByteArray();

                    // 处理计数数据
                    try (ByteArrayInputStream bais = new ByteArrayInputStream(allData)) {
                        Scanner sc = new Scanner(bais);
                        while (sc.hasNextLine()) {
                            String line = sc.next();
                            countData.add(line);
                            if (line.equals("\u001Bm\u001B3")) {
                                break;
                            }
                        }
                    }

                    // 处理二进制图像数据
                    try (ByteArrayInputStream bais = new ByteArrayInputStream(allData)) {
                        int x;
                        while (((x = bais.read()) != 109) && x != -1) {
                            s1 += String.format("%8s", Integer.toBinaryString(x & 0xFF)).replace(' ', '0');
                        }
                    }

                    // 生成图像并执行OCR
                    String startSn = "110000010011010001101100100011011000100011010000111000010001010";
                    String newLine = "01100000110110010001101100010001101000011100001000101";
                    String[] binarySerialNumberArr = s1.split(startSn);

                    for (int i = 1; i < binarySerialNumberArr.length; i++) {
                        BufferedImage img = new BufferedImage(384, 40, BufferedImage.TYPE_INT_RGB);
                        Graphics2D g2d = img.createGraphics();
                        Font font = new Font("Arial", Font.PLAIN, 2);
                        g2d.setFont(font);
                        g2d.setColor(Color.WHITE);
                        int height = g2d.getFontMetrics().getHeight();
                        int fontSize = 1;

                        for (String s : binarySerialNumberArr[i].split(newLine)) {
                            g2d.drawString(s, 0, height);
                            height += fontSize;
                        }
                        File file = new File("images\\Text" + i + ".png");
                        ImageIO.write(img, "png", file);
                        g2d.dispose();

                        String ocrString = SerialOcr(file);
                        ocrText.add(trainOcr(ocrString));
                        serialImage.add(makeIcon(img));
                    }

                    return null;
                }

                @Override
                protected void process(List<Integer> chunks) {
                    // 在EDT中更新进度条
                    int progress = chunks.get(chunks.size() - 1);
                    if (progress == -1) {
                        progressBar.setIndeterminate(true);
                    } else {
                        progressBar.setIndeterminate(false);
                        progressBar.setValue(progress);
                    }
                }

                @Override
                protected void done() {
                    // 任务完成后关闭对话框并更新UI
                    progressDialog.dispose();
                    try {
                        get(); // 捕获任务异常
                        SwingUtilities.invokeLater(() -> {
                            // 更新主窗口UI
                            MainWindow.jt_serialBinary.setText(String.join(", ", s1.split(startSn)));
                            for (int i = 0; i < ocrText.size(); i++) {
                                MainWindow.model_ocrText.add(i, ocrText.get(i));
                            }
                            for (int i = 0; i < serialImage.size(); i++) {
                                MainWindow.model_serialImage.add(i, serialImage.get(i));
                            }
                            // 处理货币和统计
                            for (String data : countData) {
                                switch (data) {
                                    case "RSD" -> insertRSD(countData, MainWindow.jt_denom);
                                    case "USD" -> insertUSD(countData, MainWindow.jt_denom);
                                    case "EUR" -> insertEUR(countData, MainWindow.jt_denom);
                                    default -> JOptionPane.showMessageDialog(parentFrame, "Odabrana valuta nije podržana", "Greška!", JOptionPane.ERROR_MESSAGE);
                                }
                            }
                            ButtonListeners.tableTotalAmountRows(MainWindow.jt_denom);
                            ButtonListeners.tableTotalAmountColumns(MainWindow.jt_denom);
                        });
                    } catch (InterruptedException | ExecutionException e) {
                        JOptionPane.showMessageDialog(parentFrame, "数据接收失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
                        e.printStackTrace();
                    }
                }

                @Override
                protected void initialize() {
                    // 在EDT中初始化进度对话框
                    SwingUtilities.invokeLater(() -> {
                        progressBar = new JProgressBar();
                        progressBar.setIndeterminate(true);
                        progressBar.setStringPainted(true);
                        progressBar.setString("正在接收数据...");

                        JPanel panel = new JPanel(new BorderLayout(5,5));
                        panel.add(new JLabel("正在接收数据,请稍候..."), BorderLayout.NORTH);
                        panel.add(progressBar, BorderLayout.CENTER);

                        progressDialog = new JDialog(parentFrame, "数据接收中", true);
                        progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); // 禁止中途关闭
                        progressDialog.getContentPane().add(panel);
                        progressDialog.pack();
                        progressDialog.setLocationRelativeTo(parentFrame);
                        progressDialog.setVisible(true);
                    });
                }
            }.execute();
        }
    });
}

2. 移除原showProgress方法

原方法逻辑错误,进度显示逻辑已整合到SwingWorker中。


优化建议

  • 进度模式选择:若设备协议包含总数据量字段,可在doInBackground开头获取该值,计算已接收字节的百分比,改为确定模式进度条。
  • 线程安全:所有Swing组件更新必须在EDT中执行,使用SwingUtilities.invokeLater或SwingWorker的process/done方法。
  • 异常处理:在done方法中捕获任务异常,友好提示用户。

内容的提问来源于stack exchange,提问作者Danilo Djurovic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 00:05:33