Into Java - Part XVII
Into Java, Part 17
Last time we added the ability to save your paintings to a file. We used the very convenient object stream facility that Java provides. Simply letting a class implement the Serializable interface made the Java object output stream able to store that class in Java's own file format. Afterwards the object input stream instantiates every object back again as if nothing ever happened. We will return to these handy streams in a future column.
Today we will continue with our example application, the PaintBox, and add the ability to save our paintings to a file name we choose ourselves. Of course we will be able to open any saved file as well. Using a file chooser found in the Swing package this will be easy. Nevertheless, a few words on the way never hurt.
As you must have seen quite a few times accessing files through a file chooser, you might run into the problem of overwriting a file you did not intend to overwrite. Or you may have run into another error, a small one perhaps but still an error. In this case, small dialog boxes show up, how do we do that in Java? At first it looks tricky, but you will find yourself comfortably using JOptionPane within a few moments.
Since there will be quite a few things to add to the previous code I sincerely hope that you have read the former [../20010416/intojava.html IntoJava] column, written the code, got it compiled, and got it running. Remember that the last column included links to a couple of Java files, but you had to add the code explained in that column yourself. Further, this time I will divide the new code into smaller chunks and comment these parts. Please, locate the lines we discuss, I will include two lines from the old code (most of the time), one as the topmost line and one as the last line showed.
The JFileChooser from Swing is pretty straightforward. Anyone familiar with the Windows user interface will recognize it immediately, whether good or bad. But as with everything done in Java, you are free to change the look and feel and of course most other details you might like to. But admittedly, this time I think that doing that will be tricky and way out of the scope of this column.
Nevertheless, you do not need to construct anything at all yourself, it comes with the box. But how do we use it? And how do we interact with it?
Let us start with the declaration of JFileChooser . It will be located in the PaintPanel class since it is there we chose to have our file opening and file saving methods. Since we have already imported javax.swing.* to the class we only need declare a JFileChooser . At the same time it seems convenient to declare a variable for the file we are currently working with, so we will use the class File that we have seen before. I chose to name them openedFile and fCho .
The JFileChooser is still not instantiated, and we do not do that in the constructor either. Why? It is a matter of using your own judgment. Sometimes we do not need a file chooser, why bother Java with creating such an instance then? This time we know we certainly will use one, but we can still wait a while, and thus save some application loading time. This is very polite programming. Do not instantiate things at once as you save time and the user finds your application quicker. On the other hand they will be waiting for these objects later on, but we will see that this is perhaps better. You decide.
Since we introduced the variable openedFile , let us make a little detour to some changes we need to do with the method saveFile that we wrote in the last column. It still works, but let us use the currently used file name. Later we will be able to change that name. (For a while now, we will not be able to save anything with PaintBox since there is no file name given yet.)
We do a test to make sure that no file name is yet chosen by the user. Starting from scratch you must use the "Save as..." alternative rather than "Save", right? Naturally, from here we could have chosen to jump to the method that will take care of that, but we still aren't in trouble yet.
But if there is a currently used file and anyone picks the menu item "Save" we will continue as before, with only one difference, we instantiate a FileOutputStream with openedFile as argument. (The menu is located in the PaintBox class, where we have yet not added the "Save as..." menu item.)
Like saveFile we have to make the same change to readFile , but there is no need to check for a sane openedFile variable since we will never come to readFile unless there is one. We will soon see to that.
FileInputStream is now constructed with openedFile as argument, the remainder is still untouched. Expect more changes.
Consider you have started PaintBox, perhaps you want to open that artistic painting you made last time. You want to open a file, in other words. We had the menu item "Open" last time and it still works without changes. You may rename the menu item to "Open..." if you like.
Clicking "Open..." you would like to see the file chooser open before your eyes. Let us do the implementation.
Recall that we did not instantiate the file chooser from the start, but now we have to. Since it is null we are sent to initFChooser , another new method. Otherwise we would continue with the last line, but let us wait on that for a moment.
This method will only be called once, the first time anyone uses the file chooser. And this method will only be called from inside this class, hence it will be private . From now on the file chooser will always be present, though most of the time it will be invisible and only brought to visibility upon requests from the user. It is also used for both open and save actions, differing only in how you call it when you need it.
We simply instantiate a new JFileChooser . That will take a while for the Java Virtual Machine, though it is much quicker when you use Java 1.3. In the future, any call to it will be handled without delay. This time we set a current directory to the directory we are executing the application from, as we construct a File object with a single dot. But of course, you can use any directory of your choice, just create another path with the File object.
The last thing we do is to set a file filter. Since we are not interested in files not recognized by PaintBox, which is too simple to show GIF or JPEG and such stuff, we would like a filter. This is the way. But hey, how is PaintFileFilter, the filter object used?
A file filter seems difficult to make. But it is not, at least not a basic one as ours is. PaintFileFilter is a class, but since we will only use it within the scope of PaintPanel we can simply add it to the PaintPanel.java file, as an add-on after the PaintPanel class.
An abstract class lacks implementation of at least one method, but may have no method at all implemented. It is solely intended to be inherited, as an ancestor to some family of classes.
An abstract class can never be instantiated in itself, it needs to be extended and the abstract methods must be implemented.
The difference between an abstract class and an interface is only that an abstract class can have methods and variables that may be inherited, interfaces do not.
This class extends an abstract class located deeper in the Swing package. The FileFilter class has two methods that we need to implement.
We start with the accept method that returns a boolean. It is simply two clauses that may return true or false . Both clauses make calls to methods of other Java classes.
The first clause asks the File object argument for its name, a String that is changed to lower case and finally tested against our own new file format "pbo" (PaintBox). If the file tried does end in ".pbo" the String class will return true , else false .
The second clause simply asks the File object if it is a directory, every directory should of course show up in any decent file chooser.
If any one of these two clauses gives a true, a true is returned.
The second method is used to set the description at the "Files of type" at the bottom of the file chooser. See the [java17a.gif file chooser image].
This is a most basic file filter class, but it serves its purpose, we have only one file type, and it instructs us how to do it. There are several pages on the Internet on how to make more complex filters.
Now, how is our filter used? Our file chooser, for each file and directory, simply asks this filter if the object at hand is of the correct type, otherwise it will not show up on the pane. And the file chooser sets the file type.
When this is done we have a file chooser set up that is ready to use.
Back again 2
With the file chooser instantiated we can ask for it to open itself. Let us do that, the first line of the image below asks our file chooser object to show an open file dialog. The argument to that method may be any kind of Component of the java.awt package. It is used to locate the file chooser near the place you called for it. We use this as parameter, that is a reference to the PaintPanel object we are using. Maybe there are other uses for it as well, I have not dug that deep into this topic.
The file chooser opens and we can highlight a file and click "Open". The file chooser turns invisible and we are ready to see the outcome, how do we do that?
We notice that we get an int back as a result. Now let us look at the Java API, at JFileChooser , and we will find quite a few constant variables of the int type. We are interested in the APPROVE_OPTION . If you clicked OK, or double clicked a file, you approved the file selection and this constant is returned. We are not interested in any other possibility in our application.
Next we test if the return value is okay and if so, then we continue. If anything else is returned we have no backup, we simply leave this method and have to start over. Our file chooser was turned invisible anyway. Please note that the if -block induces one extra closing brace at the end of this method, the readFile method.
Inside the if-block we get the selected file from the now invisible file chooser. Now we see how convenient it is using the openedFile variable of the File type; it will be preserved until the next time you open a file or save the file under a new name. It contains both the file name and in fact, the entire path if you searched for the new file in another directory, partition or even file system, and it is quietly used by saveFile anytime you click "Save". Still File only holds a handle to the file and we have to make a file reader around it, as before.
Now we are done with the readFile method, as well as saveFile. So you may certainly compile here and at least open any file of the "pbo" type you find. I think you can rename the old paint.dat and open it to try all this.
This method, also located in PaintPanel will be today's most-to-do implementation. And there will be some diversions from the plain file chooser topic on the tour. But let us start. As with readFile , the file chooser may not be instantiated. If you started to paint from scratch there is no file saved yet, thus a call is made to the initFChooser method.
After we are certain we really have a file chooser we declare a temporary File variable, why? Let us wait on the answer for a moment, but ponder over what will happen if you somewhat later cancel your actions. A temporary holder never hurts. The next variable, the boolean doSave , comes from similar reasoning. Initially no file is allowed to be saved, right? It will in a moment be much clearer how to use both these variables.
An observation I have made is that some programmers try really hard to nest their flow statements, the if-else blocks, to avoid extra variables. At the other extreme we find programmers that spread variables as seed, but they do not harvest anything good. Moderate use of variables, especially boolean variables, only makes the application easier to implement, to read, and to improve upon later on. Too heavily nested flow statements are hard to follow and amend when needed. Also such programming is more often prone to errors.
As with the open file dialog we call a similar method, showSaveDialog, that in most aspects works the same way and looks the same way. Also this method returns a result held in an int, a static constant of the JFileChooser class.
Continuing with that result, hopefully an APPROVE_OPTION constant, we ask the file chooser for the selected file. That may be a new file, if you entered a new file name, or a file you selected from the pane. Now check yourself; consider if you by mistake wrote a file name of an existing file you did not notice, wouldn't it be nice having a dialog pane warn you? Most people think so.
Using a file object of the File class, that is a simple task, merely ask the object if such a file exists, it will say true or false. If such a file already exists then we dive into a new if -block.
Here we will make use of a convenience class in the Swing package, JOptionPane is capable of dozens of different appearances. We are using the "confirm dialog", a dialog that most of the time has two or three buttons. I will set aside the next IntoJava to make a small app that can show the different styles of JOptionPane and also some other GUI tools. However, there are a zoo of options to use and you really have to make a journey with the javax.swing package and try to use the different things.
I understand that the next few lines are confusing and will explain them here
- this is a reference to the parent, int, in this case a reference to the PaintPanel object. This reference can be any object of the Component type.
- A String that contains the message. Notice that it is possible to use the new-line character \n . Please observe that the Java API asks for an Object , that means a String , an icon, a Component or an array of such objects.
- The title, always a String .
- The type of message, we chose a WARNING_MESSAGE , next month we will see four more.
- The type of option, we chose a plain OK_CANCEL_OPTION that gives two buttons.
- An icon of the Icon type, but we sent null as parameter.
The other way to use dialog panes in Java is to use the JDialog class and make the entire layout, the complete implementation and so forth, yourself. You do not like that idea, do you? As easy-going as I am, the handy JOptionPane really looks nice. Simply send the proper parameters and there you are.
When the user clicks one of the buttons, the int answer will encapsulate the result in a JOptionPane static constant. We are only interested in the OK_OPTION since any other answer says we do not want to overwrite the file. If we get an approval, we set the doSave variable to true.
Finally the outer if -block is finished, there was no file to overwrite and it seems to be okay to save the file. So far there are two ways to get an approval; there is no file to overwrite, but if there was one the user may approve the overwrite himself.
But there is still an obstacle to avoid, we have introduced our own file type and a filter for it. What will happen if we now try to save with another file type? Let us take care of that.
It is only when we have an approved doSave from the former part of the code that this test is necessary. If doSave is false , it does not matter what ending the file has, does it? So that is the first clause.
To dive into this if-block the next clause also has to indicate a possible erroneous file ending. Hence we ask the file for its name and again ask the String object that was returned from the file object if it ends with ".pbo". If so, everything is fine and we do not want to visit this block. Thus we switch a true to false with the exclamation mark. The result will be that the false that is returned from all other file endings is switched into a true , and both clauses are true. Voila!
Now we know we have another file ending, but that is still not an error, only a possible error. So, we must ask the user what his intention is. Another dialog pane is used, in every essential part it looks the same, only the message and title differ. Recall that doSave at this point is true , but any answer different from OK_OPTION will set doSave to false .
So far the outcome may be a doSave set to true or false, meaning to save the file, or not to save.
If the result so far is to save the file, then we simply will replace the old openedFile with the new file name. And now we clearly see why it is good to have such a temporary variable, if we any time change our mind and we do not want to erase the old openedFile. Now, why not let saveFile take care of the rest, avoiding duplicating that code?
On the other hand, what happens if doSave is set to false ? Remember that we entered the outermost if -block because we approved a file in the file chooser. Probably that indicates that we would like to save a file anyway, but perhaps we made a mistake or did not notice a file we do not want to overwrite. The only natural thing to do is to give the user another chance with the file chooser, a call back to this very method will do. Note, the outermost if -block does not have an else-statement, a cancel in the file chooser cancels the entire method.
Such a thing as a call-back to the method we are in is somewhat dangerous. If you remember how recursion works, you know that the thread first saves away the context of this method. Then the thread takes another trip into the method, but is not aware of the former tour. And when finished the thread jumps back to the very spot it made the recursive call from, that is the saveAsFile statement in the code above. What will happen if there are more statements to execute in this first tour in the saveAsFile method?
The best way to avoid such dangers is to make sure there is nothing more to do after such a recursive call. That is, put these kind of recursive call as the last statement, then nothing more is done after the return from the call.
Now we are done with this new method. It contains the necessary calls to the file chooser and the dialogs helping the user around.
Finally, A New Menu for PaintBox
How can we get to the "Save as..." method? We have to add a new menu item to the "File" menu.
Immediately after the "Save" menu item we simply add this "Save as..." menu item. As with the other menu items, we include an action listener that calls a method, in this case the saveAsFile method. I have also changed the other item titles to "Save" and "Open..." though that isn't shown here.
Now we are finished with the entire PaintBox example application. Though it maybe is not the most fancy paint application, it serves as an example of how to build applications in Java. Naturally there are other ways to make exactly the same outcome, but this way was useful with its evolutionary development within the IntoJava installments, spanning over a long period.
In Sweden we are facing the spring and hope to experience a lovely summer, so I give you this naive painting as the closing gift from the PaintBox era.
Over time this cute little application grew into something that is more capable than we first expected. Today we have learned how to use the JFileChooser , a handy tool that comes with the box. There are a few things I did not mention about it; it uses the language you have set your system for, it is capable of opening files in whatever file system your operating system supports, the file filters can be really complex and filter for almost anything, and you can of course create new folders from it.
We have also looked into the JOptionPane and used only one of its appearances. Multiplying every option we have we get 240 combinations, but not all of them are meaningful to us. Still, there are several combinations to pick from and we only need to give the class a different set of options to get them. I love this one, especially considering the amount of work needed to do the dialog myself from scratch.
Finally we briefly went through how to take care of different situations that may arise when we try to make an application more powerful. An estimate would say that every powerful feature you add will cause you twice as many situations to consider; what happens if this is so and that is that?, or if that is so?, and so on. Try to make it simple, your application will be less error prone and will work snappier.
Next month we will look into some more of the GUI stuff hidden in Swing. But do not think I will have the space to look into all of it, quite the contrary. The best way to get the hang of it is to play around and try and cry.
A little hint if you would like to extend PaintBox further, look into the Polygon class found in the java.awt package. With that class you may make a lot better paintings, clicking around and drawing free hand. It will be tricky, getting the mouse clicks, adding these to the polygon and getting it painted with the help of the drawPolygon and fillPolygon in java.awt.Graphics . How do you know when you are clicking around and when you are finished? What happens if you cross the lines, is that shape correctly filled later? Tricky, but in the end it will be worth the struggle.
Finally, do you have any ideas for a little app we can play with--I am soon going to discuss threads and networking with Java--please, let me know.
Have a nice month until we see each other again.