Java 的乐趣与游戏:Java grab 包的技术提示
一些 Java SE 技术提示
作者:Jeff Friesen, JavaWorld.com, 01/02/07
翻译:suli
原文地址:http://www.javaworld.com/javaworld/jw-01-2007/jw-0102-games.html
开发 Java 平台十年之久,我已经积累了一些使用 Java SE 的 grab 包加强游戏及其他 Java 平台开发的宝贵经验。 本期的
Java Fun and Games 将与您分享一些技术提示。 在文章的后半部分,将介绍如何将这些技术提示应用到一个网页抓图应用程序。
最简单的 API
不管计算机运行得有多快,我们却总是在等待某个任务的完成,比如,下载大个的文件、执行彻底搜索或者进行复杂的数学计算。 在这些费时的任务完成时,许多 Java 程序都会用一些花哨的方式提示用户,普遍方法是使用可以听得见的警告。
Java 提供了许多声音 API 可以用于创建有声警告。 可以使用 Java Speech API 告诉用户任务已经结束。 如果您希望任务完成时播放音效或音乐,Java Sound API 是一个不错的选择。 然而,因为 Java Speech 需要额外的分发文件,而 Java Sound 需要相当复杂的代码,您可能就希望使用 Audio Clip API 了。
Audio Clip API 基于 java.applet.AudioClip
和 java.applet.Applet
方法,例如:public static final AudioClip newAudioClip(URL url)
。 虽然此 API 比 Java Speech 和 Java Sound 更易于使用,但只用它来播放一段简单的声音也太过大材小用了。 对于这种简单的任务,还是考虑使用 Java 最简单的声音 API 吧。
最简单的声音 API 由 java.awt.Toolkit
的 public abstract void beep()
方法构成。 当调用此方法时,将发出简单的“哔跸”声。 为了展示 beep()
的用法,我创建了一个 CalcPi
应用程序,为 Pi 计数。 请看列表 1。
列表 1 CalcPi.java
// CalcPi.java
import java.awt.Toolkit;
import java.math.BigDecimal;
public class CalcPi
{
/* constants used in pi computation */
private static final BigDecimal ZERO = BigDecimal.valueOf (0);
private static final BigDecimal ONE = BigDecimal.valueOf (1);
private static final BigDecimal FOUR = BigDecimal.valueOf (4);
/* rounding mode to use during pi computation */
private static final int roundingMode = BigDecimal.ROUND_HALF_EVEN;
/* digits of precision after the decimal point */
private static int digits;
public static void main (String [] args)
{
if (args.length != 1)
{
System.err.println ("usage: java CalcPi digits");
return;
}
int digits = 0;
try
{
digits = Integer.parseInt (args [0]);
}
catch (NumberFormatException e)
{
System.err.println (args [0] + " is not a valid integer");
return;
}
System.out.println (computePi (digits));
Toolkit.getDefaultToolkit ().beep ();
}
/*
* Compute the value of pi to the specified number of
* digits after the decimal point. The value is
* computed using Machin's formula:
*
* pi/4 = 4*arctan(1/5) - arctan(1/239)
*
* and a power series expansion of arctan(x) to
* sufficient precision.
*/
public static BigDecimal computePi (int digits)
{
int scale = digits + 5;
BigDecimal arctan1_5 = arctan (5, scale);
BigDecimal arctan1_239 = arctan (239, scale);
BigDecimal pi = arctan1_5.multiply (FOUR).
subtract (arctan1_239).multiply (FOUR);
return pi.setScale (digits, BigDecimal.ROUND_HALF_UP);
}
/*
* Compute the value, in radians, of the arctangent of
* the inverse of the supplied integer to the specified
* number of digits after the decimal point. The value
* is computed using the power series expansion for the
* arc tangent:
*
* arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 +
* (x^9)/9 ...
*/
public static BigDecimal arctan (int inverseX, int scale)
{
BigDecimal result, numer, term;
BigDecimal invX = BigDecimal.valueOf (inverseX);
BigDecimal invX2 = BigDecimal.valueOf (inverseX * inverseX);
numer = ONE.divide (invX, scale, roundingMode);
result = numer;
int i = 1;
do
{
numer = numer.divide (invX2, scale, roundingMode);
int denom = 2 * i + 1;
term = numer.divide (BigDecimal.valueOf (denom), scale, roundingMode);
if ((i % 2) != 0)
result = result.subtract (term); else
result = result.add (term);
i++;
}
while (term.compareTo (ZERO) != 0);
return result;
}
}
列表 1 使用一种算法来计算 pi,该算法是早在 18 世纪初期由英国数学家 John Machin 发明的。 算法首先计算 pi/4 = 4*arctan(1/5)-arctan(1/239),然后将结果乘以 4 得出 pi 的值。 由于 arc (inverse) tangent 是使用一系列庞大的 term 来计算的, term 的数量越大得出的 pi 值越准确(小数点后显示的位数)
注意
列表 1 的大部分代码引用自 Sun 的远程方法调用教程的“创建一个客户端程序”部分。 |
此算法的实现依赖于 java.math.BigDecimal
和一个 arc-tangent 方法。 虽然 Java SE 5.0 等高级版本的 BigDecimal
包括常量 ZERO
和 ONE
,这些常量在 Java 1.4 中是不存在的。 同样,number-of-digits 命令行参数用于确定 arc-tangent 的数量和 pi 的精确度。
java CalcPi 0
3
java CalcPi 1
3.1
java CalcPi 2
3.14
java CalcPi 3
3.142
java CalcPi 4
3.1416
java CalcPi 5
3.14159
本文中更重要的部分是 Toolkit.getDefaultToolkit ().beep ();
,该语句用于在计算结束时发出“哗哗”的声音。 由于数字参数越大造成的计算时间越长,此声音可以让您知道 pi 的计算何时结束。 如果一声“哗”响不够用,可以按照如下方法创建其他的音效:
Toolkit tk = Toolkit.getDefaultToolkit ();
for (int i = 0; i < NUMBER_OF_BEEPS; i++)
{
tk.beep ();
// On Windows platforms, beep() typically
// plays a WAVE file. If beep() is called
// before the WAVE sound finishes, the
// second WAVE sound will not be heard. A
// suitable delay solves this problem.
// (I'm not sure if this problem occurs
// on other platforms.)
try
{
Thread.sleep (BEEP_DELAY);
}
catch (InterruptedException e)
{
}
}
窗口居中
使您的 Java 程序看起来更加专业的一种方法就是使其对话框窗口(例如,一个 "about" 窗口)位于父窗口的中间。 可以使用 java.awt.Window
的 public void setLocationRelativeTo(Component c)
方法完成此任务,该方法将相对于组件参数 —null
来创建一个居于屏幕中间的窗口。 请看列表 2。
列表 2 AboutBox1.java
// AboutBox1.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
public class AboutBox1
{
public static void main (String [] args)
{
final JFrame frame = new JFrame ("AboutBox1");
frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel ()
{
{
JButton btnWindow = new JButton ("Window center");
ActionListener l = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
new AboutBox (frame, "W").setVisible (true);
}
};
btnWindow.addActionListener (l);
add (btnWindow);
JButton btnScreen = new JButton ("Screen center");
l = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
new AboutBox (frame, "S").setVisible (true);
}
};
btnScreen.addActionListener (l);
add (btnScreen);
}
};
frame.getContentPane ().add (panel);
// frame.setLocationRelativeTo (null);
frame.pack ();
// frame.setLocationRelativeTo (null);
frame.setVisible (true);
}
}
class AboutBox extends JDialog
{
AboutBox (JFrame frame, String centerMode)
{
super (frame, "AboutBox", true /* modal */);
final JButton btnOk = new JButton ("Ok");
btnOk.addActionListener (new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
dispose ();
}
});
getContentPane ().add (new JPanel () {{ add (btnOk); }});
pack ();
setLocationRelativeTo (centerMode.equals ("W") ? frame : null);
}
}
Listing 2's AboutBox1
创建了一个 GUI,它的两个按钮建立了一个 "about" 对话框,该对话框通过 setLocationRelativeTo()
方法位于应用程序主窗口或屏幕的中心位置。 frame.pack ();
之前被注释掉的一行无法起到使主窗口居中的作用,因为主窗口的大小还没有确定。 但是,被注释掉的第二行起到了使窗口居中的作用。
getContentPane ().add (new JPanel () {{ add (btnOk); }});
也许看起来有点奇怪,因为它嵌套了许多括号。 本质上该语句可以理解为,创建一个内部匿名类(该类扩展自 javax.swing.JPanel
)的对象,通过由内层括号对标识的 object block initializer 为此对象添加一个按钮,然后将对象添加到对话框的 content pane 中。
添加阴影
如 果您想突出显示一个 "about" 对话框的标题文字,可以考虑以一定的偏移量和指定的颜色绘制背景文字以达到投放“阴影”的效果。 选择一个适当的颜色做为背景字的颜色,注意与前景文字和背景的颜色搭配。 然后使用反失真技术使边缘上的小锯齿变得平滑。 效果如图 1 所示:
图 1:阴影可以强调对话框的标题
图 1 展示了一个具有蓝色文字、黑色阴影和白色背景的 "about" 对话框。 该对话框是在 AboutBox2
程序的 AboutBox(JFrame frame, String centerMode)
构造函数内创建的。 由于该程序的代码与 AboutBox1.java
极为相似,所以我只给出其构造函数:
AboutBox (JFrame frame, String centerMode)
{
super (frame, "AboutBox", true /* modal */);
// Add a panel that presents some text to the dialog box's content pane.
getContentPane ().add (new JPanel ()
{
final static int SHADOW_OFFSET = 3;
{
// Establish the drawing panel's preferred
// size.
setPreferredSize (new Dimension (250, 100));
// Create a solid color border that both
// surrounds and is part of the drawing
// panel. Select the panel background
// color that is appropriate to this look
// and feel.
Color c =
UIManager.getColor ("Panel.background");
setBorder (new MatteBorder (5, 5, 5, 5, c));
}
public void paintComponent (Graphics g)
{
// Prevent jagged text.
((Graphics2D) g).setRenderingHint
(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// Because the border is part of the panel,
// we need to make sure that we don't draw
// over it.
Insets insets = getInsets ();
// Paint everything but the border white.
g.setColor (Color.white);
g.fillRect (insets.left, insets.top,
getWidth ()-insets.left-
insets.right,
getHeight ()-insets.top-
insets.bottom);
// Select an appropriate text font and
// obtain the dimensions of the text to be
// drawn (for centering purposes). The
// getStringBounds() method is used instead
// of stringWidth() because antialiasing is
// in effect -- and the documentation for
// stringWidth() recommends use of this
// method whenever the antialiasing or
// fractional metrics hints are in effect.
g.setFont (new Font ("Verdana",
Font.BOLD,
32));
FontMetrics fm = g.getFontMetrics ();
Rectangle2D r2d;
r2d = fm.getStringBounds ("About Box", g);
int width = (int)((Rectangle2D.Float) r2d)
.width;
int height = fm.getHeight ();
// Draw shadow text that is almost
// horizontally and vertically (the
// baseline) centered within the panel.
g.setColor (Color.black);
g.drawString ("About Box",
(getWidth ()-width)/2+
SHADOW_OFFSET,
insets.top+(getHeight()-
insets.bottom-insets.top)/2+
SHADOW_OFFSET);
// Draw blue text that is horizontally and
// vertically (the baseline) centered
// within the panel.
g.setColor (Color.blue);
g.drawString ("About Box",
(getWidth ()-width)/2,
insets.top+(getHeight()-
insets.bottom-insets.top)/2);
}
}, BorderLayout.NORTH);
final JButton btnOk = new JButton ("Ok");
btnOk.addActionListener (new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
dispose ();
}
});
getContentPane ().add (new JPanel () {{ add (btnOk); }},
BorderLayout.SOUTH);
pack ();
setLocationRelativeTo (centerMode.equals ("W") ? frame : null);
}
除了向您介绍如何在 JPanel
子类组件的 public void paintComponent(Graphics g)
方法中呈现阴影以外,构造函数还揭示了一个技巧:使用 UIManager.getColor("Panel.background")
获取与对话框背景色匹配的组件边框的颜色(即现在的外观)。
超级链接和启动浏览器
许多程序都会在 "about" 对话框中呈现超级链接。单击超级链接时,程序将启动默认浏览器,并为用户打开应用程序的网站。我想 "about" 对话框中的超级链接是可以用类来描述的,所以我创建了一个 AboutBox3
应用程序来说明其可行性。请阅读以下代码:
AboutBox (JFrame frame, String centerMode)
{
super (frame, "AboutBox", true /* modal */);
// Create a pane that presents this dialog box's text. Surround the pane
// with a 5-pixel empty border.
Pane pane = new Pane (5);
pane.setPreferredSize (new Dimension (250, 100));
// Create a title with a drop shadow for the pane.
Font font = new Font ("Verdana", Font.BOLD, 32);
Pane.TextNode tn = pane.new TextNode ("About Box", font, Color.blue,
Pane.TextNode.CENTERX,
Pane.TextNode.CENTERY, Color.black);
pane.add (tn);
// Create a link for the pane.
font = new Font ("Verdana", Font.BOLD, 12);
tn = pane.new TextNode ("Jeff Friesen", font, Color.blue,
Pane.TextNode.CENTERX, 80,
null, "http://www.javajeff.mb.ca", Color.red);
pane.add (tn);
// Add pane to the center region of the dialog box's content pane.
getContentPane ().add (pane);
// Create a button for disposing the dialog box.
final JButton btnOk = new JButton ("Ok");
btnOk.addActionListener (new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
dispose ();
}
});
// Add button via an intermediate panel that causes button to be laid
// out at its preferred size to the south region of the dialog box's
// content pane.
getContentPane ().add (new JPanel () {{ add (btnOk); }},
BorderLayout.SOUTH);
// Resize all components to their preferred sizes.
pack ();
// Center the dialog box with respect to the frame window or the screen.
setLocationRelativeTo (centerMode.equals ("W") ? frame : null);
}
AboutBox(JFrame frame, String centerMode)
构造函数创建了一个 Pane
组件,来描述一个用于绘制文字的区域。该组件的 Pane(int borderSize)
构造函数使用 borderSize
参数,来识别组件边框的大小(以像素为单位)-- 绘制区域的大小等于 Pane 的大小减去边框大小:
Pane (int borderSize)
{
// Create a solid color border that both surrounds and is part of the
// this component. Select the panel background color that is appropriate
// to this look and feel.
setBorder (new MatteBorder (borderSize, borderSize, borderSize,
borderSize,
UIManager.getColor ("Panel.background")));
}
该组件将文字存储为 Pane.TextNode
对象的数组列表。每个 TextNode
描述一个文字条目,并且创建自三个构造函数之一。最简单的构造函数是 TextNode(String text, Font font, Color color, int x, int y)
,它用于创建一个非超级链接且没有阴影的文字节点。使用的五个参数是:
text
指定要绘制的文字
font
指定要使用的字体
font
指定要使用的文字颜色
x
指定第一个字符的起始列
y
指定每个字符基线所在的行
第二简单的构造函数是:TextNode(String text, Font font, Color color, int x, int y, Color shadowColor)
.除了上述参数外,还需要为该函数指定 shadowColor
,即阴影的颜色。如果传递 null
,则不呈现阴影,这样一来该构造函数就与前一构造函数一样了。这两个构造函数都调用下面的第三个构造函数:
TextNode (String text, Font font, Color color, int x, int y,
Color shadowColor, String url, Color activeLinkColor)
{
this.text = text;
this.font = font;
this.color = color;
this.x = x;
this.y = y;
this.shadowColor = shadowColor; this.url = url;
this.activeLinkColor = activeLinkColor;
if (url != null)
{
addMouseListener (new MouseAdapter ()
{
public void mousePressed (MouseEvent e)
{
int mx = e.getX ();
int my = e.getY ();
if (mx >= TextNode.this.x &&
mx < TextNode.this.x+width &&
my > TextNode.this.y-height &&
my <= TextNode.this.y)
{
active = false;
repaint ();
}
}
public void mouseReleased (MouseEvent e)
{
int mx = e.getX ();
int my = e.getY ();
if (mx >= TextNode.this.x &&
mx < TextNode.this.x+width &&
my > TextNode.this.y-height &&
my <= TextNode.this.y)
{
active = true;
repaint ();
Launcher.
launchBrowser (TextNode.this.url);
}
}
});
addMouseMotionListener (new MouseMotionListener ()
{
public void mouseMoved (MouseEvent e)
{
int mx = e.getX ();
int my = e.getY ();
if (mx >= TextNode.this.x &&
mx < TextNode.this.x+width &&
my > TextNode.this.y-height &&
my <= TextNode.this.y)
{
if (!active)
{
active = true;
repaint ();
}
}
else
{
if (active)
{
active = false;
repaint ();
}
}
}
public void mouseDragged (MouseEvent e)
{
}
});
}
}
保存参数后,该构造函数会在 Pane 中注册一个鼠标监听器(假设 url
不为 null
)。这些侦听器将判断鼠标指针是否位于超级链接文本上。如果是,则操纵 active
变量,将呈现该节点及其他节点,并启动浏览器。
class Launcher
{
static void launchBrowser (String url)
{
try
{
// Identify the operating system.
String os = System.getProperty ("os.name");
// Launch browser with URL if Windows. Otherwise, just output the url
// to the standard output device.
if (os.startsWith ("Windows"))
Runtime.getRuntime ()
.exec ("rundll32 url.dll,FileProtocolHandler " + url);
else
System.out.println (url);
}
catch (IOException e)
{
System.err.println ("unable to launch browser");
}
}
}
Pane
的 public void paintComponent(Graphics g)
方法将在该组件及其文本节点呈现的时候被调用。该方法启用反失真技术(防止文字出现锯齿),获取组件的插入位置(所以文本不会落在边框上),清理绘制区域使其成为白色,并呈现数组列表存储的每个文字节点。
public void paintComponent (Graphics g)
{
// Prevent jagged text.
((Graphics2D) g).setRenderingHint (RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// Because the border is part of the panel, we need to make sure that we
// don't draw over it.
Insets insets = getInsets ();
// Paint everything but the border white.
g.setColor (Color.white);
g.fillRect (insets.left, insets.top, getWidth ()-insets.left-insets.right,
getHeight ()-insets.top-insets.bottom);
// Render all nodes.
Iterator iter = nodes.iterator ();
while (iter.hasNext ())
{
TextNode tn = (TextNode) iter.next ();
tn.render (g, insets);
}
}
每个文本节点都是由 TextNode
的 void render(Graphics g, Insets insets)
方法呈现的。该方法首先确定了字体,然后调用私有的 strDim()
方法来获取要绘制文字的规格尺寸等。然后呈现文本(可以选择阴影、超级链接等属性):
void render (Graphics g, Insets insets)
{
g.setFont (font);
Dimension d = strDim (g, text);
width = (int) d.width;
height = (int) d.height;
// Always drop the drop shadow (if specified) first.
if (shadowColor != null)
{
g.setColor (shadowColor);
if (x == CENTERX)
x = (getWidth ()-d.width)/2;
if (y == CENTERY)
y = insets.top+(getHeight ()-insets.bottom-insets.top)/2;
// Draw the drop shadow.
g.drawString (text, x+SHADOW_OFFSET, y+SHADOW_OFFSET);
}
// If the text is not a link, active can never be true -- the mouse
// listeners are not installed.
g.setColor ((active) ? activeLinkColor: color);
// If a drop shadow was drawn, x and y will never equal CENTERX and
// CENTERY (respectively). This is okay because x and y must contain
// the same values as specified when drawing the drop shadow.
if (x == CENTERX)
x = (getWidth ()-d.width)/2;
if (y == CENTERY)
y = insets.top+(getHeight ()-insets.bottom-insets.top)/2;
// Draw the text.
g.drawString (text, x, y);
}
g.setColor ((active) ? activeLinkColor: color);
决定是否绘制有效的超级链接文本(使用由activeLinkColor
指定的有效链接颜色),或者使用由 color
指定的颜色绘制无效超级链接文本(或非超级链接文本)。图 2 显示了此决定的结果:
图 2 当鼠标指针移动到文本上时,超级链接的文本变成了红色。单击可查看大图。
状态栏
许多应用程序都会呈现状态栏,以显示程序的名称和版本、相应于菜单的帮助文字、当前时间以及一些其他信息。由于状态栏如此有用,您可能认为 Java 包含了一个 javax.swing.JStatusBar
组件。然而,事实并非如此。幸运的是,创建自己的状态栏还算容易,如图 3 所示。
图 3. 状态栏中当前菜单项的帮助文字
图 3 显示了一个由 StatBar
应用程序创建的状态栏。该应用程序将一个 javax.swing.JLabel
组件和一个 javax.swing.event.MenuListener
结合在一起,然后使用 java.awt.event.MouseListener
显示相应于菜单的菜单项帮助文字 -- 或者在未选择菜单或菜单项时显示默认文字。请看列表 3。
列表 3. StatBar.java
// StatBar.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class StatBar extends JFrame
{
// The status label serves as this application's status bar. A description
// of the currently highlighted menu/item appears on the status bar.
JLabel status;
// The default text appears on the status bar at program startup, and when
// no other menu/item text appears.
String defaultStatusText = "Welcome to StatBar 1.0!";
// The MenuItem helper class conveniently organizes the menu items for each
// of the File and Edit menus. This organization reduces the amount of
// source code that appears in the StatBar() constructor, which hopefully
// makes it easier to study the constructor, and facilitates adding extra
// menu items in the future.
class MenuItem
{
String label; // menu text
ActionListener al; String desc; // menu description for status bar
MenuItem (String label, ActionListener al, String desc)
{
this.label = label;
this.al = al;
this.desc = desc;
}
}
// Construct StatBar's GUI and indirectly start AWT helper threads.
public StatBar (String title)
{
// Pass application title to superclass, so that it appears on the title
// bar.
super (title);
// When the user initiates a close operation from the System menu or by
// clicking the tiny x window on a Microsoft Windows' window title bar,
// terminate this application.
setDefaultCloseOperation (EXIT_ON_CLOSE);
// Construct the application's menu bar.
JMenuBar mb = new JMenuBar ();
// Create a menu listener shared by all menus on the menu bar. This menu
// listener either displays default text or menu-specific text on the
// status bar.
MenuListener menul;
menul = new MenuListener ()
{
public void menuCanceled (MenuEvent e)
{
}
public void menuDeselected (MenuEvent e)
{
status.setText (defaultStatusText);
}
public void menuSelected (MenuEvent e)
{
JMenu m = (JMenu) e.getSource ();
status.setText (m.getActionCommand ());
}
};
// Create a mouse listener shared by all menu items on all menus. This
// mouse listener displays menu-item specific text on the status bar
// whenever the mouse pointer enters the menu item. It displays default
// text when the mouse pointer exits a menu item.
MouseListener statusl = new MouseAdapter ()
{
public void mouseEntered (MouseEvent e)
{
JMenuItem mi = (JMenuItem) e.getSource ();
status.setText (mi.getActionCommand ());
}
public void mouseExited (MouseEvent e)
{
status.setText (defaultStatusText);
}
};
// The first menu to appear on the menu bar is File. The user invokes
// menu items on this menu to open, save, and print documents, and to
// terminate the application.
JMenu menuFile = new JMenu ("File");
menuFile.addMenuListener (menul);
menuFile.setActionCommand ("Open document, save changes, print document "
+ "and terminate StatBar.");
// Create a listener for each menu item on the File menu.
ActionListener openl;
openl = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.out.println ("Open listener invoked.");
}
};
ActionListener saveasl;
saveasl = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.out.println ("Save as listener invoked.");
}
};
ActionListener savel;
savel = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.out.println ("Save listener invoked.");
}
};
ActionListener printl;
printl = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.out.println ("Print listener invoked.");
}
};
ActionListener exitl;
exitl = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.exit (0);
}
};
// Identify menu items to be installed on the File menu.
MenuItem [] itemsFile =
{
new MenuItem ("Open...", openl, "Open a document."),
new MenuItem ("Save", savel, "Save changes to current document."),
new MenuItem ("Save as...", saveasl, "Save current document to new "
+ "document."),
new MenuItem ("Print...", printl, "Print current document."),
new MenuItem (null, null, null),
new MenuItem ("Exit", exitl, "Terminate StatBar.")
};
// Install all of the previous menu items on the File menu.
for (int i = 0; i < itemsFile.length; i++)
{
if (itemsFile [i].label == null)
{
menuFile.addSeparator ();
continue;
}
JMenuItem mi = new JMenuItem (itemsFile [i].label);
mi.addActionListener (itemsFile [i].al);
mi.setActionCommand (itemsFile [i].desc);
mi.addMouseListener (statusl);
menuFile.add (mi);
}
// Add the file menu to the menu bar.
mb.add (menuFile);
// The second menu to appear on the menu bar is Edit. The user invokes
// menu items on this menu to undo any changes and perform copy/cut/paste
// operations on the current document.
JMenu menuEdit = new JMenu ("Edit");
menuEdit.addMenuListener (menul);
menuEdit.setActionCommand ("Perform various editing tasks and undo " +
"changes.");
// Create a listener for each menu item on the Edit menu.
ActionListener undol;
undol = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.out.println ("Undo listener invoked.");
}
};
ActionListener copyl;
copyl = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.out.println ("Copy listener invoked.");
}
};
ActionListener cutl;
cutl = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.out.println ("Cut listener invoked.");
}
};
ActionListener pastel;
pastel = new ActionListener ()
{
public void actionPerformed (ActionEvent e)
{
System.out.println ("Paste listener invoked.");
}
};
// Identify menu items to be installed on the Edit menu.
MenuItem [] itemsEdit =
{
new MenuItem ("Undo", undol, "Restore document."),
new MenuItem (null, null, null),
new MenuItem ("Copy", copyl, "Copy text to clipboard."),
new MenuItem ("Cut", cutl, "Cut text to clipboard."),
new MenuItem ("Paste", pastel, "Paste text from clipboard.")
};
// Install all of the previous menu items on the Edit menu.
for (int i = 0; i < itemsEdit.length; i++)
{
if (itemsEdit [i].label == null)
{
menuEdit.addSeparator ();
continue;
}
JMenuItem mi = new JMenuItem (itemsEdit [i].label);
mi.addActionListener (itemsEdit [i].al);
mi.setActionCommand (itemsEdit [i].desc);
mi.addMouseListener (statusl);
menuEdit.add (mi);
}
// Add the edit menu to the menu bar.
mb.add (menuEdit);
// Install StatBar's menu bar.
setJMenuBar (mb);
// Create a status bar for displaying help text associated with the menus
// and their items.
status = new JLabel (defaultStatusText);
status.setBorder (BorderFactory.createEtchedBorder ());
// Add the status bar to the bottom of the application's contentpane.
getContentPane ().add (status, BorderLayout.SOUTH);
// Establish a suitable initial size for displaying a document.
setSize (450, 300);
// Display GUI and start GUI processing.
setVisible (true);
}
// Application entry point.
public static void main (String [] args)
{
// Create the application's GUI and start the application.
new StatBar ("StatBar");
}
}
在使用默认状态栏文字创建了 status
JLabel
之后,StatBar
刻画了一个边框 (通过 status.setBorder (BorderFactory.createEtchedBorder ());
)以使状态栏标签与 GUI 的其他部分区别开来。然后,标签被添加到框架窗口内容窗格的南侧区域,这是状态栏的常见位置。
注意
如果不希望状态栏显示任何默认文字,则需要至少输入一个空格来代替文字。如果使用空字符串 ("" ) 来代替,则不会显示状态栏(虽然会显示其边框)。必须处理状态栏标签的首选字体大小,因为这涉及到当前的字体和那个不可缺少的字符。如果状态栏标签不包含任何字符(空字符串),其首选字体大小就会是 0,造成状态栏无法显示。 |
MenuItemListener
接口描述了一个 "File" 和 "Edit" 菜单的侦听程序。此接口的 public void menuSelected(MenuEvent e)
方法将在选择这些菜单时被调用,然后会显示菜单的帮助文字。选中一个菜单时,调用 public void menuDeselected(MenuEvent e)
显示文字。
MouseListener
接口描述每个菜单项的侦听程序。其 public void mouseEntered(MouseEvent e)
在鼠标进入一个菜单项时被调用。然后,菜单项的帮助文字会显示在状态栏中。鼠标指针移动到菜单项外时,调用 public void mouseExited(MouseEvent e)
,然后显示默认文字。
每个侦听程序都依赖于 javax.swing.JMenu
或 javax.swing.JMenuItem
的继承 public void setActionCommand(String command)
方法,此方法曾在指定每个菜单或菜单项的状态栏文字时被调用。文字是在侦听程序内部进行检索的,方法是调用相关的 public String getActionCommand()
方法。
抓图程序
几年前,我构建过一个基于命令行的 GetImages
应用程序 -- 请看列表 4 -- 来获取网页中的图像,并将它们存放到我的磁盘上。此应用程序只有一个用于连接到 HTML 文档的 URL 参数。解析 HTML 文档,将
标记的 src
属性值提取为可识别的图像文件,然后下载这些文件。
列表 4 GetImages.java
// GetImages.java
import java.io.*;
import java.net.*;
import java.util.regex.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import javax.swing.text.html.parser.ParserDelegator;
public class GetImages
{
public static void main (String [] args)
{
// Validate number of command-line arguments.
if (args.length != 1)
{
System.err.println ("usage: java GetImages URL");
return;
}
// Create a Base URI from the solitary command-line argument. This URI
// will be used in the handleSimpleTag() callback method to convert a
// potentially relative URI in an tag's src attribute to an
// absolute URI.
final URI uriBase;
try
{
uriBase = new URI (args [0]);
}
catch (URISyntaxException e)
{
System.err.println ("URI is improperly formed");
return;
}
// Convert the URI to a URL, so that the HTML document can be read and
// parsed.
URL url;
try
{
url = new URL (args [0]);
}
catch (MalformedURLException e)
{
System.err.println ("URL is improperly formed");
return;
}
// Establish a callback whose handleSimpleTag() method is invoked for
// each tag that does not have an end tag. The tag is an example.
HTMLEditorKit.ParserCallback callback;
callback = new HTMLEditorKit.ParserCallback ()
{
public void handleSimpleTag (HTML.Tag tag,
MutableAttributeSet aset,
int pos)
{
// If an tag is encountered ...
if (tag == HTML.Tag.IMG)
{
// Get the value of the src attribute.
String src = (String)
aset.getAttribute (HTML.Attribute.SRC);
// Create a URI based on the src value, and then
// resolve this potentially relative URI against
// the document's base URI, to obtain an absolute
// URI.
URI uri = null;
try
{
// Handle this situation:
//
// 1) http://www.javajeff.mb.ca
//
// There is no trailing forward slash.
//
// 2) common/logo.jpg
//
// There is no leading forward slash.
//
// 3) http://www.javajeff.mb.cacommon/logo.jpg
//
// The resolved URI is not valid.
if (!uriBase.toString ().endsWith ("/") &&
!src.startsWith ("/"))
src = "/" + src;
uri = new URI (src);
uri = uriBase.resolve (uri);
System.out.println ("uri being " +
"processed ... " + uri);
}
catch (URISyntaxException e) {
System.err.println ("Bad URI");
return;
}
// Convert the URI to a URL so that its input
// stream can be obtained.
URL url = null;
try
{
url = uri.toURL ();
}
catch (MalformedURLException e)
{
System.err.println ("Bad URL");
return;
}
// Open the URL's input stream.
InputStream is;
try
{
is = url.openStream ();
}
catch (IOException e)
{
System.err.println ("Unable to open input " +
"stream");
return;
}
// Extract URL's file component and remove path
// information -- only the filename and its
// extension are wanted.
String filename = url.getFile ();
int i = filename.lastIndexOf ('/');
if (i != -1)
filename = filename.substring (i+1);
// Save image to file.
saveImage (is, filename);
}
}
};
// Read and parse HTML document.
try
{
// Read HTML document via an input stream reader that assumes the
// default character set for decoding bytes into characters.
Reader reader = new InputStreamReader (url.openStream ());
// Establish a ParserDelegator whose parse() method causes the
// document to be parsed. Various callback methods are called and
// the document's character set is not ignored. The parse() method
// throws a ChangedCharSetException if it encounters a tag
// with a charset attribute that specifies a character set other
// than the default.
new ParserDelegator ().parse (reader, callback, false);
}
catch (ChangedCharSetException e)
{
// Reparse the entire file using the specified charset. A regexp
// pattern is specified to extract the charset name.
String csspec = e.getCharSetSpec ();
Pattern p = Pattern.compile ("charset=\"?(.+)\"?\\s*;?",
Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher (csspec);
String charset = m.find () ? m.group (1) : "ISO-8859-1";
// Read and parse HTML document using appropriate character set.
try
{
// Read HTML document via an input stream reader that uses the
// specified character set to decode bytes into characters.
Reader reader;
reader = new InputStreamReader (url.openStream (), charset);
// This time, pass true to ignore the tag with its charset
// attribute.
new ParserDelegator ().parse (reader, callback, true);
}
catch (UnsupportedEncodingException e2)
{
System.err.println ("Invalid charset");
}
catch (IOException e2)
{
System.err.println ("Input/Output problem");
e.printStackTrace ();
}
}
catch (IOException e)
{
System.err.println ("Input/Output problem");
e.printStackTrace ();
}
}
public static void saveImage (InputStream is, String filename)
{
FileOutputStream fos = null;
try
{
fos = new FileOutputStream (filename);
int bYte;
while ((bYte = is.read ()) != -1)
fos.write (bYte);
}
catch (IOException e)
{
System.err.println ("Unable to save stream to file");
}
finally
{
if (fos != null)
try
{
fos.close ();
}
catch (IOException e)
{
}
}
}
}
列表 4 使用 javax.swing.text.html.parser.ParserDelegator
类创建一个 HTML 文档解析器,调用解析器对象的 public void parse(Reader r, HTMLEditorKit.ParserCallback cb, boolean ignoreCharSet)
实行解析。此方法使用了三个参数:
r
标识一个用于读取 HTML 文档的 java.io.Reader
对象。
cb
标识一个处理所解析的标记及其属性的 javax.swing.text.html.HTMLEditorKit.ParserCallback
对象。
ignoreCharSet
标识是否忽略文档的
标记(如果存在)中的 charset
属性。
解析器在解析过程中调用了各种各样的 ParserCallback
方法。但只有一个方法是 GetImages
需要的:即 public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos)
方法。这是一个为没有结束标记的标记(例如,
)调用的方法,它使用了三个参数:
t
通过 HTML.Tag
对象对标记进行标识。
a
通过 javax.swing.text.MutableAttributeSet
对象标识标记内的属性。
pos
标识当前解析到的位置
现在您已经清楚地认识到 GetImages
的工作原理,应该试着运行一下了。例如,您可以指定 java GetImages http://www.javajeff.mb.ca
将该网页上的图像下载到自己的 Web 站点的主页上。结果应该与以下类似(当前目录下出现了一些新图像文件):
uri being processed ... http://www.javajeff.mb.ca/common/logo.jpg
uri being processed ... http://www.javajeff.mb.ca/common/logo.gif
uri being processed ... http://www.javajeff.mb.ca/na/images/wom.jpg
但是 GetImages
在命令行模式下再多么有用,也不及使用 GUI 抓取和查看图像来得方便。我的 IG
应用程序,结合 GetImages
源代码和前面的一些技巧以及一些 GUI 代码正是为此而生。图 4 展示了 GetImages
的 GUI,建议您查看本文的代码归档文件(资源)以获取源代码。
图 4 使用抓图程序的 GUI 方便地抓取和查看图像
结束语
此图像抓取程序将文章中介绍的技巧很好地结合在一起,但是还遗留了一些问题,关于这些问题的解决方法就当成家庭作业吧。问题 1:抓图程序下载图像,图像的标记需要指明相对 URL (例如,
),但是无法下载指明绝对 URL 的图像(例如,
)。
问题 2:抓图程序无法下载动态生成
的 src
属性的图像。作为例子,以下
标记的 src
属性是我最近在 JavaWorld 主页上发现的:src="http://ad.doubleclick.net/ad/idg.us.nwf.jw_home/;abr=!ie;pos=top;sz=728x90;ptile=1;type=;ord=063423?"
。
作者简介
Jeff Friesen 是一位自由职业的软件开发者,擅长的领域是 C、C++ 和 Java 技术。
资源
<externallinks no-escaping="yes"></externallinks>