Taking a look at SWT Images

Taking a look at SWT Images

Summary
SWT's Image class can be used to display images in a GUI. The most common source of images is to load from a standard file format such as GIF, JPEG, PNG, or BMP. Some controls, including Buttons and TreeItems, are able to display an Image directly through the setImage(Image) method, but any control's paint event allows images to be drawn through the callback's graphic context. SWT's ImageData class represents the raw data making up an SWT Image and determines the color for each pixel coordinate. This article shows the correct uses of ImageData and Image, shows how to load images from files, and how to achieve graphic effects such as transparency, alpha blending, animation, scaling, and custom cursors.

By Joe Winchester, IBM
September 10th, 2003


This first section of this article gives an introduction to colors and shows how an image records the color value of each pixel.

The next section describes image transparency, alpha blending, animation, and how to scale images. Finally, the article shows how to create cursors from images, by using a source image together with a mask.

Introduction

The simplest way to create an SWT Image is to load it from a recognized graphic file format. This includes GIF, BMP (Windows format bitmap), JPG, and PNG. The TIFF format is also supported in more recent Eclipse releases. Images can be loaded from a known location in the file system using the constructor Image(Display display, String fileLocation):

Image image = new Image(display,
   "C:/eclipse/eclipse/plugins/org.eclipse.platform_2.0.2/eclipse_lg.gif");

Instead of hard-coding the location of the image, it's more common to load the Image from a folder location relative to a given class. This is done by creating an InputStream pointing to the file with the method Class.getResourceAsStream(String name), and using the result as the argument to the constructor Image(Display display, InputStream inputStream).

The Eclipse package explorer below shows the class com.foo.ShellWithButtonShowingEclipseLogo and the eclipse_lg.gif in the same folder. To following code would load the graphic from its location relative to the class.

Image image = new Image(display,
    ShellWithButtonShowingEclipseLogo.class.getResourceAsStream(
      "eclipse_lg.gif"));

Once the image has been created it can be used as part of a control such as a Button or Label that is able to render the graphic as part of their setImage(Image image) methods.

Button button = new Button(shell,SWT.PUSH);
button.setImage(image);

Images can be drawn onto using a graphics context that is created with the constructor GC(Drawable drawable) with the Image as the argument.

GC gc = new GC(image);
gc.setForeground(display.getSystemColor(SWT.COLOR_WHITE));
gc.drawText("I've been drawn on",0,0,true);
gc.dispose();

Using a GC to draw onto an Image permanently alters the graphic. More information on how to use a GC is covered in the article Graphics Context - Quick on the draw.

Image lifecycle

When an image is loaded, the first step is to create device independent ImageData represented by the class org.eclipse.swt.graphics.ImageData. Following this, the data is prepared for a specific device by creating an actual Image instance.

As well as loading an Image directly from a file, you can separately create the ImageData object and then construct the Image using Image(Device device, ImageData imageData). The data for an existing Image can be retrieved using getImageData(), although this will not be the same object that was used to create the image. This is because when preparing an image to be drawn onto a screen, properties such as its color depth might be different from the initial image data.

Instances of Image represent an underlying resource that has been prepared for a specific device and they must be disposed when they are no longer required to free up the allocated resource. There is no finalization of resources in SWT when an object is garbage collected. For more information see SWT: The Standard Widget Toolkit: Managing Operating System Resources.

ImageData

ImageData can be thought of as the model for an image, whereas the Image is the view that's been prepared for output onto a specific device. The ImageData has a width and height, and a pixel value for each coordinate. The raw data of the image is a byte[], and the depth of the image specifies the number of bits that are used for each coordinate. An image depth of 1 can store two possible values for each pixel (0 and 1), a depth of 4 can store 2^4=16, a depth of 8 can store 2^8=256 values and a depth of 24 can represent 2^24=16 million different values per pixel. The larger the depth of an image the more bytes are required for its pixels, and some formats such as GIF that were initially designed for download across internet connections only support a maximum depth of 8. How the value of each pixel value translates into an actual color depends on its palette represented by the class org.eclipse.swt.graphics.PaletteData.

The next section describes how colors are represented by their RGB values, and how PaletteData maps a map pixel value to a particular color.

Color

The class org.eclipse.swt.graphics.Color is used to manage resources that implement the RGB color model. Each color is described in terms of its red, green and blue component (each expressed as an integer value from 0 for no color to 255 for full color).

