[转载] 标准窗口小部件工具箱的 Java 二维作图

SWT (标准窗口小部件工具箱,Standard Widget Toolkit)是在 Eclipse 平台上使用的窗口小部件工具箱。它也可以作为 Swing/AWT 的一个重要替代产品,用于构建任何类型的 Java GUI 应用程序。随着 Eclipse 平台在过去两年里的日趋流行,SWT 已经进入大家的视线,并且最近它已经开始在一些应用程序中取代 Swing/AWT。SWT 的流行源自这样一个事实:它是跨平台的工具箱,利用了窗口小部件的本性,并有一个与 Swing 及其他现代工具箱同样强大的功能。使用 SWT,就不用在可移植性、功能和性能之间做取舍了。

事实上,Swing/AWT 只在一个方面明显强于 SWT,这就是 Java 2D。Java 2D 是一个强大的 API,是在 JDK 1.2 中引入的。它使 Java 开发人员在 AWT 组件上绘制时可以使用复杂的二维变换(平移、旋转、缩放、错切等)。不幸的是,Java 2D 设计为只在 AWT 或者 Swing 工具箱上使用,而 SWT 还没有提供这种扩展的二维能力。因此,许多开发人员发现他们必须选择是在 Java 平台上使用 Java 2D 还是放弃它令人兴奋的功能而使用 SWT。

不过,在本文中您将了解到如何同时拥有这两方面的好处。我将展示一个简单的技术,利用该技术可以在 SWT 组件和 Draw2D 图像上绘制 Java 2D 图像。为了理解这个例子,读者应当熟悉 Java 2D、AWT 和 SWT。具有一些 Eclipse 平台的 GEF(图形编辑框架,Graphical Editing Framework)的经验也是有帮助的。

屏外图像技术
本文展示一种简单的技术,利用该技术,您可以用 Java 2D 功能在任何 SWT 窗口小部件或者 Draw2D 图像上绘制。为了弥补 SWT 上缺少 Java 2D 的不足,用一个屏外(offscreen)AWT 图像接收 Java 2D 绘制操作,并将它们转换为独立于工具箱的像素值。再用另一个由 SWT 工具箱创建的 屏外图像将这些像素信息绘制在任何 SWT 组件上。图 1 显示了 AWT 屏外图像转换为 SWT 图像再绘制在 SWT 窗口小部件上的过程。

图 1. 屏外图像技术允许在 SWT 上使用 Java 2D

图 1 中显示的屏外 AWT 图像被初始化为透明背景。然后对屏外图像的图形上下文调用 Java 2D 方法。像所有 AWT 图像一样,屏外图像的图形上下文自动支持 Java 2D。完成了所有特定于 Java 2D 的绘制后,提取 AWT 图像的像素值并传送到一个屏外 SWT 图像中。然后用工具箱的 GC.drawImage(...) 方法在 SWT 组件上绘制这个 SWT 图像,就像所有正常图像一样。

我将在下面几节中完成这个过程中的每一步。

创建 AWT 屏外图像
对于 AWT 屏外图像,要使用 java.awt.image.BufferedImage 实例。BufferedImage 是一个可以通过它的 API 访问其像素数据的图像。访问到了图像的像素值就可以在以后将它转换为 SWT 图像。

