驾驭“野马”-- 探索Java SE 6的一些新特性

摘要

这篇文章介绍了Mustang(野马)——Sun Microsystems将在今年晚些时候正式发布的最新Java平台。Jeff Friesen探讨了Mustang有关控制台输入/输出和分区空间的新方法、用于启动画面以及与系统托盘交互的新API。

sun 将要在今年的晚些时候发布最新的Java平台(开发代号Mustang)。作为正式的Java平台,Standard Edition 6这个版本关注了几个重要的主题,例如兼容性和稳定性。有关完整的主题列表,参阅Java Specification Request 270,JSE 6的版本目录。

Mustang预期拥有的新特性包括(除了别的以外):
·        一个编译器API
·        控制台输入/输出(I/O)
·        一个启动画面API
·        众多的Java 2D性能改进
·        XML数字签名
·        一个系统托盘API
·        java.io.File类的分区空间方法
·        Java数据库连接(JDBC)4.0
·        公共注释(Common annotations)
·        脚本支持(Scripting)
·        一个用于XML的流(streaming)API
·        排序、过滤和加亮javax.swing.JTable内容的能力
·        Javadoc标记的更新
·        可编程操作网络属性(例如广播地址和子网掩码)
·        方便地打印javax.swing.text.JTextComponent的内容的能力

Mustang拥有远远超出一篇文章探讨范围的新特性,因此,本文只关注新特性的一小部份。确切地说,本文将讨论用于控制台输入/输出和分区空间的方法、用于启动画面以及与系统托盘交互的API。

警告
由于Mustang目前没有最终发布,一些特性还可能会被改变或者去掉。所以,当Sun公司最终发布Mustang的时候,本文中的一些代码可能会需要改动或者变得完全不相干了。

注意
我使用Sun公司的Java 2 SDK版本1.6.0-rc (build 62)创建和测试了本文的代码。底层平台是Microsoft Windows ME。

控制台输入/输出

在1997年5月,Sun收到了一个改进控制台输入/输出的增强请求(RFE)。申请人特别要求一种可以提示用户输入密码并且允许用户输入密码(任意长度)而不会在控制台显示出密码字符的方法。申请人指出,抽象窗口工具包(AWT)的setEchoChar()方法并不合用,因为它依赖于GUI的可用性,然而很多基于服务器的操作系统根本不使用GUI。

在2005年后期,Sun回应了RFE #4050435,为Mustang (build 57)添加了java.io.Console类。这个类所提供的方法可以访问与当前虚拟机相关联的基于字符的控制台设备。但是在调用这些方法之前,需要首先调用System的public static Console console()方法来获取一个Console对象。该方法将返回一个用来与控制台设备交互的Console对象,但是如果控制台设备不存在就会返回null,例如当你重定向标准输入或标准输出(或二者皆有)的时候。在调用System.console()来返回Console对象之后,下面的一段代码将检查返回的Console实例是否为null来测定控制台设备是否存在:

Console console = System.console ();
if (console == null)
{
    System.err.println ("Console not available");
    return;
}


假设控制台设备是存在的,你可以从控制台输入流读取密码和整行的字符,还可以向控制台输出流写入字符。为了读取密码(而不会将密码字符显示到控制台输出流),你必须调用Console的两个readPassword()方法之一。这两个方法不允许换行符作为密码的一部分,如果达到了控制台输入流的字符数目限制,他们将返回null。

举例来说,你可以调用public char [] readPassword(String fmt, Object... args)来提示用户输入密码,其中提示是由java.util.Formatter-类型格式化字符串(fmt)和它的变量表(args)来描述的,然后从一个字符数组中返回用户选择的密码。下面的代码段反复调用readPassword(String fmt, Object... args)来提示用户输入密码,直到用户输入了一个字符长度不小于MIN_PWD_LEN的密码为止。
char [] pwd;
do
{

   pwd = console.readPassword ("Enter password (%d characters min):",
                MIN_PWD_LEN);
}
while (pwd.length < MIN_PWD_LEN);


在密码被存储到pwd之后,就可以按需使用了。然而,出于安全性考虑,在不需要使用密码的时候,应该将pwd清零。
除了readPassword()方法之外,Console提供了两个readLine()方法以便于从控制台读取一整行的字符,并且将这些字符(不包括换行符)存储在一个String中。如果达到了控制台输入流的字符数目限制,这两个方法都将返回null。
举例来说,你可以在不提示用户的情况下,调用public String readLine()来返回一行字符。下列代码段演示了这个方法:

关于两个readPassword()方法和两个readLine()方法有一些有趣的事。当这些方法遇到输入/输出错误时,它们不抛出java.io.IOException对象(例如会被System.in.read()抛出),而是抛出一个java.io.IOError对象。由于IOError是Error的子类,你无需像捕捉IOException那样来捕捉这个对象。

向控制台输出流输出字符的配套方法是public Console format(String fmt, Object... args)和作用相同的public Console printf(String fmt, Object... args)方法,后者内部调用了format()。下列代码段演示了printf():
console.printf ("%s", input);


输出字符之后,format()——和printf()(按扩展名)——自动转储清除控制台输出流。

注意
控制台对象与唯一的一个java.io.Reader对象和唯一的一个java.io.PrintWriter对象相关联。调用控制台的public Reader reader()方法可以返回Reader。例如你可以把Reader传递给java.util.Scanner的一个构造函数来对控制台输入流进行复杂的解析。调用控制台的public PrintWriter writer()方法可以获取PrintWriter。然后你可以调用各种各样有用的方法来输出不同类型的数据到控制台上。为方便起见,控制台提供了一个public void flush()方法来调用PrinterWriter的flush()方法。

