Articles Archive
Articles Search
Director Wiki
 

Keeping Tabs On Director

August 28, 2004
by James Newton

Tabs are a basic interface element that Director has tended to ignore. Director MX 2004 introduces a series of new FlashComponent member types, to renew and extend the built-in controls, but there is no tab member to be found among them.

Tabs combine organization and navigation. They indicate to the user that the current interface provides access to a related set of concepts. For example, your project may require a Preferences window. You could provide one tab for each theme that your preferences need to cover. When the user clicks on a tab, the appropriate screen will appear. Each screen will contain its own input fields, checkboxes and other buttons.

This article shows how to create a set of tabs, using customized bitmap members and a simple generic behavior. You'll be learning about:

Here is a preview of the movie that you are going to create:

You can find the completed movie at:
Macintosh: tabs.sit
Windows: tabs.zip

Proof of Concept

In the first half of this article, you will create a very simple Tab behavior, just to show that it is possible. In the second half, you will build a more elaborate behavior which will merit a place in your code library.

Getting Started

Let's start by creating a simple movie with three placeholder screens.

1. In the Score window, add markers named "Marker 1", "Marker 2" and "Marker 3" to frames 2, 32 and 62.
2. Double-click on the Script channel in frame 28, and enter the following script in the window which opens:Create a behavior with the handler...

on exitFrame
  go loop
end

3. Drop the new behavior member on the Frame Script channel of the Score in frames 58 and 88.
4. Using the rectangle tool from the Tool Palette, draw a rectangular Shape sprite on the Stage, in channel 1 and starting in frame 1, and extending to frame 28.
5. Select the new sprite in the Score window, and Alt-drag it (Windows) or Option-drag it (Macintosh) to create new sprites in frames 31 to 58 and again in frames 61 to 88.
6. Using the Property Inspector at the Sprite tab, set the color of these three Shape sprites to red, green and blue. This allows you to tell which section of your movie is playing at any given time.
7. Set the color of the background to something other than white, so that you can create a highlight lighter than the stageColor. I chose color 200 from the Mac System palette - rgb("#006699"). This simple movie is enough for you to test the tabs feature. If you want to make it more like a Preferences window, please feel free to add your own input fields, buttons and other controls at each of the markers.

Creating the Bitmaps

Let's start by creating a set of bitmap members, one for each of the three tabs that you are going to need. I created my tab bitmap in the Paint Window, in eight steps (see Image 1):

1. Creating the text
2. Drawing a box around it in a color lighter than the stageColor - rgb("#0099CC") in my case.
3. Drawing a darker line down the right-hand side - rgb("#003366")
4. Removing the top corners
5. Dragging the a 3x3 square in the top corners in and down, to create a rounded appearance
6. Filling the box with the same color as the stage
7. Coloring the bottom of the box in the same color, including the bottom-right pixel
8. Adding a line to either side of the tab in the same color I used in step 2, to make the bitmap the same width as the stage.

If you prefer, you could use icons instead of text. When you are satisfied, drag the bitmap member onto the Stage, and set its ink to Matte, using the Property Inspector (see Image 2).


Make two other bitmap members for the other tabs. I simply created a copy of the original, and edited that.


Name each bitmap member with the name of one of the markers in Score window. You'll see why we use marker names for the bitmap members in a moment.

Interacting with the Tabs

Place all three bitmaps on the Stage, using Matte ink. You'll notice that the tab in the highest-numbered channel appears to be in front of all the others. I'll assume that you have your "Marker 1" tab in sprite 2, "Marker 2" in sprite 3 and "Marker 3" in sprite 4.

Run your movie, and type the following command in the Message window:

sprite(2).locZ = 5

The tab in sprite 2 should now appear in front of the others. Try again, this time setting the locZ of sprite 3, and then sprite 4. What happens when you try using sprite 2 again?


Normally, the order in which sprites are drawn on the Stage depends on the sprite channel they are in. A sprite in channel 2 will appear in front of a sprite in channel 1. When you move a sprite around the stage, you change the value of its locH and locV properties - its horizontal and vertical location. The locZ property affects the layer in which the sprite appears.

As you have seen, you can force Director to draw the sprites in a different order. When you set the locZ of a sprite to integer number, you tell Director to treat it as if it were in the channel with that number. You can thus give several sprites the same locZ value. However, Director will still distinguish between sprites with the same locZ: it uses their original sprite channel numbers to determine the order. Now that sprites 2, 3 and 4 all have a locZ of 5, sprite 4 will appear in front of the others.

In order to make sprite 2 appear in front, you have to set the locZ of the others back to their original value. By default, the locZ of a sprite is the same as the number of its sprite channel.

How a Behavior Knows Which Sprite It's In

Lingo has a specific keyword for this number: spriteNum. In a behavior, you can declare spriteNum as a property, and then use sprite(spriteNum) in order to refer to the sprite to which the behavior is attached. The property declaration must occur before any reference to the property. Traditionally, all the properties used by a script are declared at the beginning.

