[Chapter 11] 11.2 Scrolling An Image

#

| Chapter 11 Scrolling |

## 11.2 Scrolling An Image

Example 11.1 is a Java application that displays any image in the current directory in a viewing area. The viewing area scrolls to accommodate larger images; the user can use the scrollbars or keypad keys to scroll the image. In Java 1.1, it is trivial to implement this example with a `ScrollPane`; however, if you're using 1.0, you don't have this luxury. Even if you're using 1.1, this example shows a lot about how to use scrollbars.

Our application uses a `Dialog` to select which file to display; a `FilenameFilter` limits the list to image files. We use a menu to let the user request a file list or exit the program. After the user picks a file, the application loads it into the display area. Figure 11.3 shows the main scrolling window.

#### Figure 11.3: Scrolling an image

The code for the scrolling image consists of a `ScrollingImage` class, plus several helper classes. It places everything into the file ScrollingImage.java for compilation.

#### Example 11.1: Source Code for Scrolling an Image

    import java.awt.*;
    import java.io.FilenameFilter;
    import java.io.File;

The first class contains the `FilenameFilter` used to select relevant filenames: that is, files that are likely to contain GIF, JPEG, or XBM images. If the filename has an appropriate ending, the `accept()` method returns `true`; otherwise, it returns `false`.

    // True for files ending in jpeg/jpg/gif/xbm
    class ImageFileFilter implements FilenameFilter {
        public boolean accept (File dir, String name) {
            String tempname = name.toLowerCase();
            return (tempname.endsWith ("jpg") || tempname.endsWith ("jpeg") ||
                    tempname.endsWith ("gif") || tempname.endsWith ("xbm"));
        }
    }

The `ImageListDialog` class displays a list of files from which the user can select. Instead of using `FileDialog`, we created a customized `List` to prevent the user from roaming around the entire hard drive; choices are limited to appropriate files in the current directory. When the user selects an entry (by double-clicking), the image is then displayed in the scrolling area.

    class ImageListDialog extends Dialog {
        private String name = null;
        private String entries[];
        private List list;
        ImageListDialog (Frame f) {
            super (f, "Image List", true);
            File dir = new File (System.getProperty("user.dir"));
            entries = dir.list (new ImageFileFilter());
            list = new List (10, false);
            for (int i=0;i<entries.length;i++) {
                list.addItem (entries[i]);
            }
            add ("Center", list);
            pack();
        }
        public String getName () {
            return name;
        }
        public boolean action (Event e, Object o) {
            name = (String)e.arg;
            ((ScrollingImage)getParent()).processImage();
            dispose();
            return true;
        }
    }

The code in this class is straightforward. The constructor reads the current directory from the system property list, uses the `list()` method of the `File` class to create a list of files that match our filename filter, and then creates a `List` object that lists these files. `getName()` returns the name of the selected file. `action()` is called when the user selects a file; it sets the name of the selected file from the `arg` field of the `Event` and then calls the `processImage()` method of its parent container. The parent container of our `ImageListDialog` is the `ScrollingImage` class we are defining; its `processImage()` method displays a scrollable image.

The next class, `ImageCanvas`, is the `Canvas` that the image is drawn onto. We use a separate `Canvas` rather than drawing directly onto the `Frame` so that the scrollbars do not overlap the edges of the image. You will notice that the `paint()` method uses negative `x` and `y` values. This starts the drawing outside the `Canvas` area; as a result, the `Canvas` displays only part of the image. This is how we do the actual scrolling. (`xPos`, `yPos`) are the values given to us from the scrollbars; by positioning the image at (`-xPos`, -`yPos`), we ensure that the point (`xPos`, `yPos`) appears in the upper left corner of the canvas.

    class ImageCanvas extends Canvas {
        Image image;
        int xPos, yPos;
        public void redraw (int xPos, int yPos, Image image) {
            this.xPos = xPos;
            this.yPos = yPos;
            this.image = image;
            repaint();
        }
        public void paint (Graphics g) {
            if (image != null)
                g.drawImage (image, -xPos, -yPos, this);
        }
    }

