Mixing Graphics Objects and WFC Components

Up until now, the Graphics object and corresponding paint() method have been presented as a mutually exclusive alternative to the use of WFC components. There is at least one case, however, where the use of the custom paint() method must be mixed with the use of components.

One of the methods of the Graphics class, drawFocusRect(), is designed to draw a focus rectangle. The focus rectangle is the rapidly updated, dotted rectangle that some software packages use to signify that the user has selected an area of the screen, or to signify that a control has focus.

It might be desirable in some apps to use a focus rectangle to enable the user to select and manipulate WFC components. The following SelectComponents program does exactly that: it allows the user to select a few WFC components and drag them about within the form almost as if they were still under the control of the Forms Designer. The program will also demonstrate that all the while the components are being dragged about, they are still completely functional.

Forms Designer Work

Create a standard Visual J Plus Plus Windows app. Call it SelectComponents. Set the text property of the form to Select Components to match the name of the app.

We will need to add a few WFC components for the user to select. Arrange four Button controls vertically on the form, leaving the default names for each. Below the four buttons, place an Edit control. Change the name property of the Edit control to edit. Set the initial text property of the control to Drag mouse to select objects, to indicate to the user what the purpose of the app is. In addition, set the click property of each button to point to a method that will simply display the message "buttonx clicked" in the Edit control, where x is the number of the button. We'll use this later to prove that even though they have been moved, the buttons continue to work.

The completed form in the Forms Designer is shown in Figure 9-12.

Screenshot

Screenshot-12. The completed Forms Designer work for SelectComponents.

Code

The code for SelectComponents is involved and somewhat lengthy. To simplify the program as much as possible, I have broken the source code up into a number of simpler classes. With the support of these helper classes, the actual Form1.java source listing becomes almost simple.

ControlLoc class

Looking ahead, it's clear that SelectComponents will need to be able to keep track of the Control objects the user selects with the focus rectangle.

NOTE
I'm not so clever that I can look ahead to determine exactly what I will need in a class. Such development is in practice iterative. I first created ControlLoc guessing what I would need. As development progressed, I returned to add capabilities that I hadn't anticipated initially.

To make tracking selected objects easier, create the following ControlLoc class.

import com.ms.wfc.ui.*;
/**
 * Retain a control and its original location.
 */
public class ControlLoc
{
 Control c = null;
 Point originalLoc = null;
 /**
 * Save the control and its original location.
 */
 ControlLoc(Control c)
 {
 this.c = c;
 this.originalLoc = c.getLocation();
 }
 /**
 * Get the control.
 */
 Control getControl()
 {
 return c;
 }
 /**
 * Get original location of the control.
 */
 Point getOriginalLocation()
 {
 return new Point(originalLoc);
 }
 /**
 * Set the current location of the control.
 */
 void setLocation(Point loc)
 {
 c.setLocation(loc);
 }
}


The ControlLoc class represents a Control object, c, and the object's original location on the screen, originalLoc. The methods of ControlLoc are straightforward. There's a constructor that saves the control and queries its location using the Control.getLocation() method, to establish the original location. In addition, the methods ControlLoc.getControl() and ControlLoc.getOriginalLocation() allow querying of the data members. The method ControlLoc.setLocation() updates the current location of the control on the screen.

ControlLocList class

It's also clear that the ControlLoc objects created to keep track of the controls selected by the focus rectangle will need to be maintained in a list. To simplify this chore, create the following ControlLocList class.

import com.ms.wfc.ui.*;
import com.ms.wfc.util.List;
/**
 * Maintain a list of ControlLoc objects.
 */
public class ControlLocList
{
 List list = new List();
 /**
 * Add a control.
 */
 public void addControl(Control c)
 {
 list.addItem(new ControlLoc(c));
 }
 /**
 * Reset the list to empty.
 */
 public void reset()
 {
 list.setSize(0);
 }
 /**
 * Retrieve a ControlLoc by index.
 */
 public ControlLoc getControlLoc(int index)
 {
 return (ControlLoc)list.getItem(index);
 }
 /**
 * Get the number of ControlLocs in the list.
 */
 public int getSize()
 {
 return list.getSize();
 }
}


This class uses the List class to maintain the actual list. The methods addControl(), reset(), getControlLoc(), and getSize() provide simplified access to this list of ControlLoc objects.

SelectedControls class

Now that we have a class for maintaining information about a selected control and a class for maintaining a list of the selected controls, we can set about writing a class for selecting control objects. This class SelectedControls appears as follows:

import com.ms.wfc.ui.*;
/**
 * Selected controls are those WFC controls that fall
 * within the focus rectangle.
 */