让我们用关于控制台输入/输出的知识来做一个实际应用——数据库访问。最终,我建立了一个Microsoft Access sales.mdb销售数据库,它随着本文的代码一同发布(可以在资源下载)。该数据库包含一个单独的territories表,它有两列:Name代表售货员的名字,Territory代表售货员可以合法地出售产品的区域。我将下面的数据输入到了数据库中并且为它设置了密码保护——密码是mustang。

| ------------------------ |
| Name        | Territory  |
| ------------------------ |
| John Doe    | North      |
| Jane Doe    | South      |
| Paul Smith  | East       |
| Kathy Smith | West       |
| ------------------------ |

销售数据库由一个Sales应用软件来访问。在访问数据库之前,程序请求用户输入一个用户名和密码。然后它使用这些数据通过JDBC-ODBC bridge驱动来连接sales数据源(你可以用Windows控制面板的ODBC数据源小程序来创建它,用于识别sales.mdb的位置)。如果连接成功,程序将输出territories表的全部行和列的值。列表1显示了Sales程序的源代码。

Listing 1. Sales.java
// Sales.java

import java.io.*;
import java.sql.*;
import java.util.*;

class Sales
{
   public static void main (String [] args)
                 throws ClassNotFoundException, SQLException
   {
      // Attempt to obtain a console.
      // 尝试获取控制台

      Console console = System.console ();
      if (console == null)

      {
          System.err.println ("sales: unable to obtain console");
          return;
      }

      // Obtain username.
      // 获取用户名

      String username = console.readLine ("Enter username: ");

      // Obtain password.
      // 获取密码

      String password = new String (console.readPassword ("Enter password: "));

      // Create a Vector datastructure for holding table records.
      // 建立一个向量数据结构来存储表记录

      Vector<Object []> v = new Vector<Object []> ();

      Connection con = null;
      Statement stmt = null;
      ResultSet rs;

      try
      {
         // Attempt to connect to the sales datasource.
         // 尝试连接sales数据源

         con = DriverManager.getConnection ("jdbc:odbc:sales", username,
                                            password);

         // Garbage collect the password—not a good idea to keep passwords
         // hanging around.
         // 对密码进行垃圾收集——始终留着密码可不是个好主意

         password = null;

         // Attempt to create a statement.
         // 尝试建立一个statement

         stmt = con.createStatement ();

         // Establish the maximum number of rows that can be returned.
         // 设置允许返回的最大行数

         stmt.setMaxRows (10);

         // Attempt to fetch all rows from the territories table.
         // 尝试读取territories表的所有行

         String query = "SELECT * FROM territories";
         rs = stmt.executeQuery (query);

         // Get number of columns.
         // 获取列数

         int nCols = rs.getMetaData ().getColumnCount ();

         // Read all rows of all columns into storage.
         // 读取所有列的每一行到向量存储

         int i = 0;

         while (rs.next ())
         {
            Object [] buffer = new Object [nCols];

            for (int j = 0; j < nCols; j++)
                 buffer [j] = rs.getObject (j + 1);

            // NOTE: getObject requires a 1-based column index.
             // 注意:getObject要求列索引号从1开始

            v.add (buffer);
         }

         // Extract rows from the array.
         // 从数组中取出全部行

         Object [] rows = v.toArray ();
         for (i = 0; i < rows.length; i++)
         {
              // Extract columns from the row.
               // 取出一行中的每列值

              Object [] cols = (Object []) rows [i];

              // Print out the values from each column.
               // 显示输出每一列的值

              for (int j = 0; j < cols.length; j++)
                   console.printf ("%s ", cols [j]);

              console.printf ("/n");
         }

         console.printf ("Value at Row 1, Col 0 = %s/n", getValue (v, 1, 0));
      }
      catch (SQLException e)
      {
         console.printf ("sales: %s/n", e.getMessage ());
      }
      finally
      {
         if (stmt != null)
             try
             {
                 stmt.close ();
             }
             catch (SQLException e2) {}

         if (con != null)
             try
             {
                 con.close ();
             }
             catch (SQLException e2) {}
      }
   }

   static Object getValue (Vector v, int row, int col)
   {
      // The following conversion should really not be done in this method,
      // because it's inefficient. Placing the conversion here is a matter of
      // convenience.
      // 由于效率低下,下列的转换其实本不该在这个方法中进行,把转换放在这里只是为了方便。

      Object [] rows = v.toArray ();

      Object [] cols = (Object []) rows [row];

      return cols [col];
   }
}


编译Sales.java然后执行程序,首先呈现在你面的是Enter username:的提示。你可以随意输入任何用户名,但至少要输入点什么。接下来你将被提示输入密码,确定这里要使用mustang。假如连接成功建立了,你将会看到我在上面已经展示的表值。

列表1使用了Console的printf()方法来输出表的内容。然而,这种方法有一个不十分明显的问题,如果territories表有很多行的数据,在不滚动的情况下,并不是所有的数据都适合控制台窗口的显示。为了获取这些行,通常需要重定向标准输出到一个文件,但是如果你这样做的话,System.console()将返回null,你也就无法使用控制台输入/输出了。因此,在你的应用中使用Console的printf()和format()方法之前要考虑清楚,不要使用它们向屏幕输出大量的数据,否则你可能就会无法看到全部数据了。

注意
仔细地分析列表1的代码,它并没有通过Class.forName()来尝试加载JDBC-ODBC bridge驱动。由于Mustang支持JDBC 4.0——它提供了一种内在机制来使DriverManager定位和加载驱动类——你不再需要通过Class.forName()来显式地加载驱动类了。

