In this paper we will look at drawing graphics in a graphical user interface. We did a bit of this in the previous two papers, actually since we called the drawString method of the Graphics object to show the mouse clicks. We noticed, I hope, that when you draw as we did then into the GUI the results are not preserved if the window needs to be redrawn, such as when it is covered up and then uncovered. Here we will see how to do better.
The original version of this paper had a different outline.
In the earlier parts of this series of papers, when we have drawn something into our applet or application, it has disappeared when the containing frame is repainted for any reason. Now we would like to see how to present information and have it remain when the window is resized, covered and then uncovered, or repainted for another reason. If your information is just text, you can do this easily by just using JLabels, JTexFields, and JTextAreas. These components take care of repainting themselves and their contents as necessary. The problem comes if you want to do something graphical.
Java.awt.component has two methods that help us here. These are paint and repaint. Normally repaint is never overridden in new classes, but it is fairly frequently called. It is also called by the infrastructure when a component needs to be updated for any reason. On the other hand, paint is almost never called from code you write, but it is often overridden. The prototypes for these methods are
public void repaint();
public void paint(java.awt.Graphics g);
We are going to look at a simple applet that might be the early stages of the design of an object modeling applet or application. The general idea is that we want to draw labeled figures and connect them with lines. The figures represent classes in an application and the lines represent relationships between the classes, such as the inheritance relationship. Below, we show a very simple example of the kind of thing we want to do. The code that produces this is not very sophisticated and we shall want to make a few modifications in the sequel.
The key to this is the paint method of the JPanel object that we have placed into the applet. A JPanel is both a container of other components and a drawing component. The painting process starts at the applet which sees to calling the repaint method of each of its parts as necessary. This is the real reason why we "add" components to the applet GUI rather than just creating them. The contentPanel Container remembers all of the components added to it and when the applet frame needs to be repainted all of the components added to it are painte as well. The applet is painted by painting each of its components. Therefore, we have overridden the paint method of a JPanel subclass class so that what we draw onto the pane won't disappear the next time the pane is repainted. To see the real effect of this you need to run the applet and then cover its window with another window and then see that when it is uncovered again the window is refreshed. This will also happen if you resize the window. Note that we don't call the paint method either directly or indirectly (by calling repaint). The system takes care of that for us.
package jbgui;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.Color;
import javax.swing.JApplet;
import javax.swing.JPanel;
public class PaintApplet extends JApplet
{
public void init()
{ JPanel drawArea = new TestCanvas();
getContentPane().add (drawArea);
drawArea.setBackground(Color.white);
}
public Dimension getPreferredSize()
{ return new Dimension(400, 300);
}
class TestCanvas extends JPanel
{ public Dimension getPreferredSize()
{ return new Dimension(400, 300);
}
public void paint( Graphics g )
{ g.drawLine(55, 75, 225, 75);
g.drawRect(30, 50, 50, 50 );
g.drawRect(200, 50, 50, 50 );
g.setColor(Color.white);
g.fillRect(31, 51, 49, 49);
g.fillRect(201, 51, 49, 49);
g.setColor(Color.red);
g.drawString("A", 55, 75);
g.drawString("B", 225, 75);
}
}
}
There are a few important points to make about this simple applet. First, the getPreferredSize method of the TestCanvas class is essential. Without it, the canvas will be drawn so small (zero by zero) as to be invisible. All we would see this the gray background of the applet itself. This is often forgotten.
The other point is the need for the two calls to fillRect within the paint method. Without them only the outlines of the rectangles are drawn which means that the line, which was drawn from the center of the rectangle, would be visible "beneath" the rectangle. Like this:
Note that we draw the filled rectangle one pixel over, one pixel down and one pixel smaller in each dimension than the rectangle itself. This avoids painting over the boundary line, but covers all of the inside.
The method that we used to draw the figures in the above applet is not very flexible and is quite error prone. The centers of the rectangles were computed by hand. I didn't make any errors this time when I first tried it, but I often do. Getting the bounds of the filled rectangle aligned with that of the outer boundary rectangle is tedious and error prone. If we wanted to move one of the rectangles we would have a bit of recomputation to do and the editing would again be open to errors. We would be much better off if we encapsulate the figures to be drawn as objects themselves.
Therefore we want a RectangleFigure class that represents a drawable, movable, labeled rectangle. The Rectangle class of java.awt is not adequate for this. It doesn't even know about drawing. It is the graphics class that draws such rectangles. We want the RectangleFigure classes to take over this task for us. For our purposes here we want them to be filled rectangles with boundaries and we want to display a label as above. Below we see such a class. It could have extended Rectangle. The reason for not doing so will become clear in a bit.
public class RectangleFigure implements java.io.Serializable
{ private Rectangle bounds;
private String label;
public RectangleFigure(String label, int x, int y, int w, int h)
{ bounds = new Rectangle(x, y, w, h);
this.label = label;
}
public void draw(Graphics g)
{ int x = bounds.x, y = bounds.y, h = bounds.height, w = bounds.width;
g.drawRect(x, y, w, h);
Color c = g.getColor();
g.setColor(Color.white);
g.fillRect(x+1, y+1, w - 1, h - 1);
g.setColor(Color.red);
g.drawString(label, x + w/2, y + h/2);
g.setColor(c);
}
public Rectangle rectangle(){ return bounds; }
public Point location(){ return new Point(bounds.x, bounds.y); }
public Point center()
{ return new Point(bounds.x+bounds.width/2, bounds.y+bounds.height/2);
}
public Dimension dimension()
{ return new Dimension(bounds.width, bounds.height);
}
public void move(int x, int y)
{ bounds.x = x; bounds.y = y;
}
public void drawConnect(RectangleFigure b, Graphics g)
{ g.drawLine(center().x, center().y, b.center().x, b.center().y);
}
}
The interesting methods here are draw and drawConnect. Method draw draws the rectangle in the graphics current color, then changes the color to white and fills the interior. Before doing so it obtains the current color since we want to set the color back to this value at the end. If we don't then all other uses of this graphics object will result in coloring with the color that we set last. Note that the arguments to the draw routines that we call from within paint, being symbolic, are easier to understand and verify than those of our original version.
Also note, and it is very important, that we have a number of accessor (getter) and mutator (setter) methods for this class. Method rectangle, location, center, and dimension are all accessors. Method move is a mutator. Note that these are much more than just getting and setting corresponding instance variables, however. We have established here a more logical interface, independent of the actual instance variables used in the implementation.
Method drawConnect just draws a line from the center of the "this" figure to the center of the parameter figure. Note that we need to call drawConnect to draw the line between the boxes before we draw the boxes themselves, so that the hidden parts of the line will be covered up.
I made the RectangleFigure class implement the Serializable interface. This permits these objects to be stored in files and transmitted over the net using the object serialization API.
We could use this class in the following applet to achieve exactly the same effect that we saw originally. Notice especially how much simpler the paint method is than in the original version.
package jbgui;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.Color;
import javax.swing.JApplet;
import javax.swing.JPanel;
public class PaintDrawApplet extends JApplet
{ Canvas drawArea = new TestCanvas();
public void init()
{ add (drawArea);
drawArea.setBackground(Color.white);
}
RectangleFigure a = new RectangleFigure("A", 30, 50, 50, 50);
RectangleFigure b = new RectangleFigure("B", 200, 50, 50, 50);
public Dimension getPreferredSize()
{ return new Dimension(500, 300);
}
class TestCanvas extends JPanel
{ public Dimension getPreferredSize()
{ return new Dimension(400, 300);
}
public void paint( Graphics g )
{ a.drawConnect(b, g);
a.draw(g);
b.draw(g);
}
}
}
This doesn't show off our new flexibility, however. We can also move these new figures. A simple extension of the above applet will use a button to move one of the two RectangleFigures. As usual, we just add a button and make it a listener that will do what we want when the button is clicked. Ignore the print information in this code on your first reading. It will be discussed below.
package jbgui;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Point;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.PrintJob;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import javax.swing.JApplet;
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.JPanel;
public class PaintDrawApplet extends JApplet
{ private TestCanvas drawArea = new TestCanvas();
private Toolkit myToolkit = Toolkit.getDefaultToolkit();
private JFrame myFrame;
public PaintDrawApplet(JFrame f){myFrame = f; }
public void init()
{ Container contentPane = getContentPane();
contentPane.setLayout(new FlowLayout());
contentPane.setBackground(Color.white);
contentPane.add (drawArea);
contentPane.add(moveButton);
contentPane.add(printButton);
moveButton.addActionListener(new MoveListener());
printButton.addActionListener(new PrintListener());
}
RectangleFigure a = new RectangleFigure("A", 30, 50, 50, 50);
RectangleFigure b = new RectangleFigure("B", 200, 50, 50, 50);
JButton moveButton = new JButton("Move");
JButton printButton = new JButton("Print");
public void print(){drawArea.print();}
public Dimension getPreferredSize()
{ return new Dimension(600, 340);
}
class TestCanvas extends JPanel
{
public Dimension getPreferredSize()
{ return new Dimension(400, 300);
}
public void paint( Graphics g )
{ a.drawConnect(b, g);
a.draw(g);
b.draw(g);
}
public void print()
{ PrintJob printing = myToolkit.getPrintJob(myFrame, "Testing", null);
if(printing == null)
System.out.println("No print job");
else
{ Graphics drawer = printing.getGraphics();
if(drawer == null)
System.out.println("No Graphics.");
else
{ paint(drawer);
drawer.dispose();
}
printing.end();
}
}
}
class MoveListener implements ActionListener
{ public void actionPerformed(ActionEvent e)
{ b.move(b.location().x, b.location().y + 20);
repaint();
}
}
class PrintListener implements ActionListener
{ public void actionPerformed(ActionEvent e)
{ print();
}
}
}
All we have added here is the action that moves the "b" figure 20 pixels vertically. Not very interesting, but it shows the basic technique. Note that we do need to call repaint here to get the new figure shown. If we hadn't done this we would not have seen any effect until the window needs to be repainted for some other reason.
After pushing the Move button a few times we get the figure shown below. We will discuss the Print button in a bit.
There are a number of improvements that we could make to this. Feel free to implement as many as you like. The first and simplest is to center the label in the figure. What we see here is that the lower left corner of the label is at the center of the rectangle. It should really be the center of the label. The FontMetrics class can be used to give the height and length of a string.
Next we could be more flexible about the drawing and filling colors. The RectangleFigure class could be given new instance variables and means of getting and setting the colors to be used. We could set these in constructors, of course, but it is advantageous to permit a bit more flexibility.
We have used fixed sizes here. This means that either the labels must be kept short, or they will run outside the boxes. In some applications it might be a good thing to let the box resize itself if it has a long label. You probably want to maintain a minimum size, however. We could also provide an accessor and a mutator for the label.
A much more extensive change, however, comes from rethinking what kind of things we want to draw on the screen. Most likely, if we are trying to build an object modeling tool, we want to draw more than rectangles and lines. We might want ovals and circles, as well as more general polygons. They should be connectable with lines and should generally behave like our rectangles do.
To do this requires that we build a hierarchy of drawable figures. Perhaps we should start with a class named Figure that has much of the functionality of our rectangle class and probably all of its instance variables as well. It most likely is an abstract class and implements its two draw methods as no-ops (methods that do nothing), or leave them unimplemented.
Then RectangleFigure could extend Figure and implement the draw methods as here. We could then have additional classes that also extend Figure. Note that most of these classes will also want the bounds rectangle, even if their visual representation is not rectangular. We might also need one or more Line classes to implement various kinds of connections between the other Figures.
With this change, we will be inheriting a number of methods, such as move and location, that behave the same in all figures. In the future, a change to this superclass (Figure) will have its effects seen in all of its subclasses automatically.
Once we have the added flexibility of a hierarchy of Figures, we might want to make the painting easier. One way to do this is to give the applet a jave.util.ArrayList, in which to hold information about the Figures to be drawn. We "create" a drawing by putting Figure objects into this "draw vector". The paint method of the applet (or here, canvas), just draws everything in the ArrayList. Note that the fact that all the objects in the ArrayList are subclasses of a class with a draw method is critical to making this work. So inheritance helps us here.
If we add a draw list as int he paragraph above, we need to think again about the connections between the various figures. We don't want to put lines into the draw vector since the boxes that they connect must be movable. When we move a box, we need to redraw the line that connects it to other boxes, but these lines have moved as well. We need a way to directly represent the connection between objects, beyond the fact that we represent this as a line.
One way to do this successfully, is to have a Figure called a Connection. It has two Figure references as instance variables. When we draw a connection we draw a line between the centers of these two Figures. ( If one or both of these figures can be lines themselves, we need to be a bit more sophisticated, though.) The advantage of this scheme is that we can put these objects into the draw vector along with the other objects. Note that we need to draw all of the connections first, however.
Once we have Connection objects, the drawConnect method that we have used above becomes a method to return a new Connection object for the two Figures in question instead of a drawing method to draw the line directly.
Finally, and most ambitious, is a means of allowing the user to "grab" and move the figures directly using the mouse. To do this a method called rubberbanding is used. A very good source to learn about rubberbanding is the book Graphic Java, by David Geary (Prentice-Hall, 1999, Volume 1). Be sure to get the latest edition. He shows how rubberbanding can be used to draw objects, but the same techniques can be used to move and resize them as well.
Now that I have given you all of these skills, you can do something for me. An assignment, actually. Learn about the Unified Modeling Language (UML), which is a graphical technique for designing object oriented software. Build me (in Java) a simple UML modeling tool. Send me a copy when you've finished.
One more thing. If you create nice object modeling diagrams as suggested above, you will certainly want to save them in a file and print them out. More about printing next.
If you have presented some useful information in an application, your user may want to print it. Printing from an Applet is problematical, since an unfriendly applet downloaded off of the net could take over your printer. Therefore we must use an application if we want to print. We will modify the above applet to allow printing by first providing a frame in which to run the applet and then turning the applet into an application, Just as we did with the AllenApp in the first part of this tutorial. In fact the same class AllenFrame.java can be modified by just changing a few names.
We also add a new constructor to the PaintDrawApplet so that we can get and remember the Frame in which the applet code is running. We also did this with the AllenApp.
Frame myFrame;
public PaintDrawApplet(Frame f){myFrame = f; }
Next we are going to add a new button labeled "Print" to cause the contents of the drawArea to be printed. We do this in init, of course. We also add a new actionPerformed method that will cause the Applet's new print method to be called.
class PrintListener implements ActionListener
{ public void actionPerformed(ActionEvent e)
{ PaintDrawApplet.this.print();
}
}
The print method of the applet just calls the print method of the JPanel. All of the interesting things happen within this new method of the TestCanvas class.
The key to printing is the paint method that we have already seen. We don't need to modify it however. All we need to do is see that it is called with a special parameter: a graphics object that knows how to print. This can be obtained from the toolkit of the application. We saw before that we can get a toolkit for processing images. The same is used for printing.
First we ask the toolkit for a PrintJob, passing it a (non null) frame, a title for the job, and additional, system dependent parameters. Usually you just pass null for this last argument, preserving cross platform portability, but giving up some finer control of your printer. This call to getPrintJob causes the standard system print dialog to appear so that you can select a destination and number of copies and the like.
If the user cancels this print dialog, the PrintJob returned will be null. Therefore this needs to be checked for.
public void print()
{ PrintJob printing = myToolkit.getPrintJob(myFrame, "Testing", null);
if(printing == null)
System.out.println("No print job");
else
{ Graphics drawer = printing.getGraphics();
if(drawer == null)
System.out.println("No Graphics.");
else
{ paint(drawer);
drawer.dispose();
}
printing.end();
}
}
Then for each page to be printed (here just one) we ask that PrintJob for its graphics object, by calling its getGraphics method. We then just call our ordinary paint method, passing this graphics context as a parameter. Then we should dispose of the graphics object as usual. If there are several pages to be printed you do this three step process in a loop: getGraphics, paint, dispose. Finally when you are all done printing call the end method of the PrintJob. That's all there is to it. There are some refinements, however, as you can discover from the usual reference material.
That is all. Enjoy.
Last Updated: December 16, 2000