Creating a Simple Behavior

Let's make the tab sprites react to a click. Select all three tab sprites, then right-click (Windows) or Ctrl-click (Mac) and select the Script item in the contextual menu that opens.


This will open the Script window. Enter the code that appears above.

Note that this is a quick and dirty piece of code. It uses hard-coded sprite numbers. This is bad practice. If you move your tab sprites to a different set of channels, your behavior will no longer work. We'll see in a moment how to make the code more generic.

When you click on the sprite, Director sends it two messages: #mouseDown and #mouseUp. In this case, you do not want to do anything when the user first presses the mouse, so you ignore the #mouseDown message. The on mouseUp handler is the recipe that tells the sprite what to do when the user releases the mouse button over the tab sprite.

The me parameter is Director's way of referring to the behavior itself. When you attach a behavior to a sprite, you create a connection between that sprite and a script member. When the Director playback engine first meets the sprite, it creates an instance of the script. Each instance shares the same code, but is stored in a separate place in the computer's Random Access Memory. The me parameter stores the address in memory used for that particular instance on that particular sprite.

The word "behavior" is often used ambiguously. Sometimes it means "the script itself"; sometimes it means "an instance of the script in RAM". In this article, I use the words "script" and "instance" explicitly where it is necessary to draw the distinction between the two meanings. We will be taking a closer look at me in a moment.

Now run your movie and click on each of the tabs in turn. They pop to the front, just like the real thing. However, the playback head stays at the same marker. You need to add another line of code:

go marker(sprite(spriteNum).member.name)

The name of each tab bitmap member is the same as the marker that you want to display when the user clicks on that tab sprite. You can therefore use the name of the member to jump to the appropriate marker.

Using Variables

Do you notice that you are now using sprite(spriteNum) twice? This forces Director to go back and consult its internal look-up tables a second time. It's much more efficient to save the value for the sprite in a variable, and use the variable instead.

In Lingo, a variable is simply a container; you can put any Lingo value into it. You don't have different variable types for numbers, text strings or sprite references. In Code 2, you will see that I have used two variables: tSprite and tMarker. The "t" at the beginning of the variable name stands for "temporary". We will shortly be creating more permanent properties, whose names will begin with a "p". You don't need to use prefixes like this. Director doesn't understand prefixes. Humans do, however, and it can be very helpful when debugging your code to use a variable naming convention.

A temporary variable doesn't need to be declared. It's like a scrap of paper that Director takes notes on while working inside a particular handler. Once the handler is finished, Director throws the scrap away.


Why Use Matte Ink?

I told you earlier to use Matte ink for your tab sprites. Let's see why. Select all your tab sprites, and set them to copy ink. Marker 3 covers the others with a band of white. Try Background Transparent ink.

The sprites now look good, but what happens when you run the movie? Try clicking on the "Marker 2" tab. The playback head jumps to Marker 3. Why? Because, although the white background of the sprite is not visible, Director uses the entire bounding box of the bitmap to detect clicks.

With Matte ink, when you click on a sprite Director ignores any white pixels which are not enclosed by non-white pixels. It lets the click pass through to any sprites in lower-numbered channels. Even though the "Marker 3" sprite covers the other sprites entirely, Director lets the mouse events reach the lower-numbered sprites in the area where the "Marker 3" sprite is transparent. Matte ink is the only ink which affects the way mouse clicks are detected. Other inks affect the way Director draws the sprite.

Time Out

You now have all the code you need to implement a Tab feature in a Director project. If you are Lingo-intolerant, you can stop reading this article now. However, the next time you need to use tabs, you will have to rewrite your behavior script, using the appropriate hard-coded sprite channel numbers. Wouldn't it be better to do a little more work now, so that the next time you need to use tabs, you don't have to open the Script editor? If you think so, read on.

Making the Behavior Generic

To make the behavior generic, you need to ensure that it works regardless of where the tab sprites appear. That means that the behavior has to be able to learn where it is, and where the other sprites with the same behavior are.

Custom Events

A sprite can tell which channel it is in using the spriteNum property. But how can one tab sprite know where the other tab sprites are, so that it can move in front? One technique would be to send a message to all the other sprites, telling them to reset their locZ values. The sendAllSprites() command allows you to do this.

When you click on a sprite, Director automatically sends it a #mouseUp event, which is intercepted by the on mouseUp handler. You can create your own custom events, and match them up with custom handlers with the same name.

Try this in your behavior script, using the code shown in Image 6. The on mouseUp handler will now send a #ResetLocZ event to all sprites, and the on ResetLocZ handler knows what to do with it.

First of all, it prints in the Message window the spriteNum of the sprite that received the event, along with information on where that sprite's behavior is stored in the computer's memory (the me parameter). Note that the sprites receive the message in order, the lowest-numbered sprite first. The value of locZ has no effect on this order. Note also that, although all the sprites share the same behavior script, the value of each instance's spriteNum property is different.

Secondly, the handler resets the locZ of the sprite to the same value as its spriteNum, which is the bit we are interested in.