尽管Console解决了最初的RFE问题,但是很多开发者都对这个类存在异议,主要的批评包括:
·        System.console()方法应该被命名为System.getConsole()。然而一些不赞成的人争辩说“get”前缀只应用在bean上面,而System并不是bean。此外,console()是一个静态方法,这意味着IDE将不会认为这个方法是用来获取property的getter方法了。
·        Console类应该提供不使用缓冲来读取单个字符的能力。换句话说,程序不应该等到用户按下回车之后才能读取用户的输入。一个类似C语言的kbhit()函数、通过返回一个Boolean值来判断是否有按键被按下的方法将可以回应这种批评,如果返回true,程序就可以调用另一个方法来返回下一个等待在BIOS按键缓冲中的按键了。
·        Console应该提供打开/关闭控制台字符显示的能力。两个类似C语言中getch()函数(获得字符而不在控制台显示,并且只当没有字符等待在BIOS按键缓冲中时才有效)和getche()函数(获得字符同时在控制台显示,并且只当没有字符等待在BIOS按键缓冲中时才有效)的方法将可以回应这种批评。
·        Console应该提供清除屏幕的能力和其他可以在基于Unix的curses库中找到的特性。
·        Console应该提供一个detach()方法来将应用程序从控制台分离出来并且送入后台。
除了第一条之外,对Sun来说,在Mustang的正式版本中回应上述批评的一部份(如果不是全部的话),也许还不是太晚。


分区空间方法
在收到改进控制台输入/输出的RFE #4050435的一个月之前,Sun收到了另外一个RFE,要求提供一种方法来得到可用的磁盘空间。Sun回应了这个RFE #4057701,为File类加入了3个分区空间方法:
·        public long getFreeSpace():返回以抽象路径名命名的分区的未分配空间字节数。 如果抽象路径名不是磁盘分区的名字,返回值为空。
·        public long getTotalSpace():返回以抽象路径名命名的分区的总磁盘空间。 如果抽象路径名不是磁盘分区的名字,返回值为空public long getUsableSpace():返回以抽象路径名命名的虚拟机可用的空间字节数。 如果抽象路径名不是磁盘分区的名字,返回值为空.

对于Windows版本的Mustang,这些方法是按照Microsoft的GetDiskFreeSpaceEx函数实现的。让我们基于这个函数的文档重新定义一下这些方法:
·        getFreeSpace() 将返回当前根目录的磁盘的全部空闲字节数,若当前根目录是一个CD-ROM驱动器则返回0. 如果有不可写CD在一个CD-RW驱动器中会返回非0值。.
·        getTotalSpace()将返回与调用线程相关的用户可用的磁盘字节总数。如果用户配额被开启,这个值可能会小于磁盘的空闲字节总数。getUsableSpace()将返回与调用线程相关的用户可用的磁盘全部空闲字节数。如果用户配额被开启,这个值可能会小于磁盘的空闲字节总数。如果当前根目录是一个CD-ROM驱动器则返回0,如果有不可写CD在一个CD-RW驱动器中会返回非0值。

注意
很多的现代操作系统允许限制每个用户可用的最大磁盘空间。这个最大空间叫做用户的配额,为用户留出的磁盘空间区域叫做该用户的分区。在这种情况下,getTotalSpace()返回的是与调用线程相关的用户的配额。如果用户没有配额限制(只有一个用户),该方法返回磁盘字节总数。同样,getUsableSpace()返回的是与调用线程相关的用户的配额限制内的可用字节数。如果用户没有配额限制,该方法则返回磁盘的全部空闲字节数。最后,基于GetDiskFreeSpaceEx()的文档,getFreeSpace()磁盘的空闲字节数——而不考虑用户的配额。

列表2展示了一个SpaceChecker程序的源代码,它将显示文件系统的每一个根目录的空闲空间,以及分配给当前用户的可用空间和全部空间。

Listing 2. SpaceChecker.java

列表 2. SpaceChecker.java
// SpaceChecker.java

import java.io.File;

public class SpaceChecker
{
   public static void main (String [] args)
   {
      File [] roots = File.listRoots ();

      for (int i = 0; i < roots.length; i++)
      {
           System.out.println (roots [i]);
           System.out.println ("Free space = " + roots [i].getFreeSpace ());
           System.out.println ("Usable space = " + roots [i].getUsableSpace ());
           System.out.println ("Total space = " + roots [i].getTotalSpace ());
           System.out.println ();
      }
   }
}


在我的Windows ME平台上运行该程序时,我观察到了下列信息:
A:/
Free space = 0
Usable space = 0
Total space = 0

C:/
Free space = 27913584640
Usable space = 27913584640
Total space = 40965373952

M:/
Free space = 0
Usable space = 0
Total space = 0

N:/
Free space = 0
Usable space = 0
Total space = 0


由于磁盘配额没有被应用到Windows ME操作系统上,所以C:的空闲空间与可用空间是相同的。

启动画面 API

启动画面是基于GUI的现代应用软件的一个重要部分。启动画面不仅可以在漫长的软件启动过程中占据用户的注意力(也可能用来通报用户版本信息或者其他细节),还可以使用户确定软件正在启动中。这在Java环境中是尤其重要的,因为JVM需要一段时间来加载和启动。图1列举了一个启动画面的例子。


图1. 使用启动画面来显示版权和其他重要信息

Mustang对启动画面的实现是使用一个能够显示GIF(包括动画GIF)、PNG或者JPEG图像的无修饰窗口。Java应用程序加载器创建一个启动画面窗口,然后响应命令行参数或者JAR manifest入口,从而在窗口中显示指定的图片:
·        -splash命令行参数创建一个启动画面窗口并且显示一个指定的图片。例如,java -splash:mylogo.gif MyApp将创建启动画面窗口并且在这个窗口中显示mylogo.gif 确定的图像(见图1),然后加载和启动JVM,最后使JVM加载MyApp.class并且执行该类的public static void main(String [] args)方法。
·        由于你最有可能把重要的应用打包进一个jar文件, Mustang提供了一个SplashScreen-Image manifest项目,用来从jar文件的manifest中访问启动画面的图像。例如SplashScreen-Image:mylogo.gif把mylogo.gif识别为在启动画面窗口中显示的图像。