public class SelectedControls
{
 // create a container for selected controls
 private ControlLocList selectedControls = new ControlLocList();
 /**
 * Add the controls that hang off the parent and are
 * within the rectangle r to the list of selected
 * controls. (Mark them by setting them to boldface.) */
 public void findControls(Control parent, Rectangle r)
 {
 // first clear out the list
 selectedControls.reset();
 // now add back all the objects within the rectangle
 add(parent, r);
 }
 private void add(Control parent, Rectangle r)
 {
 int limit = parent.getControlCount();
 for (int i = 0; i < limit; i++)
 {
 // get the bounding rectangle for this control
 Control child = parent.getControl(i);
 Rectangle childR = child.getClientRect();
 Point loc = child.getLocation();
 childR.x = loc.x;
 childR.y = loc.y;
 // if it's within the focus rectangle…
 if (isWithin(childR, r))
 {
 // mark it and add it to the list
 setHighlight(child, true);
 selectedControls.addControl(child);
 }
 else
 {
 setHighlight(child, false);
 }
 // add any components hanging off this component
 add(child, r);
 }
 }
 /**
 * Either highlight or unhighlight the Control c.
 */
 private void setHighlight(Control c, boolean on)
 {
 Font font = c.getFont();
 if (font == null)
 {
 return;
 }
 // if highlight is on…
 if (on)
 {
 // set the font to bold;…
 font = new Font(font, FontWeight.BOLD,
 false, false, false);
 c.setFont(font);
 }
 // otherwise,…
 else
 {
 // set to normal
 font = new Font(font, FontWeight.NORMAL,
 false, false, false);
 c.setFont(font);
 }
 }
 /**
 * Move the selected objects by delta.
 */
 Point controlPt = new Point();
 public void move(Point delta)
 {
 int size = selectedControls.getSize();
 for (int i = 0; i < size; i++)
 {
 ControlLoc cl = selectedControls.getControlLoc(i);
 controlPt = cl.getOriginalLocation();
 controlPt.x += delta.x;
 controlPt.y += delta.y;
 cl.setLocation(controlPt);
 }
 }
 /**
 * Return true if first rectangle is completely within second.
 */
 static boolean isWithin(Rectangle childR, Rectangle parentR)
 {
 if (childR.x < parentR.x)
 {
 return false;
 }
 if (childR.y < parentR.y)
 {
 return false;
 }
 if ((childR.x + childR.width) > (parentR.x + parentR.width))
 {
 return false;
 }
 if ((childR.y + childR.height) > (parentR.y + parentR.height))
 {
 return false;
 }
 return true;
 } }


The findControls(parent, r) method is perhaps the most important of the methods in this class. This method searches all of the WFC controls that are attached to the parent control object. Each of the controls it finds that are completely within the focus rectangle r is added to the ControlLocList data member called selectedControls.

The findControls() method starts by first resetting the selectedControls list to empty. It then calls the SelectedControls.add() method. The add() method loops through all of the child controls of parent using the method Control.getControl(). For each child that it finds, add() calculates the smallest rectangle that surrounds the child component. This is the bounding rectangle. The add() method calls SelectedControls.isWithin() to determine whether the child's bounding rectangle is completely within the focus rectangle r. If it is, the control is highlighted by calling SelectedControls.setHighlight() and the control is added to the selectedControls list.

The isWithin(childR, parentR) method returns true if the rectangle childR is completely within the focus rectangle parentR. The logic is straightforward, although a little monotonous. If the child rectangle's left edge is to the left of the parent's left edge (childR.x < parentR.x), the method returns false. Likewise, if the child rectangle's upper edge is above the parent's (childR.y < parentR.y), the method returns false. Similar checks are made of the right and bottom edges. After all edges have been checked, if the method still hasn't found any part of the child rectangle to be outside the parent rectangle, the method returns true. This is demonstrated in Figure 9-13.

As you can see in the Figure 9-13 example, component A is outside the focus window W because childR.x is less than parentR.x. Component B is outside W because childR.y is less than parentR.y. In the case of component C, both (childR.x + childR.width) is greater than (parentR.x + parentR.width) and (childR.y + childR.height) is greater than (parentR.y + parentR.height). Component D is completely within the focus window.

Java Click to view at full size.

Screenshot-13. Only control D is within the focus window W.

The method setHighlight() either highlights or unhighlights a control to indicate to the user whether the control has been selected. Although there are several possible ways to indicate a selection, I chose to boldface the control's font (if the control has a font).