构建一个 AWT 缓冲图像的最简单方法是使用构造函数 public BufferedImage(int width, int height, int imageType)。前两个参数表明图像具有的大小。第三个参数是指定要创建的图像 类型 的常量。图像类型 —— 可能的值是在类 BufferedImage 中声明的常量 TYPE_XXX 之一 —— 表明像素值是如何存储在图像中的。在彩色图像中一般使用以下几种最重要的图像类型:

  • TYPE_BYTE_INDEXED:这种图像类型的图像将使用一个索引颜色模型。索引颜色模型 的意思是图像中使用的每一种颜色都是在一组颜色中索引的。像素值只包含这个像素的颜色在颜色模型中的索引。这种类型的图像局限于 256 种颜色 —— 也就是颜色模型的大小。以这种方式存储像素信息可以很紧凑地表示图像,因为每一个像素都只用一个字节编码。

  • TYPE_INT_RGB:这种类型常量表明图像使用直接颜色模型。直接颜色模型 的意思是每一个像素的值包含关于其颜色的完整信息。TYPE_INT_RGB 表明每一个像素都是用一个整数(四字节)编码的。每一个像素中编码的信息是这个像素所使用的颜色的红、绿和蓝(RGB)成分。每一种颜色成分都用一个字节编码。因为整个像素值是用四个字节编码的,所以有一个字节是未使用的。这种图像的内部表示占用的内存是使用索引颜色模型的图像的四倍,但是这种图像可以使用的颜色数增加到了 1 百 60 万(256 x 256 x 256)。

  • TYPE_INT_ARGB:与 TYPE_INT_RGB 一样,这种类型的图像使用直接颜色模型并用四个字节编码每一个像素。不同之处在于,这里用 TYPE_INT_RGB 没有使用的第四个字节表示像素的透明度,有时也称为颜色的 alpha 成分。这种类型的图像可以有透明的背景,也可以是半透明的。

为了让屏外图像技术可以工作,需要只将受到 Java 2D 绘制影响的像素传送到 SWT 组件的表面。为了保证这一点,初始图像必须有透明背景。因此,对于 AWT 屏外图像,使用的缓冲图像类型为 TYPE_INT_ARGB

绘制和提取图像
这个过程的下一步是用 Java 2D 绘制图像。首先取得它的 Graphics2D 上下文。可以用方法 createGraphics2D() 或者调用 getGraphics() 做到这一点。在这个上下文上绘制将会自动修改图像的像素数据。在绘制完成后,可以用方法 getRGB(int startX, int startY, int w, int h, int rgbArray, int offset, int scansize) 容易且高效地提取图像的像素值。这个方法可以将图像中矩形区域的像素数据传输到一个整数数组中。getRGB() 方法的参数如下:

  • startX, startY 是要提取的区域左上角图像的坐标。

  • w, h 是要提取的区域的宽度和高度。

  • rgbArray 是接收像素值的整数数组。

  • offset 是数组中接收第一个像素值的位置的索引。

  • scansize 是图像中相邻两行中具有相同行索引的像素的索引偏移值。如果这个值与要提取的区域的宽度相同,那么一行的第一个像素就会存储在数组中前一行最后一个像素后面的索引位置。如果这个值大于提取区域的宽度,那么数组中,在一行最后和下一行开始之间就会有一些未使用的索引。

存储像素数据
图 2 显示 BufferedImage.getRGB(...) 如何在提取了 AWT 图像的矩形区域后填充整数缓冲区。图中下面的部分表示整数缓冲区。每一个框表示缓冲区中包含一个像素 4 字节 ARBG 值的一个值。括号中的数字表示像素在图像中的坐标。

图 2. 从 AWT 图像中提取像素值

在这种情况下,不使用任何 offset,这意味着第一个像素将保存在缓冲区索引 0 的位置。scansize 的值取要提取的区域的宽度,这意味着提取的一行中的第一个像素会接着前一行的最后一个像素的缓冲区位置。使用这些参数,整数的缓冲区就一定会足够大,可以包含 w*h 个整数。

当每一个像素的颜色信息都存储到了一个整数的简单缓冲区后,就可以将这些信息传输到 SWT 屏外图像中。

创建 SWT 图像
SWT Image 类似于 AWT BufferedImage,因为它的像素数据可以有直接读或者写操作访问。这意味着可以通过直接读取或者修改图像的数据,来设置或者取得图像中任何像素或者任何一组像素的颜色值。不过, SWT API 与相应的 AWT API 有很大不同,并且更容易使用。

SWT 的 Image 类提供了几个构造函数,可以完成以下任务:

  • 通过将一个文件名或者一个 InputStream 作为参数传递给构造函数装载一个现有的图像。图像的格式必须是所支持的格式之一:BMP、GIF、JPG、PNG、Windows ICO 等。

  • 构造一个指定大小的空图像。可以通过修改其像素值或者向它拷贝一个 SWT 图形上下文的内容 (GC) 来绘制该图像。

  • 构造一个用像素值的现有缓冲区进行初始化的图像。

