The Ins and Outs of Viewers[1]

Martha H. Weller

University of Illinois at Urbana-Champaign

m-weller@uiuc.edu

 

 

Introduction

 

As the title of my presentation indicates, I plan to discuss ToolBook viewers.  The “Ins and Outs” portion of the title alludes to a very important concept that you must master to take full advantage of viewers:  controlling where messages are being sent – we’ll come back to this later.

 

I would like to spend some time initially covering the basics of viewers – what they are and their key properties.  I believe the best way to understand viewers is to look at lots of examples.  Of course, the minute we begin to look at sample books there are always many related, but somewhat diversionary things to talk about.  To try to stay on topic (so we can look at more uses for viewers), I invite you to read the comments in the sample books – to explain scripting issues that may not be of primary relevance to today’s topic. 

 

Viewer Basics

 

The easiest kind of book to create in ToolBook consists of simple pages, all displayed in the default main window.   (See weller1.tbk)

 

The single-window type of book was all that was available until version 3 of ToolBook came out.  I remember working on a project in the early 90s (using version 1.53) that had me jumping through hoops.  I ended up with pages and backgrounds “piled high” with objects that I would hide or show as needed.  This approach worked, but was challenging to maintain.  Another approach I used was to show pages from a separate book by running a second instance of ToolBook.  Again, this worked – but had a heavy memory overhead.

 

The addition of viewers to the ToolBook environment was a critical advance.  Viewers allow the designer to display multiple windows on the screen simultaneously and to use OpenScript to send messages freely between and among objects in different viewers.  We will look at some specific examples of this powerful design feature in a little while.

 

So, what are viewers then, exactly?

 

Definition of Viewers

 

Viewers are windows that you can create in ToolBook to let you provide multiple-window functionality in your applications. Viewers can display any page of any book on your system without opening separate instances of ToolBook.

 

Creating Viewers

 

Viewers can be created using the ToolBook interface, at author level.  Select the Object menu, then Viewers.  The Viewers dialog window appears.  Click the New button.

 

 

 

The New Viewer dialog window then appears.  The New Viewer dialog window lets you select some initial settings for your viewer.  The screen capture below shows the default configuration.  If you click OK at this point, you will create a new viewer that will not be based on one of the predetermined styles (Dialog Box, Palette, ToolBar, Status Bar, Read-Only Popup, or Thick Frame).  Instead, it will resemble (more or less) the sample viewer displayed – it will have a normal caption bar and a thick frame border style.  It will have a system menu (the colorful icon shown in the sample’s top left corner)  Although not shown in the Sample, it will also have  minimize and maximize buttons, along with the close button in the sample’s top right corner.  The window will be movable (thanks to the presence of a caption bar) and resizable (since the borderstyle is set to thick Frame). If the viewer is resized to be smaller than the size of the background of the page being displayed in it, scrollbars will appear (since scrolling is included as part of the viewer’s style).  .

 

 

 

Notice that the Create New Page and Background option is also checked by default.  This option automatically creates a new page and background for each new viewer you create.  If you deselect the option, ToolBook assumes that you wish to use the page currently displayed in the target window as the defaultPage for your new viewer  This brings us to a very important point about viewers:

 

A viewer cannot exist without a page to display.  When you first open a viewer, ToolBook will need to locate the page it is to display.  The page it looks for is determined by the defaultPage property of the viewer.  This property is set when you initially create the viewer (but it can be modified via the interface or by OpenScript).  As the screen capture below indicates, the Default page setting is located on the Draw tab of the Properties for Viewer dialog window.  ToolBook uses the id number to identify the page.  Whenever possible, it is preferable to work with the names of objects (rather than their id numbers).  I recommend naming the page that will be a viewer’s default page and then changing the defaultPage property of the viewer to refer to the page by name.  Why bother?  It may happen that you accidentally delete the default page for a given viewer.  In this case, if you try to open the viewer (or show it), you will be presented with an error message telling you that the page is missing.  (ToolBook will then display the first page of the book in which the viewer is located. )  It is easier to understand which page is missing if the error message refers to it by name.

 

 

 

You can also create a viewer totally from OpenScript.  A simple button to create a new viewer might have this script:

 

to handle buttonClick

      new viewer

end

 

When you create a new object using OpenScript, it is critical to realize that a unique reference to the newly created viewer is stored in the special local variable it.  The it variable is non-persistent and the value it holds changes based on the various statements being executed.  For this reason, you need to process the value of it as soon as the value is set (before something else can change the value).  So, if I wanted to not only create a new viewer using OpenScript, but also name the viewer for easier manipulation later, I’d change the handler as follows:

 

to handle buttonClick

      new viewer

      name of it = “testViewer”

end

 

The properties of a viewer created using OpenScript will be the same as if you had selected the Custom setting in the New Viewer dialog window.  BUT, no new page/background will be created.  Instead, ToolBook automatically sets the page currently being displayed in the target window as the default page for the newly created viewer.

 

Why do I say “target window” instead of the “main window”? 

 

Most of the time, when you create a new viewer, the target window and the main window will be the same.  But they don’t have to be.  The targetWindow is a ToolBook system property that indicates where ToolBook will execute commands and search for objects.  You cannot set the targetWindow property directly (you can only read it).  As we will discuss later, you generally control the targetWindow property by using the in viewer control structure.  So, it is possible to imagine having a “New Viewer” button on a page that is currently being displayed in another viewer, not in the main window.

 

Assigning a page from one book as the defaultPage of a viewer in another book

 

You can do this without difficulty so long as both books are located in the same directory.  If they are not, ToolBook will have to store the complete path to the book that contains the viewer’s default page.  This will cause you problems down the road, if you distribute your books.  As we will discuss elsewhere, it is very common to first open a viewer, set its currentPage property to a given page, and then show the viewer.  Unfortunately, the instant the book is opened (but not yet displayed), ToolBook must be able to resolve the location of the viewer’s default page.  If the page is not in the same, explicitly defined location, an error will result and you’ll never have the opportunity to set either the defaultPage or the currentPage property of the viewer to a valid page.  Unless you have a very compelling reason to do otherwise, I would recommend keeping the default page of a viewer and the viewer in the same book and, if you must split them across books, to make sure they are in the same directory.  (Note:  you can set the defaultPage of a viewer before you open it.  This would allow you to get around the explicit path problem.)

 

