Workshop Chapter 8. Creating a Currency-Exchange Calculator

CONTENTS

Workshop Chapter 8. Creating a Currency-Exchange Calculator

    The first seven workshop chapters were relatively basic. Sure, there were plenty of new concepts and unique solutions to problems, but there wasn't all that much complex math or string manipulation, any interfacing with external data, and just one optional custom class. If the first seven workshop chapters were "basic," this workshop and the next four will be "intermediate" workshops. As such, some of the steps are not explained as fully as they have been in the first workshops. Instead, I'll try to reference where the topics were first covered. Don't worry, the remaining workshop chapters won't be impossible, they'll just involve different topics.

    In this workshop chapter, we'll build a currency-exchange calculator. The user will be able to input any value and, based on the exchange rate, calculate the equivalent cost in another currency. As is true with many of the workshop chapters, the finished product is not necessarily something you'll use as-is, but the process of working through the exercise will teach you some very valuable techniques. If you've read all the chapters in the "Foundation" section of this book, you'll no doubt recall the several times I referred you to this workshop. A lot is covered here. Specifically, you'll do some simple math and some slightly more complex math. You'll encounter some of the challenges when you have to convert between numbers and strings. You'll also look at ways to improve the user experience by adding a feature that enables the user to select from predefined exchange rates for different countries. Just to make it easy for us (and because exchange rates vary daily), we'll make it possible to load in the current exchange rates from external sources. Although you may never have to build a currency-exchange calculator again, I'll be willing to bet that you'll be able to use all of the topics covered here!

    Consider the following description as the specification for this workshop chapter. As you learned in Chapter 3, "The Programmer's Approach," after you have a detailed "spec," programming is almost routine.

    Provide the user with a way to convert values in one currency to the equivalent value in another currency. The user will be able to type any number into one field, adjust the exchange rate by typing into the "rate" field, and then see the resulting equivalent value appear in a third field. Additionally, you must format the value displayed in the standard money format: with at least one 0 to the left of the decimal, two decimal places shown at all times, and all fractional values rounded off to the nearest cent (the one-hundredth decimal place). Finally, the calculator must allow the import from an external source of several countries' exchange rates from U.S. dollars. The user will be able to select any country, and that country's exchange rate will automatically fill the "rate" field. Additionally, after any country's exchange rate has been selected, the user must be allowed to toggle between dollars and the other currency. For example, if the current rate is for British pounds, the user should be able to toggle from "dollars to pounds" and from "pounds to dollars." See Figure W8.1 for a rough layout. Finally, feel free to improve the interface for utmost usability, where appropriate.

    Figure W8.1. We'll build a calculator based on a specification that includes this mock-up.

    graphics/24fig01.gif

    Although this is a rather big task, realize that all great things are done in pieces. For example, we're not going to jump right in and have everything done after writing one script. Rather, we'll break things down and build each piece as we go. The whole time, we'll keep an eye out for approaches that might conflict with a future task. Luckily, this spec has been organized in a logical order with the simple tasks first. It will serve as the guide for the following steps:

    1. Place two Input Text fields onscreen, give one an instance name of rate_txt and the other an instance name of original_txt. Make sure that both fields have nice wide margins. Using the Text Options panel, set the Maximum Characters option to something high, such as 20. You might also opt for the Show Border Around Text option so that the field remains out-lined and easy to see. It's important that we allow the user to input only numbers and a period for decimals into these two fields; therefore, click the Character button, and then click the Only radio button and the Numerals check box. In the "And these characters" field, type a period (see Figure W8.2). You can label these fields by using additional plain Static Text fields.

      Figure W8.2. You can limit input to just numbers plus individually identified characters.

      graphics/24fig02.gif

    2. Create a Dynamic Text field (not an Input Text field because this field will be used to display the results) and give it an instance name of result_txt. Click the Character button and select the No Characters option.

    3. Now we can start to put code in a frame script. We can use listeners to calculate the result field's text. Type in this script:

      1 myListener=new Object();  2 myListener.onChanged=convert;  3 rate_txt.addListener(myListener);  4 original_txt.addListener(myListener);  5 function convert(){ 6   result_txt.text=original_txt.text * rate_txt.text;  7 }

      We have a currency-exchange calculator! Pretty simple, eh? The first two lines create a generic listener that triggers the convert function. Then lines 3 and 4 add that listener to the rate_txt and original_txt fields so that if either one changes, the convert() function triggers. Finally, the convert function multiplies the text in original_txt by rate_txt to display a value in result_txt. By the way, I could have defined the convert() function inline at the end of line 2, but I figure I might want to trigger convert() from another place (specifically, when the user clicks a preset or when the movie begins). Having the convert() function standing by itself will make this possible.

      If you type .70 into the rate_txt field and 10 into the original_txt field, you'll see 7 into the result_txt field. Thank you, and good night!

      //

      Not so fast. Although the preceding steps create a working calculator, we have more to consider. Try a rate_txt like .7 on an original_txt value such as 10.95. No doubt, the bug tester will determine that we're not rounding off to the nearest cent. We just need to write a round() function. Because the Math object already has a round() method, however, let's not use the same name for our function. We cannot use Math.round() directly because that function rounds to the nearest whole number. Consider the result of 0.7*10.95. Currently, we're getting a result of 7.665, but we want it to round to 7.67. If you change the assignment to result_txt.text=Math.round(original_txt.text * rate_txt.text), you'll find that 7.665 rounds to 8. However, we can still use Math.round() if we apply a fairly simple trick. Consider what happens if you first multiply result by 100 (you'd get 766.5), round that off (you'd have 767), and then divide by 100 (you'd get 7.67, which is what we want). Consider this verbose solution:

      result_txt.text=original_txt.text *rate_txt.text;  result_txt.text =result_txt.text *100;  result_txt.text =Math.round(result_txt.text);  result_txt.text =result_txt.text /100;

      Although we could do this math in a slightly easier way, I think rounding off to specific decimal places might prove to be a useful utility for other projects (if not later in this workshop chapter). Consider how this works: We used 100 to multiply and then divided by 100 because we wanted 2 decimal places. To round off to 3 places, we would use 1000. We'd use 10 for 1 decimal place.

       

    4. Consider the relationship described in the preceding note as you type this homemade rnd() function into frame 1:

      function rnd(num, places){   var multiplier = Math.pow(10, places);    var answer = Math.round(num*multiplier);    return answer/multiplier;  }

      This function accepts two parameters: num (for the number being rounded off) and places (for the number of decimal places). First we determine what we're going to multiply by (10, 100, 1000, or whatever). The Math.pow() method accepts two parameters base and exponent so that the first number is raised to the power of the second number. In this example, if places is 2,102 is 100. We then round off num multiplied by multiplier and place it in another temporary variable answer. Finally, we return the answer we just found divided by multiplier. This is exactly the process we need, and because it's now a function, we can use it any time.

    5. To test the function we just wrote, change the script for the convert() function to read as follows:

      function convert(){   rate=Number(rate_txt.text);    original=Number(original_txt.txt);    result_txt.text=rnd(original * rate,2);  }

      Notice that because I was getting real tired of typing "rate_txt.text" every time I wanted to refer to the rate, I just created two new variables: rate and original. Also, because I'm paranoid, I made sure they're number versions of the text in those two fields (by using the Number() function). Also notice that instead of preceding with var (to make local variables), I figured we might want to know what the rate is in another script. From this point forward, just think of rate and original as the values in those fields. The last line of code does three things: multiplies the two variables, rounds them off, and then places the result in the result_txt field. I suppose that I could have written this formula in two steps by first setting result_txt.text to original*rate and then reassigning result_txt.text to rnd(result_txt.text,2). The solution shown isn't too difficult if you remember that multiplication is performed first.

      //

      At this point, you might find that the rnd() function still doesn't work! 7.665 rounds off to 7.66, but it should be 7.65. There's nothing wrong with the logic used in the rnd() function; rather, you're seeing an inherent problem with all computers when using floating-point numbers (decimal values). It's called the rounding error, and it's unavoidable. Although we humans have no problem looking at a fraction such as 1/2 and deriving the accurate decimal equivalent of 0.5, computers sometimes have problems. It's obvious when you consider a fraction such as 1/3, because the computer will need to round off a decimal like 0.333 (with repeating threes). But even fractions that should be expressed in simple decimals (such as 7.665) will exhibit the problem. Our case of 10.95*0.7 exhibits the rounding error because the computer considers the answer to be 7.66499999.... You won't see it in Flash if you execute the script trace(10.95*.7), but you will see it if you execute the equivalent code in JavaScript alert(10.95*.7) (see Figure W8.3).

      Figure W8.3. This pop-up box shows the full number.

      graphics/24fig03.gif

      If you investigate this subject, you'll find that there's actually a lot to it. You can avoid the issue entirely by using integer numbers only. However, dividing one integer by another will often create a floating-point number. The common practice is to avoid floating-point numbers as long as you can (through all your calculations) and at the very end, display decimal values onscreen. For example, the exchange rate of 0.7 can be expressed as an integer if you use 7 and remember to divide the final answer by 10. We're not going to go through that effort in this workshop chapter, but it's worth considering. Instead, we'll use a quick-and-dirty trick. Just realize that the user doesn't care what's going on behind the scenes if, in the end, he sees the correct numbers onscreen.

       

    6. The rounding error is small, but quite annoying. To address it, I've developed this quick-and-dirty solution. Change the convert() function to read as follows:

      1 function convert () { 2   rate=Number(rate_txt.text);  3   original=Number(original_txt.txt);  4   var decimals=1;  5   decimals+= (original.length- 1) - original.lastIndexOf( "." );  6   decimals+= (rate.length- 1) - rate.lastIndexOf( "." );  7   fudge=1/Math.pow(10,decimals);  8   result_txt.text=rnd ((original*rate)+fudge,2);  9 }

      This fix adds a very small "fudge factor" to the floating-point product of original*rate that is always smaller than the total number of decimal places for both original and rate. In line 4, I initialize the local variable _decimals to 1. (If no decimals are found, my fudge factor will still be .1, which will have no effect.) In line 5, decimals is increased by the difference between the length of original (minus 1) and the last index of "." So, if the original was 10.95, I would subtract 2 (the index where the last "." is found) from 4 (the length of "10.95" minus 1) to determine that the number of decimal places is 2. In line 6, the number of decimals in rate is added to the decimals variable using the same expression. Finally, in line 7, I assign the fudge variable to the inverse of 10 to the power of all the decimals (1/Math.pow(10,4) returns 1/10000 or .0001, for example). That fudge variable is added to the product of original*rate that is used in the rnd() function. Wow. All that work, and I still can't promise that you'll never encounter the rounding error!

      Although our homemade rnd() function works great, we still don't have displays that look like currency. We need "trailing" zeros to two decimal places and at least one zero that "pads" to the left of the decimal. Oh, and we need a dollar sign, too, but that's a piece of cake: result="$"+result. (Don't insert this code yet.) It turns out that our needs are not all that uncommon.

    7. In addition to rnd(), here are two other utility functions I've used for years in Director. They're translated here into ActionScript. We'll use both of them, so type them into the frame script:

      function pad(num, places){   theString=String(num)    for(i=0;i<places;i++){     if (theString.length<places){       theString="0"+theString;      }    }    return theString;  }  function trail(num, places){   var theString=String(num)    for(i=0;i<places;i++){     if (theString.length>=places){       break;      }else{       theString=theString+"0";      }    }    return theString;  }

      These two functions help you turn a number into a string with a minimum number of digits. For example, you can use the pad() function to ensure that a number has at least 5 digits. For example, pad(18,5) takes 18 and returns a five-digit number. Returned, you'll have 00018. Naturally, you use a variable for the first parameter. Similarly, the trail() function ensures that your string has a minimum number of digits, but it adds zeros at the end.

      //

      I had an occasion to use the pad() function in a project that listed product numbers from a client's clothing catalog. Most items had three-digit product numbers, but several used only two digits. The items were supposed to be displayed as 004, 089, or 102. Item numbers less than 100 had to be padded with zeros. I just used the pad() function like this: stringVersion=pad(itemNum,3).

      Both the pad() and trail() functions loop through each character in the string. Until the desired number of places has been reached, the string "0" is concatenated to the end or beginning of the string that is returned. We'll use both of these functions within the currency() function that we're about to write to ensure that all decimal values have two digits.

    8. Let's get started writing the currency() function. Although this function has to do several things, I know that all dollars should be at least one digit and that all decimals should be two digits. Type this function in the first keyframe:

      function currency(num){   //round it and make it a string    var theString=String(rnd(num,2));    //find where the decimal is    var decimalLoc=theString.indexOf(".");    //separate dollars portion from decimal    var dollars=theString.substring( 0, decimalLoc );    var cents=theString.substring(decimalLoc+1);    //trail cents, pad dollars    cents=trail(cents,2);    dollars=pad(dollars,1);    //return a nice string    return "$"+dollars+"."+cents;  }

      First we round off the number supplied as num and turn it into a string. Then we get the indexOf the decimal (that is, the location of the decimal). We use two local variables to store dollars and cents, respectively. Finally, we use the pad() and trail() functions to ensure that we have the minimum number of digits, using zeros if we don't. The whole thing is concatenated to return a nice-looking string with a dollar sign.

    9. Now change the last line in the convert() function to read

      result_txt.text=currency((original*rate)+fudge);

      This way, the new currency() function will be called (instead of just the rnd() function).

      If you test this script, it appears to work only intermittently. Actually, if you didn't have the attitude of a cynic, you could easily think that the script works fine. Perhaps all the what-if numbers you've provided work out fine. But just try something as simple as a rate of .5 and an original value of 10. I get a result of $0.50 that can't be right! The solution is found in the fact that indexOf() returns -1 when the pattern being searched for (in this case ".") is not found. Therefore, our currency() function works only when the rounded number includes a decimal. Without a decimal, the decimalLoc variable starts off at -1 and the rest of the function falls apart.

    10. To fix this bug, use this adjusted version of the currency() function:

       1 function currency(num){  2   //round it and make it a string   3   var theString=String(rnd(num,2));   4   5   //find where the decimal is   6   var decimalLoc=theString.indexOf(".");   7   var dollars;   8   var cents;   9   if (decimalLoc==-1){ 10     dollars=theString;  11     cents="00";  12   }else{ 13     dollars=theString.substring( 0, decimalLoc );  14     cents=theString.substring(decimalLoc+1);  15   }  16  17   //trail cents, pad dollars  18   cents=trail(cents,2);  19   dollars=pad(dollars,1);  20   //return a nice string  21   return "$"+dollars+"."+cents;  22 }

      This version of the function considers that when there's no decimal, the entire theString must be whole dollars; therefore, cents must be "00".

      There's one last feature that I suggest adding to the currency() function: When the dollars value goes above 999, it would be nice if commas appeared to separate every thousand. For example, $43009.95 should appear as $43,009.95. Instead of walking through the entire process of writing such a script, you can simply replace the last line in the preceding currency() function (line 21, starting return ) with this code:

      var actualDollars="";  var thousands=Math.floor((dollars.length)/3);  var extra=(dollars.length)%3;  if (extra>0){   actualDollars=actualDollars+dollars.substr(0,extra);  }  if (thousands>0){   if (extra>0){     actualDollars=actualDollars+",";    }    for (i=1;i<=thousands;i++){     theseThree=dollars.subStr( extra + ((i-1)*3) ,3);      actualDollars=actualDollars+theseThree;        if (i<thousands){           actualDollars=actualDollars+",";        }    }  }  //return a nice string  return "$"+actualDollars+"."+cents;

      This code is gnarly, but let me explain the basic process. First we initialize the actualDollars variable that will ultimately contain the dollars portion of the result. Then we determine how many times 3 can be evenly divided into the length of our string and place the result in thousands. The variable extra is then assigned to the remainder of dividing our string's length by 3. For example, if the string is "10000", thousands becomes 1 (the integer part of 5/3) and extra becomes 2 (the remainder of 5/3). If extra>0, we initialize actualDollars to equal just the first few characters of the string. For example, with the string "10000", the first 2 characters are placed in actualDollars (those characters are "10"). Then, provided that there is at least one set of thousands (that extra>0), we place a comma after the actualDollars variable. In the example of the string "10000", actualDollars is now "10". At this point, we're about to start the for loop that keeps extracting the next three characters from our original dollars and attaches them to the end of the actualDollars string we're building. At the end of each loop, a comma is added, provided that we have more loops to go (if extra>0). Finally, we build our string using actualDollars, which now has commas appropriately inserted, and the original cents variable. For a good challenge, try to extract this "comma-insertion" code and create a function that can be called any time.

      Now that the main functionality of the currency-exchange calculator is working, let's move on to loading preset rates from an external source.

    11. Create a text file containing the following text and save it as rates.txt in the same folder as your working Flash file:

      name_1=CAD  &name_2=pesos  &name_3=pounds  &name_4=yen  &name_5=euros  &rate_1=0.628  &rate_2=0.11  &rate_3=1.43  &rate_4=0.0075  &rate_5=0.878

      The five name_ variables describe the currencies; the five rate_ variables contain the exchange rates from dollars to those currencies. The form is URL encoded. We'll have to eliminate the extraneous return characters at the end of each line. (You can find a discussion of URL encoding in Chapter 16, "Interfacing with External Data.") Now we can load these variables into Flash.

    12. Currently, our movie is only one frame long. Select the first keyframe and then move it to frame 2 (see Figure W8.4). In frame 1, type the following script:

      stop ();  rates=new LoadVars();  rates.onLoad = function() {   _root.nextFrame();  }  rates.load("rates.txt");
      Figure W8.4. We can move everything that we've built so far to frame 2.

      graphics/24fig04.gif

      This script stops the movie from reaching frame 2, initializes a LoadVars object, defines the onLoad callback (which takes the user to the next frame), and then starts loading variables from the rates.txt file created in the last step. This script will accurately make the user wait on frame 1 until all the data is loaded. Once the data is loaded, however, we should remove the extra carriage returns. While we're doing that, we might as well move the variables (rate_1, rate_2, and so on) from inside the rates variable to the main timeline. We can do all of that before the nextFrame() method is invoked.

    13. I have a function that will remove redundant characters from the end of a string. Because we have several strings (one for each variable loaded), we'll process one at a time. Here's the generic function that you should type into the first frame script:

      function removeExtra(fromString){   var lastChar=fromString.charCodeAt(fromString.length-1);      while (lastChar==10 || lastChar==13 ) {       fromString=fromString.substr(0,fromString.length-2);        lastChar=fromString.charCodeAt(fromString.length-1);      }    return fromString;  }

      This function just removes any carriage returns or line feeds from the end of a line as we did in Chapter 14, "Extending ActionScript." Because it's a function, we'll be able to call it from anywhere. All we need to do to fix a variable's value is to use:

      myVariable=removeExtra(myVariable)
    14. In order both to clean up the loaded variables and to move them into the main timeline, change the onLoad callback definition for the rates object to read as follows:

       1 rates.onLoad = function() {  2   var thisRate="something"   3   var i = 0   4   while(true) {  5     i++;   6     var thisName=this["name_" + i];   7     thisRate=this["rate_" + i];   8     if (thisRate==undefined){  9       break;  10     }  11     thisName=_root.removeExtra(thisName);  12     thisRate=_root.removeExtra(thisRate);  13     _root["name_"+ i]=thisName;  14     _root["rate_"+ i]=Number(thisRate);  15   }  16   _root.nextFrame();  17 }

      This code looks worse than it is. Basically, the while loop keeps sending each variable through the removeExtra() function, and then creates a duplicate (same-named) variable in the _root timeline. Line 2 initializes the thisRate variable to a dummy string. Notice that the while loop on line 4 tries to loop forever. Our "escape route" is on line 8. That is, if thisRate is ever undefined, we break out of the loop. Line 2 just makes sure that thisRate is something at the start. In order to have an incrementing variable, line 3 initializes i to 0 (and then line 5 increments on every loop). Then, inside the loop, you can see the bulk of code. The variables thisName and thisRate temporarily hold the value of a dynamically referenced name (lines 6 and 7). Therefore, thisRate is never "something" by the time we get to line 8. However, if the dynamic expression in line 7 (this["rate_"+i]) turns out to be undefined,we break out of the loop. Assuming that everything's legitimate, lines 11 and 12 reassign the two local variables (thisRate and thisName) to a cleaned-up version. Then lines 13 and 14 assign same-named variables in the _root timeline. Finally, you see the nextFrame() method is called once the loop is finished. By the way, a plain old for loop might have been easier, but I'd have to know exactly how many rates were being loaded. The while(true) loop will stop as soon as it tries to process a variable that doesn't exist but it does require a bit more work.

      You can use the Debugger to see the "before" and "after" results. That is, the rates object has a bunch of variables, including carriage returns and line feeds. The same-named versions of those variables in the main timeline should be nice and clean (see Figure W8.5). Actually, this makes me think that the very first line of code in frame 2 should read delete rates to remove that variable. (By the way, you can't delete the rates variable from inside its onLoad callback.)

      Figure W8.5. A quick look with the Debugger reveals that our variables have loaded, but with "trailing garbage."

      graphics/24fig05.gif

      We're done with frame 1. We'll add the remainder of our graphics and scripts to frame 2.

    15. Let's make a clip (of which we'll have 5 instances) that enables the user select a preset exchange rate. First draw a box with a bright thick stroke. Select just the stroke and convert it to a Movie Clip this will be our "selected" highlight. Give this new clip an instance name of highlight, and then move it offscreen.

    16. Place a Dynamic Text field on top of the remaining box shape and give the text an instance name of thisName. Select both the text and box and convert to a Movie Clip. Give the instance of your new clip the name select_1. Make 4 copies of this clip and name them select_2, select_3, and so on (see Figure W8.6).

      Figure W8.6. Five instances of a clip containing Dynamic Text will provide a way for the user to select preset exchange rates.

      graphics/24fig06.gif

    17. In order for these five buttons to display a currency name and to trigger a function that displays the highlight, type this code into frame 2 of the main timeline:

      select_1.onPress=function(){select(1);}  select_1.thisName.text =this.name_1;  select_2.onPress=function(){select(2);}  select_2.thisName.text =this.name_2;  select_3.onPress=function(){select(3);}  select_3.thisName.text =this.name_3;  select_4.onPress=function(){select(4);}  select_4.thisName.text=this.name_4;  select_5.onPress=function(){select(5);}  select_5.thisName.text=this.name_5;  function select(whichOne){    highlight._x=this["select_"+whichOne]._x;     highlight._y=this["select_"+whichOne]._y;  }

      The first 10 lines set a callback for each button as well as the text displayed in the thisName field. The select() function will do more, but for now it just displays the highlight in the same location as the button that invoked the function. For a decent challenge, try writing a loop to fill each clip's thisName text field. You should be able to test the movie and see each button's label and the highlight move when you click a button.

    18. Now we can add a bit more code to the select() function. The main thing we want to have happen when the user clicks a preset is for the appropriate rate to be selected. Add the following two lines anywhere inside the select() function:

      rate_txt.text=_root["rate_"+whichOne];  convert();

      The idea is that we change the rate_txt field's contents and then trigger the convert() function (which automatically triggers only when the user changes that field).

      At this point, I'd say the project is pretty complete. It's certainly done enough to send it out for testing. In addition to at least one minor bug, the main results that will come out if you test this are features to make it more useful. Test it out a bit and try to think of what you'd like to add. Here's my list (and the things we'll address in the rest of this workshop):

      • A text message that reads "CAD to Dollars" (or something similar) should appear so that it's clear which kind of conversion is taking place.

      • A Toggle button should be added so that the user can quickly change from "CAD to Dollars" to "Dollars to CAD."

      • The highlight, text message, and Toggle button should all disappear as soon as the user edits the rate_txt field (or it could be an inaccurate representation). Naturally, they should all reappear when the user selects a preset exchange rate again.

      These refinements really aren't too terrible and are well worth the effort to implement. With the exception of the one bug-fix that I know we need to make, the edits to add the preceding features shouldn't mess up what we already have. That is, adding layers of features can be easy and safe if you're careful not to change the core code. You'll see that we won't really tamper with the main functions already in place.

    19. For the text message, create a Dynamic Text field with the instance name of message _txt. Then add the following line of code to the select() function:

      message_txt.text=_root["name_"+whichOne]+" to Dollars";

      That's a pretty straightforward way to display a dynamic message. We'll need to adjust this code once the Toggle button is pressed so that it says either "Dollars to CAD" or "CAD to Dollars," for example.

    20. For the Toggle button, just create a button and name the instance on the Stage toggle_btn. Then write this callback (in frame 2 of the main movie) for when the button is pressed:

      toggle_btn.onPress=function(){    rate_txt.text=1/rate_txt.text;     convert();  }

      This is pretty simple in that it just fills in the rate_txt field with a reciprocal of the current value, and then triggers the convert() function to reset the display.

      At this point, there are a couple of problems with the two added features. First, the Toggle button fails to change the message_txt field. Also, the user can change the value in the rate_txt field and the message remains onscreen. A listener will be the easiest way to "listen" for any changes in the rate_txt field and then clear the message_txt (and move the highlight too). Even though there's already a listener set up for the rate_txt field (that triggers the convert() function), there's no problem adding more listeners to the same field.

    21. To add another listener to the rate_txt field, add the following code (near where you set up the other listeners way back in step 3):

      listenChangeRate=new Object();  listenChangeRate.onChanged=function(){   message_txt.text="";    highlight._x=-9000;  }  rate_txt.addListener(listenChangeRate);

      Unlike the other listener, this listener's onChanged callback function is defined right inline. If it turns out that I want to execute the code in this function (which effectively hides the message and highlight), I might take it out into a function so that other scripts can trigger it. Notice that now, as soon as the user changes the rate_txt field, the highlight and message text disappear.

      Now to fix the contents of the message_txt field. I know that I'll use one of the two scripts (not both), depending on which direction the conversion is taking:

      message_txt.text=_root["name_"+whichOne]+" to Dollars";  message_txt.text="Dollars to "+_root["name_"+whichOne];

      We just have to figure out which one (and replace whichOne with a number tied to the currently selected preset). My thinking is that either the user is converting "to dollars" or not. It's worth storing a variable that says toDollars=true when the user first clicks a preset and then toggle between true and false every time she clicks the Toggle button. In addition, we'll have to save the whichOne variable passed to the preset() function if we expect to use it later (including every time the user clicks the Toggle button). Because the message display will have to update when the user clicks both a preset and the Toggle button, I'm going to move the following code into a separate function.

    22. Write the following function somewhere in frame 2's script:

      function updateDisplay(){   if (toDollars){     message_txt.text=_root["name_"+curPreset]+" to Dollars";    }else{     message_txt.text="Dollars to "+_root["name_"+ curPreset];    }  }

      Basically, any time updateDisplay() is triggered, the message text will refresh with one or the other expressions, depending on the value of toDollars. Naturally, we'll have to initialize this variable and change it as necessary. Notice, too, that this code relies on a variable called curPreset. We'll have to make sure that is assigned whenever the user clicks a preset. Follow the next two steps to fully implement the updateDisplay() function.

    23. Change the select()function to read as follows:

      function select(whichOne){   highlight._x=this["select_"+whichOne]._x;    highlight._y=this["select_"+whichOne]._y;    rate_txt.text=_root["rate_"+whichOne];    toDollars=true;    curPreset=whichOne;    updateDisplay();    convert();  }

      Basically, we removed the code that was assigning message_txt's text property (that's handled in the updateDisplay() function). In addition, we assigned the variables toDollars and curPreset that are needed elsewhere.

    24. To make the Toggle button work, use this revised code:

      toggle_btn.onPress=function(){   toDollars=!toDollars;    rate_txt.text=1/rate_txt.text;    updateDisplay();    convert();  }

      The only difference is that toDollars is toggled (between true and false) and the updateDisplay() function is triggered.

      At this point, you can really pound on this application, and it should work pretty well. There are two tiny bugs that are worth fixing. Basically, the currency() function (added back in step 10) is not perfect. For one thing, if the user clears either the rate_txt or original_txt fields, you'll see "NaN" in the result_txt field. ("NaN" means "not a number.") Anyway, we should change it so that the result_txt field appears blank instead. Also, the dollar sign is cool and all, but it really doesn't belong there when the user is not converting "to dollars." Both these issues are easy to resolve, but because they involve changing the currency() function, we have to be careful to fully test what we add.

    25. First let's fix the NaN issue (which is not a non-issue). Just inside the currency() function, add this code:

      if (isNaN(num)){   return "";  }

      The parameter received is num. So, if the isNaN() function returns true (that is, num is not a number), we return an empty string. Not only will that string be used in the result_txt field, but return means that the rest of this function will be bypassed. If you wanted, you could change the code here to read return "enter a number".

    26. The dollar sign issue is not quite as easy. Way down at the bottom of the currency function, use the following code to replace the last line (the line that currently reads return "$"+actualDollars+"."+cents):

      if (toDollars){   var prefix="$";  }else{   var prefix=_root["name_"+curPreset]+": ";  }  return prefix+actualDollars+"."+cents;

      This just displays either a dollars sign or the actual currency name (name_x ). Even though this basically works, there's still a problem with the dollar sign issue. (You should be able to find it when you do a Test Movie.) The issue is that once the user edits the rate_txt field, the toDollars variable doesn't change nor does the curPreset value. That is, if the user clicks a preset, clicks the Toggle button, and then edits the rate_txt field, the result_txt field will erroneously display the most recent currency name. There are several ways to fix this. I tried several things that didn't work. Adding the following line of code to the listenChangeRate.onChanged callback sort of works: toDollars = true. As soon as you click the Toggle button, however, the problem reappears. (And I really liked that fix as it required so little code oh well.)

    27. The solution I came up with involves two steps. First, add the following line of code to the listenChangeRate.onChanged callback function:

      curPreset=undefined;
    28. Then, change the added code in the currency() function (which we just added in step 30) to read as follows:

      var prefix=""  if(curPreset<>undefined){   if (toDollars){     prefix="$";    }else{     prefix=_root["name_"+curPreset]+": ";    }  }  return prefix+actualDollars+"."+cents;

      Basically, I start by setting prefix to an empty string, and then I do only the dollar sign (or other) code if curPreset is defined. I know that curPreset is undefined any time the user edits the rate_txt field (because I just added it to the callback).

    That's it! If that wasn't fun, I don't know what is. Welcome to the more advanced workshop chapters. Actually, this one was not only advanced, but long. The upcoming workshop chapters won't be so involved. Keep in mind that we created a lot of code that we later needed to fix. This is typical.

    Here's a quick rundown of the techniques, both new and familiar, that came up in this workshop chapter:

    • We used Math.round()to round off and Math.floor() to find the integer portion of a number. You can learn more about the Math object in Chapter 5, "Programming Structures."

    • We exploited the length property, the subStr() method, and the ever-popular concatenation operator (+). Chapter 9, "Manipulating Strings," covers many details of the String object.

    • We used operators such as % (modulo) and ! (logical NOT). These operators are touched on in Chapter 5.

    • We discussed the dynamic referencing of clips. Dynamic referencing is covered in Chapter 7, "The Movie Clip Object."

    • We loaded data from external sources like we learned to do in Chapter 16, "Interfacing with External Data."

    • We defined callback functions for buttons, as covered in Chapter 16. We also used listeners, which are discussed in Chapter 10, "Keyboard Access and Modifying Onscreen Text," and Chapter 16.

    • We reviewed some general approaches to programming and debugging techniques. These techniques are covered in Chapter 3, "The Programmer's Approach," and Chapter 6, "Debugging," respectively.

    CONTENTS


    ActionScripting in MacromediaR FlashT MX
    ActionScripting in MacromediaR FlashT MX
    ISBN: N/A
    EAN: N/A
    Year: 2001
    Pages: 41

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net