获取文本控件的输出流

在 Swing 中,文本控件没有输出流!所谓的文本控件这里指派生自 JTextComponent 的控件,如 JTextField、JTextArea。但是,有时我们会有需要提取文本控件的输出流。通过向这个流写入文本,对应的文本控件上就会显示出来。比如,我们想把异常链给打印到控件上。Exception.printStackTrace() 方法只能接受 PrintStream 或 PrintWriter 参数。

既然控件没有流,那我们创建一个流。基于这样的思路:通过向流中写文本,在流的实现中将接收到的文本通过 JTextComponent.setText(String) 的方式写到控件上。一个简单的实现:
package ydcode.swing;

import java.awt.EventQueue;
import java.io.PrintStream;

import javax.swing.text.JTextComponent;

public class TextComponentPrintStream extends PrintStream {
	
	private JTextComponent target;
	
	private TextComponentPrintStream(final JTextComponent target) {
		super(System.out);
		
		if (target == null) throw new NullPointerException();
		
		this.target = target;
	}
	
	public static TextComponentPrintStream getTextComponentStream(JTextComponent target) {
		return new TextComponentPrintStream(target);
	}

	@Override
	public void write(byte[] buf, int off, int len) {
		final String msg = new String(buf, off, len);
		
		EventQueue.invokeLater(new Runnable() {

			@Override
			public void run() {
				target.setText(target.getText() + msg);
			}
			
		});
	}
	
}

这个实现参考了陈维的 将标准输出重定向到GUI

在 write()  方法中,我们设置控件的文本。第 13 行的 super(System.out) 是为了让基类的 PrintStream 正常工作,不报错。我们可以设置成任意一个流,super(anyStreamYouLike)。因为父类根本就没有机会向 System.out 写入哪怕一个字节。这里我们基于这样一个 PrintStream 的实现细节:所有的 print 和 write 方法最终都是调用  OutputStream.write(byte[] buf, int off, int len) 来实现的。

这么做有点 tricky,甚至有点邪恶。但是如果我们要自己搞一个流出来,这是我目前看到的唯一方法。这么写有一个问题。假设我们正在写一个遍历文件树的程序。每当找到一个文件就把它打印到多行文本框中。遍历的方法在单独的一个线程中执行。也许你认为这样程序就可以正常工作了。但是当文件树包含较多文件时(比如 C:),文本框基本上就不响应了。为什么呢?我们已经用单独的一个线程来进行遍历了,并没有占用主线程,界面是不应该当掉的呀!原因是当向流中写得太频繁时,会导致 setText() 的调用过于频繁而使 UI 界面假死。EventQueue 中充满了 setText() 的调用而使得其它的 UI 事件没有机会得到响应。解决的方法有二。一是在 UI 中调用这个流写数据时用 SwingWorker 类。这个类从 JDK 6 开始有的。二是改装一下这个流的实现,给它加一个缓冲。使得多次对 setText() 的调用合并为一个。下面是实现代码,
package ydcode.swing;

import java.awt.EventQueue;
import java.io.PrintStream;
import java.util.List;

import javax.swing.text.JTextComponent;

import sun.swing.AccumulativeRunnable;

public class TextComponentPrintStream extends PrintStream {
	
	private AccumulativeRunnable<String> doSetText;
	
	private TextComponentPrintStream(final JTextComponent target) {
		super(System.out);
		
		doSetText = new AccumulativeRunnable<String>() {

			@Override
			protected void run(List<String> textQueue) {
				if (textQueue.size() > 0) {
					StringBuilder sb = new StringBuilder();
					
					sb.append(target.getText());
					for (String str : textQueue) {
						sb.append(str);
					}
					
					target.setText(sb.toString());
				}
			}
			
		};
	}
	
	public static TextComponentPrintStream getTextComponentStream(JTextComponent target) {
		return new TextComponentPrintStream(target);
	}

	@Override
	public void write(byte[] buf, int off, int len) {
		final String msg = new String(buf, off, len);
		
		EventQueue.invokeLater(new Runnable() {

			@Override
			public void run() {
				doSetText.add(msg);
			}
			
		});
	}

}

这个实现更加 tricky。它参考了 JDK 中 SwingWorker 的实现。这里面用到了一个关键的类,sun.swing.AccumulativeRunnable。这个类可以在 rt.jar 中找到。你可以在 http://www.google.com/codesearch中找到它的源码和注释。也可以用 DJ 之类的反编译工具在 rt.jar 中查看它的源码。如果你比较懒,可以在这里看到: http://www.google.com/codesearch/p?hl=en#5nd3vJ4zpWY/src/share/classes/sun/swing/AccumulativeRunnable.java&q=AccumulativeRunnable。AccumulativeRunnable 可以使得多次对 Runnable  的调用放在一次执行。现在,我假设你正在看它的注释和源码……

Okay,现在你已经明白它的作用了。第一次调用 add() 方法会使得 submit() 被调用。submit() 将 run() 的调用放在了 Swing 的 EventQueue 中去排队列执行。而当 EventQueue 最终执行了 run() 时,这又将导致新一轮的循环。这样,我们就既不会让 EventQueue 太满导致界面假死,同时又能及时地更新 UI 了。

最后介绍一个更有意思的方法。它可以让你控制 setText() 被调用的最快频率。代码如下,
package ydcode.swing;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintStream;
import java.util.List;

import javax.swing.Timer;
import javax.swing.text.JTextComponent;

import sun.swing.AccumulativeRunnable;

public class TextComponentPrintStream extends PrintStream {
	
	private static AccumulativeRunnable<Runnable> doSubmit = new DoSubmitAccumulativeRunnable();
	
	private AccumulativeRunnable<String> doSetText;
	
	private TextComponentPrintStream(final JTextComponent target) {
		super(System.out);
		
		doSetText = new AccumulativeRunnable<String>() {

			@Override
			protected void run(List<String> textQueue) {
				if (textQueue.size() > 0) {
					StringBuilder sb = new StringBuilder();
					
					sb.append(target.getText());
					for (String str : textQueue) {
						sb.append(str);
					}
					
					target.setText(sb.toString());
				}
			}

			@Override
			protected void submit() {
				doSubmit.add(this);
			}
			
		};
	}
	
	public static TextComponentPrintStream getTextComponentStream(JTextComponent target) {
		return new TextComponentPrintStream(target);
	}

	@Override
	public void write(byte[] buf, int off, int len) {
		final String msg = new String(buf, off, len);
		
		EventQueue.invokeLater(new Runnable() {

			@Override
			public void run() {
				doSetText.add(msg);
			}
			
		});
	}
	
	private static class DoSubmitAccumulativeRunnable extends
			AccumulativeRunnable<Runnable> implements ActionListener {
			
		private final static int DELAY = 1000 / 5;

		@Override
		protected void run(List<Runnable> tasks) {
			for (Runnable task : tasks) {
				task.run();
			}
		}

		@Override
		protected void submit() {
			Timer timer = new Timer(DELAY, this);
			timer.setRepeats(false);
			timer.start();
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			run();
		}
		
	}

}

那个 DELAY 用来控制隔多少毫秒调用一次。由于本人表达能力有限,这个就不再解释了。如果你搞明白了 AccumulativeRunnable 的作用,那么理解这个应该不成问题。

你可能感兴趣的:(jdk,UI,swing,Google,sun)