您将使用第三个构造函数创建一个 SWT 图像,它是所绘制的 AWT 图像的副本。

关于 ImageData 类
有关图像的像素数据的信息包含在它的 ImageData 中。ImageData 是一个包含有关图像大小、调色板、颜色值和透明度信息的类。应当特别关注以下 ImageData 字段:

  • width 和 height 指定图像的大小。

  • depth 指定图像的颜色深度。可能的值为 1、2、4、8、16、24 或者 32,指定编码每一个像素的值所使用的位数。

  • palette 包含一个 PaletteData 对象,它存储有关图像的颜色模型的信息。与 AWT 一样,SWT 的颜色模型可以是索引或者直接的。如果颜色模型是索引的,那么 PaletteData 包含颜色索引。如果它是直接的,那么它包含转换(shift)信息,表明应当如何从像素的整数值中提取出颜色的 RGB 成分。

  • data 包含包含有像素值的字节缓冲区。与 AWT 缓冲区不同,SWT 缓冲区不是包含每一个像素的一种颜色值的整数数组。相反,它包含字节值。字节编码的方法取决于所使用的颜色深度。对于一个 8 位的图像,数组中的一个字节正好表示图像中一个像素的值。对于 16 位图像,每一个像素值编码为缓冲区中的两个字节。这两个字节以最低有效字节顺序存储。对于 24 或者 32 位图像,每一个像素值以最高有效位字节顺序编码为缓冲区中的三个或者四个字节。

  • bytesPerLine 表明缓冲区中有多少字节用于编写图像中一行像素的所有像素值。

  • transparentPixel 定义用于图像中透明度的像素值。

我们将使用带有一个透明度颜色信道的 24 位图像。图像中的每一个像素都编码为数组中的三个字节,顺序为红、绿和蓝成分。

转换图像
知道了图像数据就可以容易地将 AWT 图像转换为 SWT 图像。只要将(由 AWT 图像利用 getRGB(...) 返回的)整数缓冲区转换为 SWT 图像所使用的字节缓冲。图 3 显示了在 SWT 图像的缓冲区中这些值是如何存储的。

图 3. 将像素值写入 SWT 图像

如图 2 中一样,上图中下面的部分显示了图像缓冲区的内部表示。括号中的数字显示在缓冲区中表示其颜色值的那个像素的坐标。

尽管每一个像素都用三个字节编码,但是对于 24 位图像,缓冲区中一行像素的大小并不总是 3*width。缓冲区中两行像素之间可能有一些索引未使用。要知道图像中每一行像素真正使用了多少字节(这样就可知道缓冲区中下一行从哪个索引位置开始),必须使用 ImageData 字段的 bytesPerLine 值。

SWT 到 Java 2D 渲染器
清单 1 显示实现了屏外图像技术的一般性渲染器(renderer)的源代码。这个渲染器可以在 SWT 组件或者 Draw2D 图像上绘制时透明地使用 Java 2D 例程。

清单 1. SWT/Draw2D Java 2D renderer

package swtgraphics2d;

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;

import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.widgets.Display;

/**
 * Helper class allowing the use of Java 2D on SWT or Draw2D graphical
 * context.
 * @author Yannick Saillet
 */
public class Graphics2DRenderer {
  private static final PaletteData PALETTE_DATA =
    new PaletteData(0xFF0000, 0xFF00, 0xFF);

  private BufferedImage awtImage;
  private Image swtImage;
  private ImageData swtImageData;
  private int[] awtPixels;

  /** RGB value to use as transparent color */
  private static final int TRANSPARENT_COLOR = 0x123456;

  /**
   * Prepare to render on a SWT graphics context.
   */
  public void prepareRendering(GC gc) {
    org.eclipse.swt.graphics.Rectangle clip = gc.getClipping();
    prepareRendering(clip.x, clip.y, clip.width, clip.height);
  }

  /**
   * Prepare to render on a Draw2D graphics context.
   */
  public void prepareRendering(org.eclipse.draw2d.Graphics graphics) {
    org.eclipse.draw2d.geometry.Rectangle clip =
      graphics.getClip(new org.eclipse.draw2d.geometry.Rectangle());
    prepareRendering(clip.x, clip.y, clip.width, clip.height);
  }

