摘要
Java Fun and Games(Java娱乐和游戏)提供了通过Java的Robot类捕获主屏幕设备的功能,并且可以将整个屏幕或者选定的一部分保存为jpeg文件。
注意:现在你可以使用在线开发工具DevSquare编译和运行Java Fun and Games中提供的applet。DevSquare入门请阅读资源中提供的用户向导。
java.awt.Robot类为娱乐功能提供了一些有用的方法。其中一个包括了建立屏幕捕获工具的功能。Java Fun and Games给出了一个使用Robot捕获主屏幕设备内容的工具。
这一部分从我以前的几部分中分离出来了,因为它并不是集中在applet实现上。这篇文章以Swing应用的形式实现了屏幕捕获工具。从GUI观点介绍完这个应用之后,我将解释实现的关键部分。
版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
作者:Jeff Friesen; mydeman
原文: http://www.javaworld.com/javaworld/jw-04-2006/jw-0424-funandgames.html
Matrix: http://www.matrix.org.cn/resource/article/2006-09-15/Java+Robot_f9598e5e-445b-11db-af0b-0f766c077b58.html
关键字:Java Robot;捕获屏幕
应用程序GUI
我的Capture程序提供了一个图形用户界面(GUI,Graphic User Interface),通过它你可以选择捕获图像的一部分,修剪图像到选择内容,以及将结果图像保存为jpeg文件。图1显示了包含一个捕获示例的Capture的GUI。
图 1. 红白相间的虚线所形成的矩形表示了当前选中的区域
Capture的GUI由菜单栏和显示捕获图像的可滚动窗口组成。如图1所示,选择矩形(通过拖拽鼠标)表示了捕获图形的一个矩形区域。
菜单栏提供了File和Capture菜单:
---File提供Save As…(另存为)和Exit(退出)菜单项,可以通过文件选择器保存当前捕获为一个jpeg文件,和退出Capture。尽管你可以直接选择这些菜单项,但是你会发现使用它们的快捷键Alt-S和Alt-X会更加方便。
---Capture提供Capture(捕获)和Crop(修剪)菜单项,可以捕获当前主屏幕设备的内容和修剪一个图像为选择矩形的内容。和File菜单项一样,这些菜单项也有它们自己的方便的快捷键:Capture(Alt-C)和Crop(Alt-K)。
应用实现
有三个源文件来描述Capture的GUI:Capture.java(启动应用程序和构造GUI)、ImageArea.java( 描述了一个用来显示捕获的内容的组件,你也可以在其中选择捕获的一部分或修剪捕获的内容)和ImageFileFilter.java(限制文件选择器的选择是文件夹和jpeg文件)。在这一部分下面,我从这些源文件中摘录了一些代码片断来说明Capture的工作过程。
机器人屏幕捕获
为了使用Robot类捕获屏幕,Capture必须先创建一个Robot对象。Capture类的public static void main(String [] args)方法尝试调用Robot的public Robot()构造函数来创建这个对象。如果创建成功,就会返回一个针对主屏幕设备坐标系的Robot引用。如果平台不支持低级控制(在没有屏幕设备的环境这是成立的),将会抛出java.awt.AWTException。如果平台不允许创建Robot对象就会抛出java.lang.SecurityException。但愿你不会再遇到其他异常。
假设Robot对象已被创建,main()调用Capture类的构造函数创建一个GUI。作为GUI创建的一部分,Capture通过调用dimScreenSize = Toolkit.getDefaultToolkit().getScreenSize();获得主屏幕设备的尺寸。因为用来显示屏幕捕获的内容的Robot的public BufferedImage createScreenCapture(Rectangle screenRect)方法,需要一个java.awt.Rectangle参数,所以构造函数通过rectScreenSize = new Rectangle(dimScreenSize);将java.awt.Dimension对象转换为一个Rectangle对象。当Capture菜单项的动作监听器被调用时,下面摘录的Capture.java片断就会调用createScreenCapture()。
// Hide Capture's main window so that it does not appear in
// the screen capture.
setVisible (false);
// Perform the screen capture.
BufferedImage biScreen;
biScreen = robot.createScreenCapture (rectScreenSize);
// Show Capture's main window for continued user interaction.
setVisible (true);
// Update ImageArea component with the new image and adjust
// the scrollbars.
ia.setImage (biScreen);
jsp.getHorizontalScrollBar ().setValue (0);
jsp.getVerticalScrollBar ().setValue (0);
你不希望Capture的GUI遮住你想要捕获的任何内容。这就是为什么代码中隐藏Capture GUI优先级高于完成捕获。在获取了包含屏幕像素copy的java.awt.image.BufferedImage后,代码片断显示出GUI,并且通过图像区域组件显示出BufferedImage的内容。
子图像选择
当从一个捕获的图像中获取子图像时需要一个选择矩形。ImageArea类提供代码来创建、操作和绘制选择矩形。如下面摘录的ImageArea.java所示,这个类的构造函数以一个Rectangle实例创建选择矩形,创建java.awt.BasicStoke和java.awt.GradientPaint对象定义了矩形的轮廓外观(保持它与底层图像分离),注册鼠标和鼠标动作监听器让你能够操作选择矩形。
// Create a selection Rectangle. It's better to create one Rectangle
// here than a Rectangle each time paintComponent() is called, to reduce
// unnecessary object creation.
rectSelection = new Rectangle ();
// Define the stroke for drawing selection rectangle outline.
bs = new BasicStroke (5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
0, new float [] { 12, 12 }, 0);
// Define the gradient paint for coloring selection rectangle outline.
gp = new GradientPaint (0.0f, 0.0f, Color.red, 1.0f, 1.0f, Color.white,
true);
// Install a mouse listener that sets things up for a selection drag.
MouseListener ml;
ml = new MouseAdapter ()
{
public void mousePressed (MouseEvent e)
{
// When you start Capture, there is no captured image.
// Therefore, it makes no sense to try and select a sub-image.
// This is the reason for the if (image == null) test.
if (image == null)
return;
destx = srcx = e.getX ();
desty = srcy = e.getY ();
repaint ();
}
};
addMouseListener (ml);
// Install a mouse motion listener to update the selection rectangle
// during drag operations.
MouseMotionListener mml;
mml = new MouseMotionAdapter ()
{
public void mouseDragged (MouseEvent e)
{
// When you start Capture, there is no captured image.
// Therefore, it makes no sense to try and select a
// sub-image. This is the reason for the if (image == null)
// test.
if (image == null)
return;
destx = e.getX ();
desty = e.getY ();
repaint ();
}
};
addMouseMotionListener (mml);
当按下鼠标时,鼠标事件处理器对相同的横向鼠标坐标设置destx和srcx,对于纵向鼠标坐标亦是如此。源变量和目标变量同样表示哪些显示的选择矩形应该被移除了。它通过调用repaint(),导致public void paintComponent(Graphics g)被调用。这个方法将srcx和srcy分别与destx和desty相比较,如果他们不同,就绘制一个选择矩形:
// Draw the selection rectangle if present.
if (srcx != destx || srcy != desty)
{
// Compute upper-left and lower-right coordinates for selection
// rectangle corners.
int x1 = (srcx < destx) ? srcx : destx;
int y1 = (srcy < desty) ? srcy : desty;
int x2 = (srcx > destx) ? srcx : destx;
int y2 = (srcy > desty) ? srcy : desty;
// Establish selection rectangle origin.
rectSelection.x = x1;
rectSelection.y = y1;
// Establish selection rectangle extents.
rectSelection.width = (x2-x1)+1;
rectSelection.height = (y2-y1)+1;
// Draw selection rectangle.
Graphics2D g2d = (Graphics2D) g;
g2d.setStroke (bs);
g2d.setPaint (gp);
g2d.draw (rectSelection);
}
在选择矩形绘制以前,它的左上和右下角必须对标示出来,用来确定矩形的原点和范围。以至于你可以在不同的方向拖拽出选择矩形(例如右下或者左上方向),srcx/destx和srcy/desty的最小值表示左上角,相似地,它们的最大值表示右下角。
图像修剪
在选择子图像后,你想要修剪捕获的图像得到子图像。图像修剪启动Crop中的菜单项的动作监听器,它请求ImageArea将捕获的图像修剪为选择的子图像。若操作成果,监听器则重置ImageArea的滚动条。反之,监听器通过对话框给出一个“Out of bounds”错误信息。
// Crop ImageArea component and adjust the scrollbars if
// cropping succeeds.
if (ia.crop ())
{
jsp.getHorizontalScrollBar ().setValue (0);
jsp.getVerticalScrollBar ().setValue (0);
}
else
showError ("Out of bounds.");
因为修剪操作不重置Capture GUI的大小,所以可以同时看到主窗口的背景和结果图像(初始修剪后的)。图2显示了选择图像的一部分时还可能选中背景的一部分。
图 2. 尝试选择多于这个图像
主窗口的背景像素不是捕获的图像的一部分;就不可能把它们包含在修剪的图片内。因此,无论何时把背景像素包含在修剪区域内,操作都会失败,并且会给出一个“Out of bounds”错误信息。
修剪操作由ImageArea的public Boolean crop()方法处理。如果完成了修剪或者没有选择子图像(当没有选中内容时调用这个方法是非常方便的)该方法(如下所示)返回true。如果在选择区域中包含了背景像素则返回false。
public boolean crop ()
{
// There is nothing to crop if the selection rectangle is only a single
// point.
if (srcx == destx && srcy == desty)
return true;
// Assume success.
boolean succeeded = true;
// Compute upper-left and lower-right coordinates for selection rectangle
// corners.
int x1 = (srcx < destx) ? srcx : destx;
int y1 = (srcy < desty) ? srcy : desty;
int x2 = (srcx > destx) ? srcx : destx;
int y2 = (srcy > desty) ? srcy : desty;
// Compute width and height of selection rectangle.
int width = (x2-x1)+1;
int height = (y2-y1)+1;
// Create a buffer to hold cropped image.
BufferedImage biCrop = new BufferedImage (width, height,
BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = biCrop.createGraphics ();
// Perform the crop operation.
try
{
BufferedImage bi = (BufferedImage) image;
BufferedImage bi2 = bi.getSubimage (x1, y1, width, height);
g2d.drawImage (bi2, null, 0, 0);
}
catch (RasterFormatException e)
{
succeeded = false;
}
g2d.dispose ();
if (succeeded)
setImage (biCrop); // Implicitly remove selection rectangle.
else
{
// Prepare to remove selection rectangle.
srcx = destx;
srcy = desty;
// Explicitly remove selection rectangle.
repaint ();
}
return succeeded;
}
crop()方法调用BufferedImage的public BufferedImage getSubimage(int x, int y, int w, int h)方法摘取选择区域内的子图像。如果该方法的参数没有指定BufferedImage内的图像,它就会抛出一个java.awt.image.RasterFormatException,因此就会返回false。
图像保存
Capture允许你把捕获的图像保存为一个jpeg文件。你通过一个保存文件选择器指定文件名,选择器由Capture类的构造函数创建:
// Construct a save file-chooser. Initialize the starting directory to
// the current directory, do not allow the user to select the "all files"
// filter, and restrict the files that can be selected to those ending
// with .jpg or .jpeg extensions.
final JFileChooser fcSave = new JFileChooser ();
fcSave.setCurrentDirectory (new File (System.getProperty ("user.dir")));
fcSave.setAcceptAllFileFilterUsed (false);
fcSave.setFileFilter (new ImageFileFilter ());
为了限制文件选择器的选择是文件夹或者是以.jpg或.jpeg为后缀的文件,就使用了ImageFileFilter类的一个实例作为保存时文件选择器的文件过滤器。该方法对于任何非文件夹和后缀名非.jpg/.jpeg的文件都返回false:
public boolean accept (File f)
{
// Allow the user to select directories so that the user can navigate the
// file system.
if (f.isDirectory ())
return true;
// Allow the user to select files ending with a .jpg or a .jpeg
// extension.
String s = f.getName ();
int i = s.lastIndexOf ('.');
if (i > 0 && i < s.length ()-1)
{
String ext = s.substring (i+1).toLowerCase ();
if (ext.equals ("jpg") || ext.equals ("jpeg"))
return true;
}
// Nothing else can be selected.
return false;
}
当你选择了Save As…菜单项时,它的监听器就会显示一个保存文件选择器。假定你没有退出选择器,监听器就会确保你选择的文件名是以.jpg或.jpeg为后缀名。继续,监听器会确定文件是否存在,这样你就不会无意中覆盖一个存在的文件。
// Present the "save" file-chooser without any file selected.
// If the user cancels this file-chooser, exit this method.
fcSave.setSelectedFile (null);
if (fcSave.showSaveDialog (Capture.this) !=
JFileChooser.APPROVE_OPTION)
return;
// Obtain the selected file. Validate its extension, which
// must be .jpg or .jpeg. If extension not present, append
// .jpg extension.
File file = fcSave.getSelectedFile ();
String path = file.getAbsolutePath ().toLowerCase ();
if (!path.endsWith (".jpg") && !path.endsWith (".jpeg"))
file = new File (path += ".jpg");
// If the file exists, inform the user, who might not want
// to accidentally overwrite an existing file. Exit method
// if the user specifies that it is not okay to overwrite
// the file.
if (file.exists ())
{
int choice = JOptionPane.
showConfirmDialog (null,
"Overwrite file?",
"Capture",
JOptionPane.
YES_NO_OPTION);
if (choice == JOptionPane.NO_OPTION)
return;
}
如果文件不存在或者你允许覆盖已经存在的文件,监听器就会将捕获的内容保存为一个选择的文件。为了完成这个任务,监听器使用Java的ImageIO框架选择一个jpeg writer,指定文件作为writer的目标,设置writer的压缩品质为95%,然后把图像写入到文件中。
ImageWriter writer = null;
ImageOutputStream ios = null;
try
{
// Obtain a writer based on the jpeg format.
Iterator iter;
iter = ImageIO.getImageWritersByFormatName ("jpeg");
// Validate existence of writer.
if (!iter.hasNext ())
{
showError ("Unable to save image to jpeg file type.");
return;
}
// Extract writer.
writer = (ImageWriter) iter.next();
// Configure writer output destination.
ios = ImageIO.createImageOutputStream (file);
writer.setOutput (ios);
// Set jpeg compression quality to 95%.
ImageWriteParam iwp = writer.getDefaultWriteParam ();
iwp.setCompressionMode (ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionQuality (0.95f);
// Write the image.
writer.write (null,
new IIOImage ((BufferedImage)
ia.getImage (), null, null),
iwp);
}
catch (IOException e2)
{
showError (e2.getMessage ());
}
finally
{
try
{
// Cleanup.
if (ios != null)
{
ios.flush ();
ios.close ();
}
if (writer != null)
writer.dispose ();
}
catch (IOException e2)
{
}
}
让代码自己清理一直是一个不错的主意。我把ImageIO的清理代码放在了finally子句中,以至于无论是正常结束还是抛出异常,它都可以执行。
总结
Capture限制了捕获的内容只能在主屏幕设备内。你可能想增强Capture来捕获所有附加屏幕设备(或许是一个巨大的虚拟屏幕)的内容。增强之一,你需要包含下面的代码,它捕获所有屏幕的内容,将它和Capture.java已经存在的代码集成。
GraphicsEnvironment graphenv = GraphicsEnvironment.getLocalGraphicsEnvironment ();
GraphicsDevice [] screens = graphenv.getScreenDevices ();
BufferedImage [] captures = new BufferedImage [screens.length];
for (int i = 0; i < screens.length; i++)
{
DisplayMode mode = screens [i].getDisplayMode ();
Rectangle bounds = new Rectangle (0, 0, mode.getWidth (), mode.getHeight ());
captures [i] = new Robot (screens [i]).createScreenCapture (bounds);
}
把以上代码放到Capture菜单项的动作监听器内。然后先引入代码创建一个bigScreen要引用的足够大的BufferedImage,它可以保存被captures数组引用的所有BufferedImage内容;接着引入代码把它们的绘制到bigScreen中。Capture现在成为了多屏幕捕获器就好像是一个单屏幕捕获器。
关于作者
Jeff Friesen是一个自由软件开发者和教育家,特别是在C、C++和Java技术领域。
资源
Matrix中文Java社区: http://www.matrix.org.cn
下载文中的代码文件: http://www.javaworld.com/javaworld/jw-04-2006/games/jw-0424-funandgames.zip
你可以使用在线开发工具DevSquare编译和运行Java Fun And Games中提供的Applet。工具入门请阅读这篇用户向导:
http://www.javaworld.com/javaworld/jw-12-2005/jw-devsquare.html
DevSquare: http://www.devsquare.com/index.html