MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


REALbasic for HyperCard Users

by Matt Neuburg
09/21/2001

A computer is made to program, and what made the Macintosh a viable computer for me in 1990 was HyperCard, the scriptable interface construction kit and freeform database from Apple Computer. HyperCard allowed you simply to draw your program's interface, and then associate interface items with code responding to related user actions -- for example, in the case of a button, what should happen when the user pressed it.

But the Apple folks never really understood the brilliance of HyperCard, or how essential it was to the user community. They stopped giving HyperCard away for free, handed it over to Claris, and then took it back again. They nearly killed it, but then gave it a new lease on life for a while, and have now given up on any further development. Unsupported, HyperCard has begun to grow long in the tooth, showing signs of misbehavior under recent operating systems. This situation has long been bemoaned in TidBITS, the online journal for which I'm a contributing editor; and the search for the "new HyperCard" has more recently been taken up on O'Reilly Network.

For many of us, however, the new HyperCard is already here. It's REALbasic, the integrated development environment and application framework from REAL Software. Like HyperCard, REALbasic lets you draw the interface and assign code to its pieces. Indeed, you can drag a button and a text field into a window, and presto, without any code at all, you've written an application where the user can press the button, type into the text field, move or dismiss the window, and choose the Copy, Paste, and Quit menu items. And REALbasic has some major advantages over HyperCard:

My introduction to REALbasic

My life with REALbasic started in 1998 with a couple of weeks of intense investigation. At first, my HyperCard experience actually hampered me; but once I had shed my attachment to the card/background metaphor and to HyperCard's style of event-passing, I found REALbasic to be extraordinarily congenial, wrote several programs with it very quickly (that in HyperCard I would have found well-nigh impossible), and ended up writing a book about it for O'Reilly & Associates. On my computer, where once I reached automatically for HyperCard for all scripting needs and custom solutions, HyperCard today plays no role at all. What I automatically reach for is REALbasic.

Related reading:

HyperCard home

History and praise of HyperCard

Death of HyperCard (TidBITS)

