把 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 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
package com.unmi; import java.awt.Color; import java.awt.Dimension; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.PrintStream; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Element; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; /** * @author Unmi */ public class ConsolePane extends JScrollPane { private PipedInputStream piOut; private PipedInputStream piErr; private PipedOutputStream poOut; private PipedOutputStream poErr; private JTextPane textPane = new JTextPane(); private static ConsolePane console = null; public static synchronized ConsolePane getInstance() { if (console == null) { console = new ConsolePane(); } return console; } private ConsolePane() { setViewportView(textPane); piOut = new PipedInputStream(); piErr = new PipedInputStream(); try { poOut = new PipedOutputStream(piOut); poErr = new PipedOutputStream(piErr); } catch (IOException e) { } // Set up System.out System.setOut(new PrintStream(poOut, true)); // Set up System.err System.setErr(new PrintStream(poErr, true)); textPane.setEditable(true); setPreferredSize(new Dimension(640, 120)); // Create reader threads new ReaderThread(piOut).start(); new ReaderThread(piErr).start(); } /** * Returns the number of lines in the document. */ private final int getLineCount() { return textPane.getDocument().getDefaultRootElement().getElementCount(); } /** * Returns the start offset of the specified line. * * @param line The line * @return The start offset of the specified line, or -1 if the line is * invalid */ private int getLineStartOffset(int line) { Element lineElement = textPane.getDocument().getDefaultRootElement() .getElement(line); if (lineElement == null) return -1; else return lineElement.getStartOffset(); } private void replaceRange(String str, int start, int end) { if (end < start) { throw new IllegalArgumentException("end before start"); } Document doc = textPane.getDocument(); if (doc != null) { try { if (doc instanceof AbstractDocument) { ((AbstractDocument) doc).replace(start, end - start, str, null); } else { doc.remove(start, end - start); doc.insertString(start, str, null); } } catch (BadLocationException e) { throw new IllegalArgumentException(e.getMessage()); } } } class ReaderThread extends Thread { private PipedInputStream pi; ReaderThread(PipedInputStream pi) { this.pi = pi; } public void run() { final byte[] buf = new byte[1024]; while (true) { try { final int len = pi.read(buf); if (len == -1) { break; } SwingUtilities.invokeLater(new Runnable() { public void run() { try { StyledDocument doc = (StyledDocument) textPane .getDocument(); // Create a style object and then set the style // attributes Style style = doc.addStyle("StyleName", null); Color foreground = pi == piOut ? Color.BLACK : Color.RED; // Foreground color StyleConstants.setForeground(style, foreground); // Append to document String outstr = new String(buf, 0, len); doc.insertString(doc.getLength(), outstr, style); } catch (BadLocationException e) { // e.printStackTrace(); } // Make sure the last line is always visible textPane.setCaretPosition(textPane.getDocument() .getLength()); // Keep the text area down to a certain line count int idealLine = 150; int maxExcess = 50; int excess = getLineCount() - idealLine; if (excess >= maxExcess) { replaceRange("", 0, getLineStartOffset(excess)); } } }); } catch (IOException e) { // e.printStackTrace(); } } } } } |
当前还有一些 Bug 未解决:
1. final int len = pi.read(buf); 开始时会产生 java.io.IOException: Write end dead 异常,好像这还是 JDK 本身的 Bug,但不影响使用
2. 输出时有时会缺几个字母,或产生空行
3. 因为实际工作线程来输出,所以输出顺序有时不能保证
谁有兴趣的话,可以进一步研究一番;可对这个类再做润色,如增加右键菜单,可拷贝、很剪切、清除所有输出、不自动滚动。就像 Eclipse 的控制台那样。
TestConsolePane 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package com.unmi; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Date; import java.util.Random; import javax.swing.JButton; import javax.swing.JFrame; /** * @author Unmi */ public class TestConsolePane extends JFrame { public TestConsolePane() { setTitle("Redirect System.out and System.error Test Application"); setSize(640, 240); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); getContentPane().add(ConsolePane.getInstance(), BorderLayout.CENTER); JButton button = new JButton("Click Me to Output Message"); getContentPane().add(button, BorderLayout.SOUTH); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Random random = new Random(); int num = random.nextInt(10); String msg = ": Hello Unmi, Redirect " + ((num % 2 == 1) ? "\"System.out\"" : "\"System.err\"") + " to ConsolePane, Today: "; if (num % 2 == 1) System.out.println(num + msg + new Date()); else System.err.println(num + msg + new Date()); } }); setVisible(true); } public static void main(String[] args) { new TestConsolePane(); } } |
参考: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 Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
这个要赞!