wangxichen 6 ngày trước cách đây
mục cha
commit
2c05511c04

+ 3 - 3
config/print.properties

@@ -1,4 +1,4 @@
-# OP40 工位打印机配置
-# 打印机类型: hp (激光打印机打流转卡) / tsc (热敏标签机) / none (不打印)
 printer.type=hp
-printer.name=HP LaserJet 3004
+printer.name=HP-3004dn
+# 纸张:A4 或 A5,默认 A5(流转卡模板就是 A5 设计)
+printer.paper=A5

BIN
lib/mes-print.jar


+ 38 - 42
src/com/mes/print/PrintHelper.java

@@ -1,54 +1,45 @@
 package com.mes.print;
 
-import com.mes.print.Printer;
-import com.mes.print.PrinterException;
 import com.mes.print.model.FlowCardData;
 import com.mes.print.model.LabelData;
 
-import java.text.SimpleDateFormat;
-import java.util.Date;
-
 /**
  * 打印辅助类:封装 mes-print 模块调用
  *
  * 根据 print.properties 配置的打印机类型自动选择打印方式:
- * - hp: 打印流转卡
- * - tsc: 打印标签
+ * - hp: 打印流转卡(A4/A5,走 HP 激光)
+ * - tsc: 打印标签(走 TSC 热敏)
  * - none: 不打印
+ *
+ * 说明:FlowCardData 当前只承载 sn(客户码)和 steelSn(钢印码),
+ * 其他字段(工位/批次/操作员等)由 PDF 模板固定,无需业务传入。
  */
 public class PrintHelper {
 
     /**
-     * 打印流转卡(用于 HP 激光打印机)
+     * 打印流转卡(HP 激光打印机)
      *
-     * @param sn 产品序列号
-     * @param opNo 工位号(如 OP040A)
-     * @param batch 批次号
-     * @param operator 操作员
-     * @param remark 备注
-     * @return 是否打印成功
+     * @param sn 产品序列号(客户码)
+     * @param steelSn 钢印码(可为 null)
+     * @return true=成功或已跳过;false=失败
      */
-    public static boolean printFlowCard(String sn, String opNo, String batch, String operator, String remark) {
+    public static boolean printFlowCard(String sn, String steelSn) {
         try {
             String type = Printer.getCurrentType();
             if ("none".equals(type)) {
-                System.out.println("[打印] 当前配置为不打印,跳过流转卡打印");
+                System.out.println("[打印] 配置为不打印,跳过流转卡: " + sn);
                 return true;
             }
-
             if (!"hp".equals(type)) {
-                System.out.println("[打印] 当前打印机类型为 " + type + ",不支持流转卡打印");
+                System.out.println("[打印] 打印机类型为 " + type + ",不支持流转卡");
                 return false;
             }
 
             FlowCardData data = new FlowCardData();
             data.setSn(sn);
-            data.setProduct("+KB24"); // 产品型号,根据实际业务调整
-            data.setOpNo(opNo);
-            data.setBatch(batch != null ? batch : "");
-            data.setDate(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()));
-            data.setOperator(operator != null ? operator : "");
-            data.setRemark(remark != null ? remark : "");
+            if (steelSn != null) {
+                data.setSteelSn(steelSn);
+            }
 
             Printer.printFlowCard(data);
             System.out.println("[打印] 流转卡打印成功: " + sn);
@@ -66,25 +57,24 @@ public class PrintHelper {
     }
 
     /**
-     * 打印标签(用于 TSC 热敏标签机)
+     * 打印标签(TSC 热敏标签机)
      *
      * @param sn 产品序列号
-     * @param barcode 条码内容
-     * @param qrcode 二维码内容
-     * @param title 标题
-     * @param copies 打印份数
-     * @return 是否打印成功
+     * @param barcode 条码内容(null 时用 sn)
+     * @param qrcode 二维码内容(null 时用 sn)
+     * @param title 标题(null 时用默认「产品标签」)
+     * @param copies 打印份数(≤0 时按 1 份)
+     * @return true=成功或已跳过;false=失败
      */
     public static boolean printLabel(String sn, String barcode, String qrcode, String title, int copies) {
         try {
             String type = Printer.getCurrentType();
             if ("none".equals(type)) {
-                System.out.println("[打印] 当前配置为不打印,跳过标签打印");
+                System.out.println("[打印] 配置为不打印,跳过标签: " + sn);
                 return true;
             }
-
             if (!"tsc".equals(type)) {
-                System.out.println("[打印] 当前打印机类型为 " + type + ",不支持标签打印");
+                System.out.println("[打印] 打印机类型为 " + type + ",不支持标签");
                 return false;
             }
 
@@ -111,29 +101,35 @@ public class PrintHelper {
     }
 
     /**
-     * 根据当前配置的打印机类型自动打印
+     * 根据当前配置自动打印
+     * - hp: 打流转卡(steelSn 传 null)
+     * - tsc: 打标签(barcode/qrcode 都用 sn)
+     * - none: 跳过
+     */
+    public static boolean autoPrint(String sn) {
+        return autoPrint(sn, null);
+    }
+
+    /**
+     * 根据当前配置自动打印
      *
      * @param sn 产品序列号
-     * @param opNo 工位号
-     * @param operator 操作员
-     * @return 是否打印成功
+     * @param steelSn 钢印码(仅流转卡使用,可为 null)
      */
-    public static boolean autoPrint(String sn, String opNo, String operator) {
+    public static boolean autoPrint(String sn, String steelSn) {
         try {
             String type = Printer.getCurrentType();
-
             if ("none".equals(type)) {
-                System.out.println("[打印] 当前配置为不打印,跳过");
+                System.out.println("[打印] 配置为不打印,跳过");
                 return true;
             } else if ("hp".equals(type)) {
-                return printFlowCard(sn, opNo, "", operator, "");
+                return printFlowCard(sn, steelSn);
             } else if ("tsc".equals(type)) {
                 return printLabel(sn, sn, sn, "产品标签", 1);
             } else {
                 System.err.println("[打印] 未知的打印机类型: " + type);
                 return false;
             }
-
         } catch (Exception e) {
             System.err.println("[打印] 自动打印失败: " + e.getMessage());
             e.printStackTrace();

+ 439 - 0
src/com/mes/print/PrintTest.java

@@ -0,0 +1,439 @@
+package com.mes.print;
+
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.PrintStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * mes-print 模块独立测试 UI
+ *
+ * 直接 Run 这个类的 main:
+ *   - 顶部:环境和当前配置
+ *   - 中间:打印机类型/名称设置(保存到 config/print.properties)
+ *   - 下方:流转卡 / 标签测试按钮
+ *   - 底部:日志区(含 System.out / System.err 输出)
+ *
+ * 依赖:
+ *   - config/print.properties 在当前工作目录(会自动检测/创建)
+ *   - 流转卡还需要 config/liuzhuanka_template.pdf
+ */
+public class PrintTest extends JFrame {
+
+    private JComboBox<String> typeBox;
+    private JComboBox<String> nameBox;
+    private JTextField cardSnField;
+    private JTextField cardSteelField;
+    private JTextField labelSnField;
+    private JTextField labelBarcodeField;
+    private JTextField labelQrcodeField;
+    private JTextField labelTitleField;
+    private JSpinner labelCopiesSpinner;
+    private JTextArea logArea;
+    private JLabel envLabel;
+
+    public PrintTest() {
+        super("mes-print 测试面板");
+        setDefaultCloseOperation(EXIT_ON_CLOSE);
+        setSize(720, 720);
+        setLocationRelativeTo(null);
+
+        JPanel root = new JPanel(new BorderLayout(8, 8));
+        root.setBorder(new EmptyBorder(10, 10, 10, 10));
+
+        root.add(buildTopPanel(), BorderLayout.NORTH);
+        root.add(buildCenterPanel(), BorderLayout.CENTER);
+        root.add(buildLogPanel(), BorderLayout.SOUTH);
+
+        setContentPane(root);
+
+        // 重定向 System.out / System.err 到日志区(保留原控制台)
+        redirectSystemStreams();
+
+        // 首次加载
+        refreshEnv();
+        refreshPrinterList();
+    }
+
+    // ============== UI 构建 ==============
+
+    private JPanel buildTopPanel() {
+        JPanel p = new JPanel(new BorderLayout(4, 4));
+        p.setBorder(new TitledBorder("环境 / 当前配置"));
+        envLabel = new JLabel("<html>加载中...</html>");
+        envLabel.setBorder(new EmptyBorder(4, 8, 4, 8));
+        p.add(envLabel, BorderLayout.CENTER);
+
+        JButton refresh = new JButton("刷新");
+        refresh.addActionListener(e -> {
+            refreshEnv();
+            refreshPrinterList();
+            log("刷新完成");
+        });
+        JPanel right = new JPanel();
+        right.add(refresh);
+        p.add(right, BorderLayout.EAST);
+        return p;
+    }
+
+    private JPanel buildCenterPanel() {
+        JPanel p = new JPanel();
+        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
+
+        p.add(buildSettingPanel());
+        p.add(Box.createVerticalStrut(6));
+        p.add(buildCardPanel());
+        p.add(Box.createVerticalStrut(6));
+        p.add(buildLabelPanel());
+        return p;
+    }
+
+    private JPanel buildSettingPanel() {
+        JPanel p = new JPanel(new GridBagLayout());
+        p.setBorder(new TitledBorder("打印机设置"));
+        GridBagConstraints c = gbc();
+
+        typeBox = new JComboBox<>(new String[]{"hp", "tsc", "none"});
+        nameBox = new JComboBox<>();
+        nameBox.setEditable(true);
+
+        c.gridx = 0; c.gridy = 0; p.add(new JLabel("类型:"), c);
+        c.gridx = 1; c.weightx = 1; p.add(typeBox, c);
+        c.gridx = 2; c.weightx = 0;
+        JButton listBtn = new JButton("列出本机打印机");
+        listBtn.addActionListener(e -> refreshPrinterList());
+        p.add(listBtn, c);
+
+        c.gridx = 0; c.gridy = 1; c.weightx = 0; p.add(new JLabel("名称:"), c);
+        c.gridx = 1; c.gridwidth = 2; c.weightx = 1; p.add(nameBox, c);
+        c.gridwidth = 1;
+
+        c.gridx = 0; c.gridy = 2; c.gridwidth = 3; c.weightx = 1;
+        JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        JButton saveBtn = new JButton("保存到 print.properties");
+        saveBtn.addActionListener(this::onSaveConfig);
+        JButton reloadBtn = new JButton("Reload(不改文件)");
+        reloadBtn.addActionListener(e -> {
+            Printer.reload();
+            refreshEnv();
+            log("已 reload 配置");
+        });
+        JButton openDialogBtn = new JButton("打开自带 UI (PrinterSettingDialog)");
+        openDialogBtn.addActionListener(e -> {
+            try {
+                // 反射调用避免硬引用(jar 里的 dialog 可能签名有版本差异)
+                Class<?> cls = Class.forName("com.mes.print.ui.PrinterSettingDialog");
+                Object dlg = cls.getConstructor(java.awt.Frame.class).newInstance(this);
+                cls.getMethod("setVisible", boolean.class).invoke(dlg, true);
+                refreshEnv();
+            } catch (Throwable ex) {
+                log("[!] 无法打开 PrinterSettingDialog: " + ex.getMessage());
+            }
+        });
+        btns.add(saveBtn);
+        btns.add(reloadBtn);
+        btns.add(openDialogBtn);
+        p.add(btns, c);
+
+        return p;
+    }
+
+    private JPanel buildCardPanel() {
+        JPanel p = new JPanel(new GridBagLayout());
+        p.setBorder(new TitledBorder("测试流转卡(HP,走 PrintHelper.printFlowCard)"));
+        GridBagConstraints c = gbc();
+
+        cardSnField = new JTextField("+KB24IS3031T721000201");
+        cardSteelField = new JTextField("GY100000LX");
+
+        c.gridx = 0; c.gridy = 0; p.add(new JLabel("SN:"), c);
+        c.gridx = 1; c.weightx = 1; p.add(cardSnField, c);
+
+        c.gridx = 0; c.gridy = 1; c.weightx = 0; p.add(new JLabel("SteelSN:"), c);
+        c.gridx = 1; c.weightx = 1; p.add(cardSteelField, c);
+
+        c.gridx = 0; c.gridy = 2; c.gridwidth = 2; c.weightx = 1;
+        JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        JButton doIt = new JButton("打印流转卡");
+        doIt.addActionListener(this::onPrintCard);
+        btns.add(doIt);
+        p.add(btns, c);
+        return p;
+    }
+
+    private JPanel buildLabelPanel() {
+        JPanel p = new JPanel(new GridBagLayout());
+        p.setBorder(new TitledBorder("测试标签(TSC,走 PrintHelper.printLabel)"));
+        GridBagConstraints c = gbc();
+
+        labelTitleField = new JTextField("TEST LABEL");
+        labelSnField = new JTextField("TEST0001");
+        labelBarcodeField = new JTextField("TEST0001");
+        labelQrcodeField = new JTextField("TEST0001");
+        labelCopiesSpinner = new JSpinner(new SpinnerNumberModel(1, 1, 99, 1));
+
+        c.gridx = 0; c.gridy = 0; p.add(new JLabel("标题:"), c);
+        c.gridx = 1; c.weightx = 1; p.add(labelTitleField, c);
+
+        c.gridx = 0; c.gridy = 1; c.weightx = 0; p.add(new JLabel("SN:"), c);
+        c.gridx = 1; c.weightx = 1; p.add(labelSnField, c);
+
+        c.gridx = 0; c.gridy = 2; c.weightx = 0; p.add(new JLabel("条码:"), c);
+        c.gridx = 1; c.weightx = 1; p.add(labelBarcodeField, c);
+
+        c.gridx = 0; c.gridy = 3; c.weightx = 0; p.add(new JLabel("二维码:"), c);
+        c.gridx = 1; c.weightx = 1; p.add(labelQrcodeField, c);
+
+        c.gridx = 0; c.gridy = 4; c.weightx = 0; p.add(new JLabel("份数:"), c);
+        c.gridx = 1; c.weightx = 1;
+        JPanel wrap = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
+        wrap.add(labelCopiesSpinner);
+        p.add(wrap, c);
+
+        c.gridx = 0; c.gridy = 5; c.gridwidth = 2; c.weightx = 1;
+        JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        JButton doIt = new JButton("打印标签");
+        doIt.addActionListener(this::onPrintLabel);
+        btns.add(doIt);
+        p.add(btns, c);
+        return p;
+    }
+
+    private JPanel buildLogPanel() {
+        JPanel p = new JPanel(new BorderLayout());
+        p.setBorder(new TitledBorder("日志"));
+        p.setPreferredSize(new Dimension(700, 220));
+
+        logArea = new JTextArea();
+        logArea.setEditable(false);
+        logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+
+        JScrollPane sp = new JScrollPane(logArea);
+        p.add(sp, BorderLayout.CENTER);
+
+        JPanel bar = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+        JButton clr = new JButton("清空日志");
+        clr.addActionListener(e -> logArea.setText(""));
+        bar.add(clr);
+        p.add(bar, BorderLayout.SOUTH);
+        return p;
+    }
+
+    // ============== 事件处理 ==============
+
+    private void onSaveConfig(ActionEvent e) {
+        String type = (String) typeBox.getSelectedItem();
+        Object nameObj = nameBox.getSelectedItem();
+        String name = nameObj == null ? "" : nameObj.toString().trim();
+        try {
+            Printer.setPrinter(type, name);
+            log("已保存: type=" + type + " name=" + name);
+            refreshEnv();
+        } catch (Throwable ex) {
+            log("[!] 保存失败: " + ex.getMessage());
+        }
+    }
+
+    private void onPrintCard(ActionEvent e) {
+        String sn = cardSnField.getText().trim();
+        String steel = cardSteelField.getText().trim();
+        if (sn.isEmpty()) {
+            log("[!] SN 不能为空");
+            return;
+        }
+        // 提示模板来源(jar 里已内置,不存在会自动 fallback 到 classpath 资源)
+        File pdf = new File("config/liuzhuanka_template.pdf");
+        if (!pdf.exists()) {
+            log("[i] " + pdf.getAbsolutePath() + " 不存在,将使用 jar 内置模板");
+        }
+        // 打印前预检打印机状态,脱机的话直接提示,不阻塞 UI
+        checkPrinterStatus();
+
+        log("[card] 开始 sn=" + sn + " steelSn=" + steel);
+        new SwingWorker<Boolean, Void>() {
+            @Override protected Boolean doInBackground() {
+                return PrintHelper.printFlowCard(sn, steel.isEmpty() ? null : steel);
+            }
+            @Override protected void done() {
+                try {
+                    log("[card] 结果: " + (get() ? "成功(任务已提交到打印队列)" : "失败"));
+                } catch (Exception ex) {
+                    log("[card] 异常: " + ex.getMessage());
+                }
+            }
+        }.execute();
+    }
+
+    /** 通过 javax.print API 查一下目标打印机的状态标志 */
+    private void checkPrinterStatus() {
+        try {
+            String name = Printer.getCurrentName();
+            javax.print.PrintService svc = null;
+            for (javax.print.PrintService s : java.awt.print.PrinterJob.lookupPrintServices()) {
+                if (name != null && !name.isEmpty()
+                        && s.getName().toLowerCase().contains(name.toLowerCase())) {
+                    svc = s;
+                    break;
+                }
+            }
+            if (svc == null) {
+                log("[!] 未找到匹配 name=" + name + " 的打印机(先用「列出本机打印机」检查名字)");
+                return;
+            }
+            javax.print.attribute.standard.PrinterIsAcceptingJobs accept =
+                    svc.getAttribute(javax.print.attribute.standard.PrinterIsAcceptingJobs.class);
+            javax.print.attribute.standard.PrinterState state =
+                    svc.getAttribute(javax.print.attribute.standard.PrinterState.class);
+            javax.print.attribute.standard.PrinterStateReasons reasons =
+                    svc.getAttribute(javax.print.attribute.standard.PrinterStateReasons.class);
+            javax.print.attribute.standard.QueuedJobCount queued =
+                    svc.getAttribute(javax.print.attribute.standard.QueuedJobCount.class);
+
+            log("[state] 打印机=" + svc.getName()
+                    + " 接受任务=" + accept
+                    + " 状态=" + state
+                    + " 队列中=" + (queued == null ? "?" : queued.getValue())
+                    + " 原因=" + reasons);
+            if (accept != null
+                    && accept == javax.print.attribute.standard.PrinterIsAcceptingJobs.NOT_ACCEPTING_JOBS) {
+                log("[!] 打印机当前拒绝接受任务(脱机/暂停/驱动异常)——请先在 Windows 里把它上线");
+            }
+        } catch (Throwable t) {
+            log("[!] 查打印机状态失败: " + t.getMessage());
+        }
+    }
+
+    private void onPrintLabel(ActionEvent e) {
+        // 直接用底层 Printer + LabelData,参数更清晰
+        LabelData d = new LabelData();
+        d.setTitle(labelTitleField.getText().trim());
+        d.setSn(labelSnField.getText().trim());
+        d.setBarcode(labelBarcodeField.getText().trim());
+        d.setQrcode(labelQrcodeField.getText().trim());
+        d.setCopies((Integer) labelCopiesSpinner.getValue());
+        if (d.getSn().isEmpty()) {
+            log("[!] SN 不能为空");
+            return;
+        }
+        log("[label] 开始 sn=" + d.getSn() + " copies=" + d.getCopies());
+        new SwingWorker<Void, Void>() {
+            @Override protected Void doInBackground() {
+                try {
+                    Printer.printLabel(d);
+                    log("[label] 已发送到打印机");
+                } catch (PrinterException ex) {
+                    log("[label] 打印异常: " + ex.getMessage());
+                } catch (Throwable ex) {
+                    log("[label] 异常: " + ex.getMessage());
+                }
+                return null;
+            }
+        }.execute();
+    }
+
+    // ============== 辅助 ==============
+
+    private void refreshEnv() {
+        String type, name;
+        try { type = Printer.getCurrentType(); } catch (Throwable t) { type = "?(" + t.getMessage() + ")"; }
+        try { name = Printer.getCurrentName(); } catch (Throwable t) { name = "?"; }
+
+        File cfg = new File("config/print.properties");
+        File pdf = new File("config/liuzhuanka_template.pdf");
+
+        String pdfHint = pdf.exists() ? "存在=true" : "存在=false(将用 jar 内置模板)";
+        String html = "<html>"
+                + "工作目录: " + esc(new File(".").getAbsolutePath()) + "<br>"
+                + "Java 版本: " + esc(System.getProperty("java.version")) + "<br>"
+                + "配置文件: " + esc(cfg.getAbsolutePath()) + "  存在=" + cfg.exists() + "<br>"
+                + "流转卡模板: " + esc(pdf.getAbsolutePath()) + "  " + pdfHint + "<br>"
+                + "当前配置: <b>type=" + esc(type) + "  name=" + esc(name) + "</b>"
+                + "</html>";
+        envLabel.setText(html);
+
+        // 把当前配置同步到编辑控件(不触发保存)
+        if (typeBox != null && type != null) {
+            for (int i = 0; i < typeBox.getItemCount(); i++) {
+                if (type.equalsIgnoreCase(typeBox.getItemAt(i))) {
+                    typeBox.setSelectedIndex(i);
+                    break;
+                }
+            }
+        }
+        if (nameBox != null && name != null && !name.isEmpty()) {
+            nameBox.setSelectedItem(name);
+        }
+    }
+
+    private void refreshPrinterList() {
+        try {
+            List<String> names = Printer.listAvailablePrinters();
+            String cur = nameBox.getSelectedItem() == null ? "" : nameBox.getSelectedItem().toString();
+            nameBox.removeAllItems();
+            for (String n : names) nameBox.addItem(n);
+            if (!cur.isEmpty()) nameBox.setSelectedItem(cur);
+            log("发现打印机 " + names.size() + " 台");
+        } catch (Throwable t) {
+            log("[!] 列打印机失败: " + t.getMessage());
+        }
+    }
+
+    private void log(String msg) {
+        String line = new SimpleDateFormat("HH:mm:ss").format(new Date()) + " " + msg;
+        SwingUtilities.invokeLater(() -> {
+            logArea.append(line + "\n");
+            logArea.setCaretPosition(logArea.getDocument().getLength());
+        });
+    }
+
+    /** 把 System.out / System.err 抄一份到日志区(保留原控制台输出) */
+    private void redirectSystemStreams() {
+        System.setOut(new PrintStream(new LoggingStream(System.out, false), true));
+        System.setErr(new PrintStream(new LoggingStream(System.err, true), true));
+    }
+
+    /** 按字节缓存,遇到换行才用平台默认编码 decode,避免中文两字节被拆成两个 char */
+    private class LoggingStream extends java.io.OutputStream {
+        private final java.io.OutputStream orig;
+        private final boolean err;
+        private final java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream(256);
+        LoggingStream(java.io.OutputStream orig, boolean err) { this.orig = orig; this.err = err; }
+        @Override public void write(int b) throws java.io.IOException {
+            orig.write(b);
+            if (b == '\n') {
+                String s = new String(buf.toByteArray()); // 平台默认编码
+                buf.reset();
+                log((err ? "[stderr] " : "") + s);
+            } else if (b != '\r') {
+                buf.write(b);
+            }
+        }
+    }
+
+    private static GridBagConstraints gbc() {
+        GridBagConstraints c = new GridBagConstraints();
+        c.fill = GridBagConstraints.HORIZONTAL;
+        c.insets = new Insets(3, 6, 3, 6);
+        c.anchor = GridBagConstraints.WEST;
+        return c;
+    }
+
+    private static String esc(String s) {
+        if (s == null) return "";
+        return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
+    }
+
+    public static void main(String[] args) {
+        try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Throwable ignored) {}
+        SwingUtilities.invokeLater(() -> new PrintTest().setVisible(true));
+    }
+}