DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 11.22 Using Tables to Arrange Form Elements

11.22.1 Problem

You want to use ActionScript to arrange the elements of a form on the screen.

11.22.2 Solution

Create and use a custom Table class.

11.22.3 Discussion

You can use ActionScript to control the position of any graphical object such as a movie clip, a button, or a text field, using the _x and _y properties. Additionally, each graphical object has properties that return its height and width (_height and _width). You can use these properties in concert to position instances on the Stage relative to one another.

For example, we can position a movie clip horizontally such that its x coordinate is five pixels to the right of a text field. The text field's _x property plus its _width property yields the x coordinate of the right edge of the text field. In this example, we add five pixels to allow for spacing between the movie clip and the text field. We also align the top of the movie clip with the top of the text field.

myMovieClip_mc._x = myTextField_txt._x + myTextField_txt._width + 5;
myMovieClip_mc._y = myTextField_txt._y;

In this example, we position the movie clip such that its y coordinate is five pixels below the bottom of the text field. This example is similar to the preceding one but uses the _y and _height properties to calculate the vertical position. We also align the left edges of the movie clip and the text field.

myMovieClip_mc._y = myTextField_txt._y + myTextField_txt._height + 5;
myMovieClip_mc._x = myTextField_txt._x;

The code we've looked at up to this point assumes that the movie clip we are positioning has its registration point in its upper-left corner. However, as you are likely aware, the registration point of a clip defaults to its center and can be placed at an arbitrary location. Fortunately, it is relatively simple to correct for this offset by subtracting the movie clip's minimum x and y values as obtained from the getBounds( ) method. Here, we revisit the two previous code examples, shown with offsets that accommodate movie clips with registration points of any kind.

This code positions a movie clip to the right of the text field:

bnds = myMovieClip_mc.getBounds(  );
myMovieClip_mc._x = myTextField_txt._x + myTextField_txt._width + 5 - bnds.xMin;
myMovieClip_mc._y = myTextField_txt._y - bnds.yMin;

This code positions the movie clip below the text field:

bnds = myMovieClip_mc.getBounds(  );
myMovieClip_mc._y = myTextField_txt._y + myTextField_txt._height + 5 - bnds.yMin;
myMovieClip_mc._x = myTextField_txt._x - bnds.xMin;

The preceding code examples show the logic that you can use to position instances relative to one another within a Flash movie. However, when you start working with more objects, your code can start to get long and confusing. To arrange 20 elements on a form requires at least 40 lines of code. A more effective way to approach this problem is to create a Table class that you can use to position any kind of graphical object on the Stage.

The following code creates a custom Table class that you can use to effectively arrange graphical objects. The Table class relies on two other custom classes—TableColumn and TableRow—that are also defined in the code that follows. Every table is composed of one or more rows, and every row is composed of one or more columns. The columns are, in turn, composed of elements. The elements can be any kind of graphical object (movie clip, button, or text field). A cell (the intersection of a row and a table) can contain a single table element. Alternatively, nested tables within tables allow a single cell to contain multiple elements, allowing for more advanced layouts.

Here is our code to define the Table, TableColumn, and TableRow classes. It should be placed in a Table.as file in your Include directory for future use. The Table class is designed so that the spacing works as it does in HTML tables—a single value is used for both the vertical and horizontal spacing applied around each element, column, and row. It is left as an exercise for you to redefine the class constructors to accept and implement separate horizontal and vertical spacing parameters.

// Define the TableColumn constructor. spacing defines the amount of space (in
// pixels) between elements in the column.
_global.TableColumn = function (spacing) {
  // Store the spacing parameter in an instance property of the same name. spacing 
  // defaults to 5 if not otherwise specified.
  this.spacing = (spacing == undefined) ? 5 : spacing;

  // The _width and _height properties store the total width and height of the
  // column. Initialize them to 0.
  this._width  = 0;
  this._height = 0;

  // The elements array holds all the elements of the column.
  this.elements = new Array(  );

  // If any parameters are passed to the constructor, from the second position
  // onward, add these values (assumed to be references to graphical objects 
  // or to a Table object) to the column using the addElement(  ) method.
  for (var i = 1; i < arguments.length; i++) {
    this.addElement(arguments[i]);
  }
};