Color cyanColor = new Color(display,0,255,255);
// ... Code to use the Color
cyanColor.dispose();

The convenience class org.eclipse.swt.graphics.RGB exists in SWT that combines a color's red, green and blue values into a single object.

RGB cyanRGB = new RGB(0,255,255);
Color cyanColor = new Color(display,cyanRGB);
// ... Code to use the Color
cyanColor.dispose();

The Color instance should be disposed when it is no longer required, whereas the RGB has no need to be disposed. This is similar to the relationship between an Image and its ImageData, where Color and Image are device specific objects using underlying native resources, while RGB and ImageData are the underlying model data.

To avoid having to create and manage instances of the commonly used colors, the Display class allow these to be retrieved using the method Display.getSystemColor(int id).

Color cyanColor = display.getSystemColor(SWT.COLOR_CYAN)

When a Color is obtained by an SWT program using the method Display.getSystemColor(int id) method, it must not be disposed. The rule of thumb that works for any SWT resource is "If you created it, you are responsible for disposing it". Because the statement above retrieved the cyan color instance, and didn't explicitly construct it, it should not be disposed.

How a Color is actually represented on the display depends on factors such as the resolution and depth of the display. For more information on this and the SWT color model see SWT Color Model.

PaletteData

There are two kinds of PaletteData, an indexed palette and a direct palette. PaletteData is a model of how pixel values map to RGB values, and because they do not represent an underlying resource, they do not require disposing.

Indexed palette

With an indexed palette each pixel value represents a number that is then cross indexed with the palette to determine the actual color. The range of allowable pixel values is the depth of the image.

The example below is a 48 by 48 square image created with a depth of 1, and an indexed color palette. The indexed palette assigns 0 to be red and 1 to be green (by virtue of their order in the RGB[] in the constructor). The ImageData's un-initialized pixel values will initially be 0 (red), and two for loops set a 34 by 34 square in the center of the ImageData to be 1 (green).

PaletteData paletteData = new PaletteData(
        new RGB[] {new RGB(255,0,0), new RGB(0,255,0)});

    ImageData imageData = new ImageData(48,48,1,paletteData);
for(int x=11;x<35;x++){
        for(int y=11;y<35;y++){
            imageData.setPixel(x,y,1);
        }
    }
    Image image = new Image(display,imageData);

The above example has a depth of 1 so it can store 2 colors, but as the color depth of the ImageData increases then so can the number of colors in the palette.  An indexed palette can have a 1, 2, 4, or 8 bit depths, and an 8 bit depth provides 2^8 = 256 possible colors.  To have a higher color depth (such as 16, 24, or 32) a direct palette must be used.

Direct palette

Instead of having each pixel value represent an index in the palette corresponding to its color, a direct palette allows each pixel value to directly record its red, green and blue component.  A direct PaletteData defines red, green and blue masks.  These masks are number of bits required to shift a pixel value to the left so the high bit of the mask aligns with the high bit of the first byte of color.  For example, a 24 bit direct palette can divide itself into 3 portions, storing red in the lowest 8 bits, green in the central 8 bits and blue in the highest 8 bits.  The red shift mask would be 0xFF, green 0xFF00 and blue 0xFF0000.

The value for each pixel represents a combination of the red, green and blue components into a single 24 bit integer. To construct an indexed palette the constructor used allows the red, green and blue color masks to be specified.

PaletteData palette = new PaletteData(0xFF , 0xFF00 , 0xFF0000);
    ImageData imageData = new ImageData(48,48,24,palette);

Using the same technique as earlier, the code iterates over every pixel coordinate setting it to either 0xFF (for red) or 0xFF00 (for green).

    for (int x=0;x<48;x++){
        for(int y=0;y<48;y++){
            if(y > 11 && y < 35 && x > 11 && x < 35){
             imageData.setPixel(x,y,0xFF00);   // Set the center to green
            } else {
                imageData.setPixel(x,y,0xFF);   // and everything else to red
         }
        }
    };
    Image image = new Image(display,imageData);

This creates the result below where the image is red with a green center.

Because you can use color depths of 16, 24 and 32 bits with direct palettes, you can represent more colors than are available with an indexed palette whose maximum depth is 8. A color depth of 24 allows you to represent 16 million colors (2^24). The tradeoff however is size, because an indexed palette with a depth of 8 requires one byte per image coordinate whereas a direct palette with a depth of 24 requires three bytes per image coordinate.