  /**
   * Prepare the AWT offscreen image for the rendering of the rectangular
   * region given as parameter.
   */
  private void prepareRendering(int clipX, int clipY, int clipW, int clipH) {
    // check that the offscreen images are initialized and large enough
    checkOffScreenImages(clipW, clipH);
    // fill the region in the AWT image with the transparent color
    java.awt.Graphics awtGraphics = awtImage.getGraphics();
    awtGraphics.setColor(new java.awt.Color(TRANSPARENT_COLOR));
    awtGraphics.fillRect(clipX, clipY, clipW, clipH);
  }

  /**
   * Returns the Graphics2D context to use.
   */
  public Graphics2D getGraphics2D() {
    if (awtImage == null) return null;
    return (Graphics2D) awtImage.getGraphics();
  }

  /**
   * Complete the rendering by flushing the 2D renderer on a SWT graphical
   * context.
   */
  public void render(GC gc) {
    if (awtImage == null) return;

    org.eclipse.swt.graphics.Rectangle clip = gc.getClipping();
    transferPixels(clip.x, clip.y, clip.width, clip.height);
    gc.drawImage(swtImage, clip.x, clip.y, clip.width, clip.height,
                 clip.x, clip.y, clip.width, clip.height);
  }

  /**
   * Complete the rendering by flushing the 2D renderer on a Draw2D
   * graphical context.
   */
  public void render(org.eclipse.draw2d.Graphics graphics) {
    if (awtImage == null) return;

    org.eclipse.draw2d.geometry.Rectangle clip =
      graphics.getClip(new org.eclipse.draw2d.geometry.Rectangle());
    transferPixels(clip.x, clip.y, clip.width, clip.height);
    graphics.drawImage(swtImage, clip.x, clip.y, clip.width, clip.height,
                       clip.x, clip.y, clip.width, clip.height);
  }

  /**
   * Transfer a rectangular region from the AWT image to the SWT image.
   */
  private void transferPixels(int clipX, int clipY, int clipW, int clipH) {
    int step = swtImageData.depth / 8;
    byte[] data = swtImageData.data;
    awtImage.getRGB(clipX, clipY, clipW, clipH, awtPixels, 0, clipW);
    for (int i = 0; i < clipH; i++) {
      int idx = (clipY + i) * swtImageData.bytesPerLine + clipX * step;
      for (int j = 0; j < clipW; j++) {
        int rgb = awtPixels[j + i * clipW];
        for (int k = swtImageData.depth - 8; k >= 0; k -= 8) {
          data[idx++] = (byte) ((rgb >> k) & 0xFF);
        }
      }
    }
    if (swtImage != null) swtImage.dispose();
    swtImage = new Image(Display.getDefault(), swtImageData);
  }

  /**
   * Dispose the resources attached to this 2D renderer.
   */
  public void dispose() {
    if (awtImage != null) awtImage.flush();
    if (swtImage != null) swtImage.dispose();
    awtImage = null;
    swtImageData = null;
    awtPixels = null;
  }

  /**
   * Ensure that the offscreen images are initialized and are at least
   * as large as the size given as parameter.
   */
  private void checkOffScreenImages(int width, int height) {
    int currentImageWidth = 0;
    int currentImageHeight = 0;
    if (swtImage != null) {
      currentImageWidth = swtImage.getImageData().width;
      currentImageHeight = swtImage.getImageData().height;
    }

    // if the offscreen images are too small, recreate them
    if (width > currentImageWidth || height > currentImageHeight) {
      dispose();
      width = Math.max(width, currentImageWidth);
      height = Math.max(height, currentImageHeight);
      awtImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
      swtImageData = new ImageData(width, height, 24, PALETTE_DATA);
      swtImageData.transparentPixel = TRANSPARENT_COLOR;
      awtPixels = new int[width * height];
    }
  }
}

