Application: DrawingPad: Garage Doodler

     

Application: DrawingPad: Garage Doodler

This Swing GUI application is comprised of two separate but related bits of functionality. The main thing it does is it allows the user to draw freehand on a canvas, and then captures and saves the doodle as an image file. To get this functionality, we use the ImageIO class, available since SDK 1.4. The second thing this app does is it allows you to open an image file from your hard drive and view it in the frame.

This application is worth looking over. It may seem long at first, but there are a lot of comments in this sucker. I think that Swing apps can be fairly confusing for a number of reasons. One reason is that Sun has frequently updated the APIs that are used to make GUIs. That means that you often see several different ways of doing effectively the same thing. Some of those ways may be more recent or efficient or stable, however. So you've got to be careful.

There are a lot of things that have to happen to make a Java GUI app go. If you have used Microsoft Visual Studio even for a few minutes, you can see that you can create a functional window in under a minute without writing a line of code. In Java, you have to do a lot of the writing yourself, and some components may not behave as expected. So I have commented just about every line of the application. Remember that the aim of the toolkit here is to give you something that works, so you can see how all of the pieces fit together, and something that you can build on if you want to.

Demonstrates

This application demonstrates a number of cool things, and a number of things that are important to doing real Java development. The following Swing classes are used: JFrame, JPanel, JComponent, JOptionPane, and JFileChooser. We also incorporate many classes from the older AWT package to handle coordinates, graphics, colors, events, and layout. These classes include Rectangle, Graphics, Point, Dimension, and Color . The application also demonstrates how to change the cursor from an arrow to a crosshair, and how to use keyboard shortcuts on your menus .

We see how to implement a menu with commonly required menu items that act differently than we have worked on previously. We see how to use the ImageIO class to read and write image data, and we subclass the FileFilter class to make sure that our JFileChooser only allows image file types to be opened. Use of JFileChooser to save an image is also presented.

This all means that we have to deal with mouse motion events, event handlers and listeners, see how anonymous classes are used, and how to do good subclassing. By using the JOptionPane, we show any exceptions to the user so that he can call your direct line to tell you exactly what the problem is; this is much better than burying the exception in an out.println statement.

Limitations and Extension Points
graphics/fridge_icon.jpg

FRIDGE

Remember that one thing we're trying to do is make the code do the talking. That's why it's okay with me to have all of this code. I want the emphasis to be on the code itself, and let it do most of the communicating when possible. There are too many technical books that explain 20 things by showing you the API and then give you a lot of disconnected snippets of code; that often leaves you not knowing what to do when you sit down at the keyboard. It also makes it hard to integrate into a real app. This method is a key tenet in Extreme Programming , popularized by Kent Beck, which is an added bonus.


The application does not allow you to do a few things. First, you cannot change the background color of the pad, and you cannot change the color of the pen you use to draw with. This functionality can be added with some ease by looking into the JColorChooser API. This guy works somewhat like the JFileChooser, which should make it feel familiar after using the JFileChooser in this app. The JColorChooser provides a very rich set of controls that allow the user to pick a color using RGB values, an eyedropper, and more.

Another good extension point for this application would be to allow the user to choose a different line thickness . A simple way to do this would be to show a series of buttons on the tool bar that create the pen with the specified thickness. The way to do this is to create your desired thickness with a float value, and then call the setStroke() method on the Graphics2D object. That code would look something like this:

 

 Graphics2D g2d = (Graphics2D)g; float thickness = 5.0f; // This is solid stroke, but you can also make dashed BasicStroke stroke = new BasicStroke(thickness); g2d.setStroke(stroke); 

For a more user-friendly but complicated implementation, you could allow the user to choose the thickness with a JSlider control.