Death of HyperCard (O'Reilly Network)

Still, while I'd encourage users of HyperCard to look into REALbasic, there are also some potential pitfalls and disappointments to be aware of. There are certain things that, while dead easy in HyperCard, are much more difficult or even impossible in REALbasic. In particular, these tripwires include:

To give a simple example, in HyperCard you can simulate a click anywhere; that's not possible in REALbasic. You can usually work around this by structuring your program differently, but the point is that you do have to structure your program differently than what you are used to, and previous users of HyperCard may initially be put off by this.

Preparing to build a REALbasic application

I'm going to illustrate some of these points by constructing a small REALbasic application intended to copy an actual and fairly typical HyperCard stack. Let me start by describing the stack.

In 1991, Project Gutenberg (an early purveyor of free electronic public-domain texts) published a text file based on the 1911 Crowell edition of Roget's Thesaurus. This text is full of typos and archaisms, but it has a certain classic charm. The next year, Mark Zimmerman reformatted this text as a HyperCard stack; a card consists of the text of one entry, along with three headings at the top displaying the entry's class, part, and section classifications. A while later, I adapted this stack further, adding a hyperlinked search capability and an index card for lookup and navigation by class, part, and section.

Screenshot.
The original HyperCard stack.

In this article, I'll rewrite that stack as a REALbasic application. My purpose isn't to teach you any REALbasic -- see my book for that -- but rather to concentrate on the changes involved. Why? Because these changes reflect the typical differences between HyperCard and REALbasic, and illustrate the mental shift you'll have to make as you migrate from one environment to the other.

As part of the exercise, I'll try to make the REALbasic application look and act as much as possible like the original HyperCard stack. In real life, though, you'd probably want to rethink the interface as well.

Here's the index as displayed by REALbasic. The user clicks on a class heading from the list at the top center of the window. The list at the middle left displays the section headings belonging to that class. The user then clicks a section heading. The list at the bottom displays that section's part headings. The user clicks a part heading; the list at the middle right displays that part's actual word entries. Finally, the user clicks a word entry to display it.

Screenshot.
Index window in the REALbasic application

Here's what happens after the user clicks "573. Diffuseness" from the middle-right list in the index: The entry for "Diffuseness" is displayed. It looks much like Mark Zimmerman's original HyperCard stack, but I've added some text styling to make the entry easier to read and there are some navigation buttons at the bottom. Plus a fourth heading is displayed at the top, which is really just a copy of the first line of the entry.

From here, the user can navigate to the previous or next entry (572 and 574) using the arrow buttons at the bottom. Or, the user can click any heading to navigate by heading. Or, the user can click an entry number within the entry text to navigate to that entry -- for example, clicking "577" at the end of the second paragraph brings up entry 577 ("Inelegance"). Or, the user can click any word in the entry to search for the next occurrence of that word.

Screenshot.
Entry window in the REALbasic application

In HyperCard the difference between the index card and the entry cards is expressed as two different backgrounds. In REALbasic, there is no card/background metaphor, so we need two different windows. Let's call these "Index" and "Entry". To be more HyperCard-like, we can make these behave like a single window by having them appear only one at a time in the same position on the screen. So Index has a method doshow:

  if not self.visible then
    self.top = entry.top
    self.left = entry.left
    self.show
    entry.hide
  end

Entry has a method doshow as well, which is parallel. REALbasic doesn't easily support HyperCard-like visual effects during the transition between cards, so we won't have any.

HyperCard is natively a text database, where storage and retrieval of data is handled automatically and transparently; thus the entries are just different cards of the second background, where showing a card means showing its data automatically. To navigate from entry 573 to entry 574, for example, we simply "go next card."

In REALbasic none of that is true! There are no cards -- to show different entries we must replace the data displayed in the Entry window with other data. Thus, we must implement storage of the data in such a way that for any entry we can retrieve the data on demand. And when we retrieve new data, we must manually repopulate the Entry window.

To solve this problem, let's take advantage of the fact that REALbasic provides a native "database" file format, along with methods to access the data. This isn't as fast or compact as HyperCard's built-in storage, and of course it's nowhere near as automatic, but it will prove adequate to our purposes. So, unlike HyperCard (where the code and data together make up a stack), in REALbasic the data will live in a separate file and our application will just be a way of viewing it.

Constructing the REALbasic application

The first step is to build the database file, which we'll call rogetdb. I start by exporting the entry text from the HyperCard stack as a series of styled text files, because the entries have styling I want to preserve. The files are named "roget1", "roget2", and so forth. Because the cards are exported in order, the numeric part of these names can be used as each entry's index value.

In the REALbasic IDE, I choose "New REAL Database" to create the database file and double-click the database entry in the Project Window to edit its schema. This consists of a single table called "data". REALbasic's database file format has no provision for storing styled text, so I need to hold the text information and the style information in two different fields, which I call txt and styl. I also add a numeric field num to hold the index value. I write and run a routine to open each styled text file in turn, capturing its text and style information, along with the index value from the file's name, and adding a record to the database containing this information.

Next, I export the HyperCard stack's class, section, and part headings -- along with the information from the top line of the entry -- as a tab-delimited and return-delimited text file. In REALbasic, I add class, section, part, and numword fields to the database file's schema, and run code to import the data by parsing the text file. I'm not showing you any of that code, because it isn't part of the final project. Once the database file is built, the code that built it can be thrown away! Besides, all this isn't really so different from how you'd initially construct a HyperCard stack.

Now we are ready to create our actual REALbasic application. The first thing it must do is link up with its data. We assume that rogetdb is in the same folder as our application. Since this linkage belongs to the application as a whole, we create an application subclass and give it a database property called d. In the application subclass's open event, which is the first code executed as the application starts up, we put this code:

self.d = openrealdatabase(getfolderitem("").child("rogetdb"))

Now let's consider how to navigate among entries. Only the Entry window needs to do this, so we give it a database cursor property called c and a method dodisplay. The idea of this architecture is that any routine can set c to point to the desired record, and call dodisplay to show it.

Manipulation of c will be performed using a SQL select command to obtain a virtual table of records, and possibly the movenext command to cycle forward through the virtual table, until the c property's current record is the right one. Here is the dodisplay code:

  txt.text = ""
  txt.settextandstyle 
    c.field("txt").stringvalue, c.field("styl").stringvalue
  txt.sellength = 0
  txt.selstart = 0
  class.text = c.field("class").stringvalue
  section.text = c.field("section").stringvalue
  part.text = c.field("part").stringvalue
  numword.text = c.field("numword").stringvalue
  idx = c.field("num").integervalue
  doshow

class, section, part, and numword are the StaticText controls displaying the headings at the top of the window; txt is the EditField displaying the text of the entry.

idx is an integer property of the Entry window. Its purpose is to make it easy to move to the next or previous record. For example, here is the code for the method gonext which is called by the action event handler of the Next button:

  c = app.d.sqlselect(
    "select * from data where num=" + str(idx+1))
  if c.eof then
    beep
    return
  end
  dodisplay

That's our equivalent of HyperCard's "go next." It's a good illustration of how c and dodisplay are intended to be used. The code for goprev is completely parallel.

When the user clicks a heading, the display should cycle forward to the first entry that has a different heading in that category. In HyperCard, the way I did this for, say, the class heading was to give the class field this mouseup handler:

  get me
  lock screen
  repeat while it is bg fld "class"
    set cursor to busy
    go next cd of this bg
  end repeat

The REALbasic approach is almost exactly the same. All three headings work the same way, so we move the common code out into a method called nextdifferent, which takes two string parameters -- the current value displayed by the particular static text, s, and the name of the database field to look in, fld. Here is the nextdifferent method:

  c = app.d.sqlselect(
    "select * from data where num > " + str(idx))
  while (not c.eof) and c.field(fld).stringvalue = s
    c.movenext
  wend
  dodisplay

To illustrate, here is the class element's mousedown event handler:

  nextdifferent me.text, "class"

Now let's talk about what happens when the user clicks in the text of an entry. Recall that if the user clicks in a number we want to navigate to that card; otherwise we want to find the word clicked in. In HyperCard, here's the code I was using in the txt field's mouseup handler:

  put the clicktext into what
  if what is a number then
    find what in bg fld "numword"
  else
    put "find word" && quote & the clicktext & quote 
        && "in bg fld " & quote & "txt" & quote
    returnkey
    returnkey
  end if

This relies on two things we get for free in HyperCard: the clicktext property, and the ability to "find." But we don't get either of those in REALbasic! Thus we must implement them ourselves. So the first thing that the txt EditField's mouseup event handler must do is to work out the "click text." To do this, I start with the point of the selection and expand in both directions until I hit a character that isn't a word character:

  dim sel as integer
  dim first,last as integer
  dim s as string
  dim acc as string
  dim r as regex
  dim m as regexmatch
  acc = "abcdefghijklmnopqrstuvwxyz1234567890"
  sel = me.selstart
  s = me.text
  first = sel+1
  last = first
  do until instr(acc, mid(s,first,1)) = 0
    first = first - 1
  loop
  do until instr(acc, mid(s,last,1)) = 0
    last = last + 1
  loop
  s = mid(s, first+1, last-first-1)

Now s contains the click text, and we need to search for it. If s is a number, the REALbasic approach is very like HyperCard's:

if str(val(s)) = s then
 c = app.d.sqlselect(
   "select * from data where numword like '" + s + ".%'")
 dodisplay
 return true

In HyperCard, if s is text, recall that we simply use "find word" to cycle through every entry looking for a word consisting of the text we specify. In REALbasic, though, we must implement this ourselves.

To handle the fact that the search is for a word, we'll take advantage of REALbasic's new regular-expression support. To handle the fact that the search is from card to card, we rely on a utility method, canfind, which accepts a regular expression and a SQL command specifying a set of entries to cycle through, and calls dodisplay if the search succeeds. We look first in the same entry we're already in; if we don't find the text, we look in the entries that follow it; if we still don't find the text, we look in the entries that precede it. Finally, if we've found the text, we select it.

else
  r = new regex
  r.searchpattern = "\b" + s + "\b" // whole word
  r.options.casesensitive = false
  m = r.search(me.text, last)
  if m = nil then // try subsequent
    m = canfind(r, "select * from data where num>" + str(idx))
    if m = nil then // try previous
      m = canfind(r, "select * from data where num<" + str(idx))
      if m = nil then
        beep // not found
        return false
      end
    end
  end // found it, mark it
  me.selstart = m.subexpressionstart(0)
  me.sellength = last-first-1
end

Here is the canfind code (r and s are the incoming regex and string parameters):

  dim m as regexmatch
  c = app.d.sqlselect(s)
  while not c.eof
    m = r.search(c.field("txt").stringvalue,0)
    if m <> nil then
      dodisplay
      return m
    end
    c.movenext
  wend
  return nil

Finally, we turn to the Index window. In HyperCard, the interface here is made up of four fields with "Lock Text", "Don't Wrap", and "Auto Select" turned on. In REALbasic, they're ListBox controls named class, section, part, and numword.

Recall that when the user clicks on a ListBox, we want the other ListBoxes populated appropriately. For example, if the user chooses from the class ListBox, the section ListBox should be filled with the different section headings for that class. We could implement this by searching the database every time the user clicks, but this would be too slow. We want the ListBoxes repopulated instantaneously. Because the structure of the database won't be changing, we can implement this by loading the heading information at startup into a data structure that can be queried immediately.

This is a situation where REALbasic is far stronger and more elegant than HyperCard. In HyperCard, structured data storage, aside from fields and cards, is primitive. Basically, we have to use strings (or fields, which amount to the same thing), and we can subdivide these only by the built-in notions of character, item, and line.

My solution in HyperCard was thus to use a large number of hidden fields whose names were the key to obtaining their contents. REALbasic on the other hand has true arrays, and lets us create custom classes that function both as data structures and as repositories of code.

Therefore we'll use an array where each element consists of text plus a pointer to a further array, creating a hierarchy identical to the hierarchical behavior of our ListBoxes. Here's a diagram of the resulting structure, which obviously has four levels: After starting with a pointer to embody the structure as a whole, we have the class level (which is shown in its entirety), the section level and part level (of which a few entries are shown), and the numword level (not shown).

Diagram.
Schematic diagram of the index data structure.

So we create a list class with two properties: text, a string; and list, an array of lists dimensioned initially to "-1" (meaning an empty array). We give the application subclass a list property, theList, which will hold our populated data structure. To build the data structure at startup, we run through the entire database, gathering the headings and handing them to our data structure. Here's the code from the application subclass's open event handler:

  dim theList as list
  dim c as databasecursor
  dim anarray(-1) as string
  c = d.sqlselect("select * from data")
  theList = new list
  while not c.eof
    redim anarray(-1)
    anarray.append c.field("class").stringvalue
    anarray.append c.field("section").stringvalue
    anarray.append c.field("part").stringvalue
    anarray.append c.field("numword").stringvalue
    theList.accept anarray
    c.movenext
  wend
  self.theList = theList

The question is then, what the list class's accept method does. It turns out that we can formulate this very elegantly, because the rule is the same no matter what level we're at. If the text for this level is the same as the text of the last existing entry, there is nothing to do at this level. We just want to hand the rest of the information down to the next levels.

Otherwise, we need to create a new element at this level, and then hand the rest of the information down to the next levels. It should now be clear why we use an array as the accept method's parameter: The first element in this array is the text intended for our level, so this is what we examine to decide what to do. When we're done, we strip it off and use the remaining array as the parameter for the call to accept at the next level down. Here is the list class's accept method:

  dim u as integer
  u = ubound(self.list)
  if u = -1 or anarray(0) <> self.list(u).text then
    self.list.append new list
    u = u + 1
    self.list(u).text = anarray(0)
  end
  anarray.remove 0
  if ubound(anarray) > -1 then
    self.list(u).accept anarray
  end

theList is now in place, so it's easy to populate the ListBoxes. When the Index window opens for the first time, the class ListBox's open event handler populates it just by cycling through the top level of theList:

  dim i,u as integer
  u = ubound(app.theList.list)
  for i = 0 to u
    me.addrow app.theList.list(i).text
  next
  me.listindex = 0

The last line selects the first line of the class ListBox -- just as if the user had selected it -- and this triggers a chain reaction. When a selection is made in a ListBox, the ListBox gets a change event. The class ListBox's change event handler populates the section ListBox:

  dim i,k,u as integer
  dim aList as list
  k = me.listindex
  if k = -1 then
    return
  end
  aList = app.theList.list(k)
  u = ubound(aList.list)
  section.deleteAllRows
  for i = 0 to u
    section.addrow aList.list(i).text
  next
  section.listindex = 0

(A listindex value of "-1" means no selection; we start by checking for this because we don't want to do anything if the ListBox is being emptied in preparation for being repopulated.) Continuing the chain reaction, the section ListBox's change event handler populates the part ListBox -- the code is absolutely identical to that of the class ListBox, except that the list being cycled through is one level down:

  dim i,k,u as integer
  dim aList as list
  k = me.listindex
  if k = -1 then
    return
  end
  aList = app.theList.list(class.listindex).list(k)
  u = ubound(aList.list)
  part.deleteAllRows
  for i = 0 to u
    part.addrow aList.list(i).text
  next
  part.listindex = 0

The part ListBox method's change event handler is parallel; and so we come at last to the numword ListBox method's change event handler. Here the user's selection specifies an actual entry, and we want to respond by showing that entry in the Entry window. The key lines are very simple, because the Entry window is set up to allow us to do this:

  entry.c = app.d.sqlselect(
    "select * from data where numword='" + me.text + "'")
  entry.dodisplay

Final thoughts

REALbasic: The Definitive Guide, 2nd EditionREALbasic: The Definitive Guide, 2nd Edition
By Matt Neuburg
2nd Edition September 2001
0-596-00177-0, Order Number: 1770
752 pages, $39.95

This completes the discussion. Not all the code of our application has been shown, but most of it has -- enough to show how the application works. And more important, how it works differently from the HyperCard model but achieves the same effect. (You can download the entire code, or the built application, from my web site.)

Basically, you can do just about anything with REALbasic that you were doing with HyperCard, but you won't necessarily be doing it the same way. Some things that were easy before will now take more work. On the other hand, some things that were hard or impossible before will now be easy.

If you're now thinking about migrating, you can download a free 30-day demo version of REALbasic from REAL's site. Along with my book, that should be enough time for you to become comfortable with the paradigm shifts involved, and to decide whether you'll be using REALbasic as the new HyperCard.

Matt Neuburg is the author of O'Reilly's "AppleScript: The Definitive Guide," "REALbasic: The Definitive Guide," and "Frontier: The Definitive Guide," and is a former editor of MacTech magazine.


Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.