Finally, the method move() is provided to move all of the selected controls by some delta. This method will prove important once the user has selected several controls with the focus rectangle and wants to move them.

FocusRectangle class

The final support class is FocusRectangle. As its name implies, this class helps maintain the focus rectangle itself. The code for this class appears as follows:

import com.ms.wfc.ui.*;
/**
 * Represent a focus rectangle.
 */
public class FocusRectangle
{
 // the actual rectangle
 private Rectangle frame;
 // the mouse location at the time the rectangle
 // was started
 private Point originalLoc;
 // the upper left corner of the focus
 // rectangle prior to it being moved
 private Point upperLeft;
 /**
 * Create a focus rectangle at the specified
 * mouse location.
 */
 public FocusRectangle(Point mouseLoc)
 {
 originalLoc = new Point(mouseLoc);
 upperLeft = new Point(originalLoc);
 frame = new Rectangle(mouseLoc.x,
 mouseLoc.y,
 0,
 0);
 }
 /**
 * Move the focus rectangle by the specified amount.
 */
 public void move(Point delta)
 {
 frame.x = upperLeft.x + delta.x;
 frame.y = upperLeft.y + delta.y;
 }
 /**
 * Resize the frame based on mouse movement.
 */
 public void resize(Point mouseLoc)
 {
 // if mouse moved down…
 if (mouseLoc.y > originalLoc.y)
 {
 // then keep the top of the focus area set
 // to the first mouse location and move the bottom down
 frame.y = originalLoc.y;
 frame.height = mouseLoc.y - originalLoc.y;
 }
 // if mouse moved up…
 else
 {
 // set the top to the current mouse location and
 // calculate the bottom of the frame to be the first
 // mouse location
 frame.y = mouseLoc.y;
 frame.height = originalLoc.y - mouseLoc.y;
 }
 // repeat for width
 if (mouseLoc.x > frame.x)
 {
 frame.x = originalLoc.x;
 frame.width = mouseLoc.x - originalLoc.x;
 }
 else
 {
 frame.x = mouseLoc.x;
 frame.width = originalLoc.x - mouseLoc.x;
 }
 // update the upper left corner
 upperLeft.x = frame.x;
 upperLeft.y = frame.y;
 }
 /**
 * Retrieve the rectangle.
 */
 public Rectangle getRectangle()
 {
 return frame;
 }
}


The program creates the focus rectangle as soon as the user holds down the left mouse button and begins to drag it within the form. Accordingly, the FocusRectangle() constructor saves the starting location of the mouse in originalLoc. It also creates at that location a rectangle called frame that has a height and width of 0.

The FocusRectangle.resize() method is called as the user drags the mouse to resize the focus rectangle. The logic for this method is slightly more complicated than you might expect, because it must take into consideration the fact that the user might create a focus rectangle by dragging the mouse in any direction: up or down, and left or right. (The logic would be much simpler if the user were forced to start at the upper left corner and drag the mouse down and to the right in order to create a focus rectangle.)

Like the SelectedControls class, the FocusRectangle class provides a move() method that will be needed to move the focus rectangle—once it has been created—together with the components that it encompasses.

Form1 class

With all of these support classes, the actual Form1 class is fairly small, consisting primarily of event handlers.