假设下列信息被放置在了一个manifest文件中:
Manifest-Version: 1.0
Main-Class: MyApp
SplashScreen-Image:mylogo.gif


将这个manifest文件、mylogo.gif和全部相关的类文件都打包进myApp.jar,java -jar myApp.jar会在加载和启动JVM及运行应用程序之前创建启动画面并且显示mylogo.gif。

如果你调用java -splash:yourlogo.gif -jar myApp.jar将会发生什么呢?在这种情况下,yourlogo.gif将会在启动画面窗口中出现,因为命令行参数-splash优先替代了manifest设置SplashScreen-Image。

假如你希望在图像上加入视觉效果来定制启动画面,例如为一个有着漫长初始化过程的软件的启动画面图像加入一个动态的进度条来告知用户剩余的时间(参考“Mustang新的启动画面功能”一文中的SplashTest程序)。Mustang的java.awt.SplashScreen类提供了对启动画面定制的支持。

在没有启动画面窗口的情况下,一个SplashScreen对象不具有任何意义,所以你不能从SplashScreen类实例化对象,而启动画面窗口只当你指定了-splash命令行选项或者SplashScreen-Image manifest项目时才会存在。当你指定了此命令行选项或者manifest项目并且窗口被创建出来,作为其启动过程的一部分,Mustang会创建一个SplashScreen对象。调用SplashScreen的public static SplashScreen getSplashScreen()方法可以返回这个对象的一个实例。切记如果没有启动画面窗口的话,将会返回null。在调用了SplashScreen.getSplashScreen()来返回SplashScreen对象之后,下力代码段首先通过检查返回的SplashScreen实例是否为null来测定启动画面窗口是否存在:

SplashScreen ss = SplashScreen.getSplashScreen ();
if (ss != null)
{
    // Customize the splash screen.
}


假设启动画面窗口是存在的,你可以调用下列五种SplashScreen方法来获取一个图形环境,以便于在缓冲区绘制图象、得到启动画面图象、获取图象的尺寸、用其他图象替换现有图象或者是使用缓冲区中的内容来更新启动画面窗口。

·        public Graphics getGraphics():以java.awt.image.BufferedImage的形式创建一个覆盖图象,它具有与启动画面图象相同的尺寸,还有一个窗口,类型设置为BufferedImage.TYPE_INT_ARGB(给予图象一个alpha通道来设置透明度),并且返回一个实例到BufferedImage的图形环境。这幅图象的所有像素都被设为黑色并且是完全透明的。你可以调用图形环境的方法在BufferedImage中绘制。受到绘图操作影响的每个像素都会被分配一个不透明的alpha值。如果在启动画面窗口关闭之后调用这个方法,它将会抛出IllegalStateException。
·        public URL getImageURL():以URL返回当前的启动画面图象。如果在启动画面窗口关闭之后调用这个方法,它将会抛出IllegalStateException。
·        public Dimension getSize():返回启动画面窗口的尺寸,同时也就是显示的图象的尺寸。如果在启动画面窗口关闭之后调用这个方法,它将会抛出IllegalStateException。
·        public void setImageURL(URL imageURL): 将启动画面图象改为imageURL确定的图象。当图象被加载、窗口被更新之后,该方法将返回。启动画面窗口将调整为图象的同样大小同时在屏幕上居中。如果imageURL为null,该方法将抛出NullPointerException;如果在启动画面窗口关闭之后调用这个方法,它将会抛出IllegalStateException。
·        public void update():更新启动画面窗口使得覆盖图内容与当前的启动画面窗口的像素相合成。由于采用了Source-over合成,覆盖图象的透明像素可以使启动画面图象的像素显示出来,而覆盖图象的不透明像素则遮挡了它下面启动画面图象的像素。如果没有调用getGraphics()来创建覆盖图象,或者在启动画面图象关闭之后它才被调用,它将抛出IllegalStateException。

列表3通过一个PhotoAlbum软件的轮廓演示了getSplashScreen(),getGraphics(),getSize(),与update()这四个方法。这段程序定制了启动画面,在四周加上了一个红色边框,并且在水平中心显示一条信息“Registering plug-ins...”。
Listing 3. PhotoAlbum.java
列表 3. PhotoAlbum.java
// PhotoAlbum.java

import java.awt.*;

public class PhotoAlbum
{
   public static void main (String [] args)
   {
      SplashScreen ss = SplashScreen.getSplashScreen ();
      if (ss != null)
      {
          Graphics g = ss.getGraphics ();
          g.setColor (Color.red); // Color.white在我这个版本的Mustang中是默认颜色

          Dimension size = ss.getSize ();

          // 每个边框宽度都是图象的宽和高中较小值的10%

          int borderSize;
          if (size.width < size.height)
              borderSize = (int) (size.width * 0.01);
          else
              borderSize = (int) (size.height * 0.01);

          for (int i = 0; i < borderSize; i++)
               g.drawRect (i, i, size.width-1-i*2, size.height-1-i*2);

          // 计算字符串在当前字体时的宽和高

          FontMetrics fm = g.getFontMetrics ();
          int strWidth = fm.stringWidth ("Registering plug-ins...");
          int strHeight = fm.getHeight ();

          // 在字符串没有超出启动画面窗口范围时

          if (strWidth < size.width && 4*strHeight < size.height)
          {
              g.setColor (Color.blue);
              g.drawString ("Registering plug-ins...",
                            (size.width-strWidth)/2,
                            size.height-4*strHeight);
          }

          // 拷贝覆盖图象到启动画面窗口

          ss.update ();

          try
          {
              Thread.sleep (3000); // 暂停3秒钟以便查看图象
          }
          catch (InterruptedException e)
          {
          }
      }
   }
}


