![]() | Monkeys at Keyboards: The Javanomicon © Michael James Heron | ||||
| Topic: Java Programming Level: 2 Version: delta | |||||
24 - Case Study 7 - Multiuser Photo Album | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to: |
We've now learned about graphics and sound, as well as a new and powerful data structure called a HashMap. In this case study we're going to look at how we can combine these into an application that allows us to provide a multi-user photo album application, like the kind of thing you would see on a personal homepage on the internet. The application will allow for new users to be registered, and for any given user to add any selected picture to their selection of available photographs. We won't bother implementing any security since that's not really the point of this case study. We will simply assume that anyone who wants to add photos to a particular user is permitted to do so. We will develop this application using the process of incremental development, aiming for a subset of the functionality at each stage and then expanding that functionality to meet any new requirements. In the process we will see that although it sounds like a very complex application to write, it doesn't require an awful lot of code to support it. So, on with the show!
First things first, we need to decide on our interface. This is an issue that will have a direct effect on how we code this particular application. Many photo albums allow for a number of different photographs to be displayed on the screen at a single time. For simplicity's sake, we're just going to display one at a time and provide buttons for moving onto the next picture, the previous picture, and the first and last. We're also going to need a way of selecting which user's pictures we wish to view, and a way of adding photos to any given user. For completeness, we should also add a way for users to remove photographs from their album. We'll go with a fairly simple interface, providing a JComboBox for choosing the user, a large central area for displaying the current picture (which we'll do via a JPanel), and a button to the side of the combo box for adding a new picture (this will be done through the medium of a JFileChooser dialog). We also need four navigation buttons, and these will appear at the bottom of the screen:
As usual, setting up this interface is our first development aim, and by now we should be pretty comfortable with the process:
Compiling and running this gives us most of our application interface. We still need to implement the display JPanel, but we'll worry about that when actually have something to actually display:
Phase one of our master-plan is now complete - proceed to phase two, world domination. Actually, scrap that - that's a different master-plan. Best pretend you didn't read that. Step two is developing the photo album itself.
Let's ignore the whole multi-user part for a bit... in order for this to work at all it needs to have the logic for dealing with navigating around a list of photos. Before we get it working for multiple users, let's just get it working for one. We have those buttons, but they don't do anything yet... we don't even have a picture to display. The first thing we need to do is set up a suitable infrastructure for achieving the desired outcome. We looked at how to display a single graphic in the chapter on Sound and Vision - here we're really doing the same thing (displaying one image at a time), but we're going to have a number of images we can pick from. Hrm... a number of images. Say, that sounds like an excellent candidate for some sort of collection class. We've learned about HashMaps, and so it is easy to jump into the trap of thinking that our salvation is at hand with those - that may very well be the case, but not yet and not for this. Remember that the HashMap implements a relationship between two objects (like say... a user name to a list of photos), but if we just want a list of things we turn to our trusty friend the ArrayList. Now we have a design choice we need an answer to - what are we going to store in this ArrayList? We can either store images ready to be displayed, or filenames to images. Either of these are valid choices, but we're going to go with filenames for no good reason other than it appeals to the purist in me. We need to declare a class-wide ArrayList variable:
And set it up in the constructor method:
Our ArrayList is going to hold a list of filenames to images. As I mentioned, we're not going to worry about the multi-user part yet - just the part that relates to displaying images. We'll just place two image files in our working directory and reference to them directly for now. Our images are called coffee_berries.jpg and toad_eye.jpg (these photos courtesy of http://web.centre.edu/enviro/photolib.htm)
Our first step is to add these two filenames to our ArrayList... we'll do this when we create the ArrayList in the constructor:
Then, we need some code for dealing with displaying the images. We need another class wide variable - in this case, it's going to be an integer called counter that tells us where we are in a particular list of photos:
And we'll set it to 0 to start with:
Before we can display any image, we need to actually load the image from the file - this works in the same way we discussed in the Sound and Vision chapter, except that instead of the filename being hard-coded, it will come from an ArrayList. We'll create a second ArrayList that contains the neccessary Image objects... we can then step over each of these and prepare them for loading as discussed in chapter 13.6. Our second ArrayList will be called loadedImages:
Remember we need to get a Toolkit object to use graphics in an application, and we use the getImage method of the Toolkit to get our Image object. We just have to work out where we're going to do this. We'll provide a method to do this - what it'll do is step over each of the filenames, create the image and then prepare it for drawing before adding it to our loadedImages ArrayList. We'll call this method prepareImages:
Now we need to provide a method that will get the appropriate image from the ArrayList when we change the state of the counter:
Now all we need to do is call this method whenever we want to display a new image. Now that we have a system setup for serving up delicious images, we need somewhere to put them... enter our JPanel class:
We can now use this as the center component in our application. When we want to change the image it's displaying, we simply call setImage:
At the moment, all of our functionality is tied up into the application. That's no good from an MVC perspective, and so we should fix that before we go any further. Our View/Controller should just provide the interface... we should have a separate class that actually handles the photo album functionality. Let's call it PhotoAlbum. Into there we move all of the functionality that exists to handle the storage and manipulation of the images. Here we hit upon a snag - one of the things our model should do is prepare the images for displaying - but we have no access to the prepareImage method in a standard class. Luckily the method also exists in the Toolkit class... so we'll make use of that one. The parameters are a little different (it requires a height and width), but functionality it is the same:
Next step is to provide some methods for manipulating the counter variable... these are trivial to implement. We adjust the counter, and then return the appropriate image:
The next step is to link it all up. When we press the buttons, we want to be able to step through the images. We just call the appropriate methods from our model to implement this:
Excellent... when we compile and execute our program, we find ourselves with a nifty little photo album. It sure is pretty! There is one slight problem to fix - to begin with, no image is displayed. We only get one when we hit one of the buttons. We can fix this by using the getLoadedImage method when we create the applet. Our full applet code now looks like this:
Now we need to expand our application to support multiple users. 'Oh no, surely this is a task worth of Hercules alone!', I hear you scream. Really, because we have been developing incrementally, it's not very difficult. We're going to make use of our Excellent New Friend the HashMap for this particular part of the code. All of our display code is based on a pair of ArrayList, called listOfPhotos and loadedImages. The second ArrayList however is generated from the first, so if we have the listOfPhotos we can rebuild loadedImages ourselves. For a multi-user application, we're going to have many users, each with their own list of photos. So we need a way of linking together names and photos, which is a one to one mapping. Actually... that's just what a HashMap is for - a very agreeable piece of synergy by anyone's reckoning. Within our model, we'll declare a class wide HashMap called userPhotos - here though we need to delve into a very ugly piece of Generic syntax. The key is going to be a String - nothing new there. The value however is going to be an ArrayList of Strings - we defined the HashMap as such:
And then initialise it in the constructor method:
We now need a method that lets us register a new user... the username will be the key of the HashMap and to start with they will have a value of an empty ArrayList:
And then we need a way of getting the list of photos that belongs to a particular user - another method will serve us well here:
And then a method to add a photo to a particular user: Note that we don't need to put the modified ArrayList back into the HashMap... that is because an ArrayList is a reference datatype. Also note that we don't need to maintain a variable name for the ArrayLists that go onto the HashMap - we just declare them locally and then forget about them as soon as they're put onto the userPhotos HashMap. The mechanics of what is going on here are quite complex, but it's all to do with whether variables have references to them. Consider if we have the following representation of the computer's memory after our constructor method has finished executing:
Of course, this is an absurdly simplified example of what may be stored in memory, but it'll do for explaining why the above code works. When we call the newUser method (let's say with the parameter "Michael"), it creates space in memory for an ArrayList:
Then it puts an empty ArrayList into that location in memory:
And then it puts a new key value pair into the HashMap. The key is a String, the ArrayList is the same one that's at position 1 in memory in the chart above. When it adds this key value pair, it doesn't make a copy of that ArrayList - it just makes the HashMap point directly to that memory location:
And then when the method gets called again (let's say with Steve as a username), it creates the ArrayList in a suitable space of memory, and then puts a reference to that space of memory into the HashMap:
So, when we later change the ArrayList associated with one of the names (let's say Michael), it changes the location of memory rather than the HashMap itself:
The above example is misleading in many respects. For one thing, memory isn't partitioned quite so neatly. For another, Strings are also reference data types and so they wouldn't be stored in quite the way that is suggested i the ArrayLists or the HashMap. However, it hopefully explains a bit better why HashMaps actually work in this context. Variables remain in scope as long as some part of Java's code has access to them - when our method newUsers has finished terminating, we can no longer reference to the ArrayList by the name newList, but it still exists. As long as we have some way of gaining access to it, it is not actually lost. We now have to gain access to it via the HashMap. Anyway, now we have a framework that allows us to get the photos for a particular user, and add photos to their list, as well as register new photos. We have everything we need to start pulling it all together. At the moment, we're still using our bunk data (coffee_berries.jpg and toad_eye.jpg) as display images... the first step is to remove this bunk data. We're still going to use our listOfPhotos variable - it's what all our viewing code is based on. But we're going to change which ArrayList that listOfPhotos points to depending on which user has been selected. We need to add an ItemListener to our applet JComboBox to catch when a new user has been selected, which means our application also has to implement the ItemListener interface:
Once we've modified the class definition, we can add the item listener:
In order for our application to implement ItemListener, it needs to have a method called itemStateChanged. This method is going to pull the selected user from the combo box and pass it into the model. We should provide a method in the model that handles the transition for changing a user, which we'll do. When we change the user, we'll keep a note of who's photos we're currently displaying. We also clear the loadedImages ArrayList - the contents of this are dynamically generated from the list of filenames, and our filenames are about to change. We'll add a class-wide string called currentUser so we can keep track of who's images we're currently displaying:
So, our itemStateChanged method will simply call this method in the model, and then display whatever comes out of getLoadedImage:
We also need to add some users, which we do by pressing the newUser button. We need to add the event handling code for this into actionPerformed:
And then we implement the code for adding a photo, which is also based on a button being pressed. We're going to use the JFileChooser dialog to allow the user to browse to whatever image they like:
And with that, we have sewn our disparate threads into one beautiful garment that's all linked together - excellent work on our part.
Except, alas, it doesn't work. Oh no! There are a few problems with the system in place... the first is if we add a photo to the currently selected user, nothing happens until we reselect them. We need to change our addPhotoToUser method a little:
There's one more problem that we should fix. If we try and add a photo before we add a user, this is treated as a perfectly acceptable action by the application and it will proceed quite happily before it throws a NullPointerException when it tries to add the filename to a user that doesn't exist. We need to make sure this doesn't happen. There are many ways we can do this. We'll choose a nice easy way and keep an integer variable that counts how many users we have and adjust our model accordingly:
And then adjust our actionPerformed a little too:
And that's us finished, for real this time.
Implementing this application using a different data structure would have yielded a very different program. It would be possible to represent the data as an ArrayList of objects of type User:
However, in doing so it becomes a more difficult task to implement the functionality. Look at how easily we can add a user to our structure above. Consider what we'd need to implement to gain the equivalent functionality using an ArrayList. The code is not much more difficult, but it is more complex and requires careful planning of utility methods. We have no containsKey method for an ArrayList, and so we must code it ourselves:
And then we need to write the methods for adding a new user or getting the photos for a particular user:
The code becomes noticeably more complex, and gets more and more complex the more functionality we wish to include. Since we never have to step over every element of our HashMap (it is merely a way to model a one to one relationship), it is the ideal data structure for this problem. Further ReadingThe following table details further reading on the topic in this chapter, and also any external resources that you may find useful.
|
| Previous | Table of Contents | Next |
© 2004-2006 Michael James Heron