| <html> |
| |
| <head> |
| <meta HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=windows-1252"> |
| <title>A basic image viewer</title> |
| <link rel="stylesheet" href="../default_style.css"> |
| </head> |
| |
| <body LINK="#0000ff" VLINK="#800080"> |
| <div align="right"> Copyright |
| © 2004 Chengdong Li |
| <table border=0 cellspacing=0 cellpadding=2 width="100%"> |
| <tr> |
| <td align=LEFT valign=TOP colspan="2" bgcolor="#0080C0"><b><font color="#FFFFFF"> Eclipse |
| Corner Article</font></b></td> |
| </tr> |
| </table> |
| </div> |
| <div align="left"> |
| <h1><img src="images/Idea.jpg" height=86 width=120 align=CENTER></h1> |
| </div> |
| <p> </p> |
| |
| <h1 ALIGN="CENTER">A Basic Image Viewer</h1> |
| |
| <blockquote> |
| <b>Summary</b> |
| |
| <br> |
| This article shows how to extend SWT <code>Canvas</code> to implement a mini image viewer plug-in using Java2D |
| transforms. The |
| extended image canvas can be used to scroll and zoom large images, and can also |
| be extended to apply other transforms. The implementation is based on SWT and |
| the non-UI portions of AWT. The plug-in has been tested on Windows, Linux GTK, and Mac |
| OS X Carbon with Eclipse 2.1 or better. |
| <p><b> By Chengdong |
| Li (</b><a href="mailto:cdli@ccs.uky.edu">cdli@ccs.uky.edu</a><b>) |
| Research in Computing for Humanities, University of Kentucky <br> |
| </b> |
| March 15, 2004</p> |
| </blockquote> |
| |
| <hr width="100%"> |
| <h2>Contents</h2> |
| <ul> |
| <li><a href="#Introduction">Introduction</a></li> |
| <li><a href="#Overview">Classes overview</a></li> |
| <li><a href="#Implement_Canvas">Canvas implementation</a> |
| <ul> |
| <li><a href="#Loading_image">Loading images</a></li> |
| <li><a href="#Extending_Canvas">Extending org.eclipse.swt.widgets.Canvas</a></li> |
| <li><a href="#Rendering_image">Rendering images</a></li> |
| <li><a href="#Transformation">Transformations</a></li> |
| <li><a href="#Synchronize_scrollbar">Scrollbar synchronization</a></li> |
| </ul> |
| </li> |
| <li><a href="#Rotating">Rotation</a></li> |
| <li><a href="#Plug-in_Implementation">Plug-in implementation</a> |
| <ul> |
| <li><a href="#Create_view_plug-in">Create view plug-in</a></li> |
| <li><a href="#Add_viewActions_extension">Add viewActions extension</a></li> |
| </ul> |
| </li> |
| <li><a href="#Summary">Summary</a></li> |
| <li><a href="#Acknowledge">Acknowledgements</a></li> |
| <li><a href="#Reference">References</a></li> |
| </ul> |
| <h2>Conventions & Terms</h2> |
| <p>The following typographic conventions are used in this article:</p> |
| <p><i>Italic</i>:<br> |
| Used for references to articles.</p> |
| <code>Courier New:</code><br> |
| Used for code or variable names. |
| <p>The following terms are used in this article:</p> |
| <p><b>client area<br> |
| </b> The |
| drawable area of canvas. Also called the <b>paint area</b> or <b>canvas domain</b>.<br> |
| <b>source image</b><br> |
| The image constructed directly from |
| the original image data with the same width and height. Unlike image data, it is |
| device dependent. It is the <code> sourceImage</code> in source |
| code. Also called the <b>original image</b> or <b>image domain</b>.</p> |
| <h2><a name="Introduction">Introduction</a></h2> |
| <p> The goal of this article is to show you how to implement an image viewer with |
| scrolling and zooming operations using affine transforms. |
| If you are new to graphics in SWT, please read Joe Winchester's article <i><a href="http://www.eclipse.org/articles/Article-SWT-images/graphics-resources.html">Taking a look at SWT Images</a></i>. |
| A screenshot of the image viewer is shown in Figure 1:</p> |
| <p><img border="0" src="images/screen_shot.jpg" width="359" height="318"><br> |
| |
| Figure 1 - Image viewer</p> |
| <p>The implementation here is different from the implementation of the Image Analyzer example |
| that comes with Eclipse. This implementation uses affine transforms for scrolling and zooming.</p> |
| <p>The advantages of this implementation are :</p> |
| <ul> |
| <li>it offers unlimited zoom scale</li> |
| <li>it works well for large images</li> |
| </ul> |
| <p>In the following sections, we will first review the structure of the package |
| and relationship of classes, |
| then we will discuss how the image canvas works and how to implement a scrollable |
| and zoom-able image canvas - <code> SWTImageCanvas</code>. We will use |
| affine transforms to selectively render images and to generally simplify the implementation. After that, we will show |
| briefly how to use the local toolbar to facilitate image manipulation.</p> |
| <p>For the detailed steps on how to implement a view, Dave Springgay's |
| article: <i><a href="http://www.eclipse.org/articles/viewArticle/ViewArticle2.html">Creating |
| an Eclipse View</a></i> is the most helpful one.</p> |
| <p>You can compare this implementation with the Image Analyzer example by running both of |
| them:</p> |
| <ul> |
| <li>To run the example for this article unzip <a href="imageviewer.zip"> |
| imageviewer.zip</a> into your <i>eclipse/plugins/ </i>subdirectory and restart |
| Eclipse. To open the image |
| viewer view, choose Window -> Show View -> Other -> Sample Category -> |
| Image Viewer. The source plug-in project is imageviewersrc.zip which is included inside the <a href="imageviewer.zip"> |
| imageviewer.zip</a>.</li> |
| <li>To run the Image Analyzer example that comes with Eclipse, download the Example |
| Plug-ins from <a href="http://www.eclipse.org/downloads/index.php">Eclipse.org</a>, |
| and unzip to the <i>eclipse/plugins/</i> subdirectory and restart Eclipse. Then choose Window -> Show |
| View -> Other -> SWT Example Launcher -> Standalone -> Image |
| Analyzer.</li> |
| </ul> |
| <p>To compile and run the image viewer from source code, unzip the imageviewersrc.zip |
| file (inside <a href="imageviewer.zip"> |
| imageviewer.zip</a>), then import the existing project into the workspace, update the |
| class path, compile, and run.</p> |
| <h2><a name="Overview">Classes Overview</a></h2> |
| <p>The following diagram (Figure 2) shows all classes in this demo plug-in. The <code> SWTImageCanvas</code> is a subclass of |
| <code>org.eclipse.swt.widgets.Canvas</code>; |
| it implements image loading, rendering, scrolling, and zooming. The <code> ImageView</code> class is a subclass of |
| <code> |
| org.eclipse.ui.part.ViewPart</code>; it has an <code> SWTImageCanvas</code> instance. The helper class |
| <code>SWT2Dutil</code> holds utility |
| functions. <code> PushActionDelegate</code> implements <code> |
| org.eclipse.ui.IViewActionDelegate</code>; it delegates the toolbar button actions, |
| and has an <code>ImageView</code> instance. The <code> |
| ImageViewerPlugin</code> extends <code> org.eclipse.ui.plugin.AbstractUIPlugin</code> |
| (this class is PDE-generated).</p> |
| <p><img border="0" src="images/all_classes.jpg" width="640" height="308"><br> |
| Figure 2 - Class diagram ( The classes without package prefix are implemented |
| in our approach. )</p> |
| <p>A plug-in manifest file plugin.xml defines the runtime requirements and contributions (view and |
| viewActions extensions) to |
| Eclipse. Eclipse will create toolbar buttons |
| for <code> ImageView</code> and delegate toolbar actions via <code> PushActionDelegate</code>.</p> |
| <h2><a name="Implement_Canvas">Canvas implementation</a></h2> |
| <p> <code> SWTImageCanvas</code> handles image loading, |
| painting, scrolling, and |
| zooming. It saves a copy of the original SWT image (the <code> sourceImage</code>) |
| in memory, and then translates and scales |
| the image using <code>java.awt.geom.AffineTransform</code>. The |
| rendering and transformation are applied only to the portion of the image |
| visible on the |
| screen, allowing it to operate on images of any size with good performance. The <code> AffineTransform</code> |
| gets applied changes as the user scrolls the window and pushes the toolbar buttons.</p> |
| <h4><a name="Loading_image">Loading images</a> </h4> |
| <p>First, let's have a look at how to load an image into memory. There are several ways to load an |
| image:</p> |
| <ul> |
| <li> load an image from the local file system.</li> |
| <li> load an image from the workspace. </li> |
| <li>load an image from a website.</li> |
| </ul> |
| <p>In this simple implementation, we only allow the user to choose an image from |
| the local file |
| system. To improve this, you could contribute to the <code>org.eclipse.ui.popupMenus</code> |
| of the Navigator view for image files; that way, whenever an image file is selected, the menu item will be available and |
| the user can choose to load the image |
| from the workspace (you need add <code>nameFilters</code>, and you may also need to use workspace |
| API). To see how to load an image from a URL, please refer to |
| the Image Analyzer of SWT |
| examples.</p> |
| <p>The image loading process is as following (Figure 3):</p> |
| <p align="left"><img border="0" src="images/open_activity.jpg" width="402" height="255"><br> |
| Figure 3 - Image-loading diagram</p> |
| <p>Now let's take a look at the code for loading images. </p> |
| <p>SWT |
| provides <code> ImageLoader</code> to load an image into memory. |
| Image loading is done by using the <code>Image(Display,String)</code> constructor. To facilitate image loading, we provides a |
| dialog to locate all image files supported by SWT <code> ImageLoader</code>. </p> |
| <pre><b>public</b> <b>void</b> onFileOpen(){ |
| FileDialog fileChooser = <b>new</b> FileDialog(getShell(), SWT.OPEN); |
| fileChooser.setText("Open image file"); |
| <img src="images/tag_1.gif" height=13 width=24 align=CENTER> fileChooser.setFilterPath(currentDir); |
| fileChooser.setFilterExtensions( |
| <b>new</b> String[] { "*.gif; *.jpg; *.png; *.ico; *.bmp" }); |
| fileChooser.setFilterNames{ |
| <b>new</b> String[] { "SWT image" + " (gif, jpeg, png, ico, bmp)" }); |
| String filename = fileChooser.open(); |
| if (filename != <b>null</b>){ |
| <img src="images/tag_2.gif" height=13 width=24 align=CENTER> loadImage(filename); |
| <img src="images/tag_3.gif" height=13 width=24 align=CENTER> currentDir = fileChooser.getFilterPath(); |
| } |
| }<b> |
| public</b> Image loadImage(String filename) { |
| <b>if</b>(sourceImage!=<b>null</b> && !sourceImage.isDisposed()){ |
| sourceImage.dispose(); |
| sourceImage=<b>null</b>; |
| } |
| <img src="images/tag_4.gif" height=13 width=24 align=CENTER> sourceImage= <b>new</b> Image(getDisplay(),filename); |
| <img src="images/tag_5.gif" height=13 width=24 align=CENTER> showOriginal(); |
| <b>return</b> sourceImage; |
| }</pre> |
| <p>We use <code>currentDir</code> in <img src="images/tag_1.gif" height=13 width=24 align=CENTER> |
| and <img src="images/tag_3.gif" height=13 width=24 align=CENTER> |
| to remember the directory for the file open dialog, so that the user can later open other files |
| in the same directory.</p> |
| <p>The <code>loadImage</code> method (shown above) in <img src="images/tag_2.gif" height=13 width=24 align=CENTER> |
| disposes the old <code> sourceImage</code> and creates a new <code>sourceImage</code>, |
| then it calls the <code>showOriginal()</code> to notify the canvas to |
| paint new image. If loading fails, the canvas will clear the painting area |
| and disable the scrollbar. Notice that we cannot see <code>ImageLoader</code> |
| directly in the code above, however, when we call <code>Image(Display, |
| String)</code> in |
| <img src="images/tag_4.gif" height=13 width=24 align=CENTER>, Eclipse will call |
| <code>ImageLoader.load()</code> to |
| load the image into memory. <img src="images/tag_5.gif" height=13 width=24 align=CENTER> |
| is used to show the image at its original size; we will |
| discuss this in more detail <a href="#func_showOriginal">later</a>.</p> |
| <p><img src="images/tip.gif" width="62" height="13"> In fact, the above two functions |
| could be merged into one method. The reason why we separate them is we |
| may invoke them separately from other functions; for example, we may get the image file |
| name from the database, then we can reload the image by only calling <code>loadImage()</code>.</p> |
| <h4><a name="Extending_Canvas">Extending org.eclipse.swt.widgets.Canvas</a></h4> |
| <p>Now, let's see how to create a canvas to render the image and do some |
| transformations.</p> |
| <p>The <code>org.eclipse.swt.widgets.Canvas</code> |
| is suitable to be extended for rendering images. <code>SWTImageCanvas</code> |
| extends it and adds scrollbars. This is done by setting the <code>SWT.V_SCROLL</code> |
| and <code>SWT.H_SCROLL</code> style bits at the <code>Canvas</code> |
| constructor:</p> |
| <pre><b>public</b> SWTImageCanvas(<b>final</b> Composite parent, <b>int</b> style) { |
| <img src="images/tag_1.gif" height=13 width=24 align=CENTER> <b>super</b>(parent,style|SWT.BORDER|SWT.V_SCROLL|SWT.H_SCROLL |
| |SWT.NO_BACKGROUND); |
| <img src="images/tag_2.gif" height=13 width=24 align=CENTER> addControlListener(<b>new</b> ControlAdapter() { <font color="#3f7f5f">/* resize listener */</font> |
| <b>public</b> <b>void</b> controlResized(ControlEvent event) { |
| syncScrollBars(); |
| } |
| }); |
| <img src="images/tag_3.gif" height=13 width=24 align=CENTER> addPaintListener(<b>new</b> PaintListener() { <font color="#3f7f5f">/* paint listener */</font> |
| <b>public</b> <b>void</b> paintControl(PaintEvent event) { |
| paint(event.gc); |
| } |
| }); |
| <img src="images/tag_4.gif" height=13 width=24 align=CENTER> initScrollBars(); |
| }<b> |
| private</b> <b>void</b> initScrollBars() { |
| ScrollBar horizontal = getHorizontalBar(); |
| horizontal.setEnabled(<b>false</b>); |
| horizontal.addSelectionListener(<b>new</b> SelectionAdapter() {<b> |
| public</b> <b>void</b> widgetSelected(SelectionEvent event) { |
| scrollHorizontally((ScrollBar) event.widget); |
| } |
| }); |
| ScrollBar vertical = getVerticalBar(); |
| vertical.setEnabled(<b>false</b>); |
| vertical.addSelectionListener(<b>new</b> SelectionAdapter() {<b> |
| public</b> <b>void</b> widgetSelected(SelectionEvent event) { |
| scrollVertically((ScrollBar) event.widget); |
| } |
| }); |
| }</pre> |
| <p><img src="images/tip.gif" width="62" height="13"> In order to speed up the |
| rendering process and reduce flicker, we set the style to <code>SWT.NO_BACKGROUND</code> |
| in <img src="images/tag_1.gif" height=13 width=24 align=CENTER> |
| (and later we use <a href="#double-buffering">double buffering</a> to render) so that the background (client area) won't be cleared. |
| The new image will be |
| overlapped on the background. We need to fill the gaps between the new image and |
| the background when the new image does not fully cover the background.</p> |
| <p><img src="images/tag_2.gif" height=13 width=24 align=CENTER> registers a |
| resize listener to synchronize the |
| size and position of the scrollbar thumb to the image zoom scale and |
| translation; <img src="images/tag_3.gif" height=13 width=24 align=CENTER> |
| registers a paint listener (here it does <code>paint(GC gc)</code>) |
| to render the image whenever the <code>PaintEvent</code> |
| is fired; <img src="images/tag_4.gif" height=13 width=24 align=CENTER> |
| registers the <code>SelectionListener</code> |
| for each scrollbar, |
| the <code>SelectionListener</code> |
| will notify <code>SWTImageCanvas</code> to scroll and zoom |
| the image based on the current selection of scrollbars; another function of the <code>SelectionListener</code> |
| is to enable or disable the scrollbar based on the image size and zoom scale. </p> |
| <h4><a name="Rendering_image">Rendering images</a></h4> |
| <p>Whenever the SWT<code> PaintEvent</code> is fired, the paint listener (<code>paint(GC |
| gc)</code>) |
| will be called to paint the |
| damaged area. In this article, we simply paint the whole client area of the |
| canvas (see Figure 4). |
| Since we support scrolling and zooming, we need to figure out which part of |
| the original image should be drawn to which part of the client area. The painting process is as |
| following:</p> |
| <ol> |
| <li>Find a rectangle <code>imageRect</code> inside the source |
| image (<b>image domain</b>); the image inside this rectangle will be drawn to the client area of canvas (<b>canvas |
| domain</b>).</li> |
| <li>Map the <code>imageRect</code> to the client area and |
| get <code>destRect</code>.</li> |
| <li>Draw the source image within <code>imageRect</code> to <code>destRect</code> |
| (scaling it if the sizes are different).</li> |
| <li>Fill the gaps (shown as blue and green bands in the picture below) if |
| necessary.</li> |
| </ol> |
| <p><a name="Rendering_scenario_figure"></a><img border="0" src="images/render_model.jpg" width="656" height="426"> <br> |
| |
| Figure 4 - Rendering scenarios</p> |
| <p>1) and 2) can be done based on <code>AffineTransform</code>, which we will discuss next. </p> |
| <p>3) draws a part of the source image to the client area using GC's <code>drawImage</code>:<br> |
| <code> drawImage |
| (Image image, int srcX, int srcY, int srcWidth, int srcHeight, |
| int destX, int destY, int destWidth, int destHeight)</code></p> |
| <p>which copies a rectangular area from the source image into a destination rectangular |
| area and automatically scale the |
| image if the source rectangular area has a different size from the destination |
| rectangular area.</p> |
| <p>If we draw the image directly on the screen, we need to calculate the gaps in |
| 4) and fill them. Here we make use of <a href="#double-buffering"> double |
| buffering</a>, so the gaps will be filled |
| automatically.</p> |
| <p><img src="images/tip.gif" width="62" height="13">We use the following |
| approach to render the image: We save only the source image. When the canvas needs to update the visible area, |
| it copies the corresponding image area from the source image to the destination area on |
| the canvas. This approach can |
| offer a very large zoom scale and save the memory, since it does not |
| need to save the whole big zoomed image. The drawing process is also speeded up. |
| If the size of canvas is very huge, we could divide the canvas into several small |
| grids, and render each grid using our approach; so this approach is to some extent scalable.</p> |
| <p><img border="0" src="images/note.gif" width="62" height="13">The Image Analyzer example |
| that comes with Eclipse draws the whole |
| zoomed image, and scrolls the zoomed image (which is saved by system) to the right place based on the |
| scrollbar positions. This implementation works well for small images, or when the zoom |
| scale is not |
| large. However, |
| for large-sized images and zoom scale greater than 1, the scrolling becomes very |
| slow since it has to operate on a very large zoomed image. This can be shown in |
| Image Analyzer.</p> |
| <p>Now let's look at the code used to find out the corresponding rectangles in the |
| source image and the client area:</p> |
| <pre><b>private</b> <b>void</b> paint(GC gc) { |
| <b>1</b> Rectangle clientRect = getClientArea(); <font color="#3f7f5f">/* canvas' painting area */</font> |
| <b>2</b> <b>if</b> (sourceImage != <b>null</b>) { |
| <b>3</b> Rectangle imageRect=SWT2Dutil.inverseTransformRect(transform, clientRect); |
| <b>4</b> |
| <b>5</b> <b>int</b> gap = 2; <font color="#3f7f5f">/* find a better start point to render. */</font> |
| <b>6</b> imageRect.x -= gap; imageRect.y -= gap; |
| <b>7</b> imageRect.width += 2 * gap; imageRect.height += 2 * gap; |
| <b>8</b> |
| <b>9 </b>Rectangle imageBound=sourceImage.getBounds(); |
| <b>10</b> imageRect = imageRect.intersection(imageBound); |
| <b>11</b> Rectangle destRect = SWT2Dutil.<a href="#transformRect">transformRect</a>(transform, imageRect); |
| <b>12</b> |
| <b>13</b> <b>if</b> (screenImage != <b>null</b>){screenImage.dispose();} |
| <b>14</b> screenImage = <b>new</b> Image( getDisplay(),clientRect.width, clientRect.height); |
| <b>15</b> GC newGC = <b>new</b> GC(screenImage); |
| <b>16</b> newGC.setClipping(clientRect); |
| <b>17</b> newGC.drawImage( sourceImage, |
| <b>18</b> imageRect.x, |
| <b>19</b> imageRect.y, |
| <b>20</b> imageRect.width, |
| <b>21</b> imageRect.height, |
| <b>22</b> destRect.x, |
| <b>23</b> destRect.y, |
| <b>24</b> destRect.width, |
| <b>25</b> destRect.height);<b> |
| 26 </b>newGC.dispose();<b> |
| 27 |
| 28</b> gc.drawImage(screenImage, 0, 0);<b> |
| 29</b> } <b>else</b> { |
| <b>30</b> gc.setClipping(clientRect); |
| <b>31</b> gc.fillRectangle(clientRect); |
| <b>32</b> initScrollBars(); |
| <b>33</b> } |
| }</pre> |
| <p>Line 3 to line 10 are used to find a rectangle (<code>imageRect</code>) in the source image, the source image inside |
| this rectangle will be drawn to the canvas. This is done by inverse |
| transforming the canvas's client area to the image domain and intersecting it with the bounds of image. The |
| <code>imageRect</code> |
| of line 10 is the exact rectangle we need. |
| Once we have got <code>imageRect</code>, |
| we transform <code>imageRect</code> back to the canvas domain in |
| line 11 and get a rectangle <code>destRect</code> inside |
| the client area. The source image inside the <code>imageRect</code> will be |
| drawn to the client area inside <code>destRect</code>.</p> |
| <p>After we get the <code>imageRect</code> of the source |
| image and the corresponding <code>destRect</code> of the |
| client area, we can draw just the part of image to be shown, and draw it in the right place. |
| For convenience, here we use <a name="double-buffering"> double buffering</a> to ease the drawing process: |
| we first create a <code>screenImage</code> and draw image to |
| the <code>screenImage</code>, then copy the <code>screenImage</code> |
| to the canvas.</p> |
| <p>Line 30 to line 32 are used to clear the canvas and reset the scrollbar whenever the |
| source image is set to null.</p> |
| <p><img src="images/tip.gif" width="62" height="13">Line 5 to line 7 are used to |
| find a better point to start drawing the rectangular image, since the transform may compress or |
| enlarge the size of each pixel. To make the scrolling and zooming |
| smoothly, we always draw the image from the beginning of a pixel. This also guarantee |
| that the image always fills the canvas if it is larger than the canvas.</p> |
| <p>The flowchart of rendering is as following (Figure 5):</p> |
| <p><img border="0" src="images/render_flowchart.jpg" width="677" height="209"><br> |
| |
| Figure 5 - Rendering flowchart</p> |
| <p>In the code above, we use <code>inverseTransformRect()</code> |
| in line 3 and <code>transformRect()</code> in line 11 for |
| transforming rectangles between different domain. We will discuss them in detail in |
| the next section.</p> |
| <h4><a name="Transformation">Transformations</a></h4> |
| <p><img border="0" src="images/note.gif" width="62" height="13"> When we say scrolling in this |
| section, we mean scrolling the image, not the scrollbar thumb (which actually moves |
| in the opposite direction).</p> |
| <p>Our primary goal is to develop a canvas with scrolling and zooming functions. To |
| do |
| that, we must solve the following problems:</p> |
| <ul> |
| <li>How to save the scrolling and zooming parameters.</li> |
| <li>How to change the scrolling and zooming parameters.</li> |
| <li>How the scrolling and zooming parameters control the image rendering.</li> |
| </ul> |
| <p>Scrolling and zooming entails two transformations: translation and scaling (see |
| Figure 6). |
| Translation is used to change the horizontal and |
| vertical position of the image; scrolling involves translating the image in the |
| horizontal or vertical directions. Scaling is used to change the size of image; |
| scale with a rate |
| greater than 1 to zoom in; scale with a rate less than 1 to zoom out.</p> |
| <p><img border="0" src="images/transform.jpg" width="333" height="145"><br> |
| |
| Figure 6 - Translation and scaling</p> |
| <p><code>SWTImageCanvas</code> uses an <code> AffineTransform</code> to |
| save the parameters of both the translation and |
| the scaling. In this implementation, only translation and scaling are |
| used. The basic idea of <code> AffineTransform</code> is to represent the transform as a matrix and |
| then merge several transforms into one by matrix multiplication. For |
| example, a scaling <b>S</b> followed by a translation <b>T </b>can be merged into |
| a transform like: <b>T</b>*<b>S</b>. By merging first and then |
| transforming, we can reduce times for transforming |
| and speed up the process.</p> |
| <p><code>SWTImageCanvas</code> has an <code>AffineTransform</code> |
| instance transform:</p> |
| <pre><b> private</b> AffineTransform transform;</pre> |
| <p><code>AffineTransform</code> provides |
| methods to access the translation and scaling parameters of an affine |
| transform:</p> |
| <pre> <b>public double</b> getTranslateX(); |
| <b>public double</b> getTranslateY(); |
| <b>public double</b> getScaleX(); |
| <b>public double</b> getScaleY();</pre> |
| <p>To change the <code>AffineTransform</code>, we can either reconstruct an <code>AffineTransform</code> |
| by merging itself |
| and another transform, or start from scratch. <code>AffineTransform</code> provides |
| <code>preConcatenate()</code> and <code>concatenate()</code> |
| methods, which can merge two <code>AffineTransform</code>s |
| into one. Using these two methods, each time the user scrolls or |
| zooms the image, we can create a new transform based on the changes (scrolling |
| changes translation and zooming changes scaling) and the transform itself. The |
| merge operation is matrix multiplication. Since 2D <code>AffineTransform</code> uses a 3x3 matrix, so the computation is very cheap.</p> |
| <p> For |
| example, when the user scrolls the image by <b>tx</b> in the x direction and<b> |
| ty </b> in the y |
| direction:</p> |
| <pre> newTransform = oldTransform.preconcatenate(AffineTransform.getTranslateInstance(<b>tx</b>,<b>ty</b>)); </pre> |
| <p>To construct a scaling or translation transform from scratch:</p> |
| <pre> <b>static</b> AffineTransform getScaleInstance(sx, sy); |
| <b>static</b> AffineTransform getTranslateInstance(tx,ty);</pre> |
| <p> Once you have an <code>AffineTransform</code>, the transformation can be easily done. |
| To transform a point:</p> |
| <pre> <b>public static</b> Point transformPoint(AffineTransform af, Point pt) { |
| Point2D src = <b>new</b> Point2D.Float(pt.x, pt.y); |
| Point2D dest= af.transform(src, <b>null</b>); |
| Point point=<b>new</b> Point((int)Math.floor(dest.getX()),(int)Math.floor(dest.getY())); |
| <b>return</b> point; |
| }</pre> |
| <p>To get the inverse transform of a point:</p> |
| <pre> <b>static</b> Point2D inverseTransform(Point2D ptSrc, Point2D ptDst);</pre> |
| <p>Since we use only translation and scaling in our implementation, |
| transforming a rectangle can be done by first transforming the top-left point, |
| and then scaling the width and height. To do that, we need to convert an arbitrary |
| rectangle to a rectangle with positive width and length. The following code |
| shows how to transform an arbitrary rectangle using <code>AffineTransform</code> |
| (the inverse transform is almost the same):</p> |
| <pre><b>1</b> <b>public static</b> Rectangle <a name="transformRect">transformRect</a>(AffineTransform af, Rectangle src){ |
| <b>2</b> Rectangle dest= <b>new</b> Rectangle(0,0,0,0); |
| <b>3</b> src=absRect(src); |
| <b>4</b> Point p1=<b>new</b> Point(src.x,src.y); |
| <b>5</b> p1=transformPoint(af,p1); |
| <b>6</b> dest.x=p1.x; dest.y=p1.y; |
| <b>7</b> dest.width=(<b>int</b>)(src.width*af.getScaleX()); |
| <b>8</b> dest.height=(<b>int</b>)(src.height*af.getScaleY()); |
| <b>9</b> <b>return</b> dest; |
| <b>10</b> }</pre> |
| <p>The <code>absRect()</code> function in line 3 is used to |
| convert an arbitrary rectangle to a rectangle with positive width and height.</p> |
| <p>For more detail about <code>AffineTransform</code>, you can |
| read <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/awt/geom/AffineTransform.html"> the |
| Java API document from SUN website</a>.</p> |
| <p><img src="images/tip.gif" width="62" height="13"> <code>AffineTransform</code> |
| also supports shear and rotation. In this |
| article, we only need translation and scaling. <code> AffineTransform</code> is widely used in the AWT's |
| image packages, and it has no relation with UI event loop, so it can be used in |
| SWT. (Even if <code>AffineTransform</code> were unavailable, we could easily replace or rewrite it since we only use the translation and |
| scaling).</p> |
| <p>We have seen how we save the scrolling and scaling parameters in <code>AffineTransform</code>, |
| and how we can change them. But how do they control the image rendering?</p> |
| <p><img border="0" src="images/scroll_zoom_activity.jpg" width="540" height="149"><br> |
| |
| Figure 7 - Scrolling and zooming diagram</p> |
| <p>The basic idea is shown in Figure 7. When the user interacts with GUI |
| (scrollbars and toolbar buttons), her/his action will be caught by Eclipse, Eclipse will |
| invoke the listeners (for scrollbars) or delegates (for toolbar buttons) to change the parameters in |
| the transform, |
| then the canvas will update the status of scrollbars based on the transform, |
| and finally it will notify itself to repaint the image. The painter |
| will consider the updated transform when it |
| repaints the image. For |
| example, it will use transform to find out the corresponding rectangle in the source |
| image to the visible |
| area on the canvas, and copy |
| the source image inside the rectangle to the canvas with scaling.</p> |
| <p>Let's take a look at some methods which use <code>AffineTransform</code> |
| to translate and zoom images.</p> |
| <p>First let's see how to show an image at its original size:</p> |
| <pre><b><a name="func_showOriginal"></a>public</b> <b>void</b> showOriginal() {<b> |
| </b><img src="images/tag_1.gif" height=13 width=24 align=CENTER><b> </b>transform=<b>new</b> AffineTransform(); |
| syncScrollBars(); |
| }</pre> |
| <p>Here we first change transform in <img src="images/tag_1.gif" height=13 width=24 align=CENTER> |
| (defaults to a scaling rate of 1, and no translation), and then call <code>syncScrollBars()</code> to update the |
| scrollbar and repaint the canvas. It's that simple.</p> |
| <p>Now let's try another one - zooming. When we zoom the image, we will zoom it around the center of |
| the client |
| area (centered zooming). The procedure for centered zooming is:</p> |
| <ol> |
| <li>Find the center of client area: (x,y).</li> |
| <li>Translate the image by (-x,-y), so that (x,y) is at the origin.</li> |
| <li>Scale the image.</li> |
| <li>Translate the image by (x,y).</li> |
| </ol> |
| <p>The <a href="#func_syncScrollBars">syncScrollBars</a>() |
| (see next section) guarantees that the image will be centered in the client |
| area if it is smaller than the client area.</p> |
| <p>Steps 2-4 can be used to scale images around an arbitrary point (dx,dy). |
| Since the same steps will be used by many other methods, we put them in the method |
| <code>centerZoom(dx,dy,scale,af)</code>: </p> |
| <pre><b>public</b> <b>void</b> centerZoom(<b>double</b> dx,<b>double</b> dy,<b>double</b> scale,AffineTransform af) { |
| af.preConcatenate(AffineTransform.getTranslateInstance(-dx, -dy)); |
| af.preConcatenate(AffineTransform.getScaleInstance(scale, scale)); |
| af.preConcatenate(AffineTransform.getTranslateInstance(dx, dy)); |
| transform=af; |
| syncScrollBars(); |
| }</pre> |
| <p> Now the code for <code>zoomIn</code> |
| is:</p> |
| <pre><b>public</b> <b>void</b> zoomIn() { |
| <b>if</b> (sourceImage == <b>null</b>) <b>return</b>; |
| Rectangle rect = getClientArea();<b> |
| </b> <b>int</b> w = rect.width, h = rect.height; |
| <font color="#3f7f5f">/* zooming center */</font> |
| <b>double</b> dx = ((<b>double</b>) w) / 2; |
| <b>double</b> dy = ((<b>double</b>) h) / 2; |
| centerZoom(dx, dy, ZOOMIN_RATE, transform); |
| }</pre> |
| <p>Here the (<code>dx</code>,<code>dy</code>) is the zooming center, <code>ZOOMIN_RATE</code> |
| is a constant for incremental zooming in. <code>centerZoom()</code> will also call |
| <code>syncScrollBars()</code> |
| to update the scrollbar and repaint the canvas.</p> |
| <h4><a name="Synchronize_scrollbar">Scrollbar synchronization</a></h4> |
| <p>Each time user zooms or scrolls the image, the scrollbars need to update |
| themselves to synchronize with the state of image. This includes adjusting the position and the size of the |
| thumbs, |
| enabling or disabling the scrollbars, changing the range of the scrollbars, and |
| finally notifying the canvas to repaint the client area. We use <code>syncScrollBars()</code> |
| to do this:</p> |
| <pre><b><a name="func_syncScrollBars"></a>public</b> <b>void</b> syncScrollBars() { |
| <img src="images/tag_1.gif" height=13 width=24 align=CENTER> <b>if</b> (sourceImage == <b>null</b>){ |
| redraw(); |
| <b>return</b>; |
| } |
| AffineTransform af = transform;<b> |
| </b> <b>double</b> sx = af.getScaleX(), sy = af.getScaleY(); |
| <b>double</b> tx = af.getTranslateX(), ty = af.getTranslateY(); |
| <img src="images/tag_2.gif" height=13 width=24 align=CENTER> <b>if</b> (tx > 0) tx = 0; <b>if</b> (ty > 0) ty = 0; |
| |
| ScrollBar horizontal = getHorizontalBar(); |
| horizontal.setIncrement((<b>int</b>)(getClientArea().width/100)); |
| horizontal.setPageIncrement(getClientArea().width);<b> |
| </b> Rectangle imageBound = swtImage.getBounds();<b> |
| </b> <b>int</b> cw = getClientArea().width, ch = getClientArea().height; |
| <b>if</b> (imageBound.width * sx > cw) { <font color="#3f7f5f">/* image is wider than client area */</font> |
| horizontal.setMaximum((<b>int</b>) (imageBound.width * sx)); |
| horizontal.setEnabled(<b>true</b>); |
| <b>if</b> (((<b>int</b>) - tx) > horizontal.getMaximum()-cw) { |
| tx = -horizontal.getMaximum()+cw; |
| } |
| } <b>else</b> { <font color="#3f7f5f">/* image is narrower than client area */</font> |
| horizontal.setEnabled(<b>false</b>); |
| <img src="images/tag_3.gif" height=13 width=24 align=CENTER> tx = (cw - imageBound.width * sx) / 2; |
| } |
| <img src="images/tag_4.gif" height=13 width=24 align=CENTER> horizontal.setSelection((<b>int</b>) (-tx)); |
| <img src="images/tag_5.gif" height=13 width=24 align=CENTER> horizontal.setThumb((<b>int</b>)(getClientArea().width)); |
| |
| <font color="#3f7f5f">/* update vertical scrollbar, same as above. */</font> |
| ScrollBar vertical = getVerticalBar(); |
| .... |
| <font color="#3f7f5f">/* update transform. */</font> |
| <img src="images/tag_6.gif" height=13 width=24 align=CENTER> af = AffineTransform.getScaleInstance(sx, sy); |
| af.preConcatenate(AffineTransform.getTranslateInstance(tx, ty)); |
| transform=af; |
| |
| <img src="images/tag_7.gif" height=13 width=24 align=CENTER> redraw(); |
| }</pre> |
| <p>If there is no image, the paint listener will be |
| notified to clear the client area in <img src="images/tag_1.gif" height=13 width=24 align=CENTER>.</p> |
| <p>If there is an image to show, we correct the current translation to make sure it's legal (<=0). |
| The |
| point (<code>tx</code>,<code>ty</code>) in <img src="images/tag_2.gif" height=13 width=24 align=CENTER> |
| corresponds to the bottom-left corner of the zoomed image (see the right-hand |
| image in <a href="#Rendering_scenario_figure">Figure 4</a>), so it's |
| reasonable to make it no larger than zero (the bottom-left corner of the canvas |
| client area is (0,0)) except if the zoomed image is smaller than the client |
| area. In such a situation, we correct the transform in <img src="images/tag_3.gif" height=13 width=24 align=CENTER> |
| so that it will translate the |
| image to the center of client area. We change the selection in <img src="images/tag_4.gif" height=13 width=24 align=CENTER> |
| and the thumb size in <img src="images/tag_5.gif" height=13 width=24 align=CENTER>, |
| so that the horizontal scrollbar will show the relative position to the whole |
| image exactly. |
| The other lines between <img src="images/tag_2.gif" height=13 width=24 align=CENTER> |
| and <img src="images/tag_5.gif" height=13 width=24 align=CENTER> |
| set the GUI parameters for the horizontal scrollbar, you can change them to |
| control the scrolling increment. The |
| process for the vertical scrollbar is exactly the same, so we don't show |
| it here. Lines between |
| <img src="images/tag_6.gif" height=13 width=24 align=CENTER> |
| and <img src="images/tag_7.gif" height=13 width=24 align=CENTER> |
| create a new transform based on the corrected translation and |
| the scaling and update the old transform. Finally, line <img src="images/tag_7.gif" height=13 width=24 align=CENTER> |
| notifies the canvas to repaint itself.</p> |
| <h2><a name="Rotating">Rotation</a></h2> |
| <p>Joe Winchester's <i><a href="http://www.eclipse.org/articles/Article-SWT-images/graphics-resources.html">Taking a look at SWT Images</a></i> |
| explains pixel manipulation in |
| great detail. Here |
| we will show how to rearrange the pixels to get a 90<sup>0 </sup>counter-clockwise |
| rotation. In order to demonstrate how other |
| classes can interact with <code>SWTImageCanvas</code>, we put |
| the implementation in the <code>PushActionDelegate</code> class. The basic steps for our rotation are:</p> |
| <ul> |
| <li>Get image data from <code>SWTImageCanvas</code>.</li> |
| <li> Create new image data and rearrange the pixels.</li> |
| <li>Set new image data back to <code>SWTImageCanvas</code>.</li> |
| </ul> |
| <p>The code in <code>PushActionDelegate</code> |
| for rotation is:</p> |
| <pre> ImageData src=imageCanvas.getImageData(); |
| <b>if</b>(src==<b>null</b>) <b>return</b>; |
| PaletteData srcPal=src.palette; |
| PaletteData destPal; |
| ImageData dest; |
| |
| <font color="#3f7f5f">/* construct a new ImageData */</font> |
| <b>if</b>(srcPal.isDirect){ |
| <img src="images/tag_1.gif" height=13 width=24 align=CENTER> destPal=<b>new</b> PaletteData(srcPal.redMask,srcPal.greenMask,srcPal.blueMask); |
| }<b>else</b>{ |
| <img src="images/tag_2.gif" height=13 width=24 align=CENTER> destPal=<b>new</b> PaletteData(srcPal.getRGBs()); |
| } |
| <img src="images/tag_3.gif" height=13 width=24 align=CENTER> dest=<b>new</b> ImageData(src.height,src.width,src.depth,destPal); |
| <font color="#3f7f5f">/* rotate by rearranging the pixels */</font> |
| <b>for</b>(<b>int</b> i=0;i<src.width;i++){<b> |
| </b> <b>for</b>(<b>int</b> j=0;j<src.height;j++){ |
| <b>int</b> pixel=src.getPixel(i,j); |
| <img src="images/tag_4.gif" height=13 width=24 align=CENTER> dest.setPixel(j,src.width-1-i,pixel); |
| } |
| } |
| <img src="images/tag_5.gif" height=13 width=24 align=CENTER> imageCanvas.setImageData(dest);</pre> |
| <p>The code for <code>setImageData()</code> is:</p> |
| <pre><b>public</b> <b>void</b> setImageData(ImageData data) { |
| <b>if</b> (sourceImage != <b>null</b>) sourceImage.dispose(); |
| <b>if</b>(data!=<b>null</b>) |
| sourceImage = <b>new</b> Image(getDisplay(), data); |
| syncScrollBars(); |
| }</pre> |
| <p>Since we won't change the pixel value, we needn't care about |
| the RGB of each pixel. However, we must reconstruct a new <code>ImageData</code> object with different dimension. This |
| needs different <code>PaletteData</code> in <img src="images/tag_1.gif" height=13 width=24 align=CENTER> |
| and <img src="images/tag_2.gif" height=13 width=24 align=CENTER> |
| for different image formats. <img src="images/tag_3.gif" height=13 width=24 align=CENTER> |
| creates a new <code>ImageData</code> and <img src="images/tag_4.gif" height=13 width=24 align=CENTER> |
| sets the value of each pixel. <code>setImageData()</code> in <img src="images/tag_5.gif" height=13 width=24 align=CENTER> |
| will dispose the previous <code>sourceImage</code> |
| and reconstruct <code>sourceImage</code> |
| based on the new <code>ImageData</code>, then update the scrollbars |
| and repaint the canvas. We put <code>setImageData()</code> |
| inside <code>SWTImageCanvas</code> so it could be used |
| by other methods in the future.</p> |
| <h2><a name="Plug-in_Implementation">Plug-in Implementation</a></h2> |
| <p>We have known how the image canvas works. Now, let's talk briefly about how to implement the plug-in.</p> |
| <h3><a name="Create_view_plug-in">Step 1. Create view plug-in</a></h3> |
| <p>Follow the steps 1-3 in <a href="http://www.eclipse.org/articles/viewArticle/ViewArticle2.html"><i>Creating |
| an Eclipse View</i></a>, we can create a plug-in with a |
| single view. The plugin.xml is:</p> |
| <pre><plugin id="uky.article.imageviewer" |
| name="image viewer Plug-in" |
| version="1.0.0" |
| provider-name="Chengdong Li" |
| class="uky.article.imageviewer.ImageViewerPlugin"> |
| <runtime> |
| <library name="imageviewer.jar"/> |
| </runtime> |
| <requires> |
| <import plugin="org.eclipse.ui"/> |
| </requires> |
| |
| <extension point="org.eclipse.ui.views"> |
| <category name="Sample Category" |
| id="uky.article.imageviewer"> |
| </category> |
| <view name="Image Viewer" |
| icon="icons/sample.gif" |
| category="uky.article.imageviewer" |
| class="uky.article.imageviewer.views.ImageView" |
| id="uky.article.imageviewer.views.ImageView"> |
| </view> |
| </extension> |
| </plugin></pre> |
| <p>The <code>ImageViewerPlugin</code> |
| and <code>ImageView</code> |
| are as following:</p> |
| <b> |
| <pre>public class </b>ImageViewerPlugin<b> extends </b>AbstractUIPlugin { |
| <b> public </b>ImageViewerPlugin(IPluginDescriptor descriptor) { |
| <b>super</b>(descriptor); |
| } |
| } |
| <b> |
| public</b> <b>class</b> ImageView <b>extends</b> ViewPart { |
| <img src="images/tag_1.gif" height=13 width=24 align=CENTER> <b>public</b> SWTImageCanvas imageCanvas;<b> |
| public</b> ImageView() {} <b> |
| public</b> <b>void</b> createPartControl(Composite frame) { |
| <img src="images/tag_2.gif" height=13 width=24 align=CENTER> imageCanvas=<b>new</b> SWTImageCanvas(frame); |
| } |
| <b>public</b> <b>void</b> setFocus() { |
| <img src="images/tag_3.gif" height=13 width=24 align=CENTER> imageCanvas.setFocus(); |
| } |
| <b>public</b> <b>void</b> dispose() { |
| <img src="images/tag_4.gif" height=13 width=24 align=CENTER> imageCanvas.dispose(); |
| <b>super</b>.dispose(); |
| } |
| }</pre> |
| <p><img src="images/tag_1.gif" height=13 width=24 align=CENTER> |
| declares an instance variable <code>imageCanvas</code> |
| to point to an instance of <code>SWTImageCanvas</code>, |
| so that other methods can use it. <img src="images/tag_2.gif" height=13 width=24 align=CENTER> |
| creates an <code>SWTImageCanvas</code> to show the image. |
| When the view gets focus, it will set focus to <code>imageCanvas</code> |
| in <img src="images/tag_3.gif" height=13 width=24 align=CENTER>. |
| The dispose method of <code>SWTImageCanvas</code> |
| will be automatically called in <img src="images/tag_4.gif" height=13 width=24 align=CENTER> |
| whenever the view is disposed.</p> |
| <h3><a name="Add_viewActions_extension">Step 2. Add viewActions extension</a></h3> |
| <p>The image viewer view has five local toolbar buttons: <img border="0" src="images/buttons.jpg" width="138" height="22">. To |
| take the advantage of Eclipse, we contribute to the <code>org.eclipse.ui.viewActions</code> |
| extension point by adding the following lines to plugin.xml:</p> |
| <pre><extension point="org.eclipse.ui.viewActions"> |
| <viewContribution |
| targetID="uky.article.imageviewer.views.ImageView" |
| id="uky.article.imageviewer.views.ImageView.pushbutton"> |
| <action label="open" |
| icon="icons/Open16.gif" |
| tooltip="Open image" |
| <img src="images/tag_1.gif" height=13 width=24 align=CENTER> class="uky.article.imageviewer.actions.PushActionDelegate" |
| toolbarPath="push_group" |
| enablesFor="*" |
| id="toolbar.open"> |
| </action> |
| ..... |
| </viewContribution></pre> |
| <p>The delegate class <code>PushActionDelegate</code> |
| in <img src="images/tag_1.gif" height=13 width=24 align=CENTER> |
| will process all the actions from the |
| toolbar buttons. It is defined as following:</p> |
| <b> |
| <pre>public</b> <b>class</b> PushActionDelegate <b>implements</b> IViewActionDelegate { |
| <img src="images/tag_2.gif" height=13 width=24 align=CENTER> <b>public</b> ImageView view;<b> |
| </b> <b>public</b> <b>void</b> init(IViewPart viewPart) { |
| <b>if</b>(viewPart <b>instanceof</b> ImageView){ |
| <img src="images/tag_3.gif" height=13 width=24 align=CENTER> <b>this</b>.view=(ImageView)viewPart; |
| } |
| }<b> |
| public</b> <b>void</b> run(IAction action) { |
| String id=action.getId(); |
| <img src="images/tag_4.gif" height=13 width=24 align=CENTER> SWTImageCanvas imageCanvas=view.imageCanvas; |
| <b>if</b>(id.equals("toolbar.open")){ |
| imageCanvas.onFileOpen(); |
| <b>return</b>; |
| } |
| .... |
| <b> if</b>(id.equals("toolbar.zoomin")){ |
| .... |
| } |
| } |
| }</pre> |
| <p>This class implements the <code>IViewActionDelegate</code> interface. It has a view instance in <img src="images/tag_2.gif" height=13 width=24 align=CENTER>. |
| It gets the view instance during the initialization period<img src="images/tag_3.gif" height=13 width=24 align=CENTER>, |
| and later in <img src="images/tag_4.gif" height=13 width=24 align=CENTER> |
| it uses the view instance to interact with the <code>SWTImageCanvas</code>. </p> |
| <h2><a name="Summary">Summary</a></h2> |
| <p>We have shown the detail on how to implement a simple image viewer plug-in for |
| Eclipse. The <code>SWTImageCanvas</code> supports scrolling |
| and zooming functions by using AWT's <code>AffineTransform</code>. It supports unlimited |
| zoom scale and smooth scrolling even for large images.</p> |
| <p>Compared with the implementation of Image Analyzer example, this implementation is |
| both memory |
| efficient and fast.</p> |
| <p>The <code>SWTImageCanvas</code> can be used as a base |
| class for rendering, scrolling, and scaling image.</p> |
| <p>Shortcut keys are a must for usable image viewers. In the interest of space, we did not show |
| this here; however, they can be easily added.</p> |
| <h2><a name="Acknowledge">Acknowledgements</a></h2> |
| <p>I appreciate the help I received from the <a href="http://dev.eclipse.org/newslists/news.eclipse.platform.swt/msg00951.html"> Eclipse mailing |
| list</a>; the Eclipse |
| team's help in reviewing the article; Pengtao Li's photo picture; and |
| finally the support from <a href="http://www.rch.uky.edu">RCH</a> lab at the |
| University of Kentucky.</p> |
| <h2><a name="Reference">Reference</a>s</h2> |
| <p><a href="http://www.eclipse.org/articles/viewArticle/ViewArticle2.html">Creating |
| an Eclipse View</a>. Dave Springgay, 2001 <a href="http://www.eclipse.org/articles/viewArticle/ViewArticle2.html"><br> |
| </a> |
| <a href="http://www.eclipse.org/articles/Article-SWT-images/graphics-resources.html">Taking a look at SWT |
| Images</a>. Joe Winchester, 2003 <a href="http://www.eclipse.org/articles/Article-SWT-images/graphics-resources.html"><br> |
| </a> |
| <a href="http://www.amazon.com/exec/obidos/tg/detail/-/0321159640/103-6992553-2927029?v=glance"> |
| The Java Developer's Guide to ECLIPSE</a>. Sherry Shavor, Jim D'Anjou, Scott |
| Fairbrother, Dan Kehn, John Kellerman, Pat McCarthy. Addison-Wesley, 2003 |
| |
| </body> |
| </html> |