为演示PhotoAlbum程序,随本文的代码一起,我加入了一张图片palogo.jpg。当你执行java -splash:palogo.jpg PhotoAlbum之后,将首先在屏幕的中间看见palogo.jpg的图象。在JVM完成载入并且开始运行main(),你会看见如图2所示的合成图象(也是居中的)。

resized image
图2. 标识图片改变了自身的边框颜色,同时提示用户正在注册插件。点击缩略图以观看全尺寸图象。

当第一个AWT或者Swing窗口变为可见的时候,启动画面窗口会自动关闭,然而,也许你想要在软件窗口出现之前就将其关闭,或者用你自己的窗口来替换它。SplashScreen类提供了以下3个方法来帮助你达到这个目的:
·        public void close():隐藏并关闭启动画面窗口,释放分配给该窗口的全部资源。如果在启动画面窗口关闭之后调用这个方法,它将会抛出IllegalStateException。
·        public Rectangle getBounds():返回启动画面窗口的边界。如果你调用setImageURL()创建了一个不同尺寸的新启动画面图象,这些边界将会改变。如果在启动画面窗口关闭之后调用这个方法,它将会抛出IllegalStateException。
·        public boolean isVisible():如果启动画面窗口是可见的,将返回一个Boolea值true,在窗口关闭之后调用则返回false。

假如你用自己的窗口进行了替换,你可以调用getBounds()来赋予它跟启动画面窗口同样的初始坐标和尺寸。此外,当你自己的窗口变为可见的时候,启动画面窗口将自动关闭。


系统托盘API

系统托盘是桌面上一个专门的区域,它显示当前的时间和常驻内存的桌面应用的图标,并且被桌面上当前运行的所有应用共享。表3展示了Windows ME的系统托盘,它位于Windows任务栏的右侧。

image
表3. 系统托盘上的显示属性应用程序图标相关联的菜单和工具提示

用户只需要适当地轻点鼠标,就随时都可以与这些应用进行交互。例如,当鼠标位于应用软件的图标上时,双击鼠标左键通常就可以打开应用的主窗口(在Windows平台上)。同样地,把鼠标移到应用的图标上面并且单击右键,通常就可以显示特定应用的弹出菜单。

Mustang引入了类java.awt.SystemTray和java.awt.TrayIcon来与系统托盘进行交互:SystemTray代表桌面的系统托盘,TrayIcon代表可以加入到系统托盘的一个图标。

同SplashScreen一样,你不能创建SystemTray对象,而是必须调用SystemTray的public static SystemTray getSystemTray()方法来返回一个代表系统托盘区域的SystemTray实例。由于在底层平台不支持系统托盘时,该方法会抛出UnsupportedOperationException,所以你必须首先调用public static boolean isSupported()。如果系统托盘能得到最低限度的支持(除了显示图标之外,最低限度的支持包括右键点击图标时显示的弹出式菜单,或者是左键双击图标时响应的弹出事件),这个方法就将返回true,否则返回false。下列代码段演示了获取SystemTray实例的正确方式:
if (SystemTray.isSupported ())
{

    SystemTray tray = SystemTray.getSystemTray ();

    // Do stuff with tray.
}


假设系统托盘可以被支持,你可以调用下列7个方法来实现各种各样的功能:
·        public void add(TrayIcon trayIcon): 为SystemTray加入一个TrayIcon,在这之后,trayIcon所描述的图标将在系统托盘显示出来。图标加入到系统托盘的顺序取决于平台的实现。同样,当应用程序退出或者系统托盘变为不可用的时候,图标将被自动移除。如果trayIcon为null,该方法将抛出NullPointerException;如果你试图多次添加同样的TrayIcon实例,将抛出IllegalArgumentException;如果系统托盘不可用,将抛出AWTException。
·        public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener): 为trayIcons属性加入一个java.beans.PropertyChangeListener到listener表,trayIcons属性必须为propertyName的值。当程序从系统托盘添加或者删除一个TrayIcon或者图标被自动移除时,listener将被调用。该方法不抛出任何异常,即使propertyName或者listener为null。
·        public PropertyChangeListener [] getPropertyChangeListeners(String propertyName): 返回一个与指定属性相关联的PropertyChangeListener(对于当前程序)的数组。目前只支持trayIcons,并且必须以propertyName值来指定。如果你传递了null或者任何其它值,该方法将返回一个空数组。
·        public TrayIcon [] getTrayIcons(): 返回含有被应用程序加入到SystemTray 的全部TrayIcon的一个数组。所返回的数组是真实数组的一个拷贝,可以随意修改而不会反映到系统托盘的图标上。如果没有TrayIcon被加入,该方法将返回一个空数组。
·        public Dimension getTrayIconSize(): 以java.awt.Dimension对象的形式,返回图标出现在系统托盘时将占用的水平和垂直尺寸,其单位是像素。在创建图标之前调用该方法来决定图标的首选尺寸。这是TrayIcon的public Dimension getSize()方法的一种方便的实现。
·        public void remove(TrayIcon trayIcon): 从SystemTray移除指定的TrayIcon。图标将从系统托盘移除,同时将通报所有的属性改变listener。该方法不抛出任何异常,即使trayIcon为null。
·        public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener): 移除propertyName指定属性的listener,propertyName必须是trayIcon(除此之外就没有任何地方需要调用这个方法了)。该方法不抛出任何异常,即使propertyName或者listener为null。

表4是一个用来演示上述方法的SystemTrayDemo1程序。这个程序先创建了一个内部为实心红色矩形的实心黄色圆形图标,将这个图标添加到系统托盘,暂停3秒钟,之后从系统托盘将其移除,再暂停3秒钟,然后结束。
表4. SystemTrayDemo1.java
// SystemTrayDemo1.java

import java.awt.*;
import java.awt.image.*;
import java.beans.*;