Viewer Properties

 

Viewers, like all ToolBook objects, have properties that control their appearance and behavior.  (See weller2.tbk)

 

Before you actually create a new viewer for your ToolBook application, you should have a clear idea of the purpose the viewer will serve and the advantages a viewer offers over simply placing ToolBook objects either on the page or background.  Here’s one scenario where a viewer offers a distinct advantage:

 

If the objects you wish to make available to the user will appear on multiple backgrounds and/or in multiple books, in the same relative location and serving the same function, use a viewer.

 

Another:

 

If you wish to show the user variable information (e.g., definitions, examples), a viewer can allow you to provide more flexible access to the changing information.  (See weller2.tbk, page 2 for an illustration of this use.)

 

Once you know the purpose of the viewer you will create, many of the property choices fall into place.  Let’s consider some of these key properties.

 

Child or Popup Behavior

 

A viewer can be set to display either as a popup window, or as a child window.  A popup window floats on top of other windows and can move outside of its parent window. 

 

A child window is contained within its parent window.  A child window can be “clipped” by the parent window.  This means that you can position a child window in such a way that a portion of the child window is hidden by its parent.

 

When to Use Child or Popup?

 

As we will see in a moment, only popup viewers can be opened as modal windows (meaning you have to dismiss them before you can do anything else).  Consequently, a major use for popup viewers is to create dialog windows – when you need to ask the user a question, or require the user to make a choice.  Also, since popup viewers can be moved around the screen, they are useful for providing supplemental information or controls to the user. 

 

Since child viewers are contained within their parent viewer, it is less likely that you will allow the user to reposition them (although you could).  I tend to use child viewers to add features that are either always available to the user or are used extensively throughout the application.

 

A Word about Positioning Viewers

 

Child viewers are easy to position, especially if you don’t let the end user change the position.  You can set the pixel coordinates of the viewer when you create it.  These coordinates are also easy to visualize, since they are based on the top left corner of the parent viewer.  You also don’t have to worry about changes in video adapter resolution (e.g., 640 x 480 vs. 800 x 600 vs. 1024 x 768, etc.), so long as the parent window fits within the lowest resolution you require for your application.

 

If you do need to control the position of a child viewer when it is opened, the code might look like this (imagine a button that opens the child viewer):

 

to handle buttonClick

            open viewer “childDemo”

            position of viewer “childDemo” = 100,100   --these are pixels

            show viewer “childDemo”

end

 

Positioning popup viewers is a bit trickier.  In fact, most of the time you’ll probably not try to position the popup viewer explicitly.  Instead, when you create the viewer, you’ll simply choose to have the viewer appear centered on the desktop.  This is an excellent choice for popup viewers that you use to require information or decisions from your users.  Sometimes, however, you would like to set the initial location of a popup viewer –even if you plan to let the user move the viewer around the screen.  You may wish to do this for aesthetic reasons or to make sure that the initial location of the popup viewer doesn’t obscure information that the user may need to see while the popup is open.  You should also consider the possibility that your user may not wish to move the popup viewer (or may not even realize that moving the viewer is an option).

 

To get a feel for the primary difference in positioning child and popup viewers, try this experiment on your own.  On page 2 of the weller2.tbk sample, switch to Author level and open the command window (shift + F3).  Return to Reader level and select the Behavior (Child or Popup) option from the combo box.  Click the Child and Popup buttons to open both viewers.  Now, type the following OpenScript statements into the command window, pressing Enter after each.  Observe the positions of the two viewers.

 

position of viewer "childdemo" = 0,0

position of viewer "popupdemo" = 0,0

 

Even though we appear to be typing in the same coordinates (“0,0”), the results are quite different.  Both positions are being expressed in pixels, but the popup viewer’s position is relative to the entire desktop, while the child viewer is constrained by the bounds of its parent window.  If your video adapter is set to 1024 x 768, you could set the position of the popup viewer to 1000,700 and just see the top left corner of the viewer in the bottom right corner of your desktop.  The visible range for the child viewer is determined by the dimensions of the parent window.  For my sample books, I used the standard 9000 x 6000 page unit background size.  This equates to 600 x 400 in pixels.  (If you aren’t familiar with the Unit Conversion Tool provided under the Tools menu, take a look.)  If I set the position of my child window to 580, 380 – I can just barely see the top of the viewer in the bottom right corner of the main window.  (To close the viewers, just select another option from the combo box.)

 

Position a Popup Viewer Precisely, Regardless of Video Resolution

 

Given that popup viewers are positioned relative to the entire desktop, you might wonder how you can specify a set of coordinates that will work with all possible user video resolutions.  If you are designing your application while working at a screen resolution of 800 x 600 and pick a set of coordinates for a popup viewer based on what looks good (i.e., how close the popup viewer is to something else in your application), you will have a surprise when you try the application at 1024 x 768.

 

The solution is this:  position your popup viewer based on the position of some other object within your ToolBook application.  This will allow you to have your popup always appear in the same relative location, regardless of the video adapter’s resolution.

 

The weller2.tbk sample shows you two examples using the pageUnitsToScreen( )[2] function to achieve this relative positioning.

 

The first example (click button Position1) opens the popup viewer and positions it relative to the combo box in the main window.  Here’s the code from that button:

 

to handle buttonClick

--keep in mind that we are altering the position of the viewer

--in a non-persistent way.  The next time the viewer is opened