这个渲染器包含在一个实用程序类中。这个类包含并管理屏外图像技术所需要的 AWT 和 SWT 屏外图像的引用。还要注意:

  • 字段 swtImageDataawtPixels 分别是在像素转移时包含 SWT 图像的像素值的缓冲区和用于包含 AWT 图像的像素值的缓冲区。

  • 常量 TRANSPARENT_COLOR 包含一个作为 SWT 图像中透明颜色的 RGB 值。因为必须定义作为透明度信道的颜色以绘制背景,所以必须为此保留一个颜色值。在代码中我使用了随机值 0x123456。所有使用这个颜色值的像素都按透明处理。如果这个值所表示的颜色有可能在绘制操作中用到,可以用另一个值表示透明度。

渲染器是如何工作的
SWT/Draw2D Java 2D 渲染器的工作过程如下:

  • 在可以使用渲染器之前,必须以至少与要绘制的区域一样的尺寸初始化其屏外图像和缓冲区。这是通过调用方法 prepareRendering(...) 完成的。根据是在 SWT 还是 Draw2D 图形上下文中进行渲染,这个方法以一个 SWT GC 或者一个 Draw2D Graphics 对象作为参数。

  • 接下来,prepareRendering 从图形上下文中提取剪裁矩形(clip rectangle)—— 剪裁矩形 是可以修改其中像素的最大矩形区域。

  • 然后调用 private 方法 prepareRendering(int clipX, int clipY, int clipW, int clipH) 准备要渲染的屏外图像。这个方法独立于所使用的图形上下文的类型,它可以是 SWT 或者 Draw2D。prepareRendering() 方法的工作过程如下:

    • 它首先检查 AWT 和 SWT 屏外图像已实例化并足以包含要绘制的区域,这是由方法 checkOffScreenImages(clipW, clipH) 完成的。

    • 如果屏外图像已经实例化,但是不够大,就会放弃它并以所需要大小重新创建一个。如果屏外图像大于要绘制的区域,那么就会重复使用它,并只修改相应于要绘制的区域的那一部分。

    • 完成这种检查后,用为透明度信道保留的颜色 TRANSPARENT_COLOR 填充绘制区域。这个图像就可以用来进行 Java 2D 操作了。


  • 当渲染器准备好进行剪裁区域中的 Java 2D 绘制操作后,可以从 BufferedImage AWT 获得 Java 2D Graphics2D 上下文。这个图形上下文将用于所有 Java 2D 绘制例程。每一次绘制操作修改 AWT 屏外图像。

  • 当所有 Java 2D 绘制操作都完成后,绘制区域的像素必须从 AWT 转换为 SWT 屏外图像,然后绘制到 SWT 或者 Draw2D 图形上下文中。这个操作是由方法 render(GC) 或者 render(Graphics) 完成的。这两个方法都在内部调用 private 方法 transferPixels(...),该方法将像素从 AWT 转换为 SWT。

  • 如果不再需要渲染器了或者必须释放资源,可以调用 dispose() 方法,以清除渲染器所使用的屏外图像和缓冲区。这会释放资源,但是在再次需要渲染器时要花时间重新创建缓冲区。需要根据组件重新绘制的频度以及重新绘制区域应有多大来判断应当什么时候调用 dispose()

一个使用示例
清单 2 显示如何用渲染器在 SWT Canvas 上绘制一些旋转文字。

清单 2. 在 SWT Canvas 上的使用示例

Canvas canvas = new Canvas(shell, SWT.NO_BACKGROUND);
final Graphics2DRenderer renderer = new Graphics2DRenderer();

canvas.addPaintListener(new PaintListener() {
  public void paintControl(PaintEvent e) {
    Point controlSize = ((Control) e.getSource()).getSize();

    GC gc = e.gc; // gets the SWT graphics context from the event

    renderer.prepareRendering(gc); // prepares the Graphics2D renderer

    // gets the Graphics2D context and switch on the antialiasing
    Graphics2D g2d = renderer.getGraphics2D();
    g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
      RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

    // paints the background with a color gradient
    g2d.setPaint(new GradientPaint(0.0f, 0.0f, java.awt.Color.yellow,
      (float) controlSize.x, (float) controlSize.y, java.awt.Color.white));
    g2d.fillRect(0, 0, controlSize.x, controlSize.y);

    // draws rotated text
    g2d.setFont(new java.awt.Font("SansSerif", java.awt.Font.BOLD, 16));
    g2d.setColor(java.awt.Color.blue);

    g2d.translate(controlSize.x / 2, controlSize.y / 2);
    int nbOfSlices = 18;
    for (int i = 0; i < nbOfSlices; i++) {
      g2d.drawString("Angle = " + (i * 360 / nbOfSlices) + "/u00B0", 30, 0);
      g2d.rotate(-2 * Math.PI / nbOfSlices);
    }

    // now that we are done with Java 2D, renders Graphics2D operation
    // on the SWT graphics context
    renderer.render(gc);

    // now we can continue with pure SWT paint operations
    gc.drawOval(0, 0, controlSize.x, controlSize.y);
  }
});

