CPSC 225, Spring 2010
Lab 10: Files and More GUI


This week's lab continues the GUI programming project that you began last week. The main focus of the lab is on saving and reloading files, although you might end up doing most of that on your own. Before you can work with files, you need something to put into them, so you will start today's lab by adding some drawing capabilities to your program.

Be sure to turn in your work for Lab 9 before starting work on this week's lab. If you want to keep a separate copy of last week's work, you can copy and paste your lab9 project and name the copy lab10. Alternatively, just rename the lab9 project to lab10.

This lab is due next week. You should turn it in before Saturday morning, April 10. Next week's lab will be on a different topic, but we will return to this GUI program in at least one more lab.


Tools Menu

So far, the program has very simple mouse interaction. All you can do is add a rectangle by clicking the point where you want to add it. We have to make it possible for the mouse to do different things, and to do that, you need a Tools Menu. The Tools Menu will list the various things that the mouse can be used for (such as drawing a rectangle, drawing an oval, adding text, and editing a shape that is already there). To start, let's say that there will be four tools: "Rect", "Filled Rect", "Oval", and "Filled Oval". The "Rect" and "Oval" tools draw outline shapes while the other two commands draw filled shapes.

At a given time, only one of the possible tools can be selected. The program need to keep track of the currently selected tool. Use an instance variable named currentTool to do this. This variable can have four possible values (for now). It's a good idea to have symbolic constants such as RECT_TOOL to represent the possible tools.

Add a "Tools" menu to the program. Add four menu items to the menu for the "Rect", "Filled Rect", "Oval", and "Filled Oval" commands. When one of these commands is selected by the user, the only action that the ActionListener has to take is to set the value of currentTool to the correct value to represent the selected tool. (It would be nice to use JRadioButtonMenuItems instead of plain JMenuItems in the "Tools" menu, since that would give the user feedback about which tool is currently selected. This is not a required feature, but if you are interested in trying it, ask about it, and see Section 12.3.3. Another thing that would be nice would be to change the cursor that is used for the panel to something appropriate for the tool; again, this is not a required feature, but see Section 12.1.4 if you are interested.)

The place where the tool is used is in the MouseHandler. When the user presses the mouse, the action that the program takes depends on the current tool. Implement the four tools in the mousePressed() method. You will need four subclasses of Drawable, one for each possible kind of shape. They should be simple modifications of the rectangle class that you already have, and you should be able to create them using copy-paste-and-edit.

To test your code at this point, you can just add a shape when the user clicks the mouse. The shape should be added at the point where the user clicked. You can use some constant values, like 100 and 50, for the width and height.


Dragging to Draw

Of course, you really want the user to be able to click-and-drag to draw a shape. When the user presses the mouse, one corner of the shape is specified. As the user drags the mouse, the position of the mouse gives the second corner of the shape.

To implement this, you need to have several instance variables in the MouseHandler class. You need two int variables to record the starting position where the user first presses the mouse. And you need a variable to tell you which shape you are editing by dragging the mouse. (For general information about dragging, see Section 6.4.4.)

One question is, how to represent the information about the shape that is being edited. An easy easy way to do this is to store the index of the shape in the ArrayList of shapes. So, use an instance variable of type int. Use a value of -1 to indicate that no shape is being edited; this should be the initial value.

In your mousePressed() method, when you add a shape to the list, set its width and its height to zero. Record the index of the shape, something like

         shapeIndex  =  shapeList.size() - 1;

where shapeIndex is the variable that records the index of the shape that you are editing, and shapeList is the ArrayList in which the shapes are stored. And don't forget to record the mouse position in the instance variables that record the starting point of the drag operation.

In the mouseDragged() method, you should first test to make sure that there is in fact a shape that is being edited:

          public void mouseDragged(MouseEvent evt) {
              if (shapeIndex == -1)
                  return;
              Drawable shape = shapeList.get(shapeIndex);  // Shape that is being edited.
              .
              .  // Change the shape to reflect current mouse location
              .
          }

Now, you have two points: The starting point where the user pressed the mouse, recorded in instance variables, and the current mouse position, which you can get from the evt parameter. You want to change the shape so that it has these two points as corners. Unfortunately, it's a little tricky in Java to make a shape from two corners, when what you need is the upper right corner, the width, and the height. If (x1,y1) and (x2,y2) are the two corner points, here is one way to get the values that you need:

       int x = Math.min( x1, x2 );
       int y = Math.min( y1, y2 );
       int width = Math.abs( x1 - x2 );
       int height = Math.abs( y1 - y2 );

