Freehand Drawing on the Screen

Occasionally you will be called upon to write an app that enables the user to draw figures freehand. This type of app provides the simplest introduction to the use of the Graphics class.

Connecting the Dots

The simplest of all the freehand drawing apps is one we'll call ConnectTheDots. In this app, the user is free to click the mouse anywhere within the form. With every click of the left mouse button, the ConnectTheDots app draws a line between the current position of the mouse pointer and the position of the mouse pointer at every previous mouse click. Clicking the right mouse button clears the screen to allow the user to start over. Even a simple app such as this allows the user to generate some interesting patterns.

Forms Designer work

Since this app doesn't make use of any Toolbox components, there's almost no Forms Designer work to be done. Create the Windows app as you would any other MS Visual J Plus Plus v6 Windows app, and give it the name ConnectTheDots. Change the form's text property to Connect the Dots: Click mouse. Then switch the Properties window over to the active properties to add a mouseDown event handler and a paint event handler.

NOTE
The paint event is only defined for the Form class. You can't use the paint event to draw into any other WFC controls.

code

The code for ConnectTheDots is shown here:

import com.ms.wfc.app.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.html.*;
import com.ms.wfc.util.*;
public class Form1 extends Form
{
 public Form1()
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 }
 .
 .
 .
 /**
 * If the left mouse button is pressed, add the point
 * to the draw list. If the right button is pressed, * clear the list.
 */
 List points = new List();
 private void Form1_mouseDown(Object source, MouseEvent e)
 {
 if ((e.button & MouseButton.LEFT) != 0)
 {
 Point mouseLoc = new Point(e.x, e.y);
 points.addItem(mouseLoc);
 }
 if ((e.button & MouseButton.RIGHT) != 0)
 {
 points.setSize(0);
 }
 invalidate();
 }
 /**
 * Handle repaints of the form by drawing lines
 * between each of the mouse points in the draw list.
 */
 private void Form1_paint(Object source, PaintEvent e)
 {
 // first draw the kaleidoscope
 Graphics g = e.graphics;
 int length = points.getSize();
 for (int i = 0; i < length; i++)
 {
 for (int j = 0; j < i; j++)
 {
 Point p1 = (Point)points.getItem(i);
 Point p2 = (Point)points.getItem(j);
 g.drawLine(p1, p2);
 }
 }
 }
 /**
 * NOTE: The following code is required by the Visual J Plus Plus form
 * designer. It can be modified using the form editor. Do not
 * modify it using the code editor.
 */
 Container components = new Container();
 private void initForm()
 {
 // …created by the Forms Designer…
 }
 /**
 * The main entry point for the app. * …
 */
 public static void main(String args[])
 {
 app.run(new Form1());
 }
}


Other than the initForm() method, which simply sets the title for the form and assigns the two event handlers, the program consists almost entirely of the two event handlers, Form1_mouseDown() and Form1_paint().

mouseDown event handler code

