创建一个 Swing 组件 —— JImageComponent
介绍Introduction
本文展示了如何使用Java™来创建一个用来在Java™ 的applet和/或应用程序中展示图片的Swing类. 它还包括了使得图片渲染加快需要的步骤,还有在滚动容器中的使用方法.
为了更好的理解,特别是对于初学者而言,本文使用了 JImageComponent 的实现作为引用,它扩展了 Swing 的 Component.
说明
1. 创建一个子类
创建一个子类继承扩展你的类。其父类通常是Java™ Swing诸多类中的一个.
JImageComponent扩展了 Swing 的 JComponent:
public class JImageComponent extends javax.swing.JComponent { /** * Constructs a new JImageComponent object. */ public JImageComponent() { } }
2. 创建类变量
你的类将需要几个变量来持有重要的数据. 通常,这将在你扩张类的功能时进行. 一般情况下,它至少要包含两个变量:一个BufferedImage对象用来持有需要绘制出来的图像,还有一个对应的 Graphics 对象.
JImageComponent 包含两个私有变量:
/** Holds the <code>BufferedImage for the image. */ private BufferedImage bufferedImage = null; /** Holds the Graphics for the image. */ private Graphics imageGraphics = null;
3. 实现图像的Set/Change功能
你的类将会绘制其变量所描述的图像。你可能需要实现在构造时设置图像,或者在运行期间设置或者修改图像的功能.
JImageComponent 允许在构造时设置图像,或者在运行期间设置或者修改图像. 简便起见,这里我只列出了一个构造器.
将一个BufferedImage作为参数的构造器.
/** * Constructs a new JImageComponent object. * * @param bufferedImage * Image to load as default image. */ public JImageComponent(BufferedImage bufferedImage) { this.setBufferedImage(bufferedImage); }
JImageComponent 的方法用来在运行期间设置或者修改图像。至于为什么这个方法还要去设置组件的边界,将会在讨论实现用于滚动容器中的功能时解释.
/** * Sets the buffered image, and updates the components bounds. * * @param bufferedImage * The buffered image to set to. */ public void setBufferedImage(BufferedImage bufferedImage) { this.bufferedImage = bufferedImage; // Clear the graphics object if null image specified. // Clear the component bounds if null image specified. if (this.bufferedImage == null) { this.imageGraphics = null; this.setBounds(0, 0, 0, 0); } // Set the graphics object. // Set the component's bounds. else { this.imageGraphics = this.bufferedImage.createGraphics(); this.setBounds(0, 0, this.bufferedImage.getWidth(), this.bufferedImage.getHeight()); } }
4. 实现图片的设置/加载功能
你的类可能要实现通过从资源中加载一张图片来设置图像的功能.
JImageComponent 允许通过提供一个加载由一个统一资源定位符(URL)作为参数来加载一张图像的方法,来使得位于应用程序包中的图像被加载. 请注意你可能需要在图像被加载之后通过调用 JImageComponet 的 repaint() 方法来对图像进行重新绘制.
/** * Loads image from URL. * * @param imageLocation * URL to image. * @throws Exception * Throws an <code>IOException if file cannot be loaded. */ public void loadImage(URL imageLocation) throws IOException { this.bufferedImage = ImageIO.read(imageLocation); this.setBufferedImage(this.bufferedImage); }
你可以任意实现必要多的方法。 例如JImageComponent,还会有一个用来加载一张由一个文件参数指定的图像的方法.
/** * Loads image from a file. * * @param imageLocation * File to image * @throws IOException * Throws an <code>IOException if file cannot be loaded */ public void loadImage(File imageLocation) throws IOException { this.bufferedImage = ImageIO.read(imageLocation); this.setBufferedImage(this.bufferedImage); }
5.实现绘制图像的功能
这是你的类真正的核心功能。你将需要确保你的类在图像只部分可见时,能够只画出其自身的一部分, 还要确保其在设置、加载、修改或者编辑时能够重新绘制其自身. 依赖于你扩展的父类,你可能需要重写和绘制图像有关的几个方法.
Swing 的 JComponent 为我们做了大部分的重要工作 . JImageComponent 重写了 paint(java.awt.Graphics) 方法, 还有两个 paintImmediately() 方法. 注意力主要需要放在由组件的可见矩形所指定的图像的绘制上. 这将会在讨论用于滚动容器中的功能的实现时解释.
/* * @see javax.swing.JComponent#paint(java.awt.Graphics) */ @Override public void paint(Graphics g) { // Exit if no image is loaded. if (this.bufferedImage == null) { return; } // Paint the visible region. Rectangle rectangle = this.getVisibleRect(); paintImmediately(g, rectangle.x, rectangle.y, rectangle.width, rectangle.height); }; /* * @see javax.swing.JComponent#paintImmediately(int, int, int, int) */ @Override public void paintImmediately(int x, int y, int width, int height) { // Exit if no image is loaded. if (this.bufferedImage == null) { return; } // Paint the region specified. this.paintImmediately(super.getGraphics(), x, y, width, height); } /* * @see javax.swing.JComponent#paintImmediately(java.awt.Rectangle) */ @Override public void paintImmediately(Rectangle rectangle) { // Exit if no image is loaded. if (this.bufferedImage == null) { return; } // Paint the region specified. this.paintImmediately(super.getGraphics(), rectangle.x, rectangle.y, rectangle.width, rectangle.height); }
简单起见,JImageComponent 会有一个私有的方法 ,paintImmediately(Graphics, int, int, int, int) ,来做实际上的图形的绘制.
/** * Paints the image onto the component. * * @param g * The <code>Graphics object of the component onto which the * image region will be painted. * @param x * The x value of the region to be painted. * @param y * The y value of the region to be painted. * @param width * The width of the region to be painted. * @param height * The width of the region to be painted. */ private void paintImmediately(Graphics g, int x, int y, int width, int height) { // Exit if no image is loaded. if (this.bufferedImage == null) { return; } int imageWidth = this.bufferedImage.getWidth(); int imageHeight = this.bufferedImage.getHeight(); // Exit if the dimension is beyond that of the image. if (x >= imageWidth || y >= imageHeight) { return; } // Calculate the rectangle of the image that should be rendered. int x1 = x < 0 ? 0 : x; int y1 = y < 0 ? 0 : y; int x2 = x + width - 1; int y2 = y + height - 1; if (x2 >= imageWidth) { x2 = imageWidth - 1; } if (y2 >= imageHeight) { y2 = imageHeight - 1; } // Draw the image. g.drawImage(this.bufferedImage, x1, y1, x2, y2, x1, y1, x2, y2, null); }
6. 实现当处在一个滚动容器中时的组件绘制
需要解决的一个挑战是,如何绘制一张被用于滚动容器中的图像. 例如,一个开发者可能会想要将一张尺寸超出可视边界的图像放到一个 JScrollPane 容器中.
JImageComponent通过做下面三件事情来解决了这个问题:
-
当图片被设置/加载时,设置组件的边界.
(这已经是完成了的.) -
在绘制图像时,注意组件的可是矩形.
(这也已经是完成了的.) -
提供带有适当布局细节的布局管理器.
你可能需要实现在你想要确保你的组件在滚动容器中可以被正确渲染时,能提供带有适当布局细节的布局管理器的功能.
JImageComponent 通过重写如下的方法提供必要的布局详细:
/** * Returns the height of the image. */ @Override public int getHeight() { if (this.bufferedImage == null) { return 0; } return this.bufferedImage.getHeight(); } /** * Returns the size of the image. */ @Override public Dimension getPreferredSize() { if (this.bufferedImage == null) { return new Dimension(0, 0); } return new Dimension(this.bufferedImage.getWidth(), this.bufferedImage.getHeight()); } /** * Returns the size of the image. */ @Override public Dimension getSize() { if (this.bufferedImage == null) { return new Dimension(0, 0); } return new Dimension(this.bufferedImage.getWidth(), this.bufferedImage.getHeight()); } /** * Returns the width of the image. */ @Override public int getWidth() { if (this.bufferedImage == null) { return 0; } return this.bufferedImage.getWidth(); }
7. 实现编辑图像的功能
在编辑图像时许多的实例将会比持续的修改图像来得更加高效. 一个例子就是当你想要展示一个动画的时候,修改每张动画图像改变的区域,会比在内存中创建一张图像并在组件中对其进行设置更加高效.
JImageComponent 通过提供一个访问其 BufferedImage的方法,以及一个访问图像的 Graphics 对象的方法来允许这个功能.
/** * Returns the <code>BufferedImage object for the image. * * @return The buffered image. */ public BufferedImage getBufferedImage() { return this.bufferedImage; } /** * Returns the Graphics object for the image. */ @Override public Graphics getGraphics() { return this.imageGraphics; }
JImageComponent 还提供了一个用来调整/转换图像的方法:
/** * Resizes the image to the given dimensions and type. * Note that the image is "cropped" from the left top corner). * * @param width * The new width of the image. * @param height * The new height of the image. * @param imageType * The new image type (<code>BufferedImage type) * @see type java.awt.image.BufferedImage#BufferedImage(int, int, int) */ public void resize(int width, int height, int imageType) { // Create a new image if none is loaded. if (this.bufferedImage == null) { setBufferedImage(new BufferedImage(width, height, imageType)); return; } // Create a new temporary image. BufferedImage tempImage = new BufferedImage(width, height, imageType); int w = this.bufferedImage.getWidth(); int h = this.bufferedImage.getHeight(); // Crop width if necessary. if (width < w) { w = width; } // Crop height if necessary. if (height < h) { h = height; } // Copy if the type is the same. if (this.bufferedImage.getType() == imageType) { Graphics g = tempImage.getGraphics(); g.drawImage(this.bufferedImage, 0, 0, w, h, null); } // Copy pixels to force conversion. else { for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { tempImage.setRGB(x, y, this.bufferedImage.getRGB(x, y)); } } } // Set the new image. setBufferedImage(tempImage); }
使用代码
使用了JImageComponent的开发者应该记得在修改或者编辑了图像之后调用repaint()方法.
那些希望实现并测试他们的 JComponent 子类的开发者必须遵守有关Swing的执行指导方针(http://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html):
Java Applet 应该使用 SwingUtilities 的 invokeAndWait():
Java 应用程序可能也使用 SwingUtilities 的 invokeAndWait() 或者 invokeLater():
使用invokeAndWait的一个例子:
try { javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { createAndShowGUI(); } }); } catch (InvocationTargetException exception) { // TODO Auto-generated catch block exception.printStackTrace(); } catch (InterruptedException exception) { // TODO Auto-generated catch block exception.printStackTrace(); }
使用 invokeLater 的一个例子:
javax.swing.SwingUtilities.invokeLater(new Runnable() { public void run() { createAndShowGUI(); } });
更进一步
这是对创建一个 Java™ Swing的 JComponent子类用来展示图像的一个简要介绍. 从这里起步,还有许多的方面需要涉及. 例如,JImageComponent对于其便捷的设置和修改没有做出规定.
创建一个Java™ 抽象窗口工具Abstract Window Toolkit 的 Component 类的子类可能需要要遵循同样的步骤. 这些地方需要特别注意: 有一个缺陷将会需要子类做出规定,以确保有用于消除可能的闪烁的额双缓冲; 另外一个则要扩展类来正确的捕获和/或引发事件.
要点
开发者也许还会对Java™的用于图像的 2D 图形 API感兴趣.
扩展Swing的 JComponent 并不要能达成此目的唯一能被扩展的类 . 例如,开发者肯能会选择JPanel类来扩展.
在商业应用程序中,我会坚持使用像JButton和JLable这样的本地类,以方便使用. 在 2014 年, 我改进了用于滚动容器中的 JImageComponent . 现在,我会使用 JImageComponent 来做一些像测试动画,绘制分形以及绘制自定义图像这些有趣的事情.