public class SystemTrayDemo1
{
   public static void main (String [] args)
   {
      if (SystemTray.isSupported ())
      {
          // 获取系统托盘

          SystemTray tray = SystemTray.getSystemTray ();

          // 注册一个属性变化listener来通知系统托盘上图标的添加和移除

          PropertyChangeListener pcl;
          pcl = new PropertyChangeListener ()
                {
                    public void propertyChange (PropertyChangeEvent pce)
                    {
                       System.out.println ("Property changed = " +
                                           pce.getPropertyName ());
                       System.out.println ();

                       TrayIcon [] tia = (TrayIcon []) pce.getOldValue ();
                       if (tia != null)
                       {
                           System.out.println ("Old tray icon array contents: ");
                           for (int i = 0; i < tia.length; i++)
                                System.out.println (tia [i]);
                           System.out.println ();
                       }


                       tia = (TrayIcon []) pce.getNewValue ();
                       if (tia != null)
                       {
                           System.out.println ("New tray icon array contents: ");
                           for (int i = 0; i < tia.length; i++)
                                System.out.println (tia [i]);
                           System.out.println ();
                       }
                    }
                };
          tray.addPropertyChangeListener ("trayIcons", pcl);

          // 为图标创建一个图象

          Dimension size = tray.getTrayIconSize ();
          BufferedImage bi = new BufferedImage (size.width, size.height,
                                                BufferedImage.TYPE_INT_RGB);
          Graphics g = bi.getGraphics ();

          g.setColor (Color.red);
          g.fillRect (0, 0, size.width, size.height);
          g.setColor (Color.yellow);
          int ovalSize = (size.width < size.height) ? size.width : size.height;
          ovalSize /= 2;
          g.fillOval (size.width/4, size.height/4, ovalSize, ovalSize);

          // 由该图像创建一个图标并且将其添加到系统托盘。如果不能添加则结束程序。

          TrayIcon icon = null;
          try
          {
              tray.add (icon = new TrayIcon (bi));
          }
          catch (AWTException e)
          {
              System.out.println (e.getMessage ());
              return;
          }

          // 暂停以便查看系统托盘

          try
          {
              Thread.sleep (3000);
          }
          catch (InterruptedException e)
          {
          }

          // 从系统托盘移除图标

          tray.remove (icon);

          // 暂停以查看没有了图标的系统托盘

          try
          {
              Thread.sleep (3000);
          }
          catch (InterruptedException e)
          {
          }

          // 结束程序

          System.exit (0);
      }
   }
}


在获取了系统托盘实例之后,为这个实例注册一个属性改变listener,并且为图标创建一个java.awt.Image,表4基于这个图标创建了一个TrayIcon对象,然后将该对象加入到系统托盘。这个对象是通过调用TrayIcon's public TrayIcon(Image image)构造器创建的。这个构造器在TrayIcon对象中存储了image。然后,当tray.add (icon = new TrayIcon (bi));将TrayIcon加入到SystemTray的时候,这个image被取出并且显示在系统托盘上。如果image为null,该构造器和TrayIcon的另外两个构造器都抛出IllegalArgumentException。如果当前平台不支持系统托盘,这些构造器将抛出UnsupportedOperationException。

如果你在命令行运行SystemTrayDemo1,你将会在系统托盘发现发现一个新图标,在控制台窗口将会获得类似如下的输出:
Property changed = trayIcons

Old tray icon array contents:

New tray icon array contents:
java.awt.TrayIcon@199f91c

Property changed = trayIcons

Old tray icon array contents:
java.awt.TrayIcon@199f91c


几分钟之后,图标就会消失,SystemTrayDemo1程序结束,这对于持续运行的应用来说是不合适的。与此相反,你也许希望当用户右键点击程序图标以弹出菜单,并且选择了适当的项目之后,程序才会结束。你可以用如下方式实现这个行为:创建一个java.awt.PopupMenu的实例,创立一个带有动作 listener的菜单项目,使用户选择该项即可结束程序,然后将该菜单项加入到弹出菜单中,最后调用public TrayIcon(Image image, String tooltip, PopupMenu popup)构造器使弹出菜单和图标相关联。与图标的Image和相关联的PopupMenu一起,你可以指定一个String来确定工具提示的文本内容,当鼠标指针移动到图标上面时该文本就会出现。把null传递给tooltip就可以使它不再显示。下列代码段向系统托盘添加了一个带有工具提示和弹出菜单的图标:
PopupMenu popup = new PopupMenu ();
MenuItem miExit = new MenuItem ("Exit");
ActionListener al;
al = new ActionListener ()
{
     public void actionPerformed (ActionEvent e)
     {
        System.out.println ("Goodbye");
        System.exit (0);
     }
};
miExit.addActionListener (al);
popup.add (miExit);

TrayIcon ti = new TrayIcon (bi, "System Tray Demo #2", popup);

tray.add (ti); // Assume that tray was previously created.
                                 // 假定托盘在之前已经被创建了


