WFC-Style Controls

Let's write a new control that we'll call WeightEdit. This control will look like an Edit control, but it will include a new property, weight. The weight property will have three possible values: Light, Normal, and Bold. Initially, we'll require the user to enter an integer value to indicate his or her preference: 0 for Light, 1 for Normal, and 2 for Bold. Any other value will be ignored. Later on, we'll be sophisticated enough to offer the user a drop-down menu from which to choose the values Light, Normal, and Bold.

To keep things simple, the WeightEdit control will support only a single line of text, and the other properties of the Edit control won't be supported. (Later in this chapter, you'll find out how you can waive this restriction.)

Integer WeightEdit Control

Let's begin with the integer-based WeightEdit control. The plan is first to create a form for our WeightEdit control. After that, we'll need to define the weight property and add an onPaint() method capable of displaying the value of the text property in a Light, Normal, or Bold font, based on the value of weight. In this initial version of our WeightEdit control, the weight property will contain an integer value.

We'll add the weight property to the Properties window to allow the app programmer to set the value of this newly created property at design time. To complete the WeightEdit control, we'll add an edit capability to allow the user to modify the text displayed in the control.

To demonstrate our WFC-style WeightEdit control, we'll add it to the Toolbox and then use it in a test app.

Creating the control framework using the Control Wizard

Begin by choosing New Project from the Visual J Plus Plus File menu. Select Components from the Visual J Plus Plus Projects folder in the left pane of the New Project window. From there, select Control in the right pane and name the project WeightEdit . Your window should look like the one shown in Figure 13-1.

Once your window looks like the one in Figure 13-1, click Open to create the project for the WeightEdit control. Opening the project using the Project Explorer reveals a single file with the rather nondescriptive name Control1.java. Double-clicking this file opens a form much like the one that the Windows app Wizard generates. Close the form before proceeding to the next section.

Java Click to view at full size.

Screenshot-1. You can create a WFC-style control by starting the Control Wizard from the New Project window.

Code

To open the Control1.java file, select Code from the View menu. You'll notice three significant differences between this source file and the source files that we have seen in previous chapters:

The GUID is a number that would uniquely identify this control were we to convert it into an ActiveX component. Because we plan to leave WeightEdit as a WFC-style control, however, we can leave the GUID commented out.

Creating the WeightEdit framework

Rather than stick with the name Control1.java, let's rename the file to match the name of the project. First close the source listing. Now rename Control1.java by right-clicking the file in the Project Explorer window and choosing Rename. Change Control1.java to WeightEdit.java.

Open the WeightEdit file again. Within the source file window, rename the class WeightEdit. Change the name of the constructor to match. Also change the line this.setText("Control1") in the initForm() method to this.setText("WeightEdit"). Save the file.

Double-click WeightEdit in Project Explorer to open the control in the Forms Designer. The title bar of the Forms Designer will have the title WeightEdit.java [Form]. This form will be the basis of our new control.

Because our WeightEdit control supports only a single line of text, resize the form to the height of a normal font (or slightly larger) and to the length of about half that of the default length of the Forms Designer window. The actual size is unimportant because the final WeightEdit control will be resizable. Leave the remaining properties in the Properties window at their default values.

The resulting code so far should look like this:

import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
/**
 * This class is a visual component. The entry point for class execution
 * is the constructor.
 * * This class can be used as an ActiveX control. Check the checkbox * for this class on the Project Properties COM Classes tab, or remove * the // from the next line:
// * @com.register ( clsid=xxx, typelib=xxx )
 */
