Introduction It has been just over two months since our last issue. Since then, The e-Learning Authoring Conference was a big success, ToolBook 10.5 hit the streets, we've updated our major products for 10.5, and we've created a new product called Storyboarder. This issue gives you the info on all of these plus great tips on ToolBook, Flash/Flex, and .NET. Thanks for being a subscriber! |
Storyboarder Writes Word Documents From ToolBook Content We are pleased to release the Platte Canyon® Storyboarder application. This innovative tool creates a Microsoft Word storyboard document from an existing ToolBook book. The document includes an optional screen capture of each page, field, button, and other text, question answers/feedback, simulation information, and other content. The Storyboarder can be launched in author-level ToolBook or as a runtime application. The price is $295 for a particular version of ToolBook. If you want to buy a copy for another version of ToolBook (such as 10.5), the price for that is only $95. There is an evaluation copy (see link below) that works with books of up to five pages. Information Storyboarder Help Purchase Evaluation Version |
Learning & Mastering ToolBook 10.5 Released We have completely updated this popular training application for ToolBook 10.5. We have added new content on Geolocation, exporting Voice Recordings as .mp3 files, interface changes such as pasting unformatted text, and more. It also has extensive and updated content on the Quiz Summary, Certificates, XML, importing from PowerPoint, the Actions Editor, Flash, OpenScript, JavaScript, SmartPages/SmartStyles, HTTP Post, Simulations, and much more. Learning & Mastering ToolBook contains over 30 hours of training with:
Information Purchase |
Plug-In Pro 10.5 Upgrade Only $95
We are excited to announce that Plug-In Pro 10.5 is available. We jumped from version 8 to 10.5 to align with the ToolBook version we are supporting. The Plug-In Pro has great tools that add to the already formidable capabilities of TB 10.5. Version 10.5 fully supports ToolBook 10.5 and has the ability to import and export PNG, GIF, JPG, BMP, WMF, and EMF files. You can choose to import or export only particular image types. For example, you might want to replace all your BMP resources with .PNG equivalents. You can use the Plug-In Pro to export only the BMP format bitmap resources to external files. Then overwrite them with .PNG equivalents and the use the Plug-In Pro to replace all those resources with the PNG ones. You can also find which objects are using particular resources, again by image type. Version 10.5 also has great tools to create image objects and populate them with external graphic files (which are in turn imported as resources). You can put all the images on one page or put each one on its own page. If you still want to use the Web Graphic Placeholder, you can do the same thing with it. The Plug-In Pro also has the ability to copy actions between objects (even from different books!), merge pages to different backgrounds, a tool to save and restore your Command Window scripts, a tool for listing all the web graphics and adjusting their external files if desired, a tool to find all your media players and list/adjust their media, the ability to import clips and see all your clip information, the ability to write your sticky notes to a file, the addition of simulation information such as Instructions and Feedback to the generated spell-check file, and much more! The Plug-In Pro is $495 per developer. Owners of version 8 can upgrade for only $95. Owners of version 7 or older can upgrade for $330. Information Purchase |
ToolBook 10.5 Review By Jeff Rhodes, Platte Canyon Multimedia Software Corporation I have been using ToolBook 10.5 for quite some time. While it doesn't have some of the high-profile new features of some past releases, it is nice and stable and has a number of worthwhile new additions. These include:
Those of you doing web deployment will want to move to 10.5 for the updated browser support (including Safari 5, Windows Mobile 6.0 +, Android, etc.) as well as the great features listed above. Platte Canyon ToolBook Information ToolBook 10.5 User Guide ToolBook Web Site |
ToolBook 10.5 Pricing Information ToolBook 10.5 is now shipping. If you currently have a support or maintenance contract, you should have already received an email from SumTotal Systems with download instructions. If not, contact us or your local representative to see what kind of options we have. Note that SumTotal has eliminated maintenance contracts going forward. So the only renewal options will be for Support & Maintenance together. At $995 per year, a support contract is still cheaper than upgrading. Plus it gives you access to Denny Dedmore and his team of ToolBook experts. Our upgrade price from ToolBook 9.5 or 10 to 10.5 is $1,545. If you have a version older than that or are starting up your ToolBook shop, we are selling ToolBook 10.5 for $2,595 and Support & Maintenance (now mandatory for new customers) for $995. If you are in the U.S. or Canada, you can buy ToolBook and/or Support & Maintenance from Platte Canyon. There is a link below for our online store. Or you can call us at 888-866-5251 (888-ToolBk1) or 719-548-1110. Otherwise, contact your local reseller or SumTotal directly. Platte Canyon ToolBook Store |
e-Learning Authoring Conference 2010 Report The twelfth year of this conference was one of the best. We moved to a new building on campus, had lots of great sessions and discussions, played some raging "Rock Band," added new instructional design content, and much more. Developers from across the country as well as Canada previewed ToolBook 10.5, learned Flex and Flash Builder, delved into ActionScript 3, explored how to use Captivate and Acrobat, understood Programming for e-Learning Developers in ToolBook, Flash/Flex, JavaScript, and Silverlight, learned how to create ToolBook lessons, discovered when to use media appropriately, picked up some management tips, met fellow members of the ToolBook List, and much more. The "Hack Ack" theme this year was "Rock and Roll." Jeff Rhodes of Platte Canyon and Denny Dedmore of SumTotal Systems did demonstrations as Platte Canyon and SumTotal employees are ineligible. Bill Hurley of American Signature and Mauro Rech of Buckman Labs both drew overwhelming applause and were declared the co-winners by "MC" Peter Hoyt. We had entries in ToolBook (Dedmore and Rech), Captivate (Hurley), WPF (Rhodes), and AIR (Rhodes). Nice job everyone! Be sure to mark your calendars for 2011 e-Learning Authoring Conference: August 3 - 5, 2011 with preconference training August 1 and 2. e-Learning Authoring Conference 2010 Resources 2010 Archives for Attendees Application ($50) |
Expert Information from Learning & Mastering ToolBook By Tim Barham, SumTotal Systems, Inc. Using the GDI+ DLL Question: Are there any of you who are able to link to the GDI+.dll? And use the graphic functions? If yes, how do you do it? Answer: Yep, I've done a lot of it. GDI+ is primarily designed to be interacted with via a bunch of C++ classes, which you can't do from ToolBook. However, Microsoft expose what they call the "GDI+ flat API" (in other words, regular old DLL functions) that work just fine from ToolBook. The documentation can be found here: http://msdn.microsoft.com/en-us/library/ms533969(VS.85).aspx. If you are using version 10.5 of ToolBook, I added a bunch of OpenScript wrapper functions for some of the GDI+ DLL functions (not all of them, just the ones I needed). These aren't documented or officially supported, but they're there and are unlikely to change. Any other functionality you would have to handle yourself, but looking at my code may be useful. The function stubs are in the book script of the runtime system book (tb105r.sbk, for example), right at the end: TBK_GdiPlus_BitmapFromBitmapResource( bitmapRef ) This returns a GDI+ Bitmap object handle from a ToolBook bitmap resource. Note that you are responsible for releasing the Bitmap when you're done with it (via GdipDisposeImage(), which is linked by the runtime system book). TBK_GdiPlus_BitmapResourceFromBitmap( pBitmap, fileType ) Creates a ToolBook bitmap resource from the specified GDI+ Bitmap. The "fileType" parameter is any of the file types supported by GDI+ (like "png", "gif", "jpg" etc). TBK_GetBitmapResourceFileType( bitmapRef ) This isn't GDI+ specific, but useful none-the-less. It returns the file type extension (such as PNG, BMP, GIF for example) of a bitmap resource. TBK_GdiPlus_CreateBitmapFromFile( fileName ) This loads a graphic file (of a type supported by GDI+) returns a Bitmap handle. You are responsible for releasing the Bitmap when you're done with it. TBK_GdiPlus_GetImageGraphicsContext( pImage ) This create a GDI+ "graphics context" (something you can draw into) from a GDI+ Bitmap (or Image). You must release the graphics context when you're done with it by calling GdipDeleteGraphics() (which the runtime system book links). TBK_GdiPlus_CreateBitmapFromGraphics( pGraphics, width, height ) This creates a GDI+ Bitmap object from a GDI+ graphics context, and returns the handle. TBK_GdiPlus_CreateSimilarBitmap( pBitmap, width, height ) This creates a GDI+ bitmap of the specified size with characteristics the same as the bitmap you pass to it. TBK_GdiPlus_GetImageSize( pImage ) This returns the size (in pixels) of a GDI+ Image or Bitmap. TBK_GdiPlus_DrawImage( pGraphics, pImage, left, top ) This draws the specified image into the specified graphics context at the position specified. It doesn't support any resizing (there are GDI+ functions that do, but I didn't create wrappers for them as I didn't need that functionality when I created these wrappers). TBK_GdiPlus_CreateFont( pGraphics, fntFace, fntSize ) Creates a font of the specified font face and size, and returns a handle to it. You must release it when you're done with it by calling GdipDeleteFont(). TBK_GdiPlus_GraphicsDpiX( pGraphics ) TBK_GdiPlus_GraphicsDpiY( pGraphics ) Return the number of horizontal and vertical pixels per virtual inch. TBK_GdiPlus_DrawText( pGraphics, txt, fntFace, fntSize, left, top, width, height ) Draws text into the specified graphics context. The implementation of these functions is in the "gdip" page of the runtime system book. You are welcome to take a look. |
OpenScript Tip from Learning & Mastering ToolBook
By Peter Jackson, ToolBookDeveloper.com Adding a Hand Cursor in DHTML The standard way to add a hand cursor in DHTML is to have an "on click" handler in the Actions Editor. If you don't need to do anything on the click, just add a comment. However, this technique does not work if the object is part of a group. The code to the left will allow you to add the hand cursor to objects in a group where the group either has an "On click ..." event or has a hyperlink. Create a button called "AddFingerAction" with a caption "Add Finger Action" Now add an "on click ..." action to this button: Comment: Add Finger As you can see this really does nothing, but programs can't see, they just do:-) Now edit the script of the button, first clear all the scripts that the Actions Editor added. Then add the code below. NOTE: This will only affect the exported book, it does not show the finger cursor in native mode. to handle buttonClick forward send addTheFinger to self end buttonClick -- The following will loop through all the -- pages and backgrounds of the current book -- and find groups that have an "On click ..." -- event or hyperlinks. Then it checks each of -- the objects in the group and adds an "On click ..." event if appropriate to handle addTheFinger local object bookObj, pageObj, bgObj local stack pageIDs, bgIDs local array actionArray[] actionArray = ASYM_EA_ActionArray("buttonClick") of self -- The following will allow you to simply -- copy this code to your own system book -- without needing to change this script bookObj = parent of mainWindow -- First we loop through all of the pages pageIDs = pages of bookObj while pageIDs is not null pop pageIDs into pageObj get findFingerObjects(pageObj, actionArray) of self end while -- Now we loop through all of the backgrounds bgIDs = backgrounds of bookObj while bgIDs is not null pop bgIDs into bgObj get findFingerObjects(bgObj, actionArray) of self end while end addTheFinger to get findFingerObjects pageBgObj, actionArray[] by reference local object groupObj, fingerObj local int gCnt, g local stack objs gObjs = getObjectList( pageBgObj, "group", false) gCnt = itemCount(gObjs) step g from 1 to gCnt groupObj = item g of gObjs -- If you don't want the objects of a -- group to get Fingered then just add -- a userProperty called NoFinger with a -- value of true:-) if NoFinger of groupObj = true then continue step end if if "buttonClick" is not in \ ASYM_EA_HandledEvents of groupObj then -- Check if the group has a Hyperlink, -- if so then we want to check -- the objects if not ASYM_HasHyperlinks(groupObj) continue step end if end if -- We don't want to do this for a -- Question object as TB will do that if ASYM_WID_QType of groupObj is not null continue step end if objs = objects of groupObj while objs is not null pop objs into fingerObj -- If you don't want an object to get -- Fingered then just add a -- userProperty called NoFinger with a -- value of true:-) if NoFinger of fingerObj = true then continue while end if -- We don't want to do a group object -- that is in the group as it will be -- checked later if object of fingerObj = "group" then continue while end if if ASYM_HasHyperlinks(fingerObj) then -- This object has a Hyperlink, so -- it will get fingered anyway continue while end if handledEvents = ASYM_EA_HandledEvents of fingerObj if "buttonClick" is in handledEvents -- This object already has an "On -- click ..." event, maybe we did -- that with this code before and -- this is a subsequent run through -- the book. To be sure:-) continue while end if -- OK we now have an object and we add -- the Action System userProperties -- We just need the properties, no -- need for the OpenScript to be -- created ASYM_EA_ActionArray("buttonClick") of \ fingerObj = actionArray push "buttonClick" onto \ ASYM_EA_HandledEvents of fingerObj end while end step return null end findFingerObjects |
Web Hint from Learning & Mastering ToolBook
By Denny Dedmore, SumTotal Systems, Inc. Highlight Colors for Questions Question: I am wondering what controls the highlight color (when clicked) for the question response buttons (True/False buttons, choice buttons for Multiple Choice questions, etc.). I have my properties set for these questions to use custom colors (not Windows) and when I look at it in reader mode, the button is inverted when clicked. However, when I upload to the Learning Management System, instead of being inverted, a colored highlight appears around the selected button. My Multiple Choice questions have a red highlight and my True/False questions have a green highlight. I would like to get the colors to be the same for all questions. Answer: You can set the color by:
colorChosen of selection = 255,0,255
|
ActionScript Tip
By Jeff Rhodes, Platte Canyon Multimedia Software Corporation Creating an Adobe AIR Slide Show As mentioned above, the "Hack Ack" topic for this year's e-Learning Authoring Conference was Rock & Roll. I thought it would be fun to create a "Slide Show" application that would randomly grab all photos from a directory and display them. At the same time, it would randomly pick music files out of another directory, starting the next one as soon as the current one is finished. I decided to build my first incarnation in Flash/Flex (see the next article for a Windows Presentation Foundation example). But the security model of normal Flash is such that you cannot read the contents of directories on the user's hard drive without user interaction. Adobe AIR applications don't have that restriction, so I decided to tackle my first AIR application. The complete application can be downloaded as part of the Archives for Attendees, but we will look at some of the highlights. The basic design is to read all the photos into one collection and all the music into another. We randomly grab a music file and start playing it. We handle its "completed" event and launch a new one when the music file is finished. We then start a timer to use with the photos. When the timer fires, we randomly pick a photo, remove it from the collection, and display the photo. To store information between timer cycles, we need to declare some variables outside a function block as shown below. // configuration constant private var photoDelay:int = 3000; // milliseconds // shared variables private var photoArrayCollection:ArrayCollection; private var musicArrayCollection:ArrayCollection; private var timerId:Timer; private var soundChannelId:SoundChannel; We use photoDelay in our timer. The two ArrayCollection variables allow us to store our list of photos and music respectively. The timerId variable stores the reference to our timer and the soundChannelId is the SoundChannel that we use to play our music. When the application starts, we call the configureApp function as shown below: private function configureApp():void { // get list of media and photos var currentDirectory:File = File.applicationDirectory; var photoDirectory:File = currentDirectory.resolvePath("photos"); var musicDirectory:File = currentDirectory.resolvePath("music"); var photoArray:Array = photoDirectory.getDirectoryListing(); var musicArray:Array = musicDirectory.getDirectoryListing(); // use ArrayCollection so we can remove items easily after they are used photoArrayCollection = new ArrayCollection(photoArray); musicArrayCollection = new ArrayCollection(musicArray); } We use the AIR File class to find our two subdirectories ("photos" and "music") and then get a listing of all the files in them. We store them in our associated photoArrayCollection and musicArrayCollection variables. The code for the "Start" button is shown below: protected function startBtn_clickHandler(event:MouseEvent):void { // start timer and begin playing sound timerId = new Timer(photoDelay, photoArrayCollection.length); // once per photo timerId.addEventListener(TimerEvent.TIMER, timerHandler); timerId.start(); playSound(); } We create our timer using our photoDelay variable (3000 milliseconds). The second parameter is the number of times that we want the timer to fire. We use the number of photos that we have. This is nice in that we don't then have to stop the timer. The next line is where we tell Flash/Flex what function (timerHandler) to call when the timer fires. We then start the timer and call the playSound function below. private function playSound():void { if (musicArrayCollection.length > 0) { // get a random number between 0 and the length of the photoArrayCollection. // Then show that photo and remove the item from the collection var rand:Number = Math.random(); var indexNum:int = Math.round(rand * (musicArrayCollection.length - 1)); var musicFileId:File = musicArrayCollection[indexNum] as File; var musicPath:String = musicFileId.nativePath; var soundId:Sound = new Sound(); var requestId:URLRequest = new URLRequest(musicPath); soundId.load(requestId); if (soundChannelId != null) { soundChannelId.stop(); } soundChannelId = soundId.play(); soundChannelId.addEventListener(Event.SOUND_COMPLETE, soundCompletedHandler); musicArrayCollection.removeItemAt(indexNum); status = "Playing " + musicFileId.name + ". indexNum = " + indexNum + ". length = " + musicArrayCollection.length; } } private function soundCompletedHandler(e:Event):void { playSound(); } We first check to make sure that we have music files to play. If so, we use the Math.random() function to get a random number between 0 and 1. We then multiply that by the number of sound files (we subtract 1 since we are creating an index that starts from 0). We then round the number so that we don't have a decimal number. From there, we go to our musicArrayCollection and grab the associated sound file. We read its nativePath property to get its complete path. We create a Sound object and a URLRequest to actually read the file. We load sound and then check to see if a previous sound is playing. If so, we use the SoundChannel to stop it (that's why we needed to save a reference to soundChannelId). We play the sound, which gives us the SoundChannel. We call the soundCompleted function when the music file finishes. Notice that this function just calls playSound again. Very importantly, we remove the file from the musicArrayCollection. This is how we avoid playing the file again and how we know to stop when all the files have been played. Finally, we set the status property, which causes the information on the name, index, and length to show up on the application's status bar. The last piece of the puzzle is displaying the photos. That happens in the timerHandler function shown below: private function timerHandler(e:TimerEvent):void { var rand:Number = Math.random(); var indexNum:int = Math.round(rand * (photoArrayCollection.length - 1)); var photoFileId:File = photoArrayCollection[indexNum] as File; var photoPath:String = photoFileId.nativePath; photoImage.source = photoPath; photoArrayCollection.removeItemAt(indexNum); status = "Displaying " + photoFileId.name + ". indexNum = " + indexNum + ". length = " + photoArrayCollection.length; } This logic is very similar to that used to play the music files. We again get a random number between 0 and the number of photos still available (-1 to account for the fact that we are starting at 0). We get the complete path and just set the source property of our Image control to that path. We remove the file from the collection and update our status bar. If you would like to see how to implement this application in Windows Presentation Foundation and Visual Basic, see the next article. |
VBTrain.Net Nugget
By Jeff Rhodes, Platte Canyon Multimedia Software Corporation Creating a Windows Presentation Foundation Slide Show As described in the previous article on ActionScript, I wanted to create an application for the "Hack Ack" that would dynamically read photos and music from subdirectories and display them in a multimedia slide show. In addition to the Flash/AIR version, I wanted to create a Silverlight version. However, just like Flash in a browser would not have sufficient permissions to read the local hard drive, Silverlight in a browser could not either. So I decided to use the similar Windows Presentation Foundation (WPF). The complete application can be downloaded as part of the Archives for Attendees, but here are the main components. I wrote the AIR one first and knew it wouldn't take very long to duplicate the logic. There are few nuances (timers) and different capabilities (Generics), but the logic is largely the same. The design is again to read all the photos into one collection and all the music into another. We randomly grab a music file and start playing it. We handle its "completed" event and launch a new one when the music file is finished. We then start a timer to use with the photos. When the timer fires, we randomly pick a photo, remove it from the collection, and display the photo. Once all the photos are finished, we stop the timer. To store information between timer cycles, we need to declare some variables outside a function block as shown below. Private photoDelay As Integer = 3 ' seconds Private photoList As List(Of FileInfo) Private musicList As List(Of FileInfo) Private timerId As DispatcherTimer We use photoDelay in our timer. Note that it is in seconds here rather than milliseconds. The two List(Of FileInfo) variables are Generics that are similar to the ArrayCollection variables used in the AIR example but which allow us to specific the type of object (FileInfo) being stored. I could have used the similar Vector concept in AIR if I wanted. However, it lacks the removeItemAt method that was very helpful. The timerId variable stores the reference to our timer. Notice that this is a DispatcherTimer. I first used a normal Timer and found out that it runs on a background thread in WPF. That was a problem since a background thread cannot update the user interface thread. When the application starts, we call the configureApp function as shown below: Private Sub configureApp() Dim startingDir As String = AppDomain.CurrentDomain.BaseDirectory Dim photoDirectory As New DirectoryInfo(String.Format("{0}\photos", startingDir)) Dim musicDirectory As New DirectoryInfo(String.Format("{0}\music", startingDir)) Dim photoFileList As FileInfo() = photoDirectory.GetFiles() Dim musicFileList As FileInfo() = musicDirectory.GetFiles() photoList = photoFileList.ToList() musicList = musicFileList.ToList() End Sub We use the DirectoryInfo class to find our two subdirectories ("photos" and "music") and then get a listing of all the files in them. We store them in our associated photoList and musicList variables. The code for the "Start" button is shown below: Private Sub startBtn_Click(ByVal sender As Object, ByVal e As _ System.Windows.RoutedEventArgs) Handles startBtn.Click Dim timerId As New DispatcherTimer() ' use DispatcherTimer rather ' than Timers.Timer to avoid threading issues With timerId .Interval = New TimeSpan(0, 0, 0, photoDelay) AddHandler .Tick, AddressOf timerHandler .Start() End With playSound() End Sub We create our timer and then set its Interval property to be a TimeSpan that we create from our photoDelay variable (3 seconds). The next line is where we tell WPF what handler (timerHandler) to call when the timer fires. We then start the timer and call the playSound handler below. Private Sub playSound() If musicList.Count > 0 Then Dim rand As New Random() Dim indexNum As Integer = rand.Next(0, (musicList.Count - 1)) Dim musicFileId As FileInfo = musicList(indexNum) AddHandler mediaPlayer.MediaEnded, AddressOf soundCompletedHandler mediaPlayer.Source = New Uri(musicFileId.FullName, UriKind.Absolute) musicList.RemoveAt(indexNum) statusLabel.Text = String.Format("Playing {0}. indexNum = {1}. Count = {2}.", _ musicFileId.Name, indexNum, musicList.Count) End If End Sub Private Sub soundCompletedHandler(ByVal sender As Object, ByVal e As EventArgs) playSound() ' plays next sound End Sub We first check to make sure that we have music files to play. If so, we create a Random object. We then call its Next method to give us a number between 0 and the number of file (again -1 to account for starting from 0). We then grab the reference to the associated music file. We associated our MediaPlayer object's MediaEnded event with our soundCompletedHandler handler. To play the sound, we just set the Source property to a Uri that is basically the URL to the file. Notice how similar this is to the URLReqest that we used in ActionScript. Very importantly, we remove the file from the musicList. This is how we avoid playing the file again and how we know to stop when all the files have been played. Finally, we set the text of our status label to be the information on the name of the file, the index number, and count of the file. The last piece of the puzzle is displaying the photos. That happens in the timerHandler handler shown below: Private Sub timerHandler(ByVal sender As Object, ByVal e As EventArgs) If photoList.Count > 0 Then Dim rand As New Random() Dim indexNum As Integer = rand.Next(0, (photoList.Count - 1)) Dim photoFileId As FileInfo = photoList(indexNum) Dim sourceId As New BitmapImage(New Uri(photoFileId.FullName, UriKind.Absolute)) photoImage.Source = sourceId photoList.RemoveAt(indexNum) statusLabel.Text = String.Format("Displaying {0}. indexNum = {1}. Count = {2}.", _ photoFileId.Name, indexNum, photoList.Count) Else If timerId IsNot Nothing Then timerId.Stop() End If End If End Sub This logic is very similar to that used to play the music files. We again get a random number between 0 and the number of photos still available (-1 to account for the fact that we are starting at 0). We get the complete path (FullName property) and set the source property of our Image control to a Uri built from that path. We remove the file from the collection and update our status label. Once we are done, we stop the timer. If you would like to see how to implement this application in Adobe AIR and ActionScript, see the previous article. |
The EnterPage is distributed up to four times per year, with occasional special issues. Individuals who have expressed interest in Platte Canyon Multimedia Software Corporation or its products receive The EnterPage. Suggestions for articles or proposals for article submissions are welcome. Send information to ep@plattecanyon.com. Back issues of the EnterPage are available at: http://www.plattecanyon.com/enterpage.aspx |
Platte Canyon Multimedia Software Corporation, 8870 Edgefield Drive, Colorado Springs, CO 80920, (719) 548-1110 |