代码说明:

  • 创建一次渲染器,并在每次需要重新绘制画布(canvas) 时重复使用它。

  • 实例化画布,并在它上面添加一个 PaintListener 以执行绘制操作。

  • PaintEvent 获得图形上下文 gc

  • 渲染器是在图形上下文中准备的,这意味着屏外图像可以接受绘制操作。

  • 在下一步,获得 Java 2D 图形上下文 g2d

  • 然后实现一组 Java 2D 绘制操作以用渐变颜色绘制背景并且每 20 度绘制一个旋转的文字。这个过程使用图形上下文的平移和几个旋转。

  • 最后,调用 render(GC) 方法以将 Java 2D 绘制操作传输到 SWT 图形上下文。可以在同一绘制例程中使用 Java 2D 和纯 SWT 绘制操作。

渲染操作的结果
渲染操作的结果如图 4 所示。在这个例子中,没有丢弃渲染器,在每次绘制 Canvas 时可以重复使用它的屏外图像和内部缓冲区,这样可以节省实例化和垃圾收集的时间。记住,如果画布不需要经常重新绘制,并且渲染器所占用的资源非常重要,那么可以在每次绘制操作后丢弃渲染器。

图 4. 使用示例:用 Java 2D 例程帮助绘制 SWT

清单 3 显示如何在 Draw2D 图像中实现同样的例子。在这里是通过覆盖 Figure 的方法 paintClientArea(Graphics) 来实现绘制 FigureGraphics2DRenderer 的使用与上一个例子完全一样。惟一的区别是,方法 prepareRenderingrender 这一次是以一个 Draw2D Graphics 而不是一个 SWT GC 为参数调用的。

清单 3. 对 Draw2D 图的使用示例

final Graphics2DRenderer renderer = new Graphics2DRenderer();

IFigure figure = new Figure() {
  protected void paintClientArea(org.eclipse.draw2d.Graphics graphics) {
    Dimension controlSize = getSize();
    renderer.prepareRendering(graphics); // prepares the Graphics2D renderer

    // gets the Graphics2D context
    Graphics2D g2d = renderer.getGraphics2D();

    (...) // does the Java 2D painting

    // now that we are done with Java 2D, renders Graphics2D operation
    // on the Draw2D graphics context
    renderer.render(graphics);

    // now we can continue with pure Draw2D paint operations
    graphics.drawOval(0, 0, controlSize.width, controlSize.width);
  }
};

结束语
本文展示的简单技术,使得将 Java 2D 功能集成到 SWT 和 GEF 应用程序中成为可能并且相当容易。我一步一步地展示了如何结合 SWT 和 Java 2D 的最好功能并将结果绘制到任何 SWT 组件或者 GEF Draw2D 图像上。这里展示的技术就像代码示例这样简单:只用几行代码就可以实现,不需要依赖任何外部库,也不需要启动一个 AWT 线程。

本文提供的实现代码可以用于任何 SWT 或者 GEF 应用程序。屏外(offscreen)图像技术可以在所有平台上工作,包括 Linux/Motif 这样的平台,在这些地方,SWT 和 AWT 不能共同存在于同一个应用程序中。由于这种技术所依据的操作不比将像素值从 AWT 图像转换为 SWT 图像更复杂,所以也可以将这种技术用于 SWT 中其他基于 AWT 的作图 API 的转换。

你可能感兴趣的:([转载] 标准窗口小部件工具箱的 Java 二维作图)