一文解读,如何让Java控制台应用程序变得简单
介绍
关于的控制台应用程序。我一直都在写这些程序,为了创建一次性测试来探索新的编码理念。我甚至快速的串起了基于菜单的测试远程设备通信和命令集的测试应用程序,其中命令的数量可以在产品开发周期中增长。当然,这样做的好处是,为了增加应用程序,只需要在代码中添加一个菜单项和一个事件处理程序。这比在一个已经很拥挤的Windows窗体应用程序上挤出更多的字段要好得多。有时候,你不知道你拥有什么,直到它消失。

今年我决定攻克Java,感受一下这门语言工具。我通常会把写的应用程序翻译成新的语言,在遇到棘手的问题的时候,将这些问题解决掉,以此来学习一门新的(对我来说)语言。我感到惊讶的是,许多在C#中认为理所当然的事情,在Java中却变得相当困难;例如,控制台应用程序。
我选择移植到Java的应用程序,恰好是一个基于控制台的菜单驱动的应用程序。一旦我弄清楚如何用Java类接口和嵌套来代替C#的委托去获得回调行为,那么移植菜单代码就变得相当简单了。真正令人头疼的是应用程序的菜单代码在实际的Windows控制台实例中的性能。
Java应用程序可以利用STDIN、STDOUT和STDERR流,但并不拥有显示这些流的控制台实例。由于Java的目标是广泛的操作系统,所以它不包括对清除屏幕的支持。我尝试了一些工作方法来让我的应用程序按照预期的方式运行,但没有可接受的结果。
在我准备放弃时,我仍坚持采取了不同的方法,想在一个JavaGUI部件中复制控制台功能。结果发现这样做更有前途。有不少将STDOUT和STDERR重定向到JtextArea的例子,但我无法找到一个成功地将键盘输入复制到STDIN的例子。我找到的是来自其他开发者的大量聊天,表明他们对这样一个组件的渴望。
我开始测试找到的各种代码解决方案和代码段,并确定了一个特别强大的例子。然后,我添加了一些代码来管键盘字符到STDIN,添加了一个公共的clear()方法,以及一些属性来定制控制台的外观和感觉。最后,我想出了如何为JavaConsole创建一个自定义的闪烁的块状光标来完善它。
如何使用这个Java控制台
最起码,在你的项目中使用这个JavaConsole所需要的就是声明一个JavaConsole类的实例。
public static void main(String[] args) { new JavaConsole(); scanner = new Scanner(System.in); System.out.println("Hello World!"); scanner.nextLine(); System.exit(0); }
下面是一个如何定制Java控制台外观的例子。
public static void main(String[] args){ Scanner scanner = new Scanner(System.in); JavaConsole console = new JavaConsole(); //Customize the console text field console.setBackground(Color.WHITE); console.setForeground(Color.BLACK); console.setFont(new Font ("Ariel", Font.BOLD, 12)); //Customize the console frame console.setTitle("Hello World Program"); Image im = Toolkit.getDefaultToolkit().getImage("../images/MyIcon.png"); console.setIconImage(im); System.out.println("Hello World!"); scanner.nextLine(); System.exit(0);
下面是一个使用JavaConsole.clear()方法在文本块之间清除屏幕的例子。
public static void main(String[] args){ Scanner scanner = new Scanner(System.in); JavaConsole console = new JavaConsole(); System.out.println( "It was a dark and stormy night; the rain fell in torrents, except at " + "occasional intervals, when it was checked by a violent gust of wind " + "which swept up the streets (for it is in London that our scene lies), " + "rattling along the house-tops, and fiercely agitating the scanty flame " + "of the lamps that struggled against the darkness. Through one of the " + "obscurest quarters of London, and among haunts little loved by the " + "gentlemen of the police, a man, evidently of the lowest orders, was " + "wending his solitary way."); scanner.nextLine(); console.clear(); System.out.println( "He stopped twice or thrice at different shops and houses of a description " + "correspondent with the appearance of the quartier in which they were situated, " + "and tended inquiry for some article or another which did not seem easily " + "to be met with. All the answers he received were couched in the negative; " + "and as he turned from each door he muttered to himself, in no very elegant " + "phraseology, his disappointment and discontent. At length, at one house, " + "the landlord, a sturdy butcher, after rendering the same reply the inquirer " + "had hitherto received, added, \"But if this vill do as vell, Dummie, it is " + "quite at your sarvice!\""); scanner.nextLine(); System.exit(0); }
最后,这里是一个全面的JavaConsole自定义功能列表。
console.getBackground(); console.getForeground(); console.getFont(); console.setBackground(Color); console.setForeground(Color); console.setFont(Font); console.setTitle(title); console.setIconImage(Image); console.clear();
如何给控制台布线
首先我们需要做的是将STDOUT和STDERR重定向到我们的JtextArea。我开始使用的代码类是通过以下方式实现的。
private final PipedInputStream pin=new PipedInputStream(); private final PipedInputStream pin2=new PipedInputStream();
PipedOutputStream pout=new PipedOutputStream(this.pin); PipedOutputStream pout2=new PipedOutputStream(this.pin2);
System.setOut(new PrintStream(pout,true)); System.setErr(new PrintStream(pout2,true));
//Declarationsprivate Thread reader;private Thread reader2;//In the class constructorreader=new Thread(this); reader.setDaemon(true); reader.start(); reader2=new Thread(this); reader2.setDaemon(true); reader2.start();
public synchronized void run(){ try { while (Thread.currentThread()==reader) { try { this.wait(100);}catch(InterruptedException ie) {} if (pin.available()!=0) { String input=this.readLine(pin); textArea.append(input); textArea.setCaretPosition(textArea.getDocument().getLength()); //DWM 02-07-2012 } if (quit) return; } while (Thread.currentThread()==reader2) { try { this.wait(100);}catch(InterruptedException ie) {} if (pin2.available()!=0) { String input=this.readLine(pin2); textArea.append(input); textArea.setCaretPosition(textArea.getDocument().getLength()); //DWM 02-07-2012 } if (quit) return; } } catch (Exception e) { textArea.append("\nConsole reports an Internal error."); textArea.append("The error is: "+e); textArea.setCaretPosition(textArea.getDocument().getLength()); //DWM 02-07-2012 } }
1.建立一些管道式输入流。
2.将输入流挂到相关的输出流上。
3.将系统STDOUT和STDERR重定向到这些输出流。
4.旋转一些线程从管道输入流中读取。
5.监控STDOUT和STDERR将文本追加到JtextArea。
现在我们已经将STDOUT和STDERR管道化到JtextArea,我们需要监控键盘输入到JtextArea,并将字符复制到STDIN。我修改了原来的代码,如下所示。
private final PipedOutputStream pout3=new PipedOutputStream();
System.setIn(new PipedInputStream(this.pout3));
textArea.addKeyListener(new KeyListener() { public void keyPressed(KeyEvent e) {} public void keyReleased(KeyEvent e) {} public void keyTyped(KeyEvent e) { try { pout3.write(e.getKeyChar()); } catch (IOException ex) {} }});
1.建立一个管道输出流,用于读取键盘输入。
2.将输出流与相关联的输入流挂钩,并将STDIN重定向到该输入流。
3.3.添加一个KeyListener,并在键盘输入到达时将其复制出来。
制作一个自定义的块状卡口
JtextArea默认的caret是一个竖条,但我希望提示能更突出一些。原来这个功能是可以自定义的,我找到了一些例子来说明如何做到这一点。参考Acustomcaret类[^]和Fanciercustomcaret类[^]来了解它是怎么做的。下面是我的闪光块caret的代码。
public class BlockCaret extends DefaultCaret { private static final long serialVersionUID = 1L; /** * @brief Class Constructor */ public BlockCaret() { setBlinkRate(500); // half a second } /* (non-Javadoc) * @see javax.swing.text.DefaultCaret#damage(java.awt.Rectangle) */ protected synchronized void damage(Rectangle r) { if (r == null) return; // give values to x,y,width,height (inherited from java.awt.Rectangle) x = r.x; y = r.y; height = r.height; // A value for width was probably set by paint(), which we leave alone. // But the first call to damage() precedes the first call to paint(), so // in this case we must be prepared to set a valid width, or else // paint() // will receive a bogus clip area and caret will not get drawn properly. if (width <= 0) width = getComponent().getWidth(); repaint(); //Calls getComponent().repaint(x, y, width, height) to erase repaint(); // previous location of caret. Sometimes one call isn't enough. } /* (non-Javadoc) * @see javax.swing.text.DefaultCaret#paint(java.awt.Graphics) */ public void paint(Graphics g) { JTextComponent comp = getComponent(); if (comp == null) return; int dot = getDot(); Rectangle r = null; char dotChar; try { r = comp.modelToView(dot); if (r == null) return; dotChar = comp.getText(dot, 1).charAt(0); } catch (BadLocationException e) { return; } if(Character.isWhitespace(dotChar)) dotChar = '_'; if ((x != r.x) || (y != r.y)) { // paint() has been called directly, without a previous call to // damage(), so do some cleanup. (This happens, for example, when // the text component is resized.) damage(r); return; } g.setColor(comp.getCaretColor()); g.setXORMode(comp.getBackground()); // do this to draw in XOR mode width = g.getFontMetrics().charWidth(dotChar); if (isVisible()) g.fillRect(r.x, r.y, width, r.height); } }
菜单系统
在这个演示中,我加入了我非常喜欢使用的菜单系统,它是CodeProject文章[^]中介绍的易于使用的控制台菜单的Java移植。它是CodeProject文章[^]中介绍的易用的控制台菜单的Java移植。正如我在前言中提到的,在Java中使用回调与C#有些不同,Menu类很好地说明了这一点。下面是在C#中如何完成的。
delegate void MenuCallback();
private static void HandleItem1 { Console.WriteLine("You chose item 1.\n"); Console.Read(); }private static void HandleItem2 { Console.WriteLine("You chose item 2.\n"); Console.Read(); }
menu = new Menu(); menu.Add("Item 1", new MenuCallback(HandleItem1)); menu.Add("Item 2", new MenuCallback(HandleItem2)); menu.Show();
1.我声明一个委托(一个函数指针)。
2.接下来,我为菜单选择创建一些事件处理程序
3.然后,我将它们添加到菜单中,就像这样。
Java不允许传递函数指针,因此也不允许传递委托。相反,这种行为被限制在类型化的对象指针上,因此,类接口就成了相当于委托人的Java。下面是Java中的做法。
public interface MenuCallback extends EventListener { public void Invoke(); }
private static void HandleItem1 { System.out.println("You chose item 1.\n."); scanner.nextLine(); }private static void HandleItem2 { System.out.println("You chose item 2.\n."); scanner.nextLine(); }
Menu menu = new Menu(console); menu.add("Item 1", new MenuCallback() { public void Invoke() { HandleItem1(); } }); menu.add("Item 2", new MenuCallback() { public void Invoke() { HandleItem2(); } }); menu.show();
1.我声明一个接口(一个类型化的对象指针)
2.接下来,我为菜单选择创建一些事件处理程序
3.然后我把它们添加到菜单中,像这样
这就比较啰嗦了,因为我们实际上要把我们的函数指针封装在一个类中。