With both direct and indexed palettes you can go from an RGB to a pixel value and vice-versa using the public methods int getPixel(RGB rgb) and RGB getRGB(int pixelValue).

Transparency

The purpose of transparency is to make a portion of the image non-opaque, so when it is drawn on a GUI surface the original background shows through. This is done by specifying that one of the image colors is transparent. Whenever a pixel with the transparent color value is drawn, instead of using the RGB value defined in the palette the original destination background pixel color is used instead. The effect to the user is that the areas of the image that equal the transparent pixel show the background of whatever the image is being drawn over, thus achieving transparency. Persisted image file formats such as GIF or BMP allow you to specify a transparent pixel value, although only if the palette is indexed and the color depth is 8 or less.

When Images are used directly on controls such as Button or Label the native behavior may be that transparent pixels are ignored and drawn in the pixel color specified by the source. Native image transparency however is supported in SWT for operations involving a GC. To illustrate this the following file Idea.gif has a color depth of 8, and the white pixel (index 255 in the palette) set to be the transparent pixel.

The shell below has a Label on the left with a Canvas next to it. The Idea.gif is used as the label's image, and also in the paint event of the Canvas. Because the Label does not support native transparency the original white color of the transparent pixel is used as the background, however the GC in the paint event respects the transparent pixel and the grey background shows through.

    Image ideaImage = new ImageData(getClass().getResourceAsStream("Idea.gif"));
    Label label = new Label(shell,SWT.NONE);
label.setImage(ideaImage);
    Canvas canvas = new Canvas(shell,SWT.NO_REDRAW_RESIZE);
    canvas.addPaintListener(new PaintListener() {
        public void paintControl(PaintEvent e) {
         e.gc.drawImage(ideaImage,0,0);
        }
    });

For the above example I stacked the deck in my favor, because I didn't actually use the idea graphic that is included with eclipse articles in their banner. The reason is that the original graphic is a JPG file which doesn't support transparency, so I used a graphics tool to convert it to a GIF and set the value of the white pixel in the palette to be the transparency pixel. The original Idea.jpg is shown below, and although it looks the same as the Idea.gif, this is because it is on the white background of the HTML browser.

By using the original JPG file this offers a good example of how we to achieve a transparency effect progrmatically by manpulating its ImageData. The ImageData class has a public field transparentPixel to specify which pixel is transparent that can be set once a persisted image file is loaded into an ImageData instance, irrespective of whether the persisted file format supports transparency.

The code below loads the Idea.jpg file in an ImageData object and sets the transparent pixel for the ImageData to be the pixel value of the color white in the palette. The pixel value in the indexed palette that represents white is retrieved by using getPixel(RGB). The manipulated ImageData is used to create an Image transparentIdeaImage that now has the white pixel value specified to be transparent.

    ImageData ideaData = new ImageData(
        getClass().getResourceAsStream("Idea.jpg"));

int whitePixel = ideaData.palette.getPixel(new RGB(255,255,255));
ideaData.transparentPixel = whitePixel;
    Image transparentIdeaImage = new Image(display,ideaData);

Next a Shell uses the newly created image in a native Label and also in the paint event of a Canvas. A Windows Label does not support native transparency so it still appears with a white background, however the GC for the Canvas uses the existing background color whenever a white pixel is encountered in the source image, so the image appears as transparent.

    Label transparentIdeaLabel = new Label(shell,SWT.NONE);
transparentIdeaLabel.setImage(transparentIdeaImage);
    Canvas canvas = new Canvas(shell,SWT.NONE);
    canvas.addPaintListener(new PaintListener() {
        public void paintControl(PaintEvent e) {
         e.gc.drawImage(transparentIdeaImage,0,0);
        }
    });

As can be seen from the second of the two images (drawn on the Canvas with the white pixel set to transparent), there are still some patches of white. Closer analysis reveals that this is not a bug, but that these regions are not pure white (255,255,255), but are slighly off-white (such as 255,254,254). The transparent pixel of an ImageData can only be used for a single value. This now presents the next problem to be solved - locate all of the off-white pixels in the ImageData and convert them to pure-white. To do this we will iterate over each pixel in the image data and modify those that are close to white to be pure white.

Manipulating ImageData