You should modify the shape to use these values. And don't forget to call repaint(), or you won't see any change on the screen!

Finally, in the mouseReleased() method, you have to reset shapeIndex to -1 so that you know that you are no longer editing a shape. Also, before finishing with a shape, it's a good idea to check whether its width or its height is zero. If so, there is really nothing to draw, and you might as well remove it from the ArrayList.


Files

Now that you have some non-trivial data in your data structure, you can think about saving and opening files.

Add a "Save..." and an "Open..." command to the "File" menu. In the ActionListener, you should program these commands to call methods saveFile() and openFile(), which you will have to add to your program. Here are outlines of generic versions of those methods that you can use as starting points:

    public void saveFile() {
        fileDialog.setSelectedFile( new File("data.txt") ); // Default file name
        fileDialog.setDialogTitle("Select Output File");
        int option = fileDialog.showSaveDialog(this);
        if (option != JFileChooser.APPROVE_OPTION)
            return;  // user canceled
        File selectedFile = fileDialog.getSelectedFile();
        if (selectedFile.exists()) {
            int response = JOptionPane.showConfirmDialog(
                    this,
                    "A file named " + selectedFile.getName() + " already exists.\n"
                           + "Do you really want to replace it?", 
                    "Confirm Save",
                    JOptionPane.YES_NO_OPTION);
            if (response == JOptionPane.NO_OPTION)
                return;
        }
        PrintWriter out;
        try {
            out = new PrintWriter(selectedFile);
        }
        catch (IOException e) {
            JOptionPane.showMessageDialog(this, 
                                "Cannot open file for output.  Error:\n" + e);
            return;
        }
        // ... Write data to file ...  Does not throw exceptions
        out.close();  // Close the file.
        if (out.checkError())
            JOptionPane.showMessageDialog(this, 
                                "Some error occurred while writing the file.");
    }
    
    public void openFile() {
        fileDialog.setDialogTitle("Select Input File");
        int option = fileDialog.showOpenDialog(this);
        if (option != JFileChooser.APPROVE_OPTION)
            return;  // user canceled
        File selectedFile = fileDialog.getSelectedFile();
        Scanner in;
        try {
            in = new Scanner(selectedFile);
        }
        catch (IOException e) {
            JOptionPane.showMessageDialog(this, 
                                 "Cannot open file for input.  Error:\n" + e);
            return;
        }
        try {
            // Read data from the Scanner.
            in.close();
            // Replace the program's data with data from the file -- and call repaint();
        }
        catch (Exception e) {
            JOptionPane.showMessageDialog(this, 
                                "An error occurred while reading the file.");
        }
    }

Saving an Image

You've implemented Save and Open to make it possible to save the state of a drawing in a file and to reload it later for further work. However, the point of the program is to make a picture, and you haven't yet provided any way for the user to save the picture so that it can be used outside the program. It should be possible to save the picture as a PNG or as a JPEG file or both. (PNG and JPEG are two file formats for images.)

Add a menu command such as "Save Image...", "Save PNG...", or "Export JPEG" to the "File" menu (or add two commands, one for PNG and one for JPEG), and implement that command with a saveImage() method.

Java has an easy method for saving an image to a file, a static method in the ImageIO class from package javax.imageio. You only have to say:

        boolean OK = ImageIO.write(image,format,selectedFile);

where image is the BufferedImage that you want to save, format is either "PNG" or "JPEG" to indicate the image format, and selectedFile is a File object that specifies the file where the image is to be saved. If this method returns true, then the image has been saved successfully. A return value of false indicates an error, but the method can also fail by throwing an IOException. (The return value should be false only when the format string does not specify a recognized format.)

To write your saveImage() function, you can start with a copy of the saveFile() method, and modify it to save an image instead of a text file. (Also, the default file name should have an extension that matches the image format, such as "image.jpeg.) The big problem is that you don't have an image to save! The picture on the screen is not represented by an image, only by a data structure. You have to create the image before you save it. Create a new BufferedImage that is the same size as the panel. Create a graphics context for the image, and fill it with the panel's background color. Then draw the contents of the data structure to the image. Finally, you can use ImageIO.write() to save the image to the file.


David Eck, for CPSC 225