--(assuming this handler isn't run), its position will revert to its

--saved position

in mainwindow

    if not isOpen of viewer "popupDemo"

       open viewer "popupDemo"

    end if

    position of viewer "popupDemo" = pageUnitsToScreen (position of comboBox "choices", mainWindow)

    if not visible of viewer "popupDemo"

      show viewer "popupDemo"

    end if

end in

end

 

Notice that I am checking to see if the viewer is already open and later, already visible to determine whether I issue the open and show commands.  I do this because I give you two buttons, allowing you to change the position of the viewer.  Once the viewer is open (or visible), I don’t need to open it (or show it) again.

 

The second example (click button Position 2) illustrates how you can position a popup viewer relative to a child viewer.  The relevant line of code is:

 

position of viewer "popupDemo" = pageUnitsToScreen("0,0", viewer "childDemo")

 

Notice that I used a set of coordinates (“0,0”) here to refer to the top left corner of the childDemo viewer.  If I had wanted to align my popup viewer with an object displayed within the child viewer, I could have used either the object’s coordinates or an expression that referred to the object’s position .  For example, to open the viewer relative to the position of the text field:

 

position of viewer "popupDemo" = pageUnitsToScreen (position of field "text" of page "childdemo", viewer "childDemo")

           

 

Notice that I had to provide a full reference to the location of the text field by including the name of the page. (I could have used the in viewer structure and wouldn’t have needed to use the name of the page – more about this later.)

 

There are several additional conversion functions that can be useful when positioning child and popup viewers relative to one another.  The two defined below would let you change the position of child viewers or objects in a viewer to match the position of a popup viewer.  These might come in handy for designing game play.  (You will have to experiment with them on your own.)

 

ScreenToClient (<coordinates>, viewer reference)

 

Converts pixels relative to the screen into pixels relative to the origin of the client window of the specified viewer. Use this function to align a viewer that is a child of the viewer's client window with a popup window. For example, a non-tiled child window is a child of a client window.

 

screenToPageUnits(<coordinates>, <viewer reference>)

 

Converts pixels relative to the screen to page units for the specified viewer. Use this function to align an object on the page displayed in the viewer with a popup window.

 

Viewer Modality

 

By default, whenever you show a viewer, the viewer is displayed in “normal” mode.  Normal means that the most recently opened viewer is active and you can interact freely with other ToolBook viewers and objects.  To show a viewer in normal mode you use this OpenScript statement:

 

            show viewer <foo>

 

(Here’s that famouse <foo> , which simply means that you can substitute the name of any valid viewer here.)  Notice that you don’t use any parameters to indicate normal mode.

 

 

There may be times when you will wish to show a viewer in other than the normal mode.  The most obvious situation is when you wish to ask the user a question and require an answer before anything else happens.  In this case, you will need to show the viewer in “modal” mode.  The OpenScript statement would be:

 

            show viewer <foo> as modal

 

The end user must perform whatever action is required to close the viewer before any other script will be executed.

 

Finally, it is possible to open a viewer so that it is not active when it is opened. The OpenScript statement would be:

 

            show viewer <foo> as notActive

 

It is a bit harder to see when and why you might wish to do this.  In my weller2.tbk sample, I give you three ways to open the popup viewer and I’ve included a text field that doesn’t have its scripts activated (so you could type into it).  Try out each button and attempt to type immediately into the field.  When the viewer is shown as modal, the viewer is active and has the focus – so you can immediately type into the field.  When the viewer is shown as notactive, you cannot type immediately and using the tab key has no impact.  You would have to click the viewer first to make it active.  When the viewer is shown normally, the field doesn’t have the focus, but pressing the tab key places the cursor into the field and you could begin typing.

 

Style

 

A viewer’s style has to do with its overall appearance.  Do we see a caption bar?  Is there a border around the viewer and how thick is it?  Is there a system menu?  Maximize/minimize controls?  Does the viewer have its own menu bar?

 

The weller2.tbk sample lets you examine how various style settings interact.  You can examine the code in the update button of the Style Controls page (which is displayed in its own viewer) to see how these style properties can be controlled using OpenScript.

 

There are other things that can be examined concerning viewer properties.  Of particular interest are the various options available to you on the Behavior tab of the viewer properties dialog window.  (The one I use most often is the ‘close on buttonclick’ option.  This is automatically set when you create a viewer using the ‘Read-only Popup’ template.)

 

Sending your OpenScript Commands to the Correct Viewer

 

A major challenge in using viewers is knowing how to make sure the commands you send (the messages) get sent to the correct target object. 

 

In the sample book, I provide a button that is intended to change the fillcolor of the combobox that can be seen on the page in the main window.  Here’s the handler:

 

to handle buttonClick

     fillColor of  comboBox “choices” = lightGray

end

 

 

When clicked, the following error message appears:

 

 

To be able to change the object being displayed in one viewer in response to an action occurring in another viewer (like clicking on a button), you have two choices:

 

  1. Provide a complete identification of the target object.  Ex:  fillcolor of comboBox “choices” of page “keyProperties”

  2. Use an in viewer control structure to contain the messages to be sent:
             in mainwindow
                   fillColor of comboBox “choices” = lightGray
             end in

 

Using the in viewer control structure is particularly helpful when you have multiple messages to send, involving several objects.  (Remember:  I could have referred to the mainwindow as viewer id 0, or I can actually rename the mainwindow and say viewer “main”, for example.)

 

Sample Viewers from Real Projects

 

For the past few years, I’ve worked on a project originally designed and developed by Gerald Nelson (Associate Professor, Agricultural and Consumer Economics, University of Illinois).  Dr. Nelson has created computer-based courseware (DiscoverEcon) that is intended to accompany specific economics textbooks published by McGraw-Hill. The courseware is intended to supplement the textbook (not replace it) and provides many interactive exercises for the student.  The design of the software has been evolutionary.  Its original format (long before I became involved with the project) was in HyperCard.  Later, it was converted to ToolBook (version 1.5).  When I began working on the project, we undertook a major overhaul of the design.  This new design has, in fact, undergone a couple of subsequent major revisions.  One of the major thrusts has been to modify the structure of the books and develop programming strategies that would make it easier to adapt the existing courseware for additional textbooks.

 

Overall Design

 

The most recent version of the courseware was delivered to the publisher only a few weeks ago.  McGraw-Hill publishes three “flavors” of this particular economics textbook by Schiller:  The Economy Today, The Macro Economy Today, and The Micro Economy Today.  The “Macro” and the “Micro” versions are actually subsets of the full text.  McGraw-Hill will include a single “flavor” cd-rom with the corresponding textbook. 

 

The full set version of the courseware includes individual ToolBook books as follows:

 

37 chapters, plus 2 appendices and one introductory book

10 system books

 

an additional 37 books that hold data for a multiple choice exercise for each chapter

numerous graphics and an assortment of audio and video files.

 

 

Our challenge, over the years, has been to look for ways to modularize aspects of the interface, so we could reuse components both to deal with the need for multiple sets for a given textbook and to adapt the courseware to new textbooks.

 

Since I can’t provide access to the entire program here, I’ve taken some screen captures and placed them in another sample book (weller3.tbk), along with some comments.  Also, since the topic is viewers, I’ve taken the opportunity in weller3.tbk to demonstrate some additional uses of viewers.  You should look at the sample book in conjunction with reading the remainder of this paper.

 

Tab Viewer

 

The first viewer I present in the weller3.tbk sample is what we refer to as the “tab” viewer.  In one of our earlier versions, you could actually see the tab design (adapted from the tab widget included still in the library.tbk book that comes with ToolBook).  Since we have three slightly different versions of the courseware to deal with, we created three tab pages.  We keep all three versions in a single book.  The full version of the text, with all 37+ chapters, requires a slightly larger background for the tabs than do the other two versions.  We take advantage of two very important viewer features here:

 

  1. You can set the viewer to “autosize to page”.  In this way, we can use a single viewer to display any of the three tab pages and the viewer will automatically adjust.

  2. We set the viewer to tile to the top of the screen.  This automatically positions the viewer to fit horizontally within the bounds of the parent window (here, the main window), at the top edge.

 

Scripting Challenges

 

We learned early on to avoid placing scripts in objects.  Imagine correcting code buried in 37 separate tab buttons!  Instead, we use the shared script approach – attaching a single script to the multiple buttons.

 

Control Panel Viewer

 

The control panel consists of several buttons and fields that allow the user to move within each chapter and to access some special features (glossary, results, help).  It also provides areas in which we show special information such as specific pages in the corresponding textbook and the user’s location in the courseware.

 

Scripting Challenges

 

For the most part, the buttons in the control panel contain only very brief scripts – to send a message to longer handlers that reside in a system book.  Originally, we had only one system book that kept getting bigger and bigger.  For the most recent revision, we decided to divide the contents of our single system book among several.  We have tried to divide the handlers up in a systematic way, grouping routines that deal only with exercises in a single system book.  We hope that this may facilitate updates and help us track the code better.

 

It’s hard to know where you should put your handlers.  We have not followed a strict rule on this, as some do.  My instincts tell me to avoid placing long, complex handlers inside objects.  I prefer to place them at a higher level.

 

Exercises Viewer

 

Since the interactive exercises are such an important part of the DiscoverEcon courseware, we provide easy access to them by including an Exercises menu item.  This opens a viewer containing a listing of all of the exercises for the textbook.  As with the tab viewer, we have three pages (one for each version of the textbook).  We use system variables, set when the program executable is launched, to determine which textbook version is in use and this determines the page that is displayed in the viewer.

 

Preferences Viewer

 

As another menu option, the user can access a viewer that allows him to set certain preferences related to visual effects and audio.  We also have an ftp feature that students can test and they can reset the settings that indicate their web browser preference (so they can specify a different web browser to launch to access various web sites linked in the books).  As with the control panel viewer, the Preferences viewer and the page it displays exists in only one location and functions seamlessly regardless of the version of the courseware launched.

 

There are several additional viewers (Help, About, Jokes, Calculator, Results, Glossary) that all work in pretty much the same way – the user specifically requests the feature by clicking a button.

 

 

Graph Viewer

 

I want to go into more detail concerning two viewers that work together:  the Graph Viewer and a separate viewer that provides controls related to graphing.

 

 

The image above shows the graph viewer as it appears in the graphing system book.  It exists only as a single page, single background.  The image below shows the control panel for the graphing viewer, with all of the controls now visible.

 

 

Because we might get lost in the details of the scripts, I’m going to walk you through some comments to explain how we work back and forth among the main window and the graph and graphing controls viewers.


 

User Action

Message / where received

Result

User enters a page on which a graph needs to be displayed.

Enterpage handler at the page level.  In addition to setting some system variables used by the graphing routines, this handler calls another handler:  setGraph.  The setGraph handler is located in the graphing system book’s book script.  Example code:

 

send setgraph 0,0,25,6, "Quantity of typing","Price of typing"

 

Graphs vary page by page, depending on the content being taught.  We have a number of system variables that let us set the captions for each axis.  We also pass some variable data as parameters when we call a handler that sets up the graph scale.  (Note:  we could do everything by passing parameters, or use system variables, user properties, etc.  The important thing is that we have generalized our handlers that deal with graphing so that we display all of our graphs by feeding data to objects on a single page (displayed in the graph viewer).

 

 

SetGraph

 

The setgraph handler uses the system variables set in the enterpage handler, does some checking and then calls some additional handlers.

 

Example: 

 

send graphinit to background "graphback" of book grafBook

 

Notice that we are sending the message to a specific location (grafBook is a system variable that stores the dynamic path of the graphing system book). 

The graph is modified as needed for the exercise or example.

 

 

 

 

 

Note:  Handlers that are located in the book scripts of active system books (i.e., included in your sysbooks) are directly “callable”.  Unfortunately, your book script can fill up pretty quickly.  The solution is to use multiple system books and/or to divide the work to be done so you can place some of the code in additional handlers, located either in a page or background script (for example).  The handlers that do the bulk of the work to set up the graph are located in the background script of the graphing page.

User clicks the “Graph” button on the graphing controls viewer.

Here’s the entire script of the graph button:

 

to handle buttonClick

                system DEcurrentPage

                syscursor = 4

                in mainWindow

                     send drawGraph to DEcurrentPage

                end in

    sysCursor = 1

end

 

DecurrentPage is a system variable that we update to provide a complete reference to the page being displayed in the main window.  Just as the information we need to set the graphing environment up initially varies page by page, so does the specific graphing routine to be drawn.  The drawGraph handler in the page script obtains various values required to draw the graph (from information contained in the handler, or stored in hidden fields on the page) and then calls higher level routines located in the graphing system book to actually do the work.

 

Our final example from the DiscoverEcon software is the matching exercise at the end of each content chapter.  Each chapter contains a page as shown below.  The user can specify between 5 and 15 questions to try and then clicks the “Take Quiz” button.  The script of this button performs some checks and increments an array counter that tracks how many times the user has tried the quiz.  The script ends with this statement:  send startMCQuiz.

 

 

The startMCQuiz handler is located in the system book devoted to exercises and reads as follows:

 

to handle startMCQuiz

            system exerBook

            send startMCQuiz to page "MCQuiz" of book exerBook

end

 

The system variable exerBook contains the dynamic path to the exercise system book.  We are using the handler in the book script to redirect the message to the handler in the page script of a page in the book.  (This was done primarily to make sure we didn’t run out of room in our book script.)

 

MCQView Viewer

 

The exercises system book contains 5 pages that we use to present the multiple choice questions.  (We’ve allowed for 5 pages, but in practice only 2 or 3 are ever needed.)  When the startMCQuiz message is sent to page MCQuiz, a series of events occur:

 

1.      Initialize several variables.

2.      Check that the number of items entered by the user is between 5 and 15.

3.      Determine the chapter number (relative to the full set version of the textbook) of the current book; pad that number with an extra 0 (if necessary) and create a string that points dynamically to the toolbook file that contains the multiple choice questions for the current chapter.

4.      Make sure the data file is present; give a warning if it isn’t and exit the handler gracefully.

5.      Hide certain objects on the page in the main window; show a message indicating that we are preparing the quiz.

6.      Clear out any previous quiz items that have been placed on the quiz pages.

7.      Obtain a randomly selected list of numbers to use to draw corresponding quiz items from the data book.  Use these items to actually set up the quiz. During the setup process, the screen is locked so the end user doesn’t see what is going on. Once all of the quiz items have been placed on the question page(s), the MCQViewer is shown.

8.      Show  controls and field objects on the background of the page in the main window.

 

The MCQViewer is displayed in the screen capture below.  It is a scrolling viewer and its style is shadowed.  Each time a user selects a number of items to include in the quiz, the program creates a randomly generated set of numbers (ex:  3,1,7,9,23,46,2,8,4,10,50,33,13,6,15).  These numbers are then used to determine the page number in the data book from which the item data are read.  For the first item displayed, for example, the program reads the information located on page 3 of the data book that corresponds to this chapter (sch08003.tbk). 

 

 

Figuring out a way to populate our quiz pages was the biggest programming challenge in this multiple-choice exercise.  Some of the question items have graphics associated with them, many do not.  Since we are building our quizzes dynamically each time, we have no way to predict where a graphic needs to be presented.

 

The solution to our problem lies in the NviewLib dynamic link library (dll) graciously provided free by K. Nishita.  The web site listed for this is:  http://freeware.freeservers.com/  (but this actually redirects you here:  http://home.att.net/~knishita/).  This dll lets you import and display file formats (such as gif or jpg) at runtime.  Note:  if you’re creating a commercial product, stay with the jpg format rather than gif – to avoid need to license the gif format.

 

I’ve included the MCQuiz page script as an Appendix A.  As I looked through it, trying to “summarize” how it works, I realized what a complex piece of coding it really is.  The part of the script that actually places the question items on the page is the setupQuiz handler.

 

Without trying to explain all of the little details that make this work, here’s the general process:

 

  1. Read all of the selected data items into an array just before we begin building the question pages – to streamline handling the data later.
  2. Set up variables to control where the first objects will be placed on a page (i.e., the coordinates), how much space to leave between various objects.  (Even though we have a template group to start with, the length of questions and choices vary.)
  3. Begin (or continue) a loop, based on the total number of questions to display.  Examine the appropriate data item to see if there is a graphic to import – this determines which template we will copy.
  4. Keeping track of the question page you’re currently working on (so you know how to get back to it), go to the template page and copy the appropriate template group.  Return to the question page.
  5. Check to see if the variable lastgroup is null.  If it is, the group we’re about to paste is the first one on the page.  If not, we have a group’s actual name stored in the lastgroup variable and we can check the position of that group to begin to figure out where we will paste our new group.  We have to do a check each time to see if we’re about to run out of space on the page.  How much space we need at any point depends on the type of template (graphic or not).  We allow a fixed amount of extra space for a graphic, to do the “out of room on the page” test.  If we determine that we’re out of room on the page, we literally go to the next question page (we’re keeping a counter to increment for each question page).  And, we reset the starting position.
  6. We make sure that the focuswindow is the mcqviewer and then send the paste message.  (Remember, we had copied the template group, but hadn’t pasted it anywhere yet.)
  7. When you paste something in, the object you paste is the selection and we use this fact to quickly name the new group.  The name is based on the type of template group it is and the value of the loop variable.  We then store this name to the lastGroup variable.
  8. We now have a group pasted in and named, but it isn’t positioned correctly.  We have a variable called groupPos, which starts off for each page at a specific x,y point.  The x value doesn’t change, but the y coordinate is increased for each new group that is added to the page.  We use this variable to position the new group.
  9. The next part is rather complex to follow.  At this point, I need to save the newly pasted group’s name and position to some temporary variables.  I will import my graphic (using the NviewLib function) and I will use the position of the group I just pasted in as the new position of my graphic.  I find out how tall my graphic is and adjust the position of the group accordingly.  I then ungroup the group and regroup all of the items from the group along with the group into a new group, to which I assign the original group name.  (I probably left out a few details – hopefully, you can trace through the code.)
  10. At this point, I have the graphic (if needed) at the top of the group and empty fields beneath.  I now take the array data and set the text of the fields.  As I populate each field, I run a routine that adjusts the dimensions of the fields so there is no textoverflow and no extra space.  Eventually, the question item is neatly arranged on the page.
  11. The whole process described in steps 3 through 10 repeats, until all of the question items have been placed on the necessary page(s).  At this point, the screen is unlocked and the quiz appears for the user.

 


Conclusions

 

Viewers are an enormously empowering programming feature, once you get the hang of working with them.

 

Key things to remember:

 

  1. Create a new viewer when your viewer needs different properties (style, captionbar, popup vs. child), not when you just need to display a different page in a viewer.  (I’ve seen students want to create a new viewer for every page they wanted to display.)  Master this structure:

    open viewer <foo>
    currentPage of viewer <foo> = page “foo2”
    show viewer <foo>

  2. Watch where you send your messages!  As you work with viewers, take advantage of this structure:

    in viewer <foo>
            --whatever statements affect the objects displayed within the viewer
    end in

  3. Don’t forget that if you are executing a script from within a viewer, you may have to redirect messages to the mainwindow.

  4. When you still can’t seem to get things happening in the correct viewer, try setting the focusWindow to the desired viewer.  (I had to do this in my multiple choice exercise.) 





 

 

 

 


Appendix A:  MCQuiz Page Script

 

to handle startMCquiz

                system MCItemNum,currChap,clipCounter,exerbook,dbFilesPath,bookshortName,pathholder

                dbFilesPath = pathholder & "dbfiles\"

                in mainwindow

                                the scoreflag of this page = 0

                                clipCounter = 0 -- we have to have a separate dummy clip for each bitmap displayed on same page

                                sysCursor = 4

                                MCItemNum = text of recordfield "Question Number" of this page

               

                                if MCItemNum is null or \

                                   MCItemNum < 5 or \

                                   MCItemNum > 15

                                                request "Please enter a number between 5 and 15 in the yellow box above!"

                                                syscursor = 1

                                                break to system

                                end if

                                --all of the data files are numbered according to the big book numbers

                                in mainWindow

                                                currChap = the bbchnum of this book

                                end in

                                while charCount(currChap) < 3       

                                                currChap = "0" & currChap

                                end

               

                                whichDataFile = dbFilesPath & bookshortname & currchap & ".tbk"

               

                                get fileExists(whichDataFile)

                                if it < 1

                                                clear text of recordfield "question number" of this page

                                                request "Not available."

                                                syscursor = 1

                                                break

                                end if

                                hide group "setupObjs" of this background

                                quizPageCount of this book = 0

                                show field "processing" of this background

                                the totalCorrect of this book = 0

                                the totalIncorrect of this book = 0

                                the totalTries of this book = 0

                               

                                               

                                send updateScores to background "showMCQ" of book exerbook

                                send clearOldQuiz

 

                                send getMCItems (MCItemNum)

               

                                clear text of recordfield "question number" of this page

                                show group "quizControls" of this background

                end in

                sysCursor = 1

end

to handle clearOldQuiz

                system settingUp,exerbook

                settingUp = true

                syslockscreen = true

                if isOpen of viewer "mcqView" of book exerbook is false

                                open viewer "mcqView" of book exerbook

                end if

                in viewer mcqView of book exerbook

                                step i from 1 to 5

                                                currPage = "MCQuestions" & i

                                                go to page (currPage) of book exerbook

                                                syssuspend = false

                                                select objects of page (currPage) of book exerbook

                                                if selection <> null

                                                                send cut

                                                end if

                                end step

                end in

                settingUp = false

end

 

 

to handle getMCItems howMany

                system exerBook

                -- this routine will first generate an appropriate set of random

                -- numbers drawn from the total number of questions in the database

                -- for the given chapter

                --=========================================

 

                system currchap,dbFilesPath,bookshortname-- will determine which set of chapter correspondences are in effect

                local randomSet[]

                whichDataFile = dbFilesPath & bookshortname & currchap & ".tbk"

                sysPasswords = upasswords of book (exerBook)

                questionCt =  pageCount of book (whichDataFile) - 1 --modified after adding viewer to test jpg display

                --we need to first check the data tbk file and find out how many items it has in it

 

                randomSet= randomSet(questionCt,howMany)

 

                in mainwindow

                                text of field "quizNum" of this background = howMany

                end in

                --now, we take the randomSet and pass it to the setup routine

 

                send setupQuiz (randomSet)

               

end

 

 

to get randomSet questionCt, howMany

                local qset,randomSet[]

                --set up an array of question numbers 1 to questionCt

                step i from 1 to questionCt

                                item i of qset = i

                end step

                --howMany is how many MC items will be presented

                --we will now draw a random number between 1 and questionCt

                --use that number to draw an item from the qset list

                --put the question number into the randomSet arry

                --clear the item from the qset list and reduce the questionCt by 1

               

                step k from 1 to howMany

                                whichItem = random(questionCt)

                                randomSet[k] = item whichItem of qset

                                --request item whichItem of qset

                                clear item whichItem of qset

                                decrement questionCt

                end step

                return randomSet

 

end

 

to handle setupQuiz randomSet[]

                system exerbook,dbFilesPath,bookShortName,MCItemNum, dynamic qArray[][],currChap,clipCounter,settingUp

                settingUp = true

                syssuspend = false

                --the randomSet array contains the questionNumbers to be retrieved

                --from the database

 

                --this is a test to generate an appropriate number of test items, based on

                -- a.  the number of items selected by the user

                -- b.  the nature of each item (graph or not)

                -- c.  the 'running space':  is there room to present another item on the current

                --       page, or does the current quiz have to spill over onto a second page

 

 

               

                MCItemNum = dimensions(randomSet) -- this tells us how many items are in the quiz

               

                syslockscreen = true

 

                in viewer "mcqview" of book exerbook

                                go to page "MCQuestions1" of book exerbook

 

                                whichDataFile = dbFilesPath & bookshortname & currchap & ".tbk"

                                sysPasswords = upasswords of this book

 

                                step i from 1 to MCItemNum

                                                whichRecord = randomSet[i]

                                                qArray[i][1] = text of recordfield "ID" of page (whichrecord) of book (whichDataFile)

                                                                               

                                                qArray[i][2] = text of recordfield "question" of page (whichrecord) of book (whichDataFile)

                                                               

                                                qArray[i][3] = text of recordfield "choiceA" of page (whichrecord) of book (whichDataFile)

                               

                                                qArray[i][4] = text of recordfield "choiceB" of page (whichrecord) of book (whichDataFile)

                                                               

                                                qArray[i][5] = text of recordfield "choiceC" of page (whichrecord) of book (whichDataFile)

                                                               

                                                qArray[i][6] = text of recordfield "choiceD" of page (whichrecord) of book (whichDataFile)

                                                               

                                                qArray[i][7] = text of recordfield "CorrectAns" of page (whichrecord) of book (whichDataFile)

                                                               

                                                qArray[i][8] = text of recordfield "graphRef" of page (whichrecord) of book (whichDataFile)

                                                               

                                                qArray[i][9] = text of recordfield "tableRef" of page (whichrecord) of book (whichDataFile)

                                                               

                                                qArray[i][10] = text of recordfield "pages" of page (whichrecord) of book (whichDataFile)

                                                               

                                                                --FOR TESTING ONLY

                                                               

                                                                qarray[i][11] = whichRecord -- this will let me check problems in the database

                                end

                               

                                clear syspasswords

                               

                                groupPos = "60,100" --this starts the positioning for the MC groups

                                lastGroup = "" -- this will contain the name of the last group pasted onto the page

                                               -- to let us check if we have room to paste another group, or need to

                                               -- setup an additional testing page

                                Qspacer = 60 -- distance between question bottom and top of first choice

                                Cspacer = 40 -- distance between choice bottom and next choice top

                                GrpSpacer = 300 -- distance between question groups

                                cPos = 1000 

                                buttonxPos = 400

                                fldOffset = 60 -- to make the choice fields line up better with the choice buttons

                                in mainwindow         

                                                quizPageCount of this book = 1 -- default

                                                tquizPageCount = quizPageCount of this book

                                end in           

                                step i from 1 to MCItemNum

                                                --when the data is stored in the array, index #8 and #9 will screen for graph/no graph

                                                grafHeight = 0  -- if there is a graph, these 2 will be changed & will be subtracted

                                                grafSpacer = 0  -- out at the end of the step

                                                if qArray[i][8] is "" and qArray[i][9] is "" -- no graph or table

                                                                currType = "mctype1"

                                                else -- there is a graph or a table

                                                                currType = "mctype3"

                                                end if     

               

                                                go to page "MCTemplate" of book exerbook

                                                select group (currType) of page "MCTemplate" of book exerbook

                                               

                                                currHeight = item 2 of the size of the selection                            

                                                send copy

                               

                                                backPage = "MCquestions" & tquizPageCount

                               

                                                go to page (backPage) of book exerbook

                                               

                                                if lastGroup <> "" -- we've pasted in at least one group already

                                                                -- need to get its bottom coordinate

                                                                bottomPoint = item 4 of bounds of (lastgroup)

                                                                if currType is "mctype3"

                                                                                --allow a 6000 unit extra space for graphic

                                                                                tempHeight = currHeight + 6000

                                                                else

                                                                                tempHeight = currHeight

                                                                end if                                     

                                                                if bottomPoint + tempHeight > item 2 of size of this background

                                                                                in mainwindow

                                                                                                increment the quizPageCount of this book

                                                                                                increment tquizPageCount

                                                                                end in

                                                                                nextPage = "Mcquestions" & tquizpagecount

                                                                                go to page (nextPage) of book exerbook

                                                                               

                                                                                groupPos = "60,100" -- start the positioning over again from the top

                                                                end if

                                                end if

                                                focuswindow = viewer "mcqview" of book exerbook

                                                send paste

                                                name of selection = (currType) & "_" & i

                                                lastGroup = "group " & name of the selection

                                               

                                                position of selection = groupPos --this is starting point for top left corner of group

                                                if currType is "mctype3"

                                                                oldSelection = the selection

                                                                oldGroupName = the name of the selection

                                               

                                                                tempGraph = qArray[i][8]

                                                                tempTable = qArray[i][9]

                                                               

                                                                if tempGraph <> ""

                                                                                whichGraph = dbFilesPath & "graphs\" & tempGraph

                                                                else

                                                                                whichGraph = dbFilesPath & "tables\" & tempTable

                                                                end if

                                                               

 

                                                                get ImportMyGraphic(whichGraph)

                                                               

                                                                focuswindow = viewer "mcqview" of book exerbook

                                                                lineStyle of the selection = none

                                                                grafHeight = item 2 of size  of the selection + 100

                                                                position of the selection = position of (oldSelection)

                                                                tempSelection = the selection

                                                               

                                                                select (oldSelection)

                                                                increment item 2 of the position of the selection by grafHeight

                                                                send ungroup

 

                                                                extend select tempSelection

                                                                send group

                                                                name of selection = oldGroupName

                                                               

                                                                item 2 of groupPos = item 2 of groupPos + grafHeight

                                                end if

                                               

                                                               

                                                whichField = field "itemNum" of selection

                                                position of (whichField) = 60,(item 2 of groupPos)

                                                text of (whichField) = i & "."

                                               

                                                whichField = field "qStem" of selection

                                                position of (whichField) = 400,(item 2 of groupPos)

                                                tempYPos = item 2 of groupPos

                                                text of (whichField) = qArray[i][2]

                                                if char 1 of text of whichField is CR

                                                                clear char 1 of text of whichField

                                                end if

                                                if char 1 of text of whichField is LF

                                                                clear char 1 of text of whichField

                                                end if

                                                send fixHeight(whichField)

                                                position of (whichField) = 400,tempYpos

                                                nextPos = cPos,(item 4 of bounds of (whichfield) + QSpacer + fldOffset)

                                               

                                                whichField = field "choice1" of selection

                                                position of (whichField) = nextPos

                                                position of button "choice1" of selection = buttonxPos,(item 2 of nextPos - fldOffset)

                                                text of (whichField) = qArray[i][3]

                                                send fixHeight(whichField)

                                                nextPos = cPos,(item 4 of bounds of (whichfield) + cSpacer + fldOffset)

                                               

                                                whichField = field "choice2" of selection

                                                position of (whichField ) = nextPos

                                                position of button "choice2" of selection = buttonXPos,(item 2 of nextPos - fldOffset)

                                                text of (whichField) = qArray[i][4]

                                                send fixHeight(whichField)

                                                nextPos = cPos,(item 4 of bounds of (whichfield) + cSpacer + fldOffset)

                                               

                               

                                                whichField = field "choice3" of selection

                                                position of (whichField) = nextPos

                                                position of button "choice3" of selection = buttonXPos,(item 2 of nextPos - fldOffset)

                                                text of (whichField) = qArray[i][5]

                                                send fixHeight(whichField)

                                                nextPos = cPos,(item 4 of bounds of (whichfield) + cSpacer + fldOffset)

                               

                                                whichField = field "choice4" of selection

                                                position of (whichField) = nextPos

                                                position of button "choice4" of selection = buttonXPos,(item 2 of nextPos - fldOffset)

                                                text of (whichField) = qArray[i][6]

                                                send fixHeight(whichField)

                               

                                                whichGroup = name of the selection

                                                show field "feedback" of the selection

                                                groupObjs = objects of the selection

                                                send ungroup

                                                selection = null

                                                sysSuspend = false

                                                select item 1 of groupObjs

                                                step f from 2 to itemCount(groupObjs)

                                                                extend select (item f of groupObjs)

                                                end step

                                                send group

                                                hide field "feedback" of the selection

                                                name of the selection = whichgroup

                                                correctAns of selection = qArray[i][7] -- stores correct answer as a user property of the group

                                                bkpages of selection = qArray[i][10] -- stores page reference for question

                                                syssuspend = false

                                                tempHeight = item 2 of size of selection -- this is after adjustments have been made

                                               

                                                item 2 of groupPos = (item 2 of groupPos + tempHeight + grpSpacer - grafHeight - grafSpacer)

                                                               

                                end step

                                clear settingUp

                end in

                in mainwindow

                                send updateNav to this background

                end in

                show viewer "MCQView" of book exerbook

                in viewer "mcqview" of book exerbook

                                go to page "mcquestions1" of book exerbook

                                send checkState to this background

                end in

end

 

to handle fixHeight whichField

                --we'll change only the height of the field, the width remains constant

                conditions

                when the textOverflow of (whichField) > 0 -- field isn't big enough

                                do

                                                increment item 2 of size of (whichField) by 10

                                until textoverflow of (whichField) = 0

 

 

                when the textOverFlow of (whichField) = 0 -- field is larger than needed

                                do

                                                decrement item 2 of size of (whichField) by 10

                               

                                until textoverflow of (whichField) > 0

                                increment item 2 of size of (whichField) by 10

               

                end conditions

end

 

to get ImportMyGraphic GraphicNameAndPath

 -- Get the "nviewLib.dll" at http://freeware.freeservers.com/

 -- Make sure this DLL is placed in the ToolBook Directory

 -- The file will be placed on your current page as a paint object named MyPic.

 -- Call this function with the following syntax:

 -- get ImportMyGraphic(c:\myfiles\image.jpg)

  system exerbook

 linkDll "user"

  int openClipBoard(INT)

  int closeClipBoard()

  int setclipBoardData(INT,INT)

  int emptyClipBoard()

 end

 

 get sysOperatingSystem

 currsystem = it

 if"Windows 3" is not in currsystem

                 linkDLL "TBFILE32.DLL"

                                STRING getShortFileName32(STRING, WORD)

                 end

                 GraphicNameAndPath = getShortFileName32(GraphicNameAndPath,1)

 end if

 

  get fileExists(GraphicNameAndPath)

  if it <> 1

                request "You seem to be missing files.  Please report this problem."

                in mainwindow

                                hide field "processing" of this background

                                show group "setupobjs" of this background

                                syscursor = 1

                end in

                if isOpen of viewer "mcqview" of book exerBook is true

                                close viewer "mcqview" of book exerBook

                end if

                break to system

  end if

 

 linkDLL "Nviewl16.dll"

  int NViewLibLoad(STRING,INT)

 end

 

 local int handle

 sysLockscreen = true

 

 -- Note: if GraphicNameAndPath is not valid (the file is not there) an error

 -- will occur. Check for the existence of the file first, using OpenScript.

 

 handle = NViewLibLoad(GraphicNameAndPath,0)

 

  get openClipBoard(sysWindowHandle)

  get emptyClipBoard()

  get closeClipBoard()

 

 

 

 if handle is not null

 focuswindow = viewer "mcqview" of book exerbook

  get openClipBoard(sysWindowHandle)

  get setClipBoardData(2,handle)

  get closeClipBoard()

  send paste

 -- must empty the clipboard

  get openClipBoard(sysWindowHandle)

  get emptyClipBoard()

  get closeClipBoard()

 

 end

 

 sysLockscreen = false

 unLinkDll "nViewL16.dll"

 return TRUE

end

 

to handle updateNav

                system exerbook

                in mainwindow

                                tquizPageCount = the quizPagecount of this book

                end in

                if isOpen of viewer "MCQView" of book exerbook is true

                                in viewer "MCQView" of book exerbook

                                                whichPage = 0

                                               

                                                temp = last char of name of this page

                                                whichPage = temp

                                                if whichPage < tquizPageCount

                                                                my enabled = true

                                                else

                                                                my enabled = false

                                                end if

                                end in

                end if

 

end

 

to handle nextMCPage

                system exerbook

                if isOpen of viewer "MCQView" of book exerbook is true

                                in viewer "MCQView" of book exerbook

                                                temp = last char of name of this page

                                                nextPage = "Mcquestions" & (temp + 1)

                                                go to page (nextpage)

                                end in

                end if

                send updateNav

end

to handle prevMCPage

                system exerbook

                if isOpen of viewer "MCQView" of book exerbook is true

                                in viewer "MCQView" of book exerbook

                                                temp = last char of name of this page

                                                prevPage = "Mcquestions" & (temp - 1)

                                                go to page (prevpage)

                                end in

                end if

                send updateNav

end



[1] This paper and the accompanying sample ToolBook files may be distributed without additional permission from me within an instructional environment, for non-commercial purposes.

[2] pageUnitsToScreen(<coordinates>, <viewer reference>

Converts page units for a specified viewer into pixels that are relative to the origin of the screen. Use this function to align a popup window with an object on a page displayed in the viewer.