The program invokes the method Form1_mouseDown() when the user clicks either mouse button while the mouse pointer is anywhere in the form. The method receives a MouseEvent that contains the location of the mouse within the form at the time the mouse button was pressed, and that contains a button variable indicating which mouse button was pressed. This variable button consists of an OR combination of the fields LEFT, MIDDLE, and RIGHT of the enumeration class MouseButton. Thus, if the MouseButton.LEFT bit is set in e.button, the left mouse button was clicked. (More accurately, if the MouseButton.LEFT button bit is set, the left mouse button was clicked or was being held down at the same time that some other mouse button was clicked. The distinction isn't important for most apps.)

If Form1_mouseDown() determines that the left mouse button was clicked, the method creates a Point object to store the mouse location and adds the object to a list of mouse locations called points. If it was the right mouse button that was pressed, Form1_mouseDown() clears out the list of mouse locations by setting the size of points to 0. This gives the user a way to start over. Either way, Form1_mouseDown() finishes by calling the method invalidate() to display the results.

What's invalidate()?

Calling the invalidate() method tells WFC that the Form object's display is now invalid and must be repainted. WFC asks the object to repaint itself by generating a paint event, which is handled in this program by Form1_paint(). The paint event is WFC's way of asking the app to re-create the window.

NOTE
Even beginning Microsoft Visual C++ programmers will recognize the invalidate() method immediately. The only difference between the way Visual J Plus Plus v6 and other Microsoft Visual Studio languages handle the invalidation is that under Visual J Plus Plus, the paint request is processed by means of an event to the form. Experienced Microsoft Windows programmers will probably want to skip over the following explanation of the paint event code.

There are a lot of other ways besides mouse clicks that the display might become invalid. When the window of some other app obscures the window of your app, for example, Windows loses the information contained in the obscured portion. If that portion of the window later becomes visible, the window is invalid and your app needs to redraw it. Or, the user might have minimized the form and now requests that it be displayed. Even simply resizing the form to make it larger results in a display that is at least partially invalid and needs to be re-created by the app.

ISN'T THE INVALIDATE-PAINT MECHANISM OVERLY COMPLICATED?


This mechanism of generating a paint event whenever the window needs refreshing seems overly-complicated. Isn't there some other approach to refreshing the display? In fact, there is.

The problem we have to deal with is that when an app's window is obscured on the screen, Windows loses some information. To avoid this, rather than write to the real screen, our app could write to a virtual screen that is maintained by the operating system. In this virtual world, each window would be maintained individually on its own virtual screen. In this virtual world, the app's window would never be overwritten by some other window because it would always be separate.

The operating system would have to combine these virtual screens onto the single, real screen that the user sees. If a window were minimized, it would be the job of the operating system to figure out which pieces of which virtual windows would be needed to properly update the real screen.

Microsoft Windows doesn't use the virtual screen approach, however. Why?

There are several problems with the virtual screen proposal. The first issue is that it requires large amounts of memory. Each virtual screen must be maintained in RAM that, for speed reasons, can't be swapped to disk. Modern, high-resolution, multicolor screen displays can each consume 2 MB of memory or more. Today, when memory is cheap, this isn't much of a problem; but Windows was designed in an era when memory was more expensive. (Many people don't remember that the earliest versions of Windows were designed so that, together with the apps, a total of 640 KB of memory was used.) Even today, it seems wasteful to consume 16 MB or more of nonswappable RAM just to maintain windows that aren't even visible.

A second problem is performance. This copying of potentially large amounts of memory could result in slow display performance. Consider, for example, the most common case of an app writing to a completely visible window. Using the virtual screen scenario, Windows would have to write every pixel twice: first when the app wrote to the virtual screen, and again when the operating system copied the window from there to the real screen. With the approach that Windows uses, a window need only be written once to the real screen.

Finally, there is the issue of scheduling. Windows gives the paint event the lowest event priority (other than the idle event, which we'll discuss in the next chapter). If the app receives any other event notification, that event will occur before the paint event. This type of scheduling avoids needless repaints, which would be much harder to avoid using the virtual screen method.

For example, maximizing the size of Microsoft Internet Explorer not only displays images or parts of images that might not previously have been visible on the display, it also causes the browser to reformat text to fully utilize the additional display area. Windows doesn't repaint the browser window until all updates caused by the resizing have been processed. Using the virtual screen method, it would be very difficult to avoid updating the real screen from the virtual screen multiple times and thereby wasting CPU cycles.

paint event code

The Form1_paint() method is fairly simple. The first thing that all paint() methods must do is retrieve the Graphics object from the PaintEvent object. This done, Form1_paint() looks at the list of mouse down locations saved in the points variable. If this list is empty, which it will be when the program is first displayed, the method does nothing.

NOTE
One of the most common mistakes programmers make in writing paint event methods is this: they forget that the operating system sends a paint event to the app as soon as the Form1 constructor finishes, which is long before the user generates the first input event.

If the points variable's list isn't empty, the Form1_paint() method iterates through each point in the list, using the Graphics.drawLine() method to draw a line between each pair of points in the list. Figure 9-1 shows the ConnectTheDots program display after the user selected four points in a somewhat square pattern. Figure 9-2 shows just one of the interesting combinations the user can create with a small number of points.

Screenshot

Screenshot-1. The ConnectTheDots display with four points.

Screenshot

Screenshot-2. The ConnectTheDots program generating an interesting display with as few as eight points.

What's a Graphics object?

The Graphics object is the Visual J Plus Plus interface between the app and the display hardware. The Graphics object defines numerous draw methods, which the app uses to draw on the screen.

NOTE
The Visual J Plus Plus Graphics object corresponds to the Microsoft Foundation Classes (MFC) HDC class, which is known as the device context.

It's the responsibility of the Graphics object to handle differences between different types of hardware. Thus, your app doesn't need to worry about the screen resolution or the number of colors that a particular system can support. If a video card contains special hardware to increase display performance, the Graphics object also handles this hardware.

In addition, it's the job of the Graphics object to know where your app's window is being displayed on the screen. All coordinates that you supply in your app are relative to the upper-left corner of the window. The Graphics object also makes sure that the app doesn't draw outside of its window. The Graphics object maintains the integrity of the window's display.

NOTE
Most, if not all, of the graphics display chores are not actually performed by the Graphics object. Instead, most of these chores are actually handled by Windows or one of its device drivers, but this detail is unimportant. To Visual J++ programmers, the Graphics object is the only interface to the Windows device context and the display API.

FreeDraw app

The ConnectTheDots app can generate interesting displays with a relatively small amount of code, but it doesn't represent a very useful app. The following FreeDraw app, which allows the user to generate freehand drawings, is much more useful.

FreeDraw allows the user to draw varied patterns with the mouse. Each pattern (which I'll refer to as a squiggle) begins with the user dragging the mouse pointer with the left mouse button pressed and terminates with the user releasing the mouse button. Once the squiggle has been generated, the user can modify its color and width by means of menu commands. When the user starts a new squiggle, he or she can't change previous squiggles. Right-clicking the mouse erases all stored squiggles, effectively erasing the slate.

Forms Designer work

To create FreeDraw, begin by creating a standard Visual J Plus Plus Windows app. Set the text property of the form to FreeDraw, to match the app name.

Add a main menu to the form. Add two main menu items; label the first Color and the second Width. Add to the Color menu item several submenu items labeled with the names of some of the fields of the Color class, for example BLACK, AQUA, and GREEN. Create submenu items under the Width menu with labels indicating various line widths in pixels: 1, 2, 4, and 8.

Squiggle class code

As is often the case, the code for the main program is made easier by creating a helper class. The Squiggle class describes a squiggle on the screen. The Squiggle.java file contains the following code:

import com.ms.wfc.ui.*;
import com.ms.wfc.util.*;
public class Squiggle
{
 // two fundamental drawing properties
 Color color;
 int width;
 // a pen containing the selected properties
 Pen pen;
 // the points in this squiggle
 List points = new List();
 /**
 * Create a black squiggle with no points.
 */
 public Squiggle()
 {
 // set the initial color and width
 color = Color.BLACK;
 width = 1;
 // convert the properties into a pen
 calcPen();
 // make sure the list is empty
 points.setSize(0);
 }
 /**
 * Set the color of the squiggle.
 */
 public void setColor(Color color)
 {
 this.color = color;
 calcPen();
 }
 /**
 * Set the width of the squiggle.
 */
 public void setWidth(int width)
 {
 this.width = width;
 calcPen();
 }
 /**
 * Add a point to the squiggle.
 */
 public void addPoint(int x, int y)
 {
 points.addItem(new Point(x, y));
 }
 /**
 * Create a solid pen with the specified width and color.
 */
 private void calcPen()
 {
 pen = new Pen(color, PenStyle.SOLID, width);
 }
 /**
 * Return the current Pen for this squiggle.
 */
 public Pen getPen()
 {
 return pen;
 }
 /**
 * Return the number of points in this squiggle.
 */ int getSize()
 {
 return points.getSize();
 }
 /**
 * Return a point in this squiggle list.
 */
 Point getPoint(int index)
 {
 return (Point)points.getItem(index);
 }
}


A squiggle consists of a list of points representing mouse pointer locations and a Pen object to use in drawing between these points. The list is maintained in an object of type List called points. The Pen object is named pen. For our purposes, Pen has only the properties of color and width. (There are other properties that you can set for a Pen object.) The color variable stores the color property setting, and the width variable stores the width property setting.

The constructor for Squiggle creates a black pen object one pixel wide and an empty list. The next methods, setColor() and setWidth(), allow the app to change the pen object's properties. Both of these methods call the private method calcPen() to reflect the new color and width in a newly created pen. The method addPoint(x, y) creates a Point object from the x and y coordinates that are passed to the method as parameters. The method then adds this Point object to the list of points.

The Squiggle class provides query functions, such as getPen() to return the current pen object, getSize() to return the size of the list, and getPoint(i) to return the Point object at index i in the points list.

FreeDraw code

The remaining FreeDraw code is retained in the Form1.java source file:

import com.ms.wfc.app.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.html.*;
import com.ms.wfc.util.*;
import java.lang.reflect.*;
public class Form1 extends Form
{
 public Form1()
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 }
 /**
 * Handle the mouse down event by creating a current list.
 */
 List oldLists = new List();
 Squiggle currentList = null;
 private void Form1_mouseDown(Object source, MouseEvent e)
 {
 // if left mouse button…
 if ((e.button & MouseButton.LEFT) != 0)
 {
 // create a new squiggle list; // if there is an old one, add it to the
 // list of lists
 if (currentList != null)
 {
 oldLists.addItem(currentList);
 }
 currentList = new Squiggle();
 }
 // if right mouse button…
 if ((e.button & MouseButton.RIGHT) != 0)
 {
 // start over
 currentList = null;
 oldLists.setSize(0);
 invalidate();
 }
 }
 /**
 * Handle the mouse move event by adding the current
 * mouse pointer location to the current squiggle.
 */
 private void Form1_mouseMove(Object source, MouseEvent e)
 {
 // if right mouse button is not down, ignore it
 if ((e.button & MouseButton.LEFT) == 0)
 {
 return;
 }
 // now record the current mouse pointer position
 currentList.addPoint(e.x, e.y);
 // and repaint the screen
 invalidate();
 }
 /**
 * Handle the repaint event.
 */
 private void Form1_paint(Object source, PaintEvent e)
 {
 // paint the current squiggle
 Graphics g = e.graphics;
 if (currentList != null)
 {
 drawList(g, currentList);
 }
 // now paint the previous squiggles as well
 IEnumerator ie = oldLists.getItemEnumerator();
 while(ie.hasMoreItems())
 {
 Squiggle list = (Squiggle)ie.nextItem();
 drawList(g, list);
 }
 }
 /**
 * Draw squiggle by connecting the dots.
 */
 static void drawList(Graphics g, Squiggle list)
 {
 g.setPen(list.getPen());
 int length = list.getSize() - 1;
 for(int i = 0; i < length; i++)
 {
 g.drawLine(list.getPoint(i), list.getPoint(i + 1));
 }
 }
 private void colorItem_click(Object source, Event e)
 {
 try
 {
 // read the menu item's label…
 MenuItem mi = (MenuItem)source;
 String s = mi.getText();
 // convert that to one of the fields in Color…
 Class c = Color.class;
 Field f = c.getField(s);
 Color color = (Color)f.get(null);
 // and set the current squiggle's color
 currentList.setColor(color);
 invalidate();
 }
 catch(Exception ex)
 {
 }
 }
 private void widthItem_click(Object source, Event e)
 {
 // read the menu item's label…
 MenuItem mi = (MenuItem)source;
 String s = mi.getText();
 // convert that to a number…
 int width = Integer.parseInt(s);
 // and set the width
 currentList.setWidth(width);
 invalidate();
 }
 /**
 * NOTE: The following code is required by the Visual J Plus Plus form
 * designer. It can be modified using the form editor. Do not
 * modify it using the code editor.
 */
 Container components = new Container();
 MainMenu mainMenu1 = new MainMenu();
 MenuItem menuItem12 = new MenuItem();
 MenuItem menuItem11 = new MenuItem();
 MenuItem menuItem4 = new MenuItem();
 MenuItem menuItem5 = new MenuItem();
 MenuItem menuItem6 = new MenuItem();
 MenuItem menuItem7 = new MenuItem();
 MenuItem menuItem8 = new MenuItem();
 MenuItem menuItem9 = new MenuItem();
 MenuItem menuItem10 = new MenuItem();
 private void initForm()
 {
 // …created by the Forms Designer…
 }
 /**
 * The main entry point for the app. * …
 */
 public static void main(String args[])
 {
 app.run(new Form1());
 }
}


The Form1 class maintains two data members. The current squiggle is maintained in a field named currentList. This is the Squiggle object that the user is currently creating or has just created. This is the only squiggle whose color and width can be modified. The variable oldLists consists of a list of the previously drawn squiggles.

The method Form1_mouseDown() is essentially two methods in one. If the user presses the left mouse button, the method adds the current squiggle—if there is one—to the list of old squiggles before creating a new current squiggle. When the right mouse button is pressed, the method zeros the currentList pointer to remove the current squiggle and then zeros out the old squiggle list by calling oldLists.setSize(0). The call to invalidate() repaints the form with the lists removed, thereby clearing the window.

The Form1_mouseMove() method handles the mouse drag event by adding the current mouse pointer location to the current squiggle. The call to invalidate() repaints the display with the new mouse pointer location added.

The repaint method Form1_paint() first draws the currentList object by calling the local method drawList(). It then iterates through the list of older squiggles, drawing each using drawList().

The method drawList() draws a squiggle. It begins by getting the pen stored in the Squiggle object and using that pen's settings to set the pen in the Graphics object. It then uses a for loop to iterate through the points contained in the Squiggle object, calling Graphics.drawLine() to draw a line between each stored mouse pointer location (i) and the next (i + 1). Since the mouse pointer locations stored by the mouseMove() method are relatively close together, drawList() provides a relatively smooth-looking curve, especially with some of the wider pens.

The colorItem_click() method handles the click event for each of the Color menu items. This method uses Java's language reflection capability (discussed in ) to find the static member of the Color object with the same name as the menu item. It calls setColor() to establish this color as the color of the current squiggle before calling invalidate() to make the new color visible by repainting the screen. (Remember to attach colorItem_click() as the click event for all the Color submenu items.)

The widthItem_click() method uses a similar approach in handling the Width menu items. The label on the menu item is converted from a string into a number using the Integer.parsetInt() method. This integer value is used to adjust the pen width of the current squiggle by calling setWidth(). The call to invalidate() makes the change visible. (Remember to attach widthItem_click() as the click event for all the Width submenu items.)

Screenshot-3 shows an image drawn with FreeDraw.

Screenshot

Screenshot-3. A figure drawn with FreeDraw, showing the highways in and around my hometown. Comments