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

把 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 定向的输出内容):


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

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 代码 
  1package com.unmi;
  2
  3import java.awt.Color;
  4import java.awt.Dimension;
  5import java.io.IOException;
  6import java.io.PipedInputStream;
  7import java.io.PipedOutputStream;
  8import java.io.PrintStream;
  9
 10import javax.swing.JScrollPane;
 11import javax.swing.JTextPane;
 12import javax.swing.SwingUtilities;
 13import javax.swing.text.AbstractDocument;
 14import javax.swing.text.BadLocationException;
 15import javax.swing.text.Document;
 16import javax.swing.text.Element;
 17import javax.swing.text.Style;
 18import javax.swing.text.StyleConstants;
 19import javax.swing.text.StyledDocument;
 20
 21/**
 22 * @author Unmi
 23 */
 24public 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(640, 120));
 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    private 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   The line
 78     * @return The start offset of the specified line, or -1 if the line is
 79     *         invalid
 80     */
 81    private int getLineStartOffset(int line) {
 82        Element lineElement = textPane.getDocument().getDefaultRootElement()
 83                .getElement(line);
 84        if (lineElement == null)
 85            return -1;
 86        else
 87            return lineElement.getStartOffset();
 88    }
 89
 90    private void replaceRange(String str, int start, int end) {
 91        if (end < start) {
 92            throw new IllegalArgumentException("end before start");
 93        }
 94        Document doc = textPane.getDocument();
 95        if (doc != null) {
 96            try {
 97                if (doc instanceof AbstractDocument) {
 98                    ((AbstractDocument) doc).replace(start, end - start, str,
 99                            null);
100                } else {
101                    doc.remove(start, end - start);
102                    doc.insertString(start, str, null);
103                }
104            } catch (BadLocationException e) {
105                throw new IllegalArgumentException(e.getMessage());
106            }
107        }
108    }
109
110    class ReaderThread extends Thread {
111        private PipedInputStream pi;
112
113        ReaderThread(PipedInputStream pi) {
114            this.pi = pi;
115        }
116
117        public void run() {
118            final byte[] buf = new byte[1024];
119
120            while (true) {
121                try {
122                    final int len = pi.read(buf);
123                    if (len == -1) {
124                        break;
125                    }
126                    SwingUtilities.invokeLater(new Runnable() {
127                        public void run() {
128                            try {
129
130                                StyledDocument doc = (StyledDocument) textPane
131                                        .getDocument();
132
133                                // Create a style object and then set the style
134                                // attributes
135                                Style style = doc.addStyle("StyleName", null);
136
137                                Color foreground = pi == piOut ? Color.BLACK
138                                        : Color.RED;
139                                // Foreground color
140                                StyleConstants.setForeground(style, foreground);
141
142                                // Append to document
143                                String outstr = new String(buf, 0, len);
144                                doc.insertString(doc.getLength(), outstr, style);
145
146                            } catch (BadLocationException e) {
147                                // e.printStackTrace();
148                            }
149
150                            // Make sure the last line is always visible
151                            textPane.setCaretPosition(textPane.getDocument()
152                                    .getLength());
153
154                            // Keep the text area down to a certain line count
155                            int idealLine = 150;
156                            int maxExcess = 50;
157
158                            int excess = getLineCount() - idealLine;
159                            if (excess >= maxExcess) {
160                                replaceRange("", 0, getLineStartOffset(excess));  
161                            }  
162                        }  
163                    });  
164                } catch (IOException e) {  
165                    // e.printStackTrace();  
166                }  
167            }  
168        }  
169    }  
170}  

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

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

TestConsolePane 代码 
 1package 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(640, 240);   
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 的实现 永久链接 https://yanbin.blog/system-out-system-err-jtextpane/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。