Because the graphic Idea.jpg isn't as white as we'd like it, we'll iterate over its imageData and convert the off-white pixels to pure white and then we'll save this into a new file Idea_white.jpg. In practice it's unlikely that you'd ever do this kind of programming in SWT, however it's a good example to use showing how ImageData can be analyzed and manipulated.

The first step is to load the image and then iterate over each pixel individually looking at its color. Because Idea.jpg is using a direct palette, the pixel value is an int that contains the red, green and blue component as masked bit areas. These mask value can be obtained from the palette.

ImageData ideaImageData = new ImageData(
    getClass().getResourceAsStream("Idea.jpg"));

int redMask = ideaImageData.palette.redMask;
int blueMask = ideaImageData.palette.blueMask;
int greenMask = ideaImageData.palette.greenMask;

For any pixel value we can bitwise AND it with the mask to see what the color component is. The red component is the low order bits so this will be the actual value (from 0 to 255), however the green and blue values need adjusting as they are the high order bits in the pixel value. To make this adjustment the color component can be bit shifted to the right using the >> operator. If you are writing generic code to do this kind of manipulating, take care that direct palettes for color depths of 24 or 32 store their color components with red being the low order bits, however for color depth of 16 the colors are reversed and red is high order with blue being low order. The reason for this is to be the same as how Windows stores images internally so there is less conversion when creating the image.

Two for loops will iterate over the imageData. The first is traversing the image from top to bottom a line at a time, and creates an int[] to hold each line of data. The method ImageData.getPixels(int x, int y, int getWidth, int[] pixels, int startIndex) is used to extract a line at a time from the imageData's bytes. The API for this method is slightly irregular, because rather than returning the resulting data it instead is declared as void, and the resulting pixel data is injected into the int[] that is passed in as a method argument. The int[] of pixels is then iterated over and each value has its red, green and blue component extracted. The desired effect we want is to determine whether the pixel is off-white and if so to make it pure white - a rule that works well is to assume that anything whose red and green component are higher than 230 and blue component higher than 150 is an off-white.

int[] lineData = new int[ideaImageData.width];
    for (int y = 0; y < ideaImageData.height; y++) {
     ideaImageData.getPixels(0,y,width,lineData,0);
        // Analyze each pixel value in the line
        for (int x=0; x<lineData.length; x++){
           // Extract the red, green and blue component
            int pixelValue = lineData[x];
         int r = pixelValue & redShift;
         int g = (pixelValue & greenShift) >> 8;
         int b = (pixelValue & blueShift) >> 16;
         if (r > 230 && g > 230 && b > 150){
                ideaImageData.setPixel(x,y,0xFFFFFF);
            }
        }
    };

Having manipulated the raw bytes making up the ImageData we have now successfully changed the off-white values to pure white.

Saving Images

Now that we have the ImageData where all of the whitish pixels have been converted to white, and the transparency pixel of the palette has been set to be the color white, we'll save this image so that next time an SWT program needs the pure white JPF it can just load the file and use it as is. To save ImageData into a file use the class org.eclipse.swt.graphics.ImageLoader. The image loader has a public field data typed to ImageData[]. The reason the data field is an array of ImageData is to support image file formats with more than one frame such as animated GIFs or interlaced JPEG files. These are covered more in the Animation section later.

ImageLoader imageLoader = new ImageLoader();
imageLoader.data = new ImageData[] {ideaImageData};
imageLoader.save("C:/temp/Idea_PureWhite.jpg",SWT.IMAGE_JPEG);

The finished result is shown below.

It doesn't look much different to the original Idea.jpg because it is drawn on a white background, but when it is drawn on a Canvas with the white pixel set to be the transparent pixel the background shows through achieving the desired effect.

    ImageData pureWhiteIdeaImageData =
        new ImageData("C:/temp/Idea_PureWhite.jpg");

pureWhiteIdeaImageData.transparentPixel =
        pureWhiteIdeaImageData.palette.getPixel(new RGB(255,255,255));

    final Image transparentIdeaImage = new Image(display,pureWhiteIdeaImageData);
    Canvas canvas = new Canvas(shell,SWT.NONE);
    canvas.addPaintListener(new PaintListener() {
        public void paintControl(PaintEvent e) {
            e.gc.drawImage(transparentIdeaImage,0,0);
        }
    });

