![]() | Monkeys at Keyboards: The Javanomicon © Michael James Heron | ||||
| Topic: Java Programming Level: 2 Version: delta | |||||
27 - Case Study 8 - A Drawing Package | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to: |
In this chapter, we're going to make use of the techniques we discussed in chapters 25 and 26 to develop a drawing package of sorts. Obviously, we don't have all of the tools we need to develop something sophisticated - all we're going to do is let a user choose a shape to draw, and then draw it wherever they click with the mouse. We're going to make use of mouse listeners to achieve part of this functionality, but we will also have cause to revisit the ideas of polymorphism and abstract classes when we develop our library of shapes.
There's no compelling need for us to implement a splash screen for this application, but we haven't looked at how to do it yet and it's a fairly useful piece of functionality that can applied to any program to make it look More Excellent. So before we begin with the proper functionality, we'll talk about how to setup a splash screen. A splash screen is a decorative window that appears as an application is loading. It's used to reduce the perceived amount of time spent loading, and to provide the user with reassurance that something is actually happening. Proper splash screens will incorporate updates on what's actually happening within the application... ours is going to tell filthy, mendacious lies. However, if we did want to be more helpful (such as telling the user when images have finished loading), there are frameworks available to allow us to do that (for example, ImageObserver). A splash screen is the first thing that gets displayed when an application is executed... so it should be the one in which the main method is defined. Usually we don't want the standard window bar along the top of a splash screen... we decide when it's opened and closed, thanks very much. We can get a completely blank canvas by switching off decorations on the JFrame - this will get rid of all the stuff that we don't need. We do this by calling setUndecorated(true) in the constructor method:
There are two new segments of code here worthy of note. The first is the getScreenSize method in myTools (the first line of the main method). This gets us the size of the screen in pixels. We use this to make sure that our image gets drawn in the right place regardless of monitor size of resolution. The second is the setLocation method - a splash screen is usually drawn in the center of the screen. Without a setLocation call, Java draws the splash screen at the top left corner of the screen. This looks messy. We can compile and execute this to get our simple splash screen:
However, this screen will stay there and do nothing until we forcibly unload it. The user has no way of doing this themselves. Splash screens usually have three ways of unloading - one is a keypress, one is a mouse click, and the third is a timed unload. Splash screens that are actually used to track the load time of an application will usually disappear when everything has been loaded into memory. Our application is not File IO intensive, and so it loads pretty quickly. We'll go with the first two unload mechanisms for this... we don't know how to do the third one yet (but we will after the chapter on multi-threading). First, on a mouse-click:
And then on a key press:
At the moment all we're doing is disposing of the splash screen - instead, we should set up an instance of the main window and then display that in the standard way:
And now when we click on the splash screen or hit a key on the keyboard, our splash-screen disappears and our main application appears. Most excellent!
There are a number of components we're going to need to provide in order to allow our user to configure their choices appropriately. We need:
The rest of the interaction is going to be determined by mouse events. We'll use a JComboBox to provide the choice of shapes, and two JScrollBars to set the length and height of the shape. We're also going to make use of a new component called JColorChooser that provides a neat way for users to select the exact shade of colour they want for the shape. This will get invoked at the press of a button, so we will put that to one side for a moment. We have to put all this together as our interface design:
So, as usual, step one is to implement our interface:
We also need a separate JPanel class to handle the actual drawing of the shapes:
Just the thing we need to create our artistic masterpiece! Our view is now largely complete... our next task is to develop the model that deals with the actual functionality.
We'll start building the functionality by creating a simple shape - in this case, a circle. We've already seen how to do this with our bouncing ball animation - we make use of the fillOval method. Now, we really should separate out our application into suitable classes, and it makes sense that our circle would be a class of its own. If we also create a rectangle, we will need that to be a separate class as well. In fact, anything we're going to let the user draw will be a class. It seems that there is some common functionality that is going to be shared between all the shapes. We're going to need a way to set where the shape is to be drawn. We're going to have to set the length and height of the shape. All of this functionality is going to be shared by all shapes, so it makes sense if it goes into a class of its own, which we'll simply call Shape:
We'll make use of this Shape class and extend it to create individual shapes. It is only when we know what shape we're going to draw that we have the information to write a method to display a particular shape, so the implementation for this will have to go into any child class:
Now to display our circle all we have to do is call drawShape and pass a Graphics object. But we've currently got no way of linking together our interface and our shape classes - we need a class that is going to act as the model and tie the two together. We're going to allow the user to draw any number of shapes they want on the application, so we need a suitable data structure - in this case, an ArrayList would seem ideal. Every time the user draws a shape, we add it to the ArrayList. Then, when it comes time to display them, we step over every element in the ArrayList and call the drawShape method on each. Simple! Hrm... or is it? Actually, it's a little bit more complex than that, because we need to write a method that will take any shape we throw at it and add it to our ArrayList. At the moment, we only have a circle. But let's say we also had a Rectangle:
We could use method overloading to provide a suitable addShape method for each shape we create:
This is a sub-optimal solution. The more shapes we create, the more methods we need to provide. Instead, it would be great if we could just say 'What is going to go into this method is a Shape... it may be a Circle, it may be a Rectangle, but in a general sense they are both Shapes. So don't worry about what kind of shape exactly, just treat them as Shapes'. Any by golly, we can do exactly that through the power of polymorphism! We can write a single addShape method that will take a parameter of type Shape:
Arr, wonderful! That's much neater, and it will work for any class we extend from Shape, giving us a truly general solution. Makes you feel all tingly, doesn't it? The next step is to create a method that will step over every element in our ArrayList and call the drawShape method on each. We use polymorphism to accomplish this also:
Alas, here we hit a snag - it won't compile! Despite the fact that both of our shapes up until this point have a drawShape method, there's no requirement for any other class that we extend from Shape to follow that standard. Java has no way of ensuring that every class extended from Shape will implement a drawShape method, and so it throws an error. It's at this point we need to think about abstract classes and methods. There's never going to be an occasion when we create an instance of Shape - it's just a class that gives form to its children, so it really should be made an abstract class. Doing this means we can make use of abstract methods, meaning we can provide an abstract drawShape method in our Shape class which will enforce that standard on every class that extends Shape. In this way, Java will be sure that drawShape will exist in all classes that inherit Shape, and the above code will no longer be a problem. All we need to do is change our base Shape class a little:
We make these changes, and Java will allow us to compile our model object quite happily.
We're slowly developing our application, but we haven't provided a way for the user to actually draw anything on the screen. Before we go any further, we should make sure our framework as it stands is a suitable way of approaching this application by implementing the mouse event code. We're going to add the mouse listener onto the Container object, to register the mouse events all over the unused part of the application:
We need to populate our JComboBox with a list of all the suitable shapes we can draw. We'll add in a utility method to our model class to list these:
We need to create an instance of our Model class in our application - it is the Model that is going to be doing all of the real work. We create it as a class wide variable, initialise it in the constructor, and then use our new utility method to setup the combo box:
Now we can use our combo box to choose which shape we want to draw. But as yet, there's nothing that lets us click to say where. When we click the mouse, we want to send a message to our model to create a new instance of the chosen shape at the set location. Unfortunately, all we have is an addShape method that requires an instance of a Shape object to be passed in. We don't have a method to actually create that Shape object. We need to add one into our model!
Now, in the mouseClicked method of our interface, we then make a call to createShape, passing in the required information. Note that we're not doing anything with the colour yet - that will come soon.
Now we hit a little snag - we have a panel that is going to handle all of the drawing (we called it DrawingPanel), and we have a handler that actually draws what needs to be drawn. How do we link the two up? Our panel has no way of refering to the handler object. This is an example of multi-window communication as was discussed back in chapter 'Free Standing Applications'... we can solve the problem by making sure that we pass the handler object as a parameter to the panel's constructor:
And then in the paintComponent method, we make a call to our drawAllShapes method:
The only thing we need to ensure is that when we create the instance of drawingPanel, it gets the handler object passed to it:
Now, compile and execute, and set the scrollbars to the desired size. Click on the screen, and voila! There appear our shapes. Neato!
Now we need to add colour functionality. At the moment, the shapes just draw in black... we need them to draw in whatever colour is requested by the user. To begin with, we need to add two methods to our Shape class - one that sets a colour, and one that gets the colour:
We also need to change our drawShape methods a little to add in a call to set the drawing colour:
Now we're all set - we just need a way of getting the user's colour choice. There is a perfect class that exists as part of the Swing framework - it is ideal for this particular situation, and it is called JColorChooser. This class brings up a dialog where the user can select from a number of predefined colours, or choose their own according to whatever mixture of red, green and blue they desire:
We're going to make use of this to let the user choose their drawing colour. The JColorChooser class has a method called showDialog that displays the colour chooser screen. This method returns an instance of the Color class representing the colour the user picked. We're going to need a class wide variable of type Color (while we will call currentColor), and we'll set the state of that variable in the actionPerformed method that goes along with our set colour button. The showDialog method takes three parameters. The first will be the value this, the second is the text that is to appear on the colour chooser's title bar, and the third is the colour to start off with:
Now we need to modify our createShape method in our Model class to take an extra parameter - that of the drawing colour. We then use that extra parameter to set the colour of the shape we create:
Next, we need to change our mouseClicked method so that the call to createShape also passes in the user's chosen drawing colour:
Now, when we compile and run the application, we have our own little drawing utility:
This is made possible largely through the power of abstract classes and polymorphism - this would be a much more unwieldy application to write if we didn't have these tools available. We'd need to write separate methods for each shape, and when we implemented the drawAllShapes method we'd need to step over each element, use the instanceof operator to find what kind of class it was, place it in an object of the right type, and then call the drawing method. The more shapes we add, the more unwieldy the code gets. With our application though, we only need to change two methods to add a new shape in. Let's look at how we can do that by creating a simple face shape:
We need to modify our listValidShapes method in our model to make it available to the combo box:
Then we need to add a statement to our createShape method to handle shapes of that type:
And with that, we add an entirely new shape to our library of images. How cool is that? I'll tell you how cool - very cool!
Abstract classes allow us to divorce ourselves from the details of how a class implements a particular method - we are only concerned with input and output, not with the minutae of how the method is implemented. This means that we can have our method do anything we want provided it has the correct method structure. There's nothing to stop us, for example, writing a class that doesn't just draw a simple shape on the screen... we could have it draw an actual image:
And then by adjusting the two methods we must in the model:
Our application devolves responsibility for the implementation onto the child classes, and our polymorphic model looks for the most specialised implementation and executes the functionality it finds. Within these two frameworks we can have the method do whatever we wanted. If we want it to play a sound every time the image is drawn, we can do that too. Really, the sky (and Java) is the limit!
We've got one more thing we need to add to make this into a drawing application suitable for kings and emperors, and that is a method that clears the screen of our images. Since the drawing is based on iterating over an ArrayList of images, we simply clear the contents of that ArrayList:
Provided we then call repaint after calling this method, we'll have a nice clear screen ready for us to blot over with our artwork once again. And that's it - we're done.
This kind of application starts to demonstrate the benefit of a fully feature notation language such as UML. There are quite a lot of classes, and they are all interrelated. There is an abstract class that has any number of children, andn a model class used by both the main frame and the drawing panel. Seeing the relationships at a glance with just the code is difficult. Our vocabulary of UML symbols is complete as far as this book goes - there are more (so many more) than we can cover, but they are the domain of specialised systems analysts and designers. Even though we have only covered a subset of the notation available for class diagrams, we can still clearly articulate the class relationship in the program we have just devised. We've been doing things backwards as far as our class diagrams are concerned - really they should be developed before the application is written. Much as with a storyboard, they give a way of planning out a project before you have written it. We've been talking about why we make certain decisions, and so our development process has been incremental and discursive. Try developing your class diagram before you begin coding and see what difference it makes. There are many excellent tools available to help you with this - one quality tool (called BlueJ) is referenced at the end of this chapter.
Abstraction and polymorphism seem to be very dry and theoretical subjects, of interest only to those who want to spend their time developing consistent and effective class hierarchies. However, as we can see above, they are very powerful tools that allow for a great degree of flexibility in how we approach particular programming tasks. It is still the case that developing a proper object framework is very difficult, and experience is the only way to see how best to use these tools. Hopefully this case study will have demonstrated some of the benefits of the techniques when dealing with complex problems. It should also highlight the importance of the data structure to an application. Much as with the photo album case study, this is an application that is highly dependant on its data structure. It simply would not be possible to write this application quite so easily using a HashMap, for example. The right tool for the right problem should always be your guiding philosophy for software development. The framework we have discussed above is very expandable - we saw how easy it was to add in a new shape... we needed to change only two methods and the rest of the program worked perfectly. It is the power of polymorphism that drives truly generalisable solutions to problems, and it is most certainly something you should spend some time thinking about in terms of how to best apply it to your own needs. 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