重定向System.out和System.err到JTextPane,分别用黑色红色显示

时间:2023-02-15 17:45:55
把 System.out 和 System.err 重定向到 JTextArea 的做法在网上能找到不少,由于 JTextArea 不能用不同的字体分别显示内容。但我还是希望能象 Eclipse 控制台那样,标准输出为黑色,错误信息为红色,于是选择了 JTextPane 作为输出目的地。线程之间通信息用到了 PipedInputStream 、PipedOutputStream 和 SwingUtilities.invokeLater(new Runnable()。

自定义了一个 JScrollPane,类名为 ConsolePane,写成的单例类;使用时只需要在你的面板上加上 ConsolePane组件,例如: getContentPane().add(ConsolePane.getInstance(), BorderLayout.CENTER);

界面截图(黑色和红色分别显示 System.out 和 System.err 定向的输出内容):

重定向System.out和System.err到JTextPane,分别用黑色红色显示


像控制台那样也设置了最大输出缓冲行数,当超过一定行数后会自动把前面的若干行删除,防止内存占用过大。

Log4J 与 ConsolePane

作为自己应用程序的输出控制台还是不错的。有个问题,如果要捕获 Log4J 的输出必须选择 1.2.13 或以上的版本的 Log4J,并在 log4j.properties 设置

 log4j.appender.console.follow = true      #沿用 System.setOut() 或 System.setErr() 设置,默认为 false

在 1.2.13 以前的 Log4J 的 ConsoleAppender 中没有 follow 属性,Lo4J 不支持 System.out 和 System.err 的分别输出,你可以在 log4j.peroperties 中设置

lo4j.appender.console.target = System.out   #或 System.err,默认为 System.out

Log4J 输出信息到控制台要么全到 System.out,要么全到 System.err,也就是在 ConsolePane 中没法分不同颜色显示 log.error() 和 log.debug() 信息。

这个问题,可以改善的,比如 Eclipse 中就不依赖于 log4j.properties 中怎么设置的。同样在 Eclipse 中也没法让 error 和 debug 信息分不同颜色信息,除非改写 Log4J 的 ConsoleAppender 才能分颜色显示。


下面分别列出 ConsolePane 的实现代码和一个测试代码 TestConsolePane

1. ConsolePane 代码
  1. package com.unmi;   
  2.   
  3. import java.awt.Color;   
  4. import java.awt.Dimension;   
  5. import java.io.IOException;   
  6. import java.io.PipedInputStream;   
  7. import java.io.PipedOutputStream;   
  8. import java.io.PrintStream;   
  9.   
  10. import javax.swing.JScrollPane;   
  11. import javax.swing.JTextPane;   
  12. import javax.swing.SwingUtilities;   
  13. import javax.swing.text.AbstractDocument;   
  14. import javax.swing.text.BadLocationException;   
  15. import javax.swing.text.Document;   
  16. import javax.swing.text.Element;   
  17. import javax.swing.text.Style;   
  18. import javax.swing.text.StyleConstants;   
  19. import javax.swing.text.StyledDocument;   
  20.   
  21. /**  
  22.  * @author Unmi  
  23.  */  
  24. public class ConsolePane extends JScrollPane {   
  25.     private PipedInputStream piOut;   
  26.     private PipedInputStream piErr;   
  27.     private PipedOutputStream poOut;   
  28.     private PipedOutputStream poErr;   
  29.   
  30.     private JTextPane textPane = new JTextPane();   
  31.   
  32.     private static ConsolePane console = null;   
  33.   
  34.     public static synchronized ConsolePane getInstance() {   
  35.         if (console == null) {   
  36.             console = new ConsolePane();   
  37.         }   
  38.         return console;   
  39.     }   
  40.   
  41.     private ConsolePane() {   
  42.   
  43.         setViewportView(textPane);   
  44.   
  45.         piOut = new PipedInputStream();   
  46.         piErr = new PipedInputStream();   
  47.         try {   
  48.             poOut = new PipedOutputStream(piOut);   
  49.             poErr = new PipedOutputStream(piErr);   
  50.         } catch (IOException e) {   
  51.         }   
  52.   
  53.         // Set up System.out   
  54.         System.setOut(new PrintStream(poOut, true));   
  55.   
  56.         // Set up System.err   
  57.         System.setErr(new PrintStream(poErr, true));   
  58.   
  59.         textPane.setEditable(true);   
  60.         setPreferredSize(new Dimension(640120));   
  61.   
  62.         // Create reader threads   
  63.         new ReaderThread(piOut).start();   
  64.         new ReaderThread(piErr).start();   
  65.     }   
  66.   
  67.     /**  
  68.      * Returns the number of lines in the document.  
  69.      */  
  70.     public final int getLineCount() {   
  71.         return textPane.getDocument().getDefaultRootElement().getElementCount();   
  72.     }   
  73.   
  74.     /**  
  75.      * Returns the start offset of the specified line.  
  76.      *   
  77.      * @param line  
  78.      *            The line  
  79.      * @return The start offset of the specified line, or -1 if the line is  
  80.      *         invalid  
  81.      */  
  82.     public int getLineStartOffset(int line) {   
  83.         Element lineElement = textPane.getDocument().getDefaultRootElement()   
  84.                 .getElement(line);   
  85.         if (lineElement == null)   
  86.             return -1;   
  87.         else  
  88.             return lineElement.getStartOffset();   
  89.     }   
  90.   
  91.     public void replaceRange(String str, int start, int end) {   
  92.         if (end < start) {   
  93.             throw new IllegalArgumentException("end before start");   
  94.         }   
  95.         Document doc = textPane.getDocument();   
  96.         if (doc != null) {   
  97.             try {   
  98.                 if (doc instanceof AbstractDocument) {   
  99.                     ((AbstractDocument) doc).replace(start, end - start, str,   
  100.                             null);   
  101.                 } else {   
  102.                     doc.remove(start, end - start);   
  103.                     doc.insertString(start, str, null);   
  104.                 }   
  105.             } catch (BadLocationException e) {   
  106.                 throw new IllegalArgumentException(e.getMessage());   
  107.             }   
  108.         }   
  109.     }   
  110.   
  111.     class ReaderThread extends Thread {   
  112.         PipedInputStream pi;   
  113.   
  114.         ReaderThread(PipedInputStream pi) {   
  115.             this.pi = pi;   
  116.         }   
  117.   
  118.         public void run() {   
  119.             final byte[] buf = new byte[1024];   
  120.   
  121.             while (true) {   
  122.                 try {   
  123.                     final int len = pi.read(buf);   
  124.                     if (len == -1) {   
  125.                         break;   
  126.                     }   
  127.                     SwingUtilities.invokeLater(new Runnable() {   
  128.                         public void run() {   
  129.                             try {   
  130.   
  131.                                 StyledDocument doc = (StyledDocument) textPane   
  132.                                         .getDocument();   
  133.   
  134.                                 // Create a style object and then set the style   
  135.                                 // attributes   
  136.                                 Style style = doc.addStyle("StyleName"null);   
  137.   
  138.                                 Color foreground = pi == piOut ? Color.BLACK   
  139.                                         : Color.RED;   
  140.                                 // Foreground color   
  141.                                 StyleConstants.setForeground(style, foreground);   
  142.   
  143.                                 // Append to document   
  144.                                 String outstr = new String(buf, 0, len);   
  145.                                 doc.insertString(doc.getLength(), outstr, style);   
  146.   
  147.                             } catch (BadLocationException e) {   
  148.                                 // e.printStackTrace();   
  149.                             }   
  150.   
  151.                             // Make sure the last line is always visible   
  152.                             textPane.setCaretPosition(textPane.getDocument()   
  153.                                     .getLength());   
  154.   
  155.                             // Keep the text area down to a certain line count   
  156.                             int idealLine = 150;   
  157.                             int maxExcess = 50;   
  158.   
  159.                             int excess = getLineCount() - idealLine;   
  160.                             if (excess >= maxExcess) {   
  161.                                 replaceRange(""0, getLineStartOffset(excess));   
  162.                             }   
  163.                         }   
  164.                     });   
  165.                 } catch (IOException e) {   
  166.                     // e.printStackTrace();   
  167.                 }   
  168.             }   
  169.         }   
  170.     }   
  171. }   

当前还有一些 Bug 未解决:
1. final int len = pi.read(buf); 开始时会产生 java.io.IOException: Write end dead 异常,好像这还是 JDK 本身的 Bug,但不影响使用
2. 输出时有时会缺几个字母,或产生空行
3. 因为实际工作线程来输出,所以输出顺序有时不能保证

谁有兴趣的话,可以进一步研究一番;可对这个类再做润色,如增加右键菜单,可拷贝、很剪切、清除所有输出、不自动滚动。就像 Eclipse 的控制台那样。

TestConsolePane 代码
  1. package com.unmi;   
  2.   
  3. import java.awt.BorderLayout;   
  4. import java.awt.event.ActionEvent;   
  5. import java.awt.event.ActionListener;   
  6. import java.util.Date;   
  7. import java.util.Random;   
  8.   
  9. import javax.swing.JButton;   
  10. import javax.swing.JFrame;   
  11.   
  12. /**  
  13.  * @author Unmi  
  14.  */  
  15. public class TestConsolePane extends JFrame {   
  16.     public TestConsolePane() {   
  17.   
  18.         setTitle("Redirect System.out and System.error Test Application");   
  19.         setSize(640240);   
  20.         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);   
  21.   
  22.         getContentPane().add(ConsolePane.getInstance(), BorderLayout.CENTER);   
  23.         JButton button = new JButton("Click Me to Output Message");   
  24.         getContentPane().add(button, BorderLayout.SOUTH);   
  25.   
  26.         button.addActionListener(new ActionListener() {   
  27.             public void actionPerformed(ActionEvent e) {   
  28.                 Random random = new Random();   
  29.                 int num = random.nextInt(10);   
  30.                 String msg = ":  Hello Unmi, Redirect "  
  31.                         + ((num % 2 == 1) ? "/"System.out/"" : "/"System.err/"")   
  32.                         + " to ConsolePane, Today: ";   
  33.                 if (num % 2 == 1)   
  34.                     System.out.println(num + msg + new Date());   
  35.                 else  
  36.                     System.err.println(num + msg + new Date());   
  37.             }   
  38.         });   
  39.   
  40.         setVisible(true);   
  41.     }   
  42.   
  43.     public static void main(String[] args) {   
  44.         new TestConsolePane();   
  45.     }   
  46. }  

参考:1. e988. Implementing a Console Window with a JTextArea Component
        2. e989. Inserting Styled Text in a JTextPane Component
        3. e1007. Setting the Font and Color of Text in a JTextPane Using Styles
        4. JEdit syntax 中类 JEditTextArea 的实现