It might seem odd that in the above code that after loading the Idea_PureWhite.jpg file the transparent pixel was set to be white. Why not set the transparent pixel before we used the ImageLoader to create the persisted Idea_PureWhite.jpg file? The reason is that the JPEG image file format does not support transparency. A GIF file supports native transparency, however changing the file type to SWT.IMAGE_GIF on the ImageLoader would not have worked, because GIF supports a maximum image depth of 8 and uses an indexed palette, whereas the JPEG has an image depth of 24 and a direct palette. To convert between the two formats would require analyzing the colours used by the JPEG to create the best fit 256 color palette, before iterating over each JPEG pixel value and creating the GIF image data by finding the closest color. Doing this conversion is outside the scope of this article, although it can be done by most commercial graphics tools. To match pixel values as the color depth decreases from 24 to 8 involves algorithms that find the right color match for a block of pixels rather than a single pixel value, and is why image quality can sometimes be reduced when switching between different formats.

We have shown how an ImageData is an array of int values representing each pixel coordinate, and how each pixel value is mapped to a color through the palette. This allowed us to iterate over the image data for the Idea.jpg, query pixel values that were close to white, and convert these to a pure white RGB value. The end result of this was that we were able to create the Idea_PureWhite.jpg file that can be used as a transparent JPG by setting the white pixel to be transparent. Transparency works by having a source pixel value (the image being drawn), a destination pixel value (the image being drawn onto) and a rule by which the resulting destination pixel value is determined. For transparency the rule is that the source pixel value is used unless it's transparent in which case the destination pixel is used. Another technique is to use alpha values that specify the weight applied to the source relative to the destination to create the final pixel value. This allows the blending between the source image and the existing background it is being drawn onto.

Blending

Alpha blending is a technique used to merge two pixel values, where the source and destination pixel each specify an alpha value that weights how much they will affect the final destination pixel. An alpha value of 255 is full weight, and 0 is no weight. SWT supports a single alpha value for the entire ImageData, or else each pixel can have its own alpha value.

Single alpha value

The int field alphaValue of ImageData is used to specify a single value that weights how the source pixels are combined with the destination input to create the destination output. The listing below shows how the same ImageData for the Idea_PureWhite.jpg is used for three separate images. The first is the original, then an alpha of 128 is used, and finally an alpha of 64. Note that the same ImageData is continually manipulated by having its alpha changed before creating each Image, and changing the ImageData has no affect on Images already constructed using it. This is because the Image is prepared for display on the device from the ImageData at construction time.

    Shell shell = new Shell(display);
    shell.setLayout(new FillLayout());
    ImageData imageData = new ImageData("C:/temp/Idea_PureWhite.jpg");
final Image fullImage = new Image(display,imageData);
    imageData.alpha = 128;
final Image halfImage = new Image(display,imageData);
    imageData.alpha = 64;
final Image quarterImage = new Image(display,imageData);

    Canvas canvas = new Canvas(shell,SWT.NO_REDRAW_RESIZE);
    canvas.addPaintListener(new PaintListener() {
        public void paintControl(PaintEvent e) {
            e.gc.drawImage(fullImage,0,0);
            e.gc.drawImage(halfImage,140,0);
            e.gc.drawImage(quarterImage,280,0);
        }
    });

Different alpha value per pixel

As well as having a single alpha value that applied to all pixels in the source image, ImageData allows each pixel to have its own individual alpha value. This is done with the byte[] field alphaData. This allows effects to be achieved, such as having an image fade from its top to bottom.

The following code creates an alphaData byte[], and then has two loops. The outer loop y is from 0 to the imageData's height, and the inner loop creates a byte[] for the width of the imageData and initializes it with a value that increases from 0 for the top row through to 255 for the bottom row. A System.arrayCopy then builds up the alphaData byte[] with each row.

    ImageData fullImageData = new ImageData("C:/temp/Idea_PureWhite.jpg");
    int width = fullImageData.width;
    int height = fullImageData.height;
byte[] alphaData = new byte[height * width];
for(int y=0;y<height;y++){
        byte[] alphaRow = new byte[width];
     for(int x=0;x<width;x++){
            alphaRow[x] = (byte) ((255 * y) /height);
        }
     System.arraycopy(alphaRow,0,alphaData,y*width,width);
    }
    fullImageData.alphaData = alphaData;
    Image fullImage = new Image(display,fullImageData);

The resulting image is shown below, and the alphaData byte[] makes the top of the image transparent and the bottom opaque, with a gradual fading between the two.

Image effects