// The addElement(  ) method adds elements to a column.
TableColumn.prototype.addElement = function (element) {

  if (element instanceof Table) {
    // If the element is a Table object, set the containsTable 
    // property to true. Reinitialize elements to ensure that the table 
    // is the only element in the column.
    this.containsTable = true;
    this.elements = new Array(  );
    this.elements.push(element);

    // Reset the width and height of the column.
    this._width  = 0;
    this._height = 0;
  }
  else {
    // Otherwise, the element is not a table. Reinitialize all the properties if the
    // column previously held a table.
    if (this.containsTable) {
      this.containsTable = false;
      this.elements = new Array(  );
      this._width  = 0;
      this._height = 0;
    }

    // Add the element to the elements array.
    this.elements.push(element);
  }

  // Make sure the column's width reflects the width of the widest element.
  this._width = Math.max(this._width, element._width);

  // Increment the column's height by the height of the element plus the spacing. 
  this._height += element._height + this.spacing; 
};

// TableColumn.render(  ) positions the elements within the column relative to one
// another. The startx and starty parameters give the x and y coordinates for the
// first element in the column. The TableColumn.render(  ) method is called by the
// render(  ) method of the row in which the column is contained.
TableColumn.prototype.render = function (startx, starty) {

  // The startx and starty parameters default to 0 if not specified.
  if (startx == undefined) {
    startx = 0;
  }
  if (starty == undefined) {
    starty = 0;
  }

  // If the column contains a table, call the render(  ) method of that table.
  // Otherwise, set the x and y coordinates for each element in the column.
  if (this.containsTable) {
    this.elements[0].render(true, startx, starty);
  }
  else {
    var bnds;
    for (var i = 0; i < this.elements.length; i++) {

      // Get the bounds of the elements in case the 
      // registration point is not in the upper-left corner.
      bnds = this.elements[i].getBounds(  );

      // The y coordinate of the element is given by starty plus the height of the
      // previous element in the column, plus the spacing between them. To
      // accommodate any offsets due to registration points, subtract the element's
      // minimum y coordinate.
      this.elements[i]._y = this.elements[i-1]._height + this.spacing +
                            starty - bnds.yMin;

      // The x coordinate of the element is given by startx plus spacing. To
      // accommodate any offsets due to registration points, subtract the element's
      // minimum x coordinate.
      this.elements[i]._x = startx + this.spacing - bnds.xMin;

      // Increment starty each time by the height of the previous element plus the
      // spacing between elements.
      starty += this.elements[i - 1]._height + this.spacing;
    }
  }
};

// removeElementAt(  ) removes an element from a column at the given index. Note that
// the index is zero-relative (the first column is column 0).
TableColumn.prototype.removeElementAt = function (index) {
  this.elements.splice(index, 1);
};

// The TableColumn.resize(  ) method recalculates the width and height of a column. It
// is called automatically by TableRow.resize(  ) (which is, in turn, called by
// Table.resize(  )).
TableColumn.prototype.resize = function (  ) {

  // Reset the width and height to 0.
  this._width  = 0;
  this._height = 0;

  // If the column contains a table, call the resize(  ) method of the table to ensure 
  // that the correct size of that table has been calculated.
  if (this.containsTable) {
    this.elements[0].resize(  );
  }

  // Set the column width to the widest element and calculate the column height.
  for (var i = 0; i < this.elements.length; i++) {
    this._width = Math.max(this._width, this.elements[i]._width);
    this._height += this.elements[i]._height + this.spacing;
  }
};