You now have only one hard-coded value left in the behavior script: the value 5. What happens if you leave the script as it is and move the sprites up one channel? (Tip: Select the three tab sprites on the Stage or in the Score and press Ctrl-Up arrow, or Apple-Up arrow if you are on Macintosh). Now run the movie and click on the "Marker 2" tab. The playback head jumps to Marker 2, but the tab sprite does not appear in front of the sprite in channel 5 (see Image 7).


Sharing Data with Other Behaviors

One way to avoid this issue would be to set the locZ of the selected tab sprite to the lastChannel + 1. This would move it in front of all other sprites. However, if your application allowed the user to drag and drop items, they might pass behind the tab sprite, and this would look odd. In my experience, it is better to keep the tab sprites in low-numbered channels.

There is another drawback with the current technique: sendAllSprites() can badly slow down your movie. In the current case, this is not obvious. I have encountered projects where one sprite used sendAllSprites() to broadcast one event, and the sprites that received the event sent out other events, again using sendAllSprites(). And so on. What looked like a single line of code ended up in a flood of messages that almost drowned the machine. If used incorrectly, SendAllSprites could become the Lingo equivalent of the chain letter.

How can the behavior instances determine which tab sprite is in the highest-numbered channel, so that they can set the locZ of the front-most sprite to a higher value? The solution that I propose also reduces the use of sendAllSprites() to a single salvo: we'll use a list.

Lingo Lists

In Lingo, a list is like an address book: it contains information about where things are. Just as an address book does not contain the people it refers to, so a list variable does not contain the data itself. It contains the address where that data can be found in the computer's memory. If you use two variables to refer to the same list, and then change the contents of one list, the contents of the other variable will change too. Try it in the Message window:

gList1 = []
put gList1
-- []
gList2 = gList1
gList2.add(#aValue) -- Change gList2...
put gList1          -- ... and gList1 changes too.
-- [#aValue]

Note: I use "g" as a prefix here, because variables created in the Message window are global. Any script in any movie can access and alter the value of a global variable... so long as it is properly declared.

You're now ready to use this feature in your behavior.


There are six new Lingo expressions to learn about:
1. if ... then ... end if
2. listP
3. not
4. []
5. getLast()
6. call()

You will find these all explained in the Lingo dictionary and the on-line Director help. "[]" appears at the beginning of the Dictionary, among all the other non-alphabetical characters. Basically, what happens now is this:

When the behavior instances are first created, the property pTabList has no value in any of the instances. The first time you click on a tab sprite, that behavior instance realizes that pTabList is not yet a list. Obligingly, it creates a new, temporary list, and sends it out to all sprites on the back of a custom #Tab_SetList event. The custom event's name starts with #Tab_, so the chances are that only the Tab behavior will have a handler of that name. Instances of other behaviors in your movie will ignore it.

The on Tab_SetList handler in each Tab behavior instance receives the list in the aList parameter. The "a" prefix stands for "attribute", meaning that the value was sent from somewhere else, not created in the handler itself, like a temporary variable. When the on Tab_SetList handler is completed, the original tList variable will still be held in the computer's memory... just as long as it takes the initial on mouseUp handler to complete.

The on Tab_SetList handler in each behavior instance does two things: it sets its own pTabList property so that it refers to the list, and it adds a reference to itself to the list.

Each Tab behavior now knows about all the other Tab behaviors: each behavior has a pTabList property which stores all the behavior's addresses, in the order of their sprite channels. This means that the last entry in the list refers to the behavior on the sprite in the highest channel. Add 1 to the spriteNum of this last behavior, and you get a locZ in front of all the Tab sprites. Instead of sending a message to all sprites, you can now simply send a message to the list of Tab behaviors.

Let's run through that again.

In other words, the pTabList is like a private club, where everyone knows everyone. The sendAllSprites() command is used just once to create the club.

Conclusion

You need to make just one final tweak: inverse the order of the tab sprites, so that the "Marker 1" tab appears in the highest-numbered channel. That way, the correct tab will be in front when you start your movie.

You now have a generic behavior which can be used in any sprite channel. The behavior in the download movie contains a couple of changes for reasons of optimization, and is much better commented. You may want to add it to your code library.

It's taken just 21 lines of code to make a robust behavior.

How could this behavior be improved? Here's a brief shopping list of possible enhancements:

Where to Go From Here

MeccaMedialight have made available a widgets library, containing opensource code for creating Tab and other controls, via their LingoWorkshop site: http://www.lingoworkshop.com/code/widgets3_lib_tabs.php

James Newton started working with Director 5 in 1997. He wrote many of the behaviors that ship with Director since version 7. James lives in Dunoon, near Glasgow, Scotland. His company, OpenSpark Interactive, is responsible for marketing PimZ OSControl Xtra. When not coding he can be found racing his classic Flying Fifteen around the Holy Loch, making things with his four children, or catching escaped hamsters.

Copyright 1997-2024, Director Online. Article content copyright by respective authors.