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:
It generates true compiled standalone applications that run natively on Mac OS 8/9, Mac OS X (Carbon), and even Windows, as opposed to HyperCard's interpreted "stacks" which required the presence of HyperCard itself (these stacks will never be Carbonized and are not cross-platform).
Its interface integrates color, graphics, and a full range of genuine Appearance Manager/Aqua widgets, as opposed to HyperCard's lack of color, primitive graphics, and flat, "false" buttons and scrollbars.
It uses an easy, efficient language, with genuine object-oriented programming and custom data structures -- as opposed to HyperCard's verbose HyperTalk, simplified event-passing and messaging mechanisms, and simple-minded "item/line" strings.
It supports drag-and-drop, file and clipboard manipulation, Internet communications, animation, and many other functions that in HyperCard, if they were possible at all, would require an XCMD.
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.
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:
HyperCard's card/background metaphor supports a light database where storage, display, and searching come more or less free; in REALbasic, you have to work quite hard to achieve the same effect.
The very artificiality of HyperCard's interface allows the programmer to obtain information about it and control over it to an extent that in REALbasic is not so easily obtained.
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.
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
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.
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.
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.
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
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.
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
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
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
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.
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
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
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
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
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
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
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
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
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)
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 (
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
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).
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
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
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
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
open event handler populates it just by cycling through the top level of
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
change event handler populates the
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
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
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
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
REALbasic: The Definitive Guide, 2nd Edition
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.