// Define the TableRow constructor. The spacing parameter defines the number of
// pixels between columns in the row.
_global.TableRow = function (spacing) {
  // Store the spacing parameter in an instance property of the same name. spacing 
  // defaults to 5 if not otherwise specified.
  this.spacing = (spacing == undefined) ? 5 : spacing;

  // The columns array contains all the columns in the row.
  this.columns = new Array(  );

  // Initialize the width and height of the row.
  this._width  = 0;
  this._height = 0;

  // If any parameters are passed to the constructor, from the second position
  // onward, add these values (references to columns) to the row using the
  // addColumn(  ) method.
  for (var i = 1; i < arguments.length; i++) {
    this.addColumn(arguments[i]);
  }
};

// The addColumn(  ) method adds columns to the row.
TableRow.prototype.addColumn = function (column) {

  // Add the column to the columns array.
  this.columns.push(column);

  // Increase the row's width by the width of the column plus the spacing. Also, if
  // the column has a greater height than any of the existing columns, set the row's
  // height to the height of the column.
  this._width += column._width + this.spacing;
  this._height = Math.max(this._height, column._height);
};

// TableRow.render(  ) positions the columns within a row relative to one another and
// relative to x and y coordinates given by startx and starty. TableRow.render(  ) is
// called automatically by the render(  ) method of the table that contains the row.
TableRow.prototype.render = function (startx, starty) {

  // Call each column's render(  ) method. Position each column to the right of the
  // preceding one.
  for (var i = 0; i < this.columns.length; i++) {
    this.columns[i].render(startx, starty);
    startx += this.columns[i]._width + this.spacing;
  }
};

// removeColumnAt(  ) removes a column from a row at the given index. Note that the
// index is zero-relative (the first row is row 0).
TableRow.prototype.removeColumnAt = function (index) {
  this.columns.splice(index, 1);
};

// TableRow.resize(  ) recalculates the height and width of a row. It is called
// automatically by Table.resize(  ).
TableRow.prototype.resize = function (  ) {

  // Reset the width and height to 0.
  this._width  = 0;
  this._height = 0;

  for (var i = 0; i < this.columns.length; i++) {
    // Recalculate the size of each column and use those values to calculate the
    // height and width for the row.
    this.columns[i].resize(  );
    this._width += this.columns[i]._width;
    this._height = Math.max (this._height, this.columns[i]._height);
  }
};

// Define the Table constructor. spacing determines the number of pixels between 
// rows. startx and starty define the position of the table's upper-left corner.
_global.Table = function (spacing, startx, starty) {

  // Store the spacing parameter in an instance property of the same name. spacing 
  // defaults to 5 if not otherwise specified.
  this.spacing = (spacing == undefined) ? 5 : spacing;

  // Store the startx and starty parameters in instance properties of the same name.
  // Use Number(  ) to convert undefined values to 0, if necessary.
  this.startx = Number(startx);
  this.starty = Number(starty);

  // The rows array contains the rows in the table.
  this.rows = new Array(  );

  // Initialize the height and width of the table.
  this._height = 0;
  this._width  = 0;

  // If any parameters are passed to the constructor, from the fourth position
  // onward, add these values (assumed to be references to rows) 
  // to the table using addRows(  ).
  for (var i = 3; i < arguments.length; i++) {
    this.addRow(arguments[i]);
  }

  // Render the table to start.
  this.render(false, this.startx, this.starty);
};

// addRow(  ) adds a new row to the table and recalculates its height and width.
Table.prototype.addRow = function (row) {
  this.rows.push(row);
  this._height += row._height + this.spacing;
  this._width = Math.max(this._width, row._width);
};

// Table.render(  ) positions the rows within the table. The doResize parameter
// determines whether it should call Table.resize(  ). The startx and starty parameters
// determine the position of the upper-left corner of the table.
Table.prototype.render = function (doResize, startx, starty) {

  // If doResize is true, call the table's resize(  ) method. This is useful to update
  // the table size after something in the table changes.
  if (doResize) {
    this.resize(  );
  }

  // Reposition the table at (startx,starty). Position defaults to previous position
  // if a new position is not specified.
  if (startx != undefined) {
    this.startx = startx;
  }
  if (starty != undefined) {
    this.starty = starty;
  }
  var x = this.startx;
  var y = this.starty;

  // Render the rows of the table (which in turn renders the columns).
  for (var i = 0; i < this.rows.length; i++) {
    this.rows[i].render(x, y);
    y += this.rows[i]._height + this.spacing;
  }
};

// removeRowAt(  ) removes a row from the table at a given index. Note that the index
// is zero-relative (the first row is row 0).
Table.prototype.removeRowAt = function (index) {
  this.rows.splice(index, 1);
};

// The resize(  ) method calculates the height and width for a table.
Table.prototype.resize = function (  ) {
  this._width = 0;
  this._height = 0;
  for (var i = 0; i < this.rows.length; i++) {
    this.rows[i].resize(  );
    this._width = Math.max(this._width, this.rows[i]._width);
    this._height += this.rows[i]._height + this.spacing;
  }
};

Now that you have seen the code for the Table class (as well as the TableColumn and TableRow classes), let's look a little more closely at how it all works.

All three classes have the same basic functionality, so understanding one class helps to understand them all. Let's look at the TableRow class first.

Each constructor handles four basic setup tasks. First of all, the constructor records the spacing between the subunits. In the case of the TableRow class, the subunits are columns within the row. The constructor also creates an array property to store the subunits. Next, the constructor initializes the _width and _height properties. Using the names _width and _height for all subunits allows us to determine their width and height without having to know if the subunit is a table, row, column, movie clip, button, or text field. Finally, the constructor adds any subunits to the object that were passed to the constructor as parameters. This is accomplished using the arguments array so that there is no predefined limit to the number of subunits that can be passed to a constructor:

_global.TableRow = function (spacing) {
  this.spacing = spacing;
  if (spacing == undefined) {
    this.spacing = 5;
  }
  this.columns = new Array(  );
  this._width  = 0;
  this._height = 0;
  for (var i = 1; i < arguments.length; i++) {
    this.addColumn(arguments[i]);
  }
};

Next, each class defines a method to add subunits. In the case of the TableRow class this method is named addColumn( ). The method adds the subunit to the appropriate array and recalculates the height and width of the object. Using the Math.max( ) method is a shorthand way of saying "store the largest value for this dimension." For example, if an element's height is greater than that of any preceding element in the row, this._height is updated to store the new maximum height of the row.

TableRow.prototype.addColumn = function (column) {
  this.columns.push(column);
  this._width += column._width + this.spacing;
  this._height = Math.max(this._height, column._height);
};

The render( ) method of both the Table and the TableRow classes works by calling the render( ) method of each of the subunits of the object. The actual positioning of the movie clips, buttons, and text fields occurs within the TableColumn.render( ) method, so let's examine that method a little more closely. If the column contains a nested table, we call the render( ) method to render it. Otherwise, the column contains graphical objects, and each of those objects needs to be positioned. This is accomplished by looping through the elements array and setting the _x and _y properties of each element appropriately. The elements of a column should all appear one below the other, spaced by the number of pixels specified by spacing. Therefore, the _x properties are the same for all elements in a column, and the _y properties are determined by the _y value of the previous element plus the height of the previous element, plus the spacing between elements.

TableColumn.prototype.render = function (startx, starty) {
  if (startx == undefined) {
    startx = 0;
  }
  if (starty == undefined) {
    starty = 0;
  }
  if (this.containsTable) {
    this.elements[0].render(true, startx, starty);
  } else {
    for (var i = 0; i < this.elements.length; i++) {
      this.elements[i]._y = this.elements[i - 1]._height + this.spacing + starty;
      this.elements[i]._x = startx + this.spacing;
      starty += this.elements[i - 1]._height + this.spacing;
    }
  }
};

The resize( ) method for each of the three classes is used when new subunits are added or existing subunits are removed or resized. The resize( ) method recalculates the height and width of an object based on the updated subunits. This is accomplished by resetting the _width and _height properties to 0, then looping through all the subunits and incrementing the object's _width and _height properties appropriately:

TableRow.prototype.resize = function (  ) {
  this._width = 0;
  this._height = 0;
  for (var i = 0; i < this.columns.length; i++) {
    this.columns[i].resize(  );
    this._width += this.columns[i]._width;
    this._height = Math.max(this._height, this.columns[i]._height);
  }
};

Next, let's look at some code that can help you get a sense of how to use table objects in your movies. This code creates 10 text fields to start. Additionally, a push button instance is created using attachMovie( ) (so you must make sure that the PushButton component symbol is included in your Library). The text fields and the button are added to the table in six rows. The first two rows have three columns of text fields, the third and fifth rows have a single column, and the fourth row has two columns. The push button instance is added to the sixth (final) row. An example of this is shown in Figure 11-1.

Figure 11-1. A sample table layout showing how each row can have a different number of columns
figs/ascb_1101.gif

Finally, the button is assigned actions such that when it is pressed and released, a new text field is created and assigned to a random column, and the table is rerendered to accommodate the new layout. This code is for demonstration purposes only and illustrates that the table can accommodate changes dynamically.

// Include TextField.as from Chapter 8.
#include "TextField.as"
// Include MovieClip.as from Chapter 7.
#include "MovieClip.as"
// Include the recipes needed from this chapter.
#include "Form.as"
#include "Table.as"
// Create 10 text fields with names text0_txt through text9_txt.
for (var i = 0; i < 10; i++) {
  _root.createInputTextField("text" + i + "_txt", _root.getNewDepth(  ));
  _root["text" + i + "_txt"].text = "input text " + i;
}

// Create a push button instance.
_root.attachMovie("FPushButtonSymbol", "addBtn", _root.getNewDepth(  ));
addBtn.setLabel("random new text");

// Create six table columns. Each column is assigned one or more text fields
// or the push button instance. The spacing is set to five pixels.
tc0 = new TableColumn(5, text0_txt, text1_txt);
tc1 = new TableColumn(5, text2_txt, text3_txt, text4_txt);
tc2 = new TableColumn(5, text5_txt, text6_txt);
tc3 = new TableColumn(5, text7_txt);
tc4 = new TableColumn(5, text8_txt, text9_txt);
tc5 = new TableColumn(5, addBtn);

// Create three table rows and add the table columns to them.
tr0 = new TableRow(5, tc0, tc1, tc2);
tr1 = new TableRow(5, tc3, tc4);
tr2 = new TableRow(5, tc5);

// Add a table with the table rows added to it. The table is positioned at (0,0).
myTable = new Table(5, 0, 0, tr0, tr1, tr2);

// Add the actions to the button such that, when it is pressed and released, a new
// text field is created and added to the table.
addBtn.onRelease = function (  ) {

  // Randomly determine the column to which the text field should be added. We
  // introduce randomness here only for the purpose of demonstrating the table's
  // ability to dynamically accommodate changes.
  var rn = Math.round(Math.random(  ) * 4);

  // Create the new text field.
  _root.createInputTextField("text" + _root.i, _root.getNewDepth(  ));
  _root["text" + _root.i].text = "input text " + _root.i;

  // Select the random table column and add the text field to it.
  _root["tc" + rn].addElement(_root["text" + _root.i]);

  // Increment the i variable so that the next text field created will have a unique
  // name and depth.
  _root.i++;

  // Rerender the table. Specify true for the doResize parameter so that the table
  // automatically recalculates the size before rendering.
  _root.myTable.render(true);
};

Next, let's look at how a table is used to lay out a form. First, here is the code that creates a simple form and positions the elements using a table. (The result is shown in Figure 11-2.) Also, if you test this code in your own movie, make sure that you add all the necessary component symbols to the movie's Library.

#include "Form.as"
#include "Table.as"