`ScrollingImage` provides the framework for the rest of the program. It provides a menu to bring up the `Dialog` to choose the file, the scrollbars to scroll the scrolling canvas, and the image canvas area. This class also implements event handling methods to capture the scrollbar events, paging keys, and menu events.

    public class ScrollingImage extends Frame {
        static Scrollbar horizontal, vertical;
        ImageCanvas center;
        int xPos, yPos;
        Image image;
        ImageListDialog ild;
        ScrollingImage () {
            super ("Image Viewer");
            add ("Center", center = new ImageCanvas ());
            add ("South",  horizontal = new Scrollbar (Scrollbar.HORIZONTAL));
            add ("East", vertical = new Scrollbar (Scrollbar.VERTICAL));
            Menu m = new Menu ("File", true);
            m.add ("Open");
            m.add ("Close");
            m.add ("-");
            m.add ("Quit");
            MenuBar mb = new MenuBar();
            mb.add (m);
            setMenuBar (mb);
            resize (400, 300);
        }
        public static void main (String args[]) {
            ScrollingImage si = new ScrollingImage ();
            si.show();
        }
        public boolean handleEvent (Event e) {
             if (e.id == Event.WINDOW_DESTROY) {
                System.exit(0);
            } else if (e.target instanceof Scrollbar) {
                if (e.target == horizontal) {
                    xPos = ((Integer)e.arg).intValue();
                } else if (e.target == vertical) {
                    yPos = ((Integer)e.arg).intValue();
                }
                center.redraw (xPos, yPos, image);
            }
            return super.handleEvent (e);
        }

This `handleEvent()` method kills the program if the user used the windowing system to exit from it (`WINDOW_DESTROY`). More to the point, if a `Scrollbar` generated the event, `handleEvent()` figures out if it was the horizontal or vertical scrollbar, saves its value as the `x` or `y` position, and redraws the image in the new location. Finally, it calls `super.handleEvent()`; as we will see in the following code, other events that we care about (key events and menu events) we don't want to handle here--we would rather handle them normally, in `action()` and `keyDown()` methods.

        public void processImage () {
            image = getToolkit().getImage (ild.getName());
            MediaTracker tracker = new MediaTracker (this);
            tracker.addImage (image, 0);
            try {
                tracker.waitForAll();
            } catch (InterruptedException ie) {
            }
            xPos = 0;
            yPos = 0;
            int imageHeight = image.getHeight (this);
            int imageWidth = image.getWidth (this);
            vertical.setValues (0, 5, 0, imageHeight);
            horizontal.setValues (0, 5, 0, imageWidth);
            center.redraw (xPos, yPos, image);
        }

`processImage()` reads the image's filename, calls `getImage()`, and sets up a `MediaTracker` to wait for the image to load. Once the image has loaded, it reads the height and width, and uses these to set the maximum values for the vertical and horizontal scrollbars by calling `setValues()`. The scrollbars' minimum and initial values are both 0. The size of the scrollbar "handle" is set to 5, rather than trying to indicate the visible portion of the image.

        public boolean action (Event e, Object o) {
            if (e.target instanceof MenuItem) {
                if ("Open".equals (o)) {
                    // If showing already, do not show again
                    if ((ild == null) || (!ild.isShowing())) {
                        ild = new ImageListDialog (this);
                        ild.show();
                    }
                } else if ("Close".equals(o)) {
                    image = null;
                    center.redraw (xPos, yPos, image);
                } else if ("Quit".equals(o)) {
                    System.exit(0);
                }
                return true;
            }
            return false;
        }

`action()` handles menu events. If the user selected Open, it displays the dialog box that selects a file. Close redraws the canvas with a null image; Quit exits the program. If any of these events occurred, `action()` returns `true`, indicating that the event was fully handled. If any other event occurred, `action()` returns `false`, so that the system will deliver the event to any other methods that might be interested in it.

        public boolean keyDown (Event e, int key) {
            if (e.id == Event.KEY_ACTION) {
                Scrollbar target = null;
                switch (key) {
                    case Event.HOME:
                        target = vertical;
                        vertical.setValue(vertical.getMinimum());
                        break;
                    case Event.END:
                        target = vertical;
                        vertical.setValue(vertical.getMaximum());
                        break;
                    case Event.PGUP:
                        target = vertical;
                        vertical.setValue(vertical.getValue()
                                 - vertical.getPageIncrement());
                        break;
                    case Event.PGDN:
                        target = vertical;
                        vertical.setValue(vertical.getValue()
                                 + vertical.getPageIncrement());
                        break;
                    case Event.UP:
                        target = vertical;
                        vertical.setValue(vertical.getValue()
                                 - vertical.getLineIncrement());
                        break;
                    case Event.DOWN:
                        target = vertical;
                        vertical.setValue(vertical.getValue()
                                 + vertical.getLineIncrement());
                        break;
                    case Event.LEFT:
                        target = horizontal;
                        if (e.controlDown())
                            horizontal.setValue(horizontal.getValue() -
                                horizontal.getPageIncrement());
                        else
                            horizontal.setValue(horizontal.getValue() -
                                horizontal.getLineIncrement());
                        break;
                    case Event.RIGHT:
                        target = horizontal;
                        if (e.controlDown())
                            horizontal.setValue(horizontal.getValue() +
                                horizontal.getPageIncrement());
                        else
                            horizontal.setValue(horizontal.getValue() +
                                horizontal.getLineIncrement());
                        break;
                    default:
                        return false;
                }
                Integer value = new Integer (target.getValue());
                postEvent (new Event ((Object)target,
                           Event.SCROLL_ABSOLUTE, (Object)value));
                return true;
            }
            return false;
        }
    }

`keyDown()` isn't really necessary, but it adds a nice extension to our scrollbars: in addition to using the mouse, the user can scroll with the arrow keys. Pressing an arrow key generates a `KEY_ACTION` event. If we have one of these events, we check what kind of key it was, then compute a new scrollbar value, then call `setValue()` to set the appropriate scrollbar to this value. For example, if the user presses the page up key, we read the page increment, add it to the current value of the vertical scrollbar, and then set the vertical scrollbar accordingly. (Note that this works even though nondefault page and line increments aren't implemented correctly.) The one trick here is that we have to get the rest of the program to realize that the scrollbar values have changed. To do so, we create a new `SCROLL_ABSOLUTE` event, and call `postEvent()` to deliver it.

Scrollbar |  | The Adjustable Interface