DrawingApp.java
 

 package net.javagarage.apps.swing.draw; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.util.ListIterator; import java.util.Vector; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.KeyStroke; import javax.swing.filechooser.FileFilter; /**<p>  * This application allows you to do a couple  * different related things. First, it is a GUI  * interface that allows you to use a JFileChooser  * dialog to select an image file type to open  * and view.  * <p>  * The second thing you can do is draw a doodle onto  * a canvas and save it as an image file.  * <p>  * There are several classes in this file.  * The main class is called DrawingApp, and it allows  * you to open image files. Another class called  * AnImageFilter extends FileFilter, and provides a  * filter implementation that the  JFileChooser dia  * log uses in order to make sure that only  * image file types are opened by the user.<br>  * There is a class called Doodler, that registers  * mouse events to allow you to draw on the canvas.  * <p>  * You might extend this to include a JColorChooser  * dialog to allow the user to change the background  * color of the canvas, or to change the color  * of the pen.  * <p>  * thank you to rockstar pawel zurek for his very  * helpful ideas on this app.  * @author eben hewitt  **/ public class DrawingApp { //the main window protected JFrame frame; //the panels hold the content protected JPanel panel; protected JPanel contentPane; //holds the File drop-down menu protected JMenuBar menuBar; //this is where the user draws protected Doodler canvas; private boolean alreadyHasImage = false; //start the app public static void main(String[] args) { //create instance by calling default constructor new DrawingApp(); } //constructor public DrawingApp(){ //make the window pretty with static method //which must be called before instantiating the window JFrame.setDefaultLookAndFeelDecorated(true); //now make the window frame = new JFrame(); //this title will appear in upper-left of frame frame.setTitle("Garage Draw Pad"); //initialize the frame to dispose of the jvm //instance it is using when user closes the window frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); //instantiate the panel panel = new JPanel(); //give it a really simple layout type so we can //add stuff to the panel panel.setLayout(new BorderLayout()); //make it be 300x300 when it opens panel.setPreferredSize(new Dimension(300,300)); //add the working area to the window //in AWT we didn't have to do this. //we have to do it in Swing because a JFrame //has a number of layers, and the add call will be //ignored on the root pane. this makes sure we get //the layer of the frame where our stuff happens, and //not where frame management is going on. frame.getContentPane().add(panel); /*  * strangely, in my view, we have to set the  * background color of the <i>frame</i> to white if  * we want the drawing we make to be saved  * with a white background; otherwise, the  * background will be gray, instead of the white the  * user sees when he clicks New. that's because of  * the layers in a frame.  */ frame.setBackground(Color.WHITE); //create the menu menuBar = new JMenuBar(); //call our method that makes the menu and then //add the menu to the menuBar menuBar.add(makeFileMenu()); //must do this to work with it menuBar.setOpaque(true); //add the menu bar to the frame frame.setJMenuBar(menuBar); //put it at this x,y location on the screen frame.setLocation(350,350); //the pack method is inherited from java.awt.Window. //it makes the window displayable by sizing the //components to fit into it. it then validates //the layout. frame.pack(); //show the window on the screen. //the user can now interact with it, so return from //the constructor and wait until the user does //something. frame.setVisible(true); } /**  * Makes the menu by adding file items to the menu.  * It also handles the job of creating a listener  * for each of those items (such as New, Open, etc),  * to tell the app what action to perform when the  * user chooses that item.  * @return The completed menu, ready to add  * to the JMenuBar.  */ private JMenu makeFileMenu(){ //get the action command from the menu //to determine what the user wants to do //and call a separate method to do the work //ActionListener is an interface that extends EventListener ActionListener listener = new ActionListener() { //when an action event occurs (the user clicks a menu //item) the actionPerformed(ActionEvent) method is //called. so we override it to do what we want. public void actionPerformed(ActionEvent event) { String command = event.getActionCommand(); //user clicked File > New if (command.equals("New")){ //call our custom worker doNew(); } else if(command.equals("Open...")){ doOpen(); } else if(command.equals("Save...")){ doSave(); } else if (command.equals("Exit")){ doExit(); } } }; //this is the File menu that will be added to //the menubar. so we have to add each menu item //to it first. JMenu fileMenu = new JMenu("File"); //NEW JMenuItem newCmd = new JMenuItem("New"); //the user can alternatively type the control key + N //to generate the same event as clicking newCmd.setAccelerator(KeyStroke.getKeyStroke("ctrl N")); //register the listener newCmd.addActionListener(listener); //put this item onto the File menu //items get added in order fileMenu.add(newCmd);    //other commands work same way... //OPEN JMenuItem openCmd = new JMenuItem("Open..."); openCmd.setAccelerator(KeyStroke.getKeyStroke("ctrl O")); openCmd.addActionListener(listener); fileMenu.add(openCmd); //SAVE JMenuItem saveCmd = new JMenuItem("Save..."); saveCmd.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); saveCmd.addActionListener(listener); fileMenu.add(saveCmd); //QUIT JMenuItem exitCmd = new JMenuItem("Exit"); exitCmd.setAccelerator(KeyStroke.getKeyStroke("ctrl E")); exitCmd.addActionListener(listener); fileMenu.add(exitCmd); return fileMenu; } //end makeFileMenu() /**  * action on NEW  */ private void doNew() { //do work of "New" command from the File menu try { //if there is a canvas already, wipe it clean //to draw a new picture if(canvas != null) { //alert user JOptionPane.showMessageDialog(null,"This will destroy current doodle"); //gets rid of the old panel panel.remove(canvas); } //make this new object on which to draw //and set its size to 300x300 canvas = new Doodler(300, 300); //show a white background so the user sees it. //note that since we won't be saving //the <i>panel</i> to an image file, //the background of the image wouldn't be white //with only this! panel.setBackground(Color.WHITE); //put the canvas in the content panel panel.add(canvas, BorderLayout.CENTER); //create a crosshair cursor to draw with Cursor cursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); //make the canvas use the cursor we just created canvas.setCursor(cursor); //call this to repaint the window frame.validate(); } catch (Exception e){ System.out.println("Exception in doNew():          " + e.getMessage()); System.exit(1); } } /**  * action on OPEN  */ private void doOpen(){ //create new file chooser dialog box //the dialog will use the user's home directory //as a starting place JFileChooser fileChooser = new JFileChooser(); //if you wanted to specify a different dir to start //in, you could do this for example in linux: //JFileChooser f = new JFileChooser(new          File("/home/dude")); //set the file chooser to only see .gif file types //see the AnImageFilter class here for details fileChooser.addChoosableFileFilter(new          AnImageFilter()); fileChooser.setFileSelectionMode(JFileChooser.FILES_          AND_DIRECTORIES); //open the file chooser and make the frame its //parent which means, if you close the frame, the //dialog closes too int returnValue = fileChooser.showOpenDialog(frame); //when user clicks open, do this if(returnValue == JFileChooser.APPROVE_OPTION) { //create a file object based on the chosen file File file = fileChooser.getSelectedFile(); try{ if(alreadyHasImage) { JOptionPane.showMessageDialog(frame,"Opening removes the current image"); //gets rid of the old panel frame.getContentPane().remove(panel); //make a new clean one panel = new JPanel(); //add the panel to the content pane frame.getContentPane().add(panel); } //read it in as an image BufferedImage image = ImageIO.read(file.toURL()); //use the file to create an ImageIcon object //and make it the value of a label that we can //easily display on the panel JLabel imageLabel = new JLabel(new ImageIcon(image)); /*  * Note that by adding a doodler object to the  * canvas, we will be able to draw on any open image  * that is a previously saved doodle.  * But a photo we won't be able to draw on.  */ canvas = new Doodler(300, 300); panel.add(canvas); panel.add(imageLabel); alreadyHasImage = true; frame.validate(); } catch (Exception e){ System.out.println("Exception in doOpen(): " + e); System.exit(1); } //end catch } //end if } //end doOpen /**  * action on SAVE  */ private void doSave() { //file to be saved File outFile; //lets user specify where to save file JFileChooser fileChooser = new JFileChooser(); //specify some things about the file chooser dialog fileChooser.setDialogTitle("Save As...");    //show the dialog and make the frame its parent int action = fileChooser.showSaveDialog(frame); if (action != JFileChooser.APPROVE_OPTION) { //user cancelled, so quit the dialog return; } //point the file the user chose to this object outFile = fileChooser.getSelectedFile(); if (outFile.exists()) { //if file exists already, make sure that the //user wants to replace it action = JOptionPane.showConfirmDialog(frame,   "Replace existing file?"); if (action != JOptionPane.YES_OPTION) return; } try { //get the coordinates of the corners of the //canvas so we have the part we want to save Rectangle rect = canvas.getBounds(); //create an image ready for double-buffering //from the screen area bound //by that rectangle. This method returns null //if the component is not displayable. Image image = canvas.createImage(rect.width, rect.height); //creates the context required for drawing the image Graphics g = image.getGraphics(); //paint onto the canvas the lines created by the user canvas.paint(g); //save the image file using jpeg compression format ImageIO.write((RenderedImage)image, "jpg", outFile);     //catch any problems encountered during the save } catch (IOException e) { //print any exception to a messagebox (like a JS alert //or like MessageBox.Show(...) in C#) JOptionPane.showMessageDialog(frame, "IOException in doSave(): " + e.getMessage()); System.out.println(e.getCause().getMessage()); System.exit(1); } } /**  * Exit the application and stop the VM.  */ private void doExit() { // stop the application System.exit(0); } }//end DrawingApp class /**  * class to define the images only filter  * we must subclass FileFilter  */ class AnImageFilter extends FileFilter { //find the extension of this file type //so we know whether or not to include it public static String getExtension(File f) { String extension = null; String fileName = f.getName(); int i = fileName.lastIndexOf('.'); if (i > 0 && i < fileName.length() - 1) { extension = fileName.substring(i+1).toLowerCase(); } return extension; } /* define the file types we are willing  * to accept. also show directories  * so the user can navigate into them  */ public boolean accept(File f) { if (f.isDirectory()) { return true; } //we're going to save drawings with jpg compression, //so this is for show really String extension = getExtension(f); if (extension != null) { if (extension.equals("tiff")  extension.equals("tif")  extension.equals("gif")  extension.equals("jpeg")  extension.equals("jpg")) { return true; } else { return false; } } return false; } //what the user will see in "Files of Type..." public String getDescription(){ return "*.tif, *.tiff, *.gif, *.jpeg, *.jpg"; } } //end AnImageFilter /**  * Doodler class uses AWT canvas to draw on.  * Note that while you may instinctively want to  * extend the AWT Canvas class here, it is not  * necessary and will mess things up: it will make  * your File menu inaccessible! The reason is that  * Canvas is an AWT (meaning heavy-weight)  * component and JMenuBar is a lightweight Swing  * component. Remember: AWT components will ALWAYS  * cover Swing components.  * JPanel would also have worked here.  */ class Doodler extends JComponent { //these ints hold the coordinates private int lastX; private int lastY; private Vector plots = null; private Vector pointData = null; //declare two ints to hold coordinates. //remember that because these are class-level variables, //they will be initialized to their default values //(0 for int). private int x1, y1; private Graphics graphics = null; //constructor public Doodler (int width, int height) { //call the default constructor of the superclass //(JComponent)<i>not</i> Canvas! super(); this.setSize(width,height); plots = new Vector(); pointData = new Vector(); /*  * Listen for the event that is fired when the  * mouse button is pressed, because we don't want  * to draw whereever the mouse goesonly when it  * is pressed.  */ addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { x1 = e.getX(); y1 = e.getY(); pointData.add(new Point(x1, y1)); } public void mouseReleased(MouseEvent e) { plots.add((Vector)pointData.clone()); } }); /*  * Listen for the movement of the mouse being dragged  * so that we can capture each point across which it  * was dragged and add it to our vector that stores  * all the points; then let the drawLine method of  * the Graphics class connect the dots.  */ addMouseMotionListener(new MouseMotionAdapter() { public void mouseDragged(MouseEvent e) { int x2 = e.getX(); int y2 = e.getY(); pointData.add(new Point(x2, y2)); graphics = getGraphics(); graphics.drawLine(x1, y1, x2, y2); x1 = x2; y1 = y2; } }); } /*  * Paints our doodle line by storing each point over  * which the pressed mouse passes in a vector.  * @see java.awt.Component#paint(java.awt.Graphics)  */ public void paint(Graphics g) { //returns a way to iterate over //all of the elements in this list ListIterator it = plots.listIterator(); while (it.hasNext()) { Vector v = (Vector)it.next(); Point point1 = (Point)v.get(0); //remember you can do more than one thing in a for //loop's first statement for (int i=1, size = v.size(); i < size; i+=2) { Point point2 = (Point)v.get(i); g.drawLine(point1.x, point1.y, point2.x, point2.y); point1 = point2; } // end for } // end while } //end paint override }//end Doodler class 

As you can see by reading over it, there are a few different class files in this one source file. Recall that only one of the classes may be public, and it must be named the same as your source file.

Let's walk through the code and see what kinds of things happen on your screen when you execute the program.

Figure 35.9 shows what the app looks like when you first load it and click on the File menu.

Figure 35.9. The Doodler application when the application is executed and the File menu is clicked. Notice that the frame is decorated ”not just the Windows default.

graphics/35fig09.gif


First, let's read in an image. When we type Ctrl + o on the keyboard, or click the Open menu item, this executes the doOpen() method. We did not have to put this functionality into a separate method, but it does make the JMenu business a little easier to port to a new app, and is helpful for the code reader ”if she's not interested in the doOpen() method right now, it's easy to skip by it and find what she's looking for. As we'll see later with the doNew() method, there are times that you want to create a whole new object, or even a new thread, and let that guy take over for a while.

graphics/fridge_icon.jpg

FRIDGE

The DrawingPad app won't read in a bitmapped image. The ImageIO class gets an ImageReader to decode the URL passed to it. The way ours is written, we don't decode a .bmp, so null will be returned by the static call to ImageIO.read(file.toURL()); . As a consequence, a NullPo i nterException will be thrown if you try do that.


But back to our regularly scheduled program. We were opening a file. To do this, we get a JFileChooser dialog, which is a standard part of the API (see Figure 35.10). This is a really terrific component because it is lightweight and has a lot of functionality. After the user selects the file he's interested in, we use a BufferedImage reference to read in the data using an ImageInputStream and create it as an ImageIcon, which we pass to the JLabel constructor. We then add the JLabel to the panel, and ask the container to redraw itself.

Figure 35.10. The JFileChooser is a great component for both opening and saving files of all types. Notice that the filter is applied, and so we only see folders, and files of one of our pre-defined extension (image) types.

graphics/35fig10.gif


Note that the JFileChooser opens in the default user location; in the case of Windows, this is My Documents. We could also specify a different location in the constructor.

Now, you'll notice that the image may be larger than our 300x300 frame size, in which case we can just drag a corner of the window, and it will automagically resize itself to fit the image (see Figure 35.11). We could have used a JScrollPane to allow the user to view only part of the image, but that seemed like not-very-useful functionality for the added complexity. Also, we've already seen how to use JScrollPane in the RSS newsreader app.

Figure 35.11. The image you open may be larger than the frame, in which case you can drag any one side of the frame to resize the viewing area to snugly fit the image.

graphics/35fig11.gif


It would be nice if we could draw on a photographic image that we open, but we cannot do that. However, if you create a new doodle in this app, save it, and then open it later, you will be able to continue drawing on it (see Figure 35.12).

Figure 35.12. This is a picture of my kitty cat Doodlehead. It's an original artwork.

graphics/35fig12.gif


If you attempt to save a file using the name and path of a file that already exists, the application detects this and warns you that you are about to overwrite the existing file (see Figure 35.13). If you don't want to do that, you can hit Cancel and nothing happens. You can then click Save again and choose another name or path.

Figure 35.13. The drawing pad uses JOptionPane's showConfirmDialog method to create a dialog alert with a Cancel button.

graphics/35fig13.gif


Of course, clicking the Exit button on the menu cleanly exits the application and shuts down the Java Virtual Machine.

Ok. Thanks a million. I hope that you find a load of stuff in the toolkit you can pillage and use in your own apps.



Java Garage
Java Garage
ISBN: 0321246233
EAN: 2147483647
Year: 2006
Pages: 228
Authors: Eben Hewitt

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net