当用户右键单击鼠标时弹出菜单就会出现,然后用户可以选择菜单的Exit项目来执行菜单项的动作listener,它就可以终止程序。
除了鼠标右键动作之外,你也许还希望当用户在图标上使用其它的鼠标动作,例如双击。为了使程序响应这些鼠标动作,TrayIcon提供了下列注册listener的方法:
·        public void addActionListener(ActionListener listener):为TrayIcon添加一个动作listener。当用户双击图标时,这个listener的public void actionPerformed(ActionEvent e)方法将被调用。
你也许想要在多个TrayIcon之间共享一个动作listener,那么决定哪个图标响应动作事件并且调用listener就变得很重要了。你可以通过TrayIcon 的public void setActionCommand(String command)方法来为其分配一个唯一的指令。然后你就可以在listener中调用java.awt.event.ActionEvent的public String getActionCommand()方法来返回指令名称以及识别TrayIcon。为方便起见,TrayIcon也指定了一个public String getActionCommand()方法。
调用TrayIcon的public void removeActionListener(ActionListener listener)方法从TrayIcon上移除listener。你也可以通过调用public ActionListener [] getActionListeners()方法来获取一个含有全部注册的动作listener的数组。
·        public void addMouseListener(MouseListener listener): 为TrayIcon添加一个鼠标listener。当鼠标指针位于图标上方并且用户按下、释放、或者点击鼠标左键时,这个listener的各种方法(除了public void mouseEntered(MouseEvent e)和public void mouseExited(MouseEvent e)不被支持)将会被调用。
如果你调用java.awt.event.MouseEvent从父类继承的public Component getComponent()方法,你将得到一个null值。然而,MouseEvent从父类继承的public Object getSource()方法将返回与事件相关联的TrayIcon。调用MouseEvent的public int getX()和public int getY()方法可以得到鼠标坐标。这些坐标是相对于屏幕的——而不是TrayIcon。调用TrayIcon's public void removeMouseListener(MouseListener listener)方法可以从TrayIcon中移除listener。通过调用public MouseListener [] getMouseListeners()可以获取含有所有注册的鼠标listener的一个数组。
·        public void addMouseMotionListener(MouseMotionListener listener): 为TrayIcon添加一个鼠标运动listener。当用户在图标上移动鼠标指针时,这个listener的public void mouseMoved(MouseEvent e)方法将被调用。(public void mouseDragged(MouseEvent e)方法不支持)
再一次,getComponent()返回null,getSource()返回一个与事件相关联的TrayIcon,getX()和getY()返回的坐标是相对于屏幕的——而不是TrayIcon。

调用TrayIcon的public void removeMouseMotionListener(MouseMotionListener listener)方法来从TrayIcon移除listener。你可以通过调用public MouseMotionListener [] getMouseMotionListeners()来得到一个含有全部注册的鼠标动作listener的数组。

表5展示了一个拥有自己的String工具提示和PopupMenu的程序SystemTrayDemo2。该程序也调用了之前介绍的listener注册方法来注册用于响应动作、鼠标和鼠标运动事件的listener。

表5. SystemTrayDemo2.java
// SystemTrayDemo2.java

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.beans.*;

public class SystemTrayDemo2
{
   public static void main (String [] args)
   {
      if (SystemTray.isSupported ())
      {
          // 获取系统托盘

          SystemTray tray = SystemTray.getSystemTray ();

          // 为图标创建图像

          Dimension size = tray.getTrayIconSize ();
          BufferedImage bi = new BufferedImage (size.width, size.height,
                                                BufferedImage.TYPE_INT_RGB);
          Graphics g = bi.getGraphics ();

          g.setColor (Color.red);
          g.fillRect (0, 0, size.width, size.height);
          g.setColor (Color.yellow);
          int ovalSize = (size.width < size.height) ? size.width : size.height;
          ovalSize /= 2;
          g.fillOval (size.width/4, size.height/4, ovalSize, ovalSize);

          try
          {
              // 创建一个与程序的图标相关联的弹出菜单。选择菜单唯一的一个菜单项就会结束程序。
              PopupMenu popup = new PopupMenu ();
              MenuItem miExit = new MenuItem ("Exit");
              ActionListener al;
              al = new ActionListener ()
                   {
                       public void actionPerformed (ActionEvent e)
                       {
                          System.out.println ("Goodbye");
                          System.exit (0);
                       }
                   };
              miExit.addActionListener (al);
              popup.add (miExit);

              // 从图像创建一个图标,当鼠标位于图标上方式,选择显示一个工具提示,以及为图标分配弹出式菜单。

              TrayIcon ti = new TrayIcon (bi, "System Tray Demo #2", popup);

              // 创建并且关联一个listener到图标上。当你采用了适当的动作,例如在Windows下双击鼠标,actionPerformed()方法将会被调用。
              al = new ActionListener ()
                   {
                       public void actionPerformed (ActionEvent e)
                       {
                          System.out.println (e.getActionCommand ());
                       }

                   };
              ti.setActionCommand ("My Icon");
              ti.addActionListener (al);

              // 创建并关联一个鼠标listener来记录于图标相关联的鼠标事件

              MouseListener ml;
              ml = new MouseListener ()
                   {
                       public void mouseClicked (MouseEvent e)
                       {
                          System.out.println ("Tray icon: Mouse clicked");
                       }

                       public void mouseEntered (MouseEvent e)
                       {
                          System.out.println ("Tray icon: Mouse entered");
                       }

                       public void mouseExited (MouseEvent e)
                       {
                          System.out.println ("Tray icon: Mouse exited");
                       }

                       public void mousePressed (MouseEvent e)
                       {
                          System.out.println ("Tray icon: Mouse pressed");
                       }

                       public void mouseReleased (MouseEvent e)
                       {
                          System.out.println ("Tray icon: Mouse released");
                       }
                   };
              ti.addMouseListener (ml);

              //创建并关联一个鼠标运动listener来记录于图标相关联的鼠标运动事件

              MouseMotionListener mml;
              mml = new MouseMotionListener ()
                    {
                        public void mouseDragged (MouseEvent e)
                        {
                           System.out.println ("Tray icon: Mouse dragged");
                        }

                        public void mouseMoved (MouseEvent e)
                        {
                           System.out.println ("Tray icon: Mouse moved");
                        }
                    };
              ti.addMouseMotionListener (mml);

              // 将图标加入系统托盘

              tray.add (ti);
          }
          catch (AWTException e)
          {
              System.out.println (e.getMessage ());
              return;
          }
      }
   }
}