import com.ms.wfc.app.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.html.*;
import com.ms.wfc.io.*;
public class Form1 extends Form
{
 public Form1()
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 }
 // the following represents the focus rectangle
 FocusRectangle focusRectangle = null;
 Point mouseLoc = new Point();
 // members to support the moving of the selected objects
 boolean controlsMoving = false;
 Point mouseOriginalLoc = null;
 SelectedControls selectedControls = new SelectedControls();
 /**
 * Start the building of the focus area, or if there is
 * already a focus area and the mouse is within it, start
 * moving the objects within the focus area.
 */
 private void mouseDown(Object source, MouseEvent e)
 {
 // ignore anything but left mouse button
 if ((e.button & MouseButton.LEFT) == 0)
 {
 return;
 }
 // if there is a focus area…
 if (focusRectangle != null)
 {
 // and the mouse is within the focus area…
 Rectangle r = new Rectangle(e.x, e.y, 0, 0);
 if (SelectedControls.isWithin(
 r,
 focusRectangle.getRectangle()))
 {
 // start dragging the objects within
 // the focusRectangle
 controlsMoving = true;
 mouseOriginalLoc = new Point(e.x, e.y);
 return;
 }
 }
 // we aren't dragging; start building a focus area
 mouseLoc.x = e.x;
 mouseLoc.y = e.y;
 focusRectangle = new FocusRectangle(mouseLoc);
 }
 /**
 * If we are in drag control mode, then drag the controls.
 * If we are in focus mode, then resize the focus area.
 */
 private void mouseMove(Object source, MouseEvent e)
 {
 if ((e.button & MouseButton.LEFT) == 0)
 {
 return;
 }
 if (controlsMoving)
 {
 dragControls(e);
 return;
 }
 if (focusRectangle != null)
 {
 dragFocusArea(e);
 }
 }
 /**
 * If we have been moving an object, then restart the
 * focus area. Restart dragging the selected objects.
 */
 private void mouseUp(Object source, MouseEvent e)
 {
 if (controlsMoving)
 {
 focusRectangle = null;
 }
 controlsMoving = false;
 invalidate();
 }
 /**
 * Drag the selected controls.
 */
 Point mouseDelta = new Point();
 private void dragControls(MouseEvent e)
 {
 // calculate how far the mouse has moved since
 // the dragging started
 mouseDelta.x = e.x - mouseOriginalLoc.x;
 mouseDelta.y = e.y - mouseOriginalLoc.y;
 // first move the selected controls
 selectedControls.move(mouseDelta);
 // now move the focus area
 focusRectangle.move(mouseDelta);
 // repaint the form
 invalidate(); }
 /**
 * Resize the focus area as a result of mouse movement.
 */
 void dragFocusArea(MouseEvent e)
 {
 // resize the focusRectangle
 mouseLoc.x = e.x;
 mouseLoc.y = e.y;
 focusRectangle.resize(mouseLoc);
 // find out which objects are enclosed
 selectedControls.findControls(this,
 focusRectangle.getRectangle());
 // now force a repaint to update the focus rectangle
 invalidate();
 }
 /**
 * Repaint current form.
 */
 private void Form1_paint(Object source, PaintEvent e)
 {
 // if there is a focus rectangle…
 Graphics g = e.graphics;
 if (focusRectangle != null)
 {
 // then draw it
 g.drawFocusRect(focusRectangle.getRectangle());
 }
 }
 /**
 * Output the button's name to the edit window.
 */
 private void button_click(Object source, Event e)
 {
 Button button = (Button)source;
 String name = button.getText();
 edit.setText(name + " clicked");
 }
 /**
 * 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();
 Button button1 = new Button();
 Button button2 = new Button();
 Button button3 = new Button();
 Button button4 = new Button();
 Edit edit = new Edit();
 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());
 }
}


When the user clicks the left mouse button, the system is in one of two states. One state is when there is no focus rectangle, in which case the SelectComponents program assumes the user is creating one. A second state is when there is a focus rectangle already created by a previous MouseDown event, in which case the program assumes the user is trying to move the focus rectangle and the components that it encompasses.

This dichotomy is reflected in the Form1.mouseDown() method. If there is a focus rectangle already (focusRectangle!= null), and the mouse pointer location is within it, mouseDown() sets the controlsMoving flag to true in order to start the drag process. If there is no focus rectangle, the method creates one. This method begins the creation and resizing of the focus rectangle.

Similarly, Form1.mouseMove() has two parts. The method first checks the controlsMoving flag. If this is set, mouseMove() calls Form1.dragControls(), which calls selectedControls.move() and focusRectangle.move() to move the focus rectangle and any selected controls. If a focus rectangle exists and the controlsMoving flag is false, mouseMove() calls Form1.dragFocusArea(). The dragFocusArea() method resizes the focus rectangle and then recalculates which controls are now included within the resized focus rectangle by calling selectedControls.findControls().

The Form1.mouseUp() method clears the controlsMoving flag to indicate that the user is no longer moving the controls. If the controlsMoving flag was set, it also clears the focusRectangle to make the focus rectangle disappear.

The Form1_paint() method is simple. The WFC controls that are on the form know how to repaint themselves. All that Form1_paint() must draw is the focus rectangle, if it exists.

The button_click() method was created to handle the click event for each of the buttons, to demonstrate that these buttons still work even after the user has moved them about.

Results

Screenshot-14 shows the SelectComponents app running with the focus rectangle, which has been drawn to encompass two of the four buttons. Figure 9-15 shows the buttons after being dragged to a new location. Even in their new locations, all four of the buttons remain functional.

Screenshot

Screenshot-14. Creating a focus rectangle encompassing the top two buttons.

Screenshot

Screenshot-15. Moving the button controls by dragging the focus rectangle.

NOTE
To drag the focus area, the user must click on an area within the focus rectangle but outside any of the controls. If you click within a control such as the button, WFC will field the mouse down event before SelectComponts gets a chance to interpret it.
Comments