public class WeightEdit extends UserControl
{
 public WeightEdit()
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 // TODO: Add any constructor code after initForm call
 }
 /**
 * 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()
 {
 this.setSize(new Point(267, 28));
 this.setText("WeightEdit");
 }
 // NOTE: End of form designer support code
 public static class ClassInfo extends UserControl.ClassInfo
 {
 // TODO: Add your property and event infos here
 }
}


Notice that the clsid and typelib values have been replaced with xxx in this listing because these values are unique for each instance of WeightEdit.

To make sure that everything still works, build the project. No errors should occur. (Of course, our control doesn't do anything yet!)

Defining the weight property

To support the weight property, we'll need to define an integer data member named weight within the WeightEdit class. For convenience, let's add three constants representing the three legal values of weight. Set LIGHT to 0, NORMAL to 1, and BOLD to 2. Add the following code to the WeightEdit class to define the weight property:

public class WeightEdit extends UserControl
{
 // the different legal values of weight
 private final static int LIGHT = 0;
 private final static int NORMAL = 1;
 private final static int BOLD = 2;
 // the weight control value - start with weight NORMAL
 private int weight = NORMAL;
 public WeightEdit()
 {
 .
 .
 .
}


Displaying text using the proper weight

To change the way an object displays text, we'll need to write our own onPaint() method using the font weight specified by the local weight value. (For now, let's not worry about how the weight value is updated.) The following onPaint() method—added to our WeightEdit class—paints text using our weight value:

 // handle changes to the control
 protected void onPaint(PaintEvent pe)
 {
 Graphics g = pe.graphics;
 // set the font for the display
 Font f = this.getFont();
 int w;
 switch(weight)
 {
 case LIGHT: w = FontWeight.EXTRALIGHT;
 break;
 case BOLD: w = FontWeight.BOLD;
 break;
 default: w = FontWeight.NORMAL;
 }
 Font fw = new Font(f, w, false, false, false);
 g.setFont(fw);
 // get the text to display
 String s = this.getText();
 // draw text in the middle of a rectangle
 // of the specified size
 Rectangle r = getClientRect();
 g.drawString(s, r, TextFormat.HORIZONTALCENTER|
 TextFormat.VERTICALCENTER);
 }


This onPaint() method starts just as any other onPaint() method does, by retrieving the Graphics object from the PaintEvent. From there, this method sets the value w to the appropriate font weight, depending on the weight values specified by the weight property. A new font, fw, is created from the existing font but with the new font weight. The Graphics object is then updated with this font using the method setFont().

The call to Control.getText() retrieves the current string. The onPaint() method then displays the text in the middle of the rectangle specified by the size of the WeightEdit control.

Saving the user-entered text

Fortunately, the Control class has a text property. Although the text property means different things to different types of controls, we can use this property to save the text the user entered. As we saw in the onPaint() method, the Control class contains a getText() method that we can use without customizing it. However, we'll need to override the setText() method to add a call to invalidate(). The method invalidate() will force the new text string to be repainted with our onPaint() method, which reflects the weight factor. We'll also want to override the onResize() method to repaint the text, to keep the user-entered text in the middle of the control's area:

 /**
 * Display the string again whenever it changes.
 */
 public void setText(String s)
 {
 super.setText(s);
 invalidate();
 }
 /**
 * Repaint the window when it is resized.
 */
 protected void onResize(Event e)
 {
 super.onResize(e);
 invalidate();
 }


Updating the weight value

It's all very nice to define a weight property, but a property isn't much use if the user can't set it. To allow the user to set the property, add the following access methods:

 //---------the following defines the property methods----
 /**
 * Return the font weight.
 */
 public int getWeight()
 {
 return weight;
 }
 /**
 * Set the font weight.
 */
 public void setWeight(int w)
 {
 // if the new font weight is valid…
 if (w >= LIGHT && w <= BOLD)
 {
 // update the weight
 this.weight = w;
 }
 // make sure the display is updated with the
 // current weight
 this.invalidate();
 }
 /**
 * Reset the weight to its default value.
 */
 public void resetWeight()
 {
 weight = NORMAL;
 }


The Visual J Plus Plus v6 property editor uses access methods that follow the pattern getX() and setX() to access the property x. The resetX() method sets the initial value of the x property.

Adding weight to the property editor

If you look toward the bottom of the WeightEdit source file, you'll see an inner class named ClassInfo. This class is the key to adding a property or set of properties to the property editor. Update the ClassInfo class as follows:

 public static class ClassInfo extends UserControl.ClassInfo
 {
 // add our property to the property list
 public static final PropertyInfo weightProperty
 = new PropertyInfo
 (
 WeightEdit.class,
 "weight",
 int.class
 );
 public void getProperties(IProperties props)
 {
 super.getProperties(props);
 props.add(weightProperty);
 }
 }


The first line defines a static data member of class PropertyInfo. This object ties the property named weight to the current class file, WeightEdit.class, and defines this property to be of type int. This object definition tells the property editor the name to display in the Properties window (weight), the name of the class (WeightEdit) that contains the getWeight() and setWeight() methods, and the type of the weight property (int).

The class ClassInfo overrides the method getProperties(). The getProperties() method is what the property editor calls to find the properties to display in the Properties window. This new version of getProperties() starts by retrieving the previously defined properties through the call to super.getProperties(). To this list of properties, getProperties() adds the newly created property weightProperty.

Supporting user input

So far, what we've really defined is a control equivalent to a label. We now need to add some code that will allow the user to edit the control.

Open WeightEdit in the Forms Designer. Select the WeightEdit control, and in the Properties window double-click the active properties keyPress, click, mouseEnter, and mouseLeave. Update the resulting event handlers as follows:

// ----the following code creates the actual property editor---
 // maintain a flag indicating whether the user has clicked
 // within the control and whether or not the mouse is still there boolean selected = false;
 boolean clicked = false;
 private static final char BACKSPACE = (char)8;
 /**
 * Process the keypress by adding the key to the
 * displayed text if the control is currently active.
 */
 private void WeightEdit_keyPress(Object source, KeyPressEvent e)
 {
 // if the object is selected…
 if (clicked)
 {
 // then update the string
 String s = this.getText();
 char c = e.getKeyChar();
 if (c == BACKSPACE)
 {
 int length = s.length();
 s = s.substring(0, length - 1);
 }
 else
 {
 s = s + c;
 }
 setText(s);
 }
 }
 /**
 * If the mouse is within the control,
 * then set the clicked flag.
 */
 private void WeightEdit_click(Object source, Event e)
 {
 if (selected)
 {
 clicked = true;
 }
 }
 /**
 * Set the selected flag, indicating that the mouse is
 * within the area of the control.
 */
 private void WeightEdit_mouseEnter(Object source, Event e)
 {
 selected = true;
 }
 /**
 * Clear the selected and clicked flags as the mouse leaves
 * the control to indicate that the control is no longer active.
 */
 private void WeightEdit_mouseLeave(Object source, Event e)
 {
 selected = false;
 clicked = false;
 }