与SystemTrayDemo1同样,SystemTrayDemo2在系统托盘显示了同样的图标。移动鼠标到图标上,除了在控制台窗口显示信息“Mouse moved”之外,工具提示也将会出现。当鼠标指针位于图标上方时,试一下左键单击,各种按下、释放、点击消息将会在控制台窗口出现。如果你双击图标,动作指令的名字将会在控制台窗口出现。最后,右键单击图标,你将会看见一个带有Exit选项的弹出菜单。表4演示了SystemTrayDemo2的图标的两个视图。在左侧你能看见图标上的工具提示,在右侧你可以看见图标的弹出菜单。

image
表4. 托盘图标有自己的工具提示和弹出菜单

TrayIcon类提供了额外的一些方法——其中一些是通过构造器调用的——你也许会感兴趣。这些方法包括:
·        public void displayMessage(String caption, String text, TrayIcon.MessageType messageType): 在系统托盘上图标附近显示一条弹出消息。这条消息持续的时间依赖于平台,之后便会消失。(我相信这个方法并不影响与TrayIcon相关联的工具提示。)
caption显示在消息内容text的上方,caption或text都可以为null,但是如果均为null,该方法将抛出NullPointerException。最后,messageType可以为下列消息类型之一:TrayIcon.MessageType.ERROR (错误消息), TrayIcon.MessageType.INFO (通知消息), TrayIcon.MessageType.NONE (简单消息), or TrayIcon.MessageType.WARNING (警告消息)。当消息出现时,平台可以使用messageType来决定什么动作——显示图形还是发出声音——需要被执行。
这个方法并不是所有的平台都支持。例如,我在Windows ME平台上就无法显示消息。
·        public void setImage(Image image): 创建一个image作为TrayIcon的Image。这用于指示程序状态的改变是非常方便的。前一个Image会丢弃而不调用Image的flush()方法——你必须明确的调用这个方法来转储所有被前一个Image对象使用的资源(包括为了屏幕绘制而缓存的像素数据)。如果image为null,该方法将抛出NullPointerException。调用public Image getImage()方法来返回当前的Image。
·        public void setImageAutoSize(boolean autosize): 设置TrayIcon的自动调整尺寸属性。这个属性决定了Image是否自动调整尺寸来适应分配给系统托盘图标的空间。如果你传递true给autosize,Image将按照需要自动缩小或扩大。否则,Image将被裁减以适应分配的空间。调用public boolean isImageAutoSize()方法可以返回自动调整大小属性的值。
·        public void setPopupMenu(PopupMenu popup): 为TrayIcon设置弹出菜单。如果popup为null,没有PopupMenu与相关联TrayIcon。如果你试图为不同的TrayIcon设置同样的弹出菜单,该方法将抛出IllegalArgumentException。一些平台也许不支持弹出菜单,当用户右键单击图标时,他们或者不显示菜单,或者不显示本地版本的菜单。调用public PopupMenu getPopupMenu()方法来返回当前的PopupMenu。
·        public void setToolTip(String tooltip): 为TrayIcon设置工具提示。当用户把鼠标移动到系统托盘上的图标上方时,工具提示会被显示出来。传递null作为tooltip的值可以移除工具提示。工具提示在一些平台上可能被简化了。调用public String getToolTip()方法来返回当前的工具提示String。

在文章的结尾,我希望可以避开一个潜在的易混淆点。 关于isSupported()方法的SystemTray文档建议“将默认动作同时加入到动作listener和弹出菜单中”以便保证托盘图标的默认动作始终可用。这是什么意思呢?就是简单地向PopupMenu添加一个Default菜单项,它拥有与你关联到TrayIcon的动作listener同样的动作listener,像如下代码段所示:

PopupMenu popup = new PopupMenu ();

MenuItem miDefault = new MenuItem ("Default");
ActionListener alDefault;
alDefault = new ActionListener ()
     {
         public void actionPerformed (ActionEvent e)

         {
            System.out.println (e.getActionCommand ());
         }
     };
miDefault.addActionListener (alDefault);
popup.add (miDefault);

MenuItem miExit = new MenuItem ("Exit");
ActionListener alExit;
alExit = new ActionListener ()
     {
         public void actionPerformed (ActionEvent e)
         {
            System.out.println ("Goodbye");
            System.exit (0);
         }
     };
miExit.addActionListener (alExit);
popup.add (miExit);

TrayIcon ti = new TrayIcon (bi, "System Tray Demo #2", popup);
ti.addActionListener (alDefault);


结论

Mustang就要到来了,你一定已经忍不住想要在今年晚些时候正式版问世之前就体验一下这个最新的Java平台。为了帮助你顺利起步,本文展示了一些你也许可以在这个平台上找到的小示例。本文关注了4个重要的新特性:控制台输入/输出、分区空间方法、启动画面API和系统托盘API。

Jeff Friesen是一个自由软件开发者和教育者,擅长C、C++和Java技术。

资源
·         Matrix Java社区:
http://www.matrix.org.cn
·        下载本文的代码文件:
http://www.javaworld.com/javaworld/jw-01-2006/mustang/jw-0109-mustang.zip
·        “创立与巩固Java品牌” Jon Byous (Sun Developer Network, June 2005):
http://java.sun.com/developer/technicalArticles/JavaOne2005/naming.html
·        函数 GetDiskFreeSpaceEx:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/getdiskfreespaceex.asp
·        JSR 270: Java SE 6 ("Mustang") 发行目录:
http://www.jcp.org/en/jsr/detail?id=270
·        “Mustang新的启动画面功能” Oleg Semenov and Dana Nourie (Sun Developer Network, September 2005):
http://java.sun.com/developer/technicalArticles/J2SE/Desktop/mustang/splashscreen/index.html
·        RFE #4050435: 不显示字符的密码输入:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4050435
·        RFE #4057701: 空闲磁盘空间方法:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4057701
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值