As well as arbitrary image effects that can be achieved by manipulating image data, SWT provides a number of pre-defined ways of creating new images based on existing images combined with certain styles. This is used if, for example, you have an image being used on a toolbar button and you wish to create a version that can be used to indicate that the button is disabled or that it is inactive.

To create an effect based on an existing image and a style flag use the constructor Image(Display display, Image image, int flag). The flag argument is a static constant of either SWT.IMAGE_COPY, SWT.IMAGE_DISABLE or SWT.IMAGE_GRAY. Copy creates a new image based on the original but with a copy of its imageData, whereas Disable and Gray create a new image applying platform specific effects. The following code shows the Idea.jpg, together with three more images that we created using the style bits IMAGE_DISABLE, IMAGE_GRAY and IMAGE_COPY. To show that IMAGE_COPY creates a new image a GC is used to draw onto it that affects only the copied image, not the original.

    Image ideaImage = new Image(display,
        getClass().getResourceAsStream("/icons/Idea.jpg");

Image disabledImage  = new Image(display,image,SWT.IMAGE_DISABLE);
Image grayImage = new Image(display,image,SWT.IMAGE_GRAY);
Image copyImage = new Image(display,ideaImage,SWT.IMAGE_COPY);
GC gc = new GC(copyImage);
    gc.drawText("This is a copy",0,0);
    gc.dispose();

GIF Animation

Another important topic for images is understanding animation where an image can contain a number of frames that are animated in sequence. Image animation is supported by GIF images, where a single image file can contain multiple sets of ImageData. Web browsers support native animation, and the following image is made up of 15 frames showing the pen rotating and writing the words SWT beneath the Idea logo.

While the web browser you're using to read this article should show the Idea_SWT_Animation.gif file as a sequence with the moving pen, this is not true of native SWT controls displaying the graphic. The animation must be done programmatically, and the class org.eclipse.swt.examples.ImageAnalyzer shows how this can be achieved. The ImageAnalyzer class can be obtained from the SWT examples project in the Eclipse CVS repository.

When an animated GIF is loaded by the ImageLoader class, each individual frame is a separate element in the data field array typed to ImageData[].  In the animation sequence each ImageData records how many milliseconds it should be displayed for in the int field delayTime. The number of times the sequence should repeat can be retrieved from the field loader.repeatCount, a value of -1 indicates that the animation should repeat indefinitely and Idea_SWT_Animation.gif has this value. When switching from one frame to the next there are three ways that the new frame can replace the previous one, specified by the int field ImageData.disposalMethod. This can take the following values defined in the constant class org.eclipse.swt.SWT.
 

DM_FILL_NONELeave the previous image in place and just draw the image on top.  Each frame adds to the previous one.
DM_FILL_BACKGROUNDFill with the background color before painting each frame. The pixel value for this is defined in the field loader.backgroundPixel
DM_FILL_PREVIOUSRestore the previous picture 
DM_FILL_UNSPECIFIEDNo disposal method has been defined

To conserve on space, animated GIFs are generally optimized to just store the delta that needs to be applied to the previous image. In the Idea_SWT_Animation.gif above the 15 frames are shown below. Each frame stores a delta against the previous image, and this was automatically generated by the tool I create the animated GIF with. The disposal method for each frame is DM_NONE so the each image should be drawn on top of the previous one. Each individual ImageData element has the x and y for its top left corner, as well as its width and height. The overall size to use can be obtained from the fields loader.logicalScreenWidth and loader.logicalScreenHeight.

To illustrate how to display an animated GIF in SWT we'll create an initial Image from the first frame and a counter to store which frame is being displayed. The image is drawn a paint event on a Canvas, and a GC is created that will be used to draw the subsequent frames onto the image.

    ImageLoader loader = new ImageLoader();
    loader.load(getClass().getResourceAsStream("Idea_SWT_Animation.gif"));
    Canvas canvas = new Canvas(shell,SWT.NONE);
image = new Image(display,loader.data[0]);
    int imageNumber;
final GC gc = new GC(image);
    canvas.addPaintListener(new PaintListener(){
      public void paintControl(PaintEvent event){
     event.gc.drawImage(image,0,0);
      }
    });

The body of the example will create a thread that iterates through each frame, waiting until the delayTime has passed. For each frame the ImageData is retrieved from the loader and a temporary Image created. This is then drawn onto the image being displayed on the canvas, at the x and y position specified by the frame's ImageData. Because we created the temporary frameImage we must dispose it when it's no longer being used to free up the underlying resource.

    Thread thread = new Thread(){
      public void run(){
        long currentTime = System.currentTimeMillis();
        int delayTime = loader.data[imageNumber].delayTime;
     while(currentTime + delayTime * 10 > System.currentTimeMillis()){
          // Wait till the delay time has passed
        }
        display.asyncExec(new Runnable(){
          public void run(){
            // Increase the variable holding the frame number
            imageNumber = imageNumber == loader.data.length-1 ? 0 : imageNumber+1;
            // Draw the new data onto the image
         ImageData nextFrameData = loader.data[imageNumber];
            Image frameImage = new Image(display,nextFrameData);
         gc.drawImage(frameImage,nextFrameData.x,nextFrameData.y);
         frameImage.dispose();
            canvas.redraw();
          }
        });
      }
    };
    shell.open();
    thread.start();

Scaling

In the examples so far we have loaded an image from a file and drawn it on the GUI at its original size. There are times when this will not always be the case and you need to stretch or shrink the image, and there are two ways to do achieve this. The first is to use the GC to stretch and clip it, using GC.drawImage(Image image, int srcX, int srcY, int srcWidth, int srcHeight, int dstX, int dstY, int dstWidth, int dstHeight), and the second is to use ImageData.scaledTo(int width, int height) to create a new ImageData object based on scaling the receiver.

The following code loads the Idea.jpg image , and scales this to 1/2 and 2 times its original size using the ImageData.scaledTo(int width, int height). The image is also resized using GC.drawImage(...), and the example shows two ways to achieve this. The first technique is to specify the new width and height as part the paint event. This is potentially inefficient because the scaling must be done each time the canvas repaints itself. A more optimized technique is to create an image at the final desired size, construct a GC over the this and then paint onto it so a permanent scaled image exists in the program.

The end result is shown below, and both techniques produce almost identical results.

final Image image = new Image(display,
        getClass(),getResourceAsStream("Idea.jpg"));

    final int width = image.getBounds().width;
    final int height = image.getBounds().height;

final Image scaled050 = new Image(display,
        image.getImageData().scaledTo((int)(width*0.5),(int)(height*0.5)));

    final Image scaled200 = new Image(display,
        image.getImageData().scaledTo((int)(width*2),(int)(height*2)));

final Image scaledGC200 = new Image(display,(int)(width*2),(int)(height*2));
    GC gc = new GC(scaledGC200);
    gc.drawImage(image,0,0,width,height,0,0,width*2,height*2);
    gc.dispose();

    canvas.addPaintListener(new PaintListener() {
      public void paintControl(PaintEvent e) {
     e.gc.drawImage(image,0,0,width,height,0,0,(int)(width*0.5),(int)(height*0.5));
        e.gc.drawImage(scaled050,100,0);
        e.gc.drawImage(scaledGC200,0,75);
        e.gc.drawImage(scaled200,225,175);
      }
    });

When to use GC scaling, and when to use ImageData.scaledTo(...), depends on the particular scenario. The GC scaling is faster because it is native, however it does assume that you have a GC and an Image to work with. Using just the ImageData means that you don't need to have prepared an Image (that requires a native resource and requires disposing), and an ImageData can be loaded directly from a graphic file (using the constructor ImageData(String fileName) or ImageData(InputStream stream)). By using raw ImageData you are delaying the point at which you will need native display resources, however you will eventually need to create an Image from the scaled ImageData before it can be rendered onto a device.

Cursor

The final section of this article covers the class org.eclipse.swt.graphics.Cursor responsible for managing the operating system resource associated with the mouse pointer. The reason cursors are covered in an article on images is because you can create arbitrary cursors from images, and they illustrate how image masks work.

Cursors can be created in two ways, either from a pre-defined style or using source and mask images.

Platform cursors

The list of pre-defined styles are SWT constants shown below, together with sample images although these will vary depending on the operating system and platform settings.
 
CURSOR_APPSTARTINGCURSOR_IBEAMCURSOR_SIZENE
CURSOR_ARROWCURSOR_NOCURSOR_SIZENESW
CURSOR_CROSSCURSOR_SIZEALLCURSOR_SIZENS
CURSOR_HANDCURSOR_SIZEECURSOR_SIZENW
CURSOR_HELPCURSOR_SIZENCURSOR_SIZESNWSE
CURSOR_SIZESCURSOR_SIZESECURSOR_SIZESW
CURSOR_SIZEWECURSOR_UPARROWCURSOR_WAIT

Every Control can have a cursor associated with it, and when the mouse pointer moves over the control it changes to the specified cursor.  Changing a cursor also affects any child controls, so if you update the cursor on a Shell this affects the mouse pointer for anywhere on the shell, although if the child control itself has an explicit cursor, or uses its own cursor such as an I bean for Text or Combo, this takes precedence over the parent's defined cursor. The following code illustrates this, by changing the shell's cursor to be hand cursor, and the list's cursor to a cross. When the mouse is over the shell (or its childButton that has no explicit cursor) it is a hand, and when it is over the list it is a cross.

List list = new List(shell,SWT.BORDER);
Button button = new Button(shell,SWT.NONE);
button.setText("Button");
Cursor handCursor = new Cursor(display,SWT.CURSOR_HAND);
shell.setCursor(handCursor);
Cursor crossCursor = new Cursor(display,SWT.CURSOR_CROSS);
list.setCursor(crossCursor);

Cursors use underlying native resources and should be disposed when they are no longer required. In the above code this would be when the shell has been disposed and there are no remaining controls using either the handCursor or crossCursor fields.

Custom cursors

As well as using a pre-defined style, a cursor can be created from images  using the constructor Cursor(Device device, ImageData source, ImageData mask, int hotspotx, int hotspoty). The source imagedata argument is the graphic for the cursor shape, and the mask is used to specify transparency. The following example shows how to create a monochrome custom cursor, where the the source and mask image data have a color depth of 1 and indexed palettes with two colors. The ImageData’s height and width should be no larger than 32 and it does not necessarily have to be square, although the mask and source should be the same size. The hotspot is the point on the cursor that represents the precise location of the mouse pointer.

The source and mask image data pixels are combined to determine whether the cursor pixel should be white, black or transparent.
 

ImageMaskCursor color
10Transparent
00Black
11Black
01White

The ImageData can be loaded from files, and Eclipse itself does this for some of its drag and drop cursors defined in org.eclipse.ui/icons/full/dnd. You can also directly create and manipulate the image data within your program. The code sample below creates an indexed palette with two colors. The source and mask ImageData are 32 by 32 with a color depth of 1. The int[] for the source and mask define an up arrow, the 0s for the source and 1s for the mask are shown in bold to show how the arrow is defined with the arrays. 0 and 1 makes white which is the center of the arrow, 1 and 1 is black for the edge of the arrow, and 1 and 0 transparent for the remainder. The tip of the arrow is 16,3 so this is made the cursor hotspot when it is created.

PaletteData paletteData = new PaletteData(new RGB[] {
 new RGB(0,0,0) , new RGB(255,255,255)
});
ImageData sourceData = new ImageData(32,32,1,paletteData);
ImageData maskData = new ImageData(32,32,1,paletteData);

int[] cursorSource = new int[] {
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
     1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 };

int[] cursorMask = new int[] {
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
     0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, };

sourceData.setPixels(0,0,1024,cursorSource,0);
maskData.setPixels(0,0,1024,cursorMask,0);
Cursor cursor = new Cursor(display,sourceData,maskData,16,3);
shell.setCursor(cursor);

To keep the code listings narrow the above sample used an int[] to define the source and mask imageData. A byte uses less memory than an unsigned int, so when creating custom cursors it is more efficient to use a byte[] instead, such as:

byte[] cursorSource = new byte[] {
     (byte)0x00, (byte)0x00, (byte)0x01, (byte)0x01, (byte)0x00, // etc...

Custom cursors need to be disposed in just the same way as pre-defined system cursors, so when there is no remaining control using the cursor is must be send the method dispose() to free up the underlying native resource.

Although the above example is for a monochrome cursor, Eclipse 3.0 supports color cursors on platforms that allow it (such as Windows). Two SWT code snippets showing how to do this are here: snippet 1, snippet 2.

Conclusion

This article has showed how to create and manipulate SWT images. Underlying each Image is its ImageData that records the pixel value for each coordinate, and the palette that maps this to a specific color. By understanding ImageData it is possible to achieve effects such as transparency, alpha blending, animation, as well as customized cursors.

Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.

Windows is a trademark of Microsoft corporation in the United States, other countries, or both.

Other company, product, and service names may be trademarks or service marks of others.

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值