Let's start our discussion of the preceding code with the WeightEdit_mouseEnter() method. The WeightEdit_mouseEnter() method is called when the mouse pointer enters the area designated as the WeightEdit control. This method sets the selected data member to true, indicating that the mouse pointer is within the control. The method WeightEdit_mouseLeave() is invoked when the mouse pointer leaves the control. This method sets selected to false to indicate that the mouse pointer has left the WeightEdit control. (It also sets clicked to false, but we won't go over the significance of that until later in this discussion.)

When the user clicks the mouse, the method WeightEdit_click first checks the flag selected. If this flag is set to true, the method sets the flag clicked to true. Thus, the clicked flag is set to true when the mouse is clicked within the control and the mouse pointer has yet to leave the area of the control.

The WeightEdit_keyPress() method is invoked when the user presses a key. If the clicked flag isn't set, the key input is ignored. If clicked is set, WeightEdit processes the character. This control is a very simple editor (hardly worthy of the name editor): any character entered is appended to the string no matter where the user clicks within the WeightEdit control. The only character that gets special attention is the Backspace key, which deletes the last character in the string, again regardless of where the user clicks within the control. (The point of this editor is to show you the principles of creating a control. We don't want to get bogged down in detailed editor code.)

The string created by WeightEdit_keyPress() is displayed in the WeightEdit editor control by calling setText(). Keep in mind that our overriding setText() method updates the text field in the Control class before invalidating the display, so that the onPaint() method displays the resulting string to the user.

Viewing the total result

In case you're confused, I'll show you the entire WeightEdit listing here:

import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.lang.*;
/**
 * The WeightEdit class extends Control by adding the
 * weight property. // * @com.register ( clsid=xxx, typelib=xxx )
 */
public class WeightEdit extends UserControl
{
 // the different legal values of weight
 private final static int LIGHT = 0;
 private final static int NORMAL = 1;
 private final static int BOLD = 2;
 // the weight control value - start with weight NORMAL
 private int weight = NORMAL;
 public WeightEdit()
 {
 // Required for Visual J Plus Plus Form Designer support
 initForm();
 }
 /**
 * Display the string again whenever it changes.
 */
 public void setText(String s)
 {
 super.setText(s);
 invalidate();
 }
 /**
 * Repaint the window when it is resized.
 */
 protected void onResize(Event e)
 {
 super.onResize(e);
 invalidate();
 }
 /**
 * Reflect the weight property.
 */
 // handle changes to the control
 protected void onPaint(PaintEvent pe)
 {
 Graphics g = pe.graphics;
 // set the font for the display
 Font f = this.getFont();
 int w;
 switch(weight)
 {
 case LIGHT: w = FontWeight.EXTRALIGHT;
 break;
 case BOLD: w = FontWeight.BOLD;
 break;
 default: w = FontWeight.NORMAL;
 }
 Font fw = new Font(f, w, false, false, false);
 g.setFont(fw);
 // get the text to display
 String s = this.getText();
 // draw text in the middle of a rectangle
 // of the specified size
 Rectangle r = getClientRect();
 g.drawString(s, r, TextFormat.HORIZONTALCENTER|
 TextFormat.VERTICALCENTER);
 }
 //---------the following defines the property methods----
 /**
 * Return the font weight.
 */
 public int getWeight()
 {
 return weight;
 }
 /**
 * Set the font weight.
 */
 public void setWeight(int w)
 {
 // if the new font weight is valid…
 if (w >= LIGHT && w <= BOLD)
 {
 // then update the weight
 this.weight = w;
 }
 // make sure the display is updated with the
 // current weight
 this.invalidate();
 }
 /**
 * Reset the weight to its default value.
 */
 public void resetWeight()
 {
 weight = NORMAL;
 }
 // ----the following code creates the actual property editor---
 // maintain a flag indicating whether the user has clicked
 // within the control and whether or not the mouse is still there boolean selected = false;
 boolean clicked = false;
 private static final char BACKSPACE = (char)8;
 /**
 * Process the keypress by adding the key to the
 * displayed text if the control is currently active.
 */
 private void WeightEdit_keyPress(Object source, KeyPressEvent e)
 {
 // if the object is selected…
 if (clicked)
 {
 // update the string
 String s = this.getText();
 char c = e.getKeyChar();
 if (c == BACKSPACE)
 {
 int length = s.length();
 s = s.substring(0, length - 1);
 }
 else
 {
 s = s + c;
 }
 setText(s);
 }
 }
 /**
 * If the mouse is within the control, * set the clicked flag.
 */
 private void WeightEdit_click(Object source, Event e)
 {
 if (selected)
 {
 clicked = true;
 }
 }
 /**
 * Set the selected flag, indicating that the mouse is
 * within the area of the control.
 */
 private void WeightEdit_mouseEnter(Object source, Event e)
 {
 selected = true;
 }
 /**
 * Clear the selected and clicked flags as the mouse leaves
 * the control to indicate that the control is no longer active.
 */
 private void WeightEdit_mouseLeave(Object source, Event e)
 {
 selected = false;
 clicked = false;
 }
 /**
 * 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()
 {
 this.setBackColor(Color.CONTROL);
 this.setSize(new Point(235, 28));
 this.setText("");
 this.addOnClick(new EventHandler(this.WeightEdit_click));
 this.addOnKeyPress(
 new KeyPressEventHandler(this.WeightEdit_keyPress));
 this.addOnMouseEnter(
 new EventHandler(this.WeightEdit_mouseEnter));
 this.addOnMouseLeave(
 new EventHandler(this.WeightEdit_mouseLeave));
 }
 // NOTE: End of form designer support code
 /**
 * Add weight to the properties defined for this control.
 */
 public static class ClassInfo extends UserControl.ClassInfo
 {
 // add our property to the property list
 public static final PropertyInfo weightProperty
 = new PropertyInfo
 (
 WeightEdit.class,
 "weight",
 int.class
 );
 public void getProperties(IProperties props)
 {
 super.getProperties(props);
 props.add(weightProperty);
 }
 }
}


Adding the WeightEdit control to the Toolbox

Once you've compiled the WeightEdit class, you must still add the control to the Toolbox before other programs can use it. Before closing WeightEdit, select the General section in the Toolbox. (You can put the WeightEdit tool in any section of the Toolbox, but I prefer to place newly created tools within the General area.)

Right-click within the Toolbox, and choose Customize Toolbox. Select the WFC Controls tab in the Customize Toolbox window. You'll see a list of WFC controls that you can add to the Toolbox. Controls that are checked already appear in the Toolbox. These are the WFC controls that you've been using in the examples in this tutorial up until now.

Scroll toward the bottom of the list. Somewhat remarkably (at least to me), there it is, our WeightEdit control, at the bottom of the list. Select the check box next to the WeightEdit entry as shown in Figure 13-2, then choose OK to add the control to the Toolbox. (It will appear in the Toolbox as an outline of a WFC-style control icon—without a target program, this control and all others are not selectable.)

NOTE
If the control doesn't appear in the Customize Toolbox window, try closing the WeightEdit solution by choosing Close All from the File menu and then reloading the solution.

Java Click to view at full size.

Screenshot-2. The Customize Toolbox window shows our WeightEdit control being added to the Toolbox.

Handling Errors in User-Defined Controls
I've found that changes to a user-defined control often don't take effect until the project containing the control has been closed and reopened. I've also found that if I've made a mistake in creating a control such as WeightEdit, I might not discover it until I've tried to use the control in an app. The most common error that occurs in creating user-defined controls is forgetting to provide both a getX() and a setX() method, or providing one that is declared incorrectly (for example, declaring a void setX() method instead of the correct void setX(int)). Unfortunately, if you do have such an error when using a user-defined control, the Forms Designer gets confused and removes the declaration from the form's initForm() method.

For example, let's say that your initForm() method originally looked like this:

Control control = new Control();
WeightEdit weightEdit1 = new WeightEdit();
void initForm()
{
 .
 .
 .
}


After the usage error, your form code will look like this:

Control control = new Control();
void initForm()
{
 .
 .
 .
}


The references to weightEdit1 within the initForm() code remain, however, so not only does the app code not compile but you can't even open the form in the Forms Designer. (You get this "helpful" message: "Unable to show the form designer…Fix the error, and then try to view the form again.")

Thus, if you make a mistake in your user-defined control, you'll need to take three steps to fix the error:

  1. Fix the problem in the user-defined control code.
  2. Close and reopen the project containing the control.
  3. Add the declaration of the user-defined control object back into the initForm() code.

Using this round about-solution, you'll be able to fix any problems that arise in the code generated by the Forms Designer.

Using the WeightEdit control within the Forms Designer

To add the WeightEdit control to a program, you'll first need to create a program. With the WeightEdit solution still open, choose Add Project from the File menu, and then select Windows app. Enter the name WeightEditTest, make sure the Add To Current Solution option is selected, and choose Open. You've just added a new project to the solution.

NOTE
A solution consists of one or more projects. Each of these projects acts as an independent program, with one exception: Only one project can contain the main() method with which to start. You must indicate which project contains this method by using the Project Properties dialog box.

Open the WeightEditTest project in Project Explorer to reveal a conventional Windows executable-style Form1.java file. Double-click this file to open the form in the Forms Designer.

Once you've opened the form, return to the General section of the Toolbox. Notice that WeightEdit is now available. Drop a WeightEdit control onto the form. The resulting form window should look like the one shown in Figure 13-3.

Screenshot

Screenshot-3. The WeightEdit control looks like this in an app.

Now open the Properties window, and select the WeightEdit control. Notice that the text property displays the default display text for the control. Also notice the appearance of our newly added weight property with the initial value of 1. (The default of 1 came from the control's resetWeight() method.) The Properties window with these settings is shown in Figure 13-4.

The text property in the Properties window is synchronized with the text displayed within the WeightEdit control. To demonstrate this synchronization, click the WeightEdit control on the form and begin typing. (Be sure to leave the mouse pointer within the control while you're typing.) Notice that the text property within the Properties window automatically updates. Now click the text property and type something new. Notice that the contents of the WeightEdit control update as you type. This automatic updating is a direct result of using the getText() and setText() methods of the Control class to retrieve and update the text.

Screenshot

Screenshot-4. The Properties window shows an updated text property and the new weight property.

While still in the Forms Designer, and with the WeightEdit control selected, set the weight property in the Properties window to 2. The display immediately converts to Bold. Now set the value to 0, and the display turns to Light. (Some display drivers can't display the text with a Light font type to look any different than text with a Normal font type.)

Using the WeightEdit control

Set the WeightEdit control to whatever initial text and weight you prefer. Now compile the Form1 app. We're going to run the app from within the integrated development environment (IDE), but first you must tell Visual J Plus Plus which project within the solution contains the app. Point at the WeightEditTest project in Project Explorer, and hold down the right mouse button. From the drop-down menu, choose Set As StartUp Project.

When you run WeightEditTest, you'll see a WeightEdit control appearing within the form with the text you entered at the weight you specified in the Properties window. Click the text with the mouse pointer, and begin typing (or hitting the Backspace key) with the mouse pointer still at the position where you clicked. Whatever you type is appended to the text. (Remember that the text is appended to the end no matter where in the string you click. I warned you this control wasn't much of an editor, but it does demonstrate the principles of creating and using a user-defined control.)

Screenshot-5 shows the program with text displayed in the WeightEdit control in the middle of the form.

Screenshot

Screenshot-5. The WeightEditTest program runs with our newly defined WeightEdit control.

Making the WeightEdit control available in other solutions

The WeightEdit control was available to WeightEditTest from within the IDE because the two were built together as part of the same project. It would be clumsy if we had to include the WeightEdit project within a solution every time we wanted to use the control. In addition, if you attempt to run the test app by double-clicking the WeightEditTest.exe file, you'll notice that the program fails and displays an error message indicating that it couldn't find the WeightEdit.class files.

When a Visual J Plus Plus app begins, it looks in a number of different places for the .class files needed to run Toolbox controls. It always looks in the current directory first. If it doesn't find the .class files there, it next looks in the directory \Windows\Java\Classes.

Copy the two .class files from the WeightEdit directory to the \Windows\Java\Classes directory. WeightEditTest.exe now runs properly directly from Windows or from the IDE. In addition, you can now remove the WeightEdit project from the common solution we've been working with, and recompile and run the result. WeightEditTest continues to run from the IDE even without WeightEdit in the same solution.

NOTE
You'll find this version of the WeightEdit control on the companion CD in the subdirectory Custom Controls\Integer WFC-Style WeightEdit.

Enumerated WeightEdit Control

As neat as the integer version of the WeightEdit control is, it would be that much better if the user were provided with the options Light, Normal, and Bold rather than the integer values 0, 1, and 2. In this section, we'll change the integer version of the WeightEdit control so that it uses an enumerated weight property.

Exposing the weight property values

In the integer version of the WeightEdit control, we defined the different legal values of the weight property in a set of static final integers that were private to the WeightEdit class. By being private, no outside program could access these values.

To make the weight constants available to the Properties window, we'll need to make these properties public. Even this isn't enough. The class that contains the properties must also extend the class Enum.

NOTE
Recall that the class Enum provides enumerated sequences of values.

Because the enumerated weight class must be declared public, and because two public classes can't reside in the same source file, we'll need to create a separate Visual J Plus Plus source file in the WeightEdit project. To create a new public class under the WeightEdit project, choose Add Class from the Project menu and name this new class WeightValues (though the name isn't really important).

Open the new class in Visual J Plus Plus, and append the extends Enum statement to the class declaration. You'll need to import com.ms.wfc.core.Enum to gain access to the Enum class. Now move the weight constants that were defined in the WeightEdit class to this new class and change their declarations from private to public. In addition, add the public static method valid(). This new method returns true if the integer passed to it is a legal weight value and false otherwise. The Properties window and user programs use this method to filter out illegal input from the user.

Once you're finished, the WeightValues class should look like this:

import com.ms.wfc.core.Enum;
/**
 * Define the names of the different weights.
 */
public class WeightValues extends Enum
{
 // the different legal values of weight
 public final static int LIGHT = 0;
 public final static int NORMAL = 1;
 public final static int BOLD = 2;
 public static boolean valid(int i)
 {
 return (i <= LIGHT) && (i <= BOLD);
 }
}


Making changes to the WeightEdit class

All references within the WeightEdit class to these constants must now be changed to refer to this class. For example, you must change the previous reference to NORMAL (or 1) to WeightValues.NORMAL. You can also use the new valid() method to replace the line if (w >= LIGHT && w <= BOLD) in the setWeight() method with the line if (WeightValues.valid(w)).

The only other change we need to make is to the PropertyInfo object within the WeightEdit class. You must edit the ClassInfo inner class to appear as follows:

 public static class ClassInfo extends UserControl.ClassInfo
 {
 // add our property to the property list
 public static final PropertyInfo weightProperty
 = new PropertyInfo
 (
 WeightEdit.class,
 "weight",
 WeightValues.class
 );
 public void getProperties(IProperties props)
 {
 super.getProperties(props);
 props.add(weightProperty);
 }
 }


Nothing has changed within ClassInfo, other than the fact that the weightProperty declaration now refers to WeightValues.class for the type of values to expect rather than to int.class. The class passed to the PropertyInfo constructor is where the property editor looks to find the names of the enumerated constants.

Using the enumerated WeightEdit control

Return to the Toolbox, and remove the WeightEdit control by right-clicking it and selecting Delete Item. (Don't worry, the file won't be deleted.) Now recompile the entire solution. (The WeightEditTest project will be recompiled, too, which is unnecessary but doesn't hurt.) Close the solution, and reopen it to make the changes take effect.

NOTE
Keep in mind that you might need to close the solution and reopen it before changes to user-defined controls take effect..

Reopen the WeightEditTest class in the Forms Designer. Select the Properties window, and then select the WeightEdit control. (If you've deleted the control, just add it to the form again.) If you examine the weight property, rather than seeing the value 1, as you did before, you'll see the enumerated value Normal. Even better, if you select the weight property, a drop-down list that displays the valid values for weight in text form appears, as shown in Figure 13-6. Other than that, the enumerated WeightEdit control works exactly the same as the integer WeightEdit control.

NOTE
You'll find this version of the WeightEdit control on the companion CD in the subdirectory Custom Controls\Enumerated WFC-Style WeightEdit.

Screenshot

Screenshot-6. Enumerated weight values are passed on to the Properties window.

Adding Events to WeightEdit

Not only can you add properties, such as weight, to a user-defined control, but you can also add user-defined events, using a technique similar to the one we've just discussed for adding properties. Let's add a WeightEvent that occurs when the weight changes.

Adding a new event involves the following steps:

  1. Create the event class.
  2. Define a delegate for the event class.
  3. Add the event to the Properties window.
  4. Update the WeightEdit control to generate the new event.

Defining the WeightEvent class

Before we can start, we must define an event class and a purpose. Let's call this class WeightEvent, and say that we want it to report changes in the font weight of a WeightEdit control.

Because user code must be able to access WeightEvent, we must declare the class public. Therefore, we must define WeightEvent in its own WeightEvent.java file. In addition, if the user code is to access the value of the new weight, we must also declare the weight property public. (If you prefer, you can leave the weight property private, and provide a public getWeight() method.)

Select the WeightEdit project in Visual J Plus Plus Project Explorer. Now choose Add Class from the Project menu. Name the class WeightEvent. Update the class as follows:

import com.ms.wfc.core.Event;
/**
 * This event is thrown when the weight of
 * the WeightEdit class changes.
 */
public class WeightEvent extends Event
{
 // the new weight
 public int weightChange;
 /**
 * Create an event to announce a change
 * in the weight property.
 * @param weightChange - the new weight value
 */
 public WeightEvent(int weightChange)
 {
 this.weightChange = weightChange;
 }
}


WeightEvent is a true event because it extends the Event class. The Event class contains no data, and its constructor takes no arguments. The WeightEvent class has one data member, wieghtChange, to store the new weight, and its constructor accepts one argument, the value to be stored in weightChange.

Defining a WeightEvent delegate

Each new event requires a set of access methods in the same way that a new data property requires a pair of getX() and setX() methods. If you examine the event methods already supported by different types of controls in the Visual J Plus Plus v6 subset of the MSDN help library, you'll notice that for each event type X there is an addOnX method, and a removeOnX() method, and sometimes an onX() method.

The add method adds a delegate to the list of delegates prepared to handle the event. The remove method removes the specified delegate from the list.

NOTE
A delegate is an object that contains a reference to a method. All delegates are subclassed from the abstract class Delegate.

The onX() method calls each of the registered delegates, passing them the X event. Our new WeightEvent is no different.

NOTE
The order in which the delegates are invoked isn't specified and isn't the order in which they are added. In addition, if one of the delegate methods throws an exception, execution of the delegate list is halted. Control passes up the stack until the first error-handling catch of the proper type to match the thrown object is encountered.

Following the same pattern, we'll need to add the methods addOnWeightEvent(), removeOnWeightEvent(), and onWeightEvent() to our WeightEdit class. Before we can do this, however, we need to define a WeightEventDelegate class. We must declare this delegate class public; if we don't, the user-created delegate functions won't be able to process the events sent to them. As mentioned earlier, because WeightEvent is public, we are forced to create a new source file, WeightEventDelegate.java, that contains the public WeightEventDelegate class.

Here is the WeightEventDelegate class file:

/**
 * Handle the WeightEvent.
 */
import com.ms.wfc.core.*;
public final multicast delegate void WeightEventDelegate(Object sender, WeightEvent e);


As you can see, this file is nothing more than the declaration of a multicast delegate designed to handle the WeightEvent class. The delegate must be declared multicast to support the distinct possibility that two or more delegates will attach themselves to the WeightEvent event.

Adding the WeightEvent handlers to WeightEdit

In some ways, the changes we need to make to our principal class, WeightEdit, to incorporate the new event are predictable. As mentioned earlier, WeightEdit must provide an addOnWeightEvent() method, a removeOnWeightEvent() method, and an onWeightEvent() method. These methods are shown in the following code, which is extracted from the full WeightEdit code listing:

 // initially, the WeightEvent has no existing handlers;
 // these are added by calling addOnWeightEvent()
 private WeightEventDelegate allWeightDelegates = null;
 /**
 * Provide a method to add new delegates to the list of
 * delegates to notify of the WeightEvent.
 */
 public void addOnWeightEvent(WeightEventDelegate addHandler)
 {
 allWeightDelegates = (WeightEventDelegate)
 Delegate.combine(
 allWeightDelegates,
 addHandler);
 }
 /**
 * Remove a WeightEvent handler.
 */
 public void removeOnWeightEvent(WeightEventDelegate rmvHandler)
 {
 if (allWeightDelegates != null)
 {
 allWeightDelegates = (WeightEventDelegate)
 Delegate.remove(
 allWeightDelegates, rmvHandler);
 }
 }
 /**
 * Run the WeightEvent.
 */
 protected void onWeightEvent(WeightEvent event)
 {
 // if there is any WeightEvent delegate…
 if (allWeightDelegates != null)
 {
 // then process it
 allWeightDelegates.invoke(this, event);
 }
 }


The data member allWeightDelegates is a listing of all the method delegates that are interested in receiving the WeightEvent event. Initially, this list is null. Although the name of this data member isn't important, the names of the following three methods are critical.

The addOnWeightEvent() method takes a WeightEventDelegate object as its argument. The addOnWeightEvent() method adds the argument addHandler to the list allWeightDelegates by passing both a reference to the list, and the new delegate addHandler, to the Delegate.combine() method. The result is a new list containing the previous delegates plus addHandler. This list is saved back into allWeightDelegates. We declare the addOnWeightEvent() method public so that all classes have access to it.

NOTE
The Delegate class is part of the Microsoft Software Development Kit (SDK) for Java. The class has few methods, all of which are devoted to event-type operations, such as the combine() method used in our addOnWeightEvent() method. The Delegate class is included in the package com.ms.lang, which means that you'll need to add the statement import com.ms.lang.* to the beginning of WeightEdit.

Similarly, removeOnWeightEvent() uses the Delegate.remove() method to remove a WeightEventDelegate object from the list. You also need to declare this method public.

The onWeightEvent() method is invoked when the WeightEvent occurs. This method starts by examining the allWeightDelegates delegate list. If allWeightDelegates is not null, onWeightEvent() calls allWeightDelegates.invoke(), which invokes each attached WeightEventDelegate object.

Adding WeightEvent to the active properties list

Adding the addWeightEvent(), removeWeightEvent(), and onWeightEvent() methods to WeightEdit is necessary but insufficient to allow user programs to implement these methods. We must also add the WeightEvent property to the active properties displayed in the Properties window. To do this, we need to add another property to the ClassInfo inner class, as we did when we added the weight data property to the Properties list.

The following listing is the entire ClassInfo class containing the code for the new property addition:

 public static class ClassInfo extends UserControl.ClassInfo
 {
 //------------------add the data properties-------------
 // add our property to the property list
 public static final PropertyInfo weightProperty
 = new PropertyInfo
 (
 WeightEdit.class,
 "weight",
 WeightValues.class
 );
 public void getProperties(IProperties props)
 {
 super.getProperties(props);
 props.add(weightProperty);
 }
 //------------------add the event properties----------
 public static final EventInfo weightEvent
 = new EventInfo(
 WeightEdit.class,
 "weightEvent",
 WeightEventDelegate.class
 );
 public void getEvents(IEvents events)
 { super.getEvents(events);
 events.add(weightEvent);
 }
 }


The data properties section is completely unchanged from the code we added earlier to the WeightEdit class. Following that, however, is the declaration of a weightEvent object of class EventInfo. The EventInfo constructor attaches the event named weightEvent to the WeightEdit class, which contains the weightEvent handler. The last argument specifies the delegate that will handle the weightEvent.

When Visual J Plus Plus calls the getEvents() method to get the list of events, getEvents() adds our weightEvent object to those events already defined.

Updating WeightEdit to generate a WeightEvent

Creating a new weightEvent object means nothing if WeightEdit doesn't generate it when the weight is updated. Let's consider this question for a moment: When would an event to inform the system that the weight has changed be sent? The answer is, obviously, when the weight is changed, which is in the setWeight() method. The following change (shown in boldface) to setWeight() is all that is needed to send the WeightEvent event notification to the system:

 /**
 * Sets the font weight.
 */
 public void setWeight(int w)
 {
 // if the new font weight is valid…
 if (WeightValues.valid(w))
 {
 // update the weight
 this.weight = w;
 // notify the world of the change in weight
 onWeightEvent(new WeightEvent(w));
 }
 // make sure the display is updated with the
 // current weight
  this.invalidate();
 }


As you can see, the only change to setWeight() is the addition of a call to onWeightEvent() if the weight changes. Even if the value hasn't changed, you should still call onWeightEvent() to inform other classes that a weight change was attempted.

The call to onWeightEvent() passes an object of class WeightEvent—which contains the new weight w—to all classes that have previously declared their interest in this event by calling addOnWeightEvent(). Refer to the WeightEvent constructor and the onWeightEvent() method if this line of code isn't clear.

Adding the new WeightEdit control to the Toolbox

Save the modified files, and compile the solution. In the General section of the Toolbox, right-click to open the context menu and choose Customize Toolbox. To add the WeightEdit control to the Toolbox, select WeightEdit from the list of available controls on the WFC Controls tab of the Customize Toolbox dialog box.

Viewing weightEvent in the active Properties window

To know whether our WeightEvent works as planned, we must first check the list of events in the Properties window to make sure that our event is included.

First open the WeightEditTest form in the Forms Designer. Now open the Properties window. Select the WeightEdit control in the middle of the WeightEditTest form. (If you've previously deleted the WeightEdit object from the form, just add it back and select it.) Now examine the list of active properties. You should be able to find the weightEvent event listed among the other events. (Events are easier to find if you list them in alphabetical order.)

Screenshot-7 shows the event properties for the WeightEdit control. Our new weight event appears in the list. So far, so good.

Screenshot

Screenshot-7. This list of event properties for the WeightEdit class shows our new weightEvent event.

Using WeightEvent in a program

Now comes the final test. Let's process the weight event in our test program.

Still looking at the WeightEditTest form, open the Properties window, and select the WeightEdit object. Now double-click the weightEvent property. This should create an empty method in your WeightEditTest class as follows:

void weightEdit1_weightEvent(Object source, WeightEvent e)
{
}


Save the form, and then look through the code the Forms Designer generated. You'll find a new call to addOnWeightEvent(). This method adds a WeightEventDelegate object that references the newly created weightEdit1_weightEvent() method to the list of delegates interested in processing the weight event.

To prove that weightEdit1_weightEvent() does get invoked when the weight changes, let's add a call to setText() that changes the text in the WeightEdit control to a description of what the new weight value is. This will make it instantly obvious that the weightEdit1_weightEvent() method is called. Remember that it's setWeight(), not setText(), that invokes the WeightEvent.

NOTE
If the event handler makes a call to a method that invokes the event being processed, the program will hang in an endless loop until it eventually runs out of stack and crashes.

The following code should do the trick:

 /**
 * Update the text to reflect the new weight whenever a
 * weightEvent occurs.
 */
 private void weightEdit1_weightEvent(Object source, WeightEvent e)
 {
 int newWeight = e.weightChange;
 weightEdit1.setText("Weight changed to " +
 newWeight);
 }


This code is fine for processing the weightEvent when it occurs, but something has to trigger the event. In other words, some method has to change the weight property.

Even though this technique isn't very sophisticated, let's just add a double-click method to the WeightEditTest code to update the weight. With the WeightEdit control selected in Forms Designer, double-click the doubleClick active property to create the weightEdit1_doubleClick() method. Next modify the method as follows:

 /**
 * Change the weight whenever the user double-clicks the
 * WeightEdit control (simply to test the weightEvent
 * active property).
 */
 private void weightEdit1_doubleClick(Object source, Event e)
 {
 int w = weightEdit1.getWeight();
 w++;
 if (w > 2)
 {
 w = 0;
 }
 weightEdit1.setWeight(w);
 }


This version gets the current weight of the weightEdit1 object by calling getWeight(). From there, it increments the weight until it becomes larger than 2, at which point it resets the weight back to 0. It then calls setWeight() to update the weight with the new value. This call to setWeight() generates a weightEvent that the previously registered weightEdit1_weightEvent() method should process. Now recompile the entire solution.

Giving the final test

Run the WeightEditTest program. Change the weightEdit1 text in your form to something like that shown in Figure 13-5. Make note of the font weight. Now double-click the weightEdit1 object. The weight changes instantly. Even more important, the text changes to display a string indicating the new weight. This test proves that our weightEdit1_weightEvent() method is being invoked as planned. My results are shown in Figure 13-8.

Screenshot

Screenshot-8. The weightEdit1 object looks like this after it's been double-clicked.

Double-click the object again, and you'll notice that both the value and the weight continue to change. The user-defined WeightEdit WFC control works.

NOTE
You'll find this version of the WeightEdit control on the companion CD in the subdirectory Custom Controls\Active WFC-Style WeightEdit.
Comments