// Create a list box, a combo box, a push button, and two text field labels.
this.attachMovie("FListBoxSymbol", "interests_lb", 1);
this.attachMovie("FComboBoxSymbol", "favUICmpnt_cb", 2);
this.attachMovie("FPushButtonSymbol", "submitBtn", 3);
this.createTextField("interestsLabel_txt", 4, 0, 0, 120, 20);
this.createTextField("favUICmpntLabel_txt", 5, 0, 0, 120, 20);

// Populate the list box and adjust its size to accommodate the values.
interests_lb.addItem("Flash", "flash");
interests_lb.addItem("ActionScript", "as");
interests_lb.addItem("More ActionScript", "as+");
interests_lb.setSelectMultiple(true);
interests_lb.adjustWidth(  );
interests_lb.setRowCount(interests_lb.getLength(  ));

// Populate the combo box and adjust its size accordingly.
favUICmpnt_cb.addItem("Checkbox", "ch");
favUICmpnt_cb.addItem("Combo box", "cb");
favUICmpnt_cb.addItem("List box", "lb");
favUICmpnt_cb.addItem("Radio button", "rb");
favUICmpnt_cb.addItem("Push button", "pb");
favUICmpnt_cb.adjustWidth(  );

// Add a label to the push button.
submitBtn.setLabel("submit");

// Add text to the text fields.
interestsLabel_txt.text = "Interests:";
favUICmpntLabel_txt.text = "Favorite Component: ";

// Create three table rows. The first contains two columns: a label and the list box.
// The second row contains two columns: a label and the combo box. 
// The third row contains the submit button.
tr0 = new TableRow(5, new TableColumn(0, interestsLabel_txt), 
                     new TableColumn(0, interests_lb));
tr1 = new TableRow(5, new TableColumn(0, favUICmpntLabel_txt), 
                     new TableColumn(0, favUICmpnt_cb));
tr2 = new TableRow(5, new TableColumn(0, submitBtn));

// Create the table. Position the table with the upper-left corner at (120,90) and
// add the three rows.
formTable = new Table(5, 120, 90, tr0, tr1, tr2);
Figure 11-2. A basic form layout using a table
figs/ascb_1102.gif

Next, let's enhance our example further. The following code block can be appended to the previous code block. In this example we add two radio buttons, another label, and a text area with a message to the user. We then add a new table that nests the previous table to create a complex layout. (The result is shown in Figure 11-3.) Since this code includes radio buttons, make sure that you add the radio button symbol to your movie's Library if you test this code.

// Add two radio buttons.
this.attachMovie("FRadioButtonSymbol", "asRating0_rb", 6);
this.attachMovie("FRadioButtonSymbol", "asRating1_rb", 7);

// Add two text fields. One is a label, the other is a text area.
this.createTextField("words_txt", 8, 0, 0, 120, 150);
this.createTextField("asRatingLabel_txt", 9, 0, 0, 120, 20);

// Add labels to the radio buttons and add them to a radio button group.
asRating0_rb.setLabel("ActionScript is fun.");
asRating1_rb.setLabel("ActionScript is REALLY fun.");
asRating0_rb.setGroupName("asRating");
asRating1_rb.setGroupName("asRating");

// Use the adjustWidth(  ) method to make sure the radio button labels are visible.
asRating.adjustWidth(  );

// Set the value for the text label.
asRatingLabel_txt.text = "Rate of ActionScript";

// Set the text area text field to word wrap and display multiple lines. 
// Then set the text value.
words_txt.wordWrap = true;
words_txt.multiline = true;
words_txt.text = "We hope you have enjoyed this form. It was designed using" + 
         "ActionScript and the custom Table class";

// Create a table row that includes two columns: one with the nested table, and the
// other with the new elements.
mainTr0 = new TableRow(5, new TableColumn(0, formTable), new TableColumn(5, 
asRatingLabel_txt, asRating0_rb, asRating1_rb, words_txt));

// Create the new table. Position the table at (60,120). Notice that the position of
// the nested table is overwritten

mainTable = new Table(5, 60, 120, mainTr0);
Figure 11-3. A form containing a complex table structure
figs/ascb_1103.gif
    [ Team LiB ] Previous Section Next Section