如何在读取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(); }
若有更好的进度显示方案,或能在接收数据时保持提示窗口显示的方法,欢迎提出建议。
问题分析
当前代码存在几个核心问题导致进度条失效:
- showProgress逻辑错误:
bufferedInputStream.available()仅返回当前流中已就绪的字节数,并非设备要发送的总数据量,用它作为进度条最大值完全不准确。bufferedInputStream.read()会读取一个字节,破坏了后续业务代码的读取位置,导致数据缺失。dialog.setVisible(true)会阻塞当前线程,直到对话框关闭,此时数据还未开始读取,对话框一关闭就被销毁,无法实时更新进度。
- Swing线程规则违反:串口回调
serialEvent运行在非事件调度线程(EDT)中,直接更新Swing组件会导致UI卡顿或线程安全问题,进度条更新必须在EDT中执行。 - 数据读取方式不合理:重复读取输入流的
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
相关产品推荐
相关产品推荐

