REALbasic University
Welcome to REALbasic University! .................................................................................. 5
Exploring the REALbasic IDE ........................................................................................... 9
Working with Interface Elements ..................................................................................... 15
The Code Inside ................................................................................................................ 21
Helpful REALbasic........................................................................................................... 30
The Problem...................................................................................................................... 41
Starting A Project.............................................................................................................. 48
Adding Dialog Boxes........................................................................................................ 60
Adding a Module .............................................................................................................. 72
Finishing the Open File Routine ....................................................................................... 84
The buildMenu Method .................................................................................................... 95
The Rename Dialog ........................................................................................................ 106
Compiling a Standalone GenderChanger........................................................................ 121
Review: REALbasic for Dummies ................................................................................. 131
Programming for the Rest of Us ..................................................................................... 135
Two Sample In-House Programs .................................................................................... 146
REALbasic Animation: Introduction to Sprites, Part I................................................... 162
REALbasic Animation: Sprites Part II ........................................................................... 174
REALbasic Animation: Sprites Part III .......................................................................... 188
REALbasic Animation: Sprites Part IV.......................................................................... 197
REALbasic Basics .......................................................................................................... 205
REALbasic Basics II....................................................................................................... 216
REALbasic Basics III ..................................................................................................... 225
REALbasic Basics IV ..................................................................................................... 234
A New Look.................................................................................................................... 246
CSS Update..................................................................................................................... 259
Creating Custom Classes II............................................................................................. 271
RBU Pyramid I ............................................................................................................... 277
RBU Pyramid II .............................................................................................................. 284
RBU Pyramid III............................................................................................................. 292
RBU Pyramid IV............................................................................................................. 303
RBU Pyramid V.............................................................................................................. 315
RBU Pyramid VI............................................................................................................. 324
RBU Pyramid VII ........................................................................................................... 334
RBU Pyramid VIII.......................................................................................................... 345
RBU Pyramid IX............................................................................................................. 359
RBU Pyramid X.............................................................................................................. 370
RBU Pyramid XI............................................................................................................. 382
RBU Pyramid XII ........................................................................................................... 392
RBU Pyramid XIII.......................................................................................................... 405
RBU Pyramid XIV.......................................................................................................... 421
RBU Pyramid XV: Undo Part 1...................................................................................... 438
RBU Pyramid XVI: Undo Part 2 .................................................................................... 450
RBU Pyramid XVII: Polishing Part I ............................................................................. 462
RBU Pyramid XVIII: Polishing Part II........................................................................... 474
RBU Pyramid XIX: Polishing Part III ............................................................................ 485
RBU Pyramid XX: Bug Fixing....................................................................................... 496
RBU Pyramid XXI: Wrapping Up.................................................................................. 512
An Exciting Announcement............................................................................................ 521
SpamBlocker: Part I........................................................................................................ 528
SpamBlocker: Part II....................................................................................................... 536
Zoomer: Part I ................................................................................................................. 545
REALbasic Developer Magazine Update....................................................................... 555
Zoomer: Part II................................................................................................................ 559
SimplePaint: Part I .......................................................................................................... 570
SimplePaint: Part II......................................................................................................... 581
SimplePaint: Part III ....................................................................................................... 591
SimplePaint: Part IV ....................................................................................................... 601
SimplePaint: Part V......................................................................................................... 609
About the Delay .............................................................................................................. 617
SimplePaint: Part VI ....................................................................................................... 617
Monotab: Part I ............................................................................................................... 624
Monotab: Part II.............................................................................................................. 632
FontPrinter: Part One ...................................................................................................... 645
FontPrinter: Part Two ..................................................................................................... 655
FontPrinter: Part Three ................................................................................................... 668
FontPrinter: Part Four ..................................................................................................... 676
FontPrinter: Part Five...................................................................................................... 687
Review: REALbasic 4.5 (Part 1) .................................................................................... 697
Review: REALbasic 4.5 (Part 2) .................................................................................... 710
Review: REALbasic 4.5 (Part 3) .................................................................................... 719
Review: REALbasic 4.5 (Part 4) .................................................................................... 726
DoRbScript: Part I........................................................................................................... 737
DoRbScript: Part II ......................................................................................................... 744
DoRbScript: Part III........................................................................................................ 755
DoRbScript: Part IV........................................................................................................ 761
Book Review: Learning REALbasic Through Applications .......................................... 769
OOP University: Introduction......................................................................................... 775
OOP University: Part One .............................................................................................. 781
OOP University: Part Two.............................................................................................. 788
OOP University: Part Three............................................................................................ 794
OOP University: Part Four.............................................................................................. 799
OOP University: Part Five .............................................................................................. 806
OOP University: Part Six................................................................................................ 811
OOP University: Part Seven ........................................................................................... 818
OOP University: Part Eight ............................................................................................ 827
OOP University: Part Nine ............................................................................................. 834
OOP University: Part Ten............................................................................................... 845
OOP University: Part Eleven .......................................................................................... 853
OOP University: Part Twelve ......................................................................................... 864
OOP University: Part Thirteen........................................................................................ 873
OOP University: Part Fourteen....................................................................................... 882
OOP University: Part Fifteen.......................................................................................... 892
OOP University: Part Sixteen ......................................................................................... 906
OOP University: Part Seventeen..................................................................................... 919
OOP University: Part Seventeen..................................................................................... 932
OOP University: Part Nineteen....................................................................................... 941
OOP University: Part Twenty......................................................................................... 948
OOP University: Part Twenty-One................................................................................. 954
OOP University: Part Twenty-Two ................................................................................ 962
OOP University: Part Twenty-Three .............................................................................. 969
OOP Intermission: iConversation Part One.................................................................... 975
OOP Intermission: iConversation Part Two ................................................................... 985
Book Review: REALbasic Visual Quickstart Guide ...................................................... 992
OOP University: Part Twenty-Five ................................................................................ 995
OOP University: Part Twenty-Six ................................................................................ 1005
OOP University: Part Twenty-Seven............................................................................ 1012
Debugging Part One...................................................................................................... 1018
Debugging Part Two ..................................................................................................... 1023
Debugging: Part Three.................................................................................................. 1034
Debugging: Part Four.................................................................................................... 1041
Debugging: Part Five .................................................................................................... 1046
REALbasic University: Column 001
Welcome to REALbasic University!
RBU is a new weekly tutorial column on Macintosh programming, using REALbasic, the
development environment I like to call the "heir to HyperCard."
Like HyperCard, REALbasic features an elegant IDE (Integrated Development
Environment), a simple script-like language, and powerful automatic features that make
writing your own software programs almost as easy as drawing a picture of what you
want it to look like.
Unlike HyperCard, however, REALbasic applications are real Macintosh applications.
They may not have all the speed of C++, and not everything in REALbasic is as simple
as it should be, but you might be surprised how many REALbasic-created applications
you already have on your computer. Many popular programs, such Aladdin System's
Intellinews or Toby Rush's NetCD were created with REALbasic.
Those of you who read my summer series on Mac programming as part of my regular
Less Tangible column on MacOpinion should be familiar with me. I'm the author of ZWrite, a new kind of word processor which I wrote entirely in REALbasic. I'm not a
classically trained programmer, though I did take an introductory course in programming
in college. That should be a help in writing this column, as that means if there's a naive
mistake to be made, I've no doubt done it. Part of my teaching philosophy is that to learn,
you've got to make mistakes -- so we'll be exploring common mistakes and solutions
within these columns. I'm not necessarily trying to teach you proper programming
techniques, but help you learn enough to develop quick solutions for real-life problems
you face.
I have one goal with this column: I want to teach people how to program. Far too many
people think programming is a horrible, scary, complicated thing full of mathematics and
obtuse jargon, which it is, but it doesn't have to be that way. Writing your own program
can be one of the most rewarding experiences you'll ever have. Learning programming
will reduce the fear and mystery of computers and increase your amazement at the
sophistication and occasional pure stupidity of modern software.
REALbasic University is going to start at the beginning, assuming you, the reader, has
never even written an AppleScript. Most of these columns are going to be a series of
tutorials, with lots of step-by-step hand holding, so no matter what your Mac experience,
you'll be able to follow along and learn REALbasic. (These columns will build on one
another, so the archives will be invaluable for going back later, to relearn a particular
technique or start of a tutorial series.)
Thus the first few columns will be extremely basic and may not appeal to REALbasic
veterans. Take heart! While I'm gearing this toward the novice, eventually we'll be
writing some real projects -- games and handy utilities -- and along the way we'll be
tackling specific problems that REALbasic newbies often face, like adding a cool "About
Box" to your program, or saving files with your program's custom icon.
Send in Your Questions
At the end of each column, I'll be answering select reader questions -- send your
REALbasic University questions to [email protected] I can't
promise to answer every question, and keep in mind I'm not going to be writing a novel
for each answer, so priority will go to simpler questions that have universal appeal. Keep
your questions specific. (A general query like "How do I write my own web browser?"
will be rejected.) I'm also open to suggestions for projects and issues you'd like me to
cover in future columns.
Things You'll Need
Obviously, you'll need a copy of REALbasic. You can download a free demonstration
copy of REALbasic from REAL Software. The demo is fully functional for 30 days and
applications you create will stop working after five minutes, but it's sufficient for you to
get started learning programming. When you're ready to buy REALbasic, REAL
Software will simply send you a registration number which will fully activate your copy.
REALbasic is sold in Standard and Professional versions. I recommend the less
expensive Standard version unless you have particular needs for compiling Windows
applications or database programming (the two main features the Professional version
adds). The Windows compiling feature isn't bug-free (to say the least), and it's quite
complicated making a program that will run on both Windows and Mac OS. Besides, the
Standard version lets you try out the Professional features, and you can always upgrade
later if you decide you need to.
If you don't already have a copy, get ResEdit 2.1.3 from Apple as we'll eventually need
that. A graphics program like Adobe Photoshop or the shareware GraphicConverter will
also be useful.
(Note: as of February 12, 2001, the current version of RB is 3.0. This newly released
version features an improved interface and compiling for Mac OS X. For the moment,
I¹m still using RB 2.1, so some of my screen shots might look a little different. I¹ll be
switching to 3.0 soon. When you download RB 3.0, you can download either the
"Classic" version, which runs under Mac OS, or a version that only runs under OS X.)
Installing REALbasic
Once you've downloaded REALbasic, simply decompress it with Stuffit Expander and
drag the resulting "REALbasic 3.0" folder to a convenient place on your hard drive.
The first thing I usually do after installing REALbasic is to increase the amount of
memory allocated to REALbasic. REALbasic programs tend to use a lot of RAM, and
since you have very little control over how that RAM is used, your program will probably
just crash if it runs out of memory. The simplest solution (other than bugging REAL
Software to give us better memory management) is to allocate plenty of memory to your
programs. Within the REALbasic IDE (Integrated Development Environment, a fancy
way of saying "the editor where you write programs") you'll need even a little bit more
memory, since the editor itself uses a few megabytes. I usually double the default
allocation; if I'm using a machine with plenty of RAM, I'll add twenty megs. (Keep in
mind that while writing your program, before you make it more efficient, you might
inadvertently use more memory than you think.)
So while you're thinking about it, select the REALbasic application icon, do the "Get
Info" command, and increase the memory partition.
Starting Your First Project
When you launch REALbasic, it opens a blank, untitled project. A project is your
program's source. It contains everything you create for your program: code, windows,
menus, resources like sounds and graphics, etc. It is very important that you manage your
project and everything associated with it carefully.
For instance, a typical project will include graphics: icons, pictures, etc. You can place
these in your project and REALbasic will show them, but they aren't actually saved in
your project file. Instead, RB (I'll abbreviate REALbasic as RB occasionally) simply
includes a reference to the original file. This makes it easy to update your graphics (just
edit the original file), but don't you dare throw away the original graphic or your project
won't work! (REALbasic will complain and ask you to find the graphic when you try to
compile your program.)
What you should do is create a folder in the Finder for your project. I name mine the
name of my project. Inside that folder I sometimes have other folders for graphics and
other resources (help text, notes, sounds, etc.). This keeps everything organized. (On
larger projects, I have an "old stuff" folder where I put old copies of my project, graphics
I had planned to use but I'm not any longer, layered Photoshop files, etc. That way I'm
not permanently throwing away something I might later want, but it's not in my active
area.)
Advanced Tip: I do most of my programming on my laptop, but I also have a desktop
machine and sometimes I switch between the two, depending on what I'm doing. I've
found creating a small disk image file (I use Apple's DiskCopy program and create a
10MB or 20MB disk image) for my project is a great way to keep everything with a
particular project together. Since it's a disk image, I can create folders and files within it,
yet it's just a single file, ideal for transporting between machines. (It's also convenient for
making backup copies of your entire project by just duplicating a single file.)
Next Week
We'll explore the REALbasic IDE and learn our way around.
.
REALbasic University: Column 002
Exploring the REALbasic IDE
We'll begin our RB journey by exploring the IDE (Integrated Development Environment)
-- the editor where we create our program. REALbasic's editor is incredibly nice, though
it does have some limitations (which we'll eventually cover in later columns).
There are basically three key areas of the IDE: the Tools Palette, your project window,
and the Properties window. These should be visible at all times as you'll constantly be
using or referring to them.
In case you're not using REALbasic right this second, here's a graphical look at the IDE.
(Click to see the larger version.) [Note: these screenshots are from version 2 of
REALbasic. The RB 3.0 IDE is similar, but includes new toolbox icons.]
First, note that (nearly) every program you create needs a window. A window is where
you'll put interface elements, such as popup menus, checkboxes, and text fields. You can't
put an interface element (also called a control) like a checkbox into your project -- you
must place it on a window that's inside your project.
The default project that RB creates when you first launch it includes everything you need
for a very simple project: there are some basic menus (at minimum your program needs a
Quit menu item) and a blank window (called Window1). In fact, you can even run this
project -- you've created a very simple (and useless) application!
When you run a project (choose "Run" from the Debug menu or press Command-R) you
are telling REALbasic to compile your program (assemble all your project's elements and
translate your code's English-like syntax to computer-talk) and immediately activate the
resulting new program. When you run a program within the IDE, RB does not create a
new program on your hard drive. Instead, the program is loaded entirely into memory and
run as though you double-clicked its icon on the hard drive.
Why run a program in memory instead of creating a "stand-alone" application? Two
reasons: speed and debugging. Compiling to memory is faster, and while you are
programming you'll find you compile your project frequently, to test your code.
Debugging is the process of finding and fixing errors in your program. REALbasic has an
integrated debugger which is made available when you run a program within the IDE,
and it can help you find the source of your problem.
Another advantage of running your program within the editor is that if you accidentally
put your program into an "infinite loop" (basically the program waits for a condition that
will never happen, such as 5 equalling 10) you can cancel the running program by
pressing Command-Shift-period.
Here's what the empty project looks like when running, and in "debug" mode. (Click to
see the larger picture.)
We'll get into using the debugger in future columns, but for now I don't want you to be
confused when you run your program and yet can still see your project windows behind
your application. The IDE and your program look an awful lot alike. The most significant
difference is the menubar at the top. Learn to keep an eye on that so you can tell if your
program is running or you're in the IDE.
To switch to the IDE, simply click on any of the background windows (such as the
project window). Your application is suspended until your reactivate it. To switch back to
the running program, choose "Run" again from the Debug menu or press Command-R
(clicking on the deactivate program window does nothing).
For now, play with this a bit. Run the empty project, use the menus, switch between the
IDE and your program, etc. Try it a couple times until you've got the hang of it. You're
going to be spending a lot of time in the REALbasic IDE, so get used to it.
(Note: you can even edit your program in the IDE while it is running! We'll get into this
more later, of course, and there are limitations to this feature, but being able to make
changes to a "live" program is a significant time saver as RB only has to recompile the
code you changed, not the entire program.)
When you're ready to continue with this tutorial, quit from your program if it's running
(choose "Quit" from the file menu) to return to the IDE (don't quit REALbasic -- quit
your program).
The second most important part of the IDE is the Properties window. This is a floating
palette you can position anywhere you'd like (the default location is fine, though you
might want to change the size). The Properties window lets you change settings on
various objects within REALbasic. For instance, if you select "menu" in your empty
project window, the Properties window will go completely blank. That's because the
menu object has no settings (properties). Try selecting Window1 -- suddenly the
Properties window is full of various settings, such as the name of the object (Window1),
the size and kind of the window, etc. Most of these settings are unique the type of object
selected, though some, like the "name" field, are common to all objects (every object in
REALbasic has to have a name so you can refer to it).
The third most important part of the IDE is the Tools palette. Why this is called the tools
palette, I don't know. Tools, by definition, let you modify your content (i.e. a paint brush
adds a brush stroke to your canvas). In REALbasic the Tools palette is more like a library
of interface elements: you can drag items from the library palette to put them into your
program.
Here's a brief explanation of the palette (click for full-size view):
Your First Program
Now you're going create your first REALbasic program! This is going to be a useless and
very simple application, but it will show you how powerful REALbasic can be.
Starting with the default, empty project automatically created when you launched
REALbasic, we're going to drag a few interface elements to your program's main
window. Interface elements are often called controls, or user controls, because they are
things the user can interact with, such as a scrollbar or checkbox.
First, let's drag the item that looks like a "OK button" onto your project's window. Don't
worry about where you put it -- stick it anywhere you'd like (you can always move it
later). Notice that the button will be named "Untitled" not "OK" (but you can also change
that). While the button is selected (notice the black handles at each corner) the Properties
window will display the fields that allow you to change the size and content of the button.
For now, leave these alone. You can come back later and play with these settings.
Now drag the checkbox icon to your window. It also is untitled, which is fine for now.
The third item we want is an Editfield -- that's a text editing area. It looks like this:
Drag it onto your window and we're done.
.
That was a lot of work (ha ha), so let's save our project before we continue. We'll called it
-- the pi character is created by holding down the Option key and
pressing "p". (This convention is a Mac programming tradition. You don't have to follow
it, but it's a good idea.)
Now that our project is saved, let's try running it. Joyful, isn't it? You haven't typed a
single line of code, yet you have a working button, checkbox, and text editing area! Even
more amazing, the menus also work: try copying and paste text in the editfield.
That's it for a mild introduction to REALbasic. Next week we'll add features to this
demonstration project and actually type some code!
Letters
I received a number of positive e-mails about last week's debut column, with one
detraction. It came from Scott Raney, President of MetaCard Corporation, who objected
to my terming REALbasic as "the heir to HyperCard." He writes:
There is currently only one true "heir to HyperCard" and that is our product MetaCard
which actually can import HyperCard stacks and run HyperTalk scripts (something that
anyone who knows both products will tell you is not now, nor will ever be, possible with
REALbasic). Not only that, but MetaCard applications also run unmodified on Windows
and all popular UNIX systems, even the first of which REALbasic still has major trouble
doing. Of course, productivity-wise there is no comparison between the two products: by
the time you've got all your variables typed and your program compiled, a MetaCard
developer will have his much smaller and easier-to-maintain application already
debugged, which is the true benefit of using a scripting language instead of a thirdgeneration language.
That's great. I tried MetaCard years ago, when it was quite primitive (it showed great
promise), but I wasn't aware it was still in development. I'll have to examine it sometime.
I agree that REALbasic is not HyperCard. If you're interested in a HyperCard-like
product, look at MetaCard or SuperCard.
But I still stand by my statement. My point comparing REALbasic to HyperCard is that
HyperCard allowed non-programmers to create sophisticated software. If you look back
at the early years of the Macintosh, many innovative freeware and shareware products
were produced in HyperCard by non-programmers. These days, software has become
much more sophisticated, and I doubt you could market (or even give away) such simple
programs, but at the time, HyperCard projects were powerful and amazing.
What delights me about REALbasic is that it is almost as easy to use as HyperCard was,
but with professional quality results. (Please note that I'm not saying that SuperCard and
MetaCard are not professional programs; I'm talking about HyperCard, which hasn't been
updated in years.) For some users, a scripting environment is more appropriate and faster,
but others need something between scripting and C++.
Joe Strout (from REAL Software) writes:
I like what you're doing with RBU. One small correction: you write "When you
download RB 3.0, you can download either the 'Classic' version, which runs under Mac
OS, or a version that only runs under OS X." Not quite true, since the Carbon version
runs on any version of MacOS from 8.6 to OS X (as long as CarbonLib 1.1 or later is
installed).
Good point, Joe! Thanks for the clarification. I don't happen to have CarbonLib 1.1 on
my machine and I think that's why I got it into my head that it was one or the other. So
those of you planning to upgrade to Mac OS X when it ships next month, feel free to
download the Carbon version.
Finally, a question. Jamey Osborne writes:
Here's a question/topic suggestion for your new column:
How best to make the transition from spaghetti code to object-based programming?
I'm an "old-time" amateur hacker that learned the old MS-DOS and I've studied Mac
programming essentials, programming simple games in Pascal, but I've never managed to
get comfortable in the "object-oriented" world that is programming today.
For example, I've an idea for a project I'd like to undertake in RB (brand new to it). I can
see how I envision an "old-time" flowchart to go, but I have no idea how to break it into
the object mode. Kind of daunting to an old hacker....
Good luck with the column and count me as an eager reader!
Thanks, Jamey! Excellent question. Object-oriented programming (OOP) requires a
different mindset from traditional programming and making the transition can be
difficult. I still struggle with that myself, coming from a background similar to yours.
One of the things I like about RB, however, is that since it is object-objected, your own
programs tend to fall in line. For instance, if you create a document window for your
program, you can write a routine for the window that knows how to save itself. Then if
the user opens several windows, they each have their own save routine. That's OOP!
Obviously your question is too complex to be answered in a few paragraphs, but I will be
exploring OOP issues in various columns in the future.
Various comments from readers:
Saw the first column on RBU. Great! I've been waiting for something like this to come
along. I'll be there every week. --Scott Clausen
Very cool. Exactly what I have been looking for. THANK YOU! I look forward to being
a regular reader. -- J. Michael Washé
Thank you, Thank you! I've been waiting for 2 years for someone to do something like
this. Several started and never followed through. REALbasic is just a hobby with me and
I can't spend as much time learning as I would like, but if someone is going to show me
how. I can't wait. --Bill Tucker
I'm very excited about your REALbasic University and Realbasic Developer magazine.
As a REALbasic developer myself I congratulate you on your efforts to spread the
gospel. -- Terry Findlay
Thank you so very much for providing RealBasic instruction to a novice such as myself. I
hope to find that RealBasic suits my right brain! --Carol Goodell
That's it! Keep the letters coming and come back next week.
.
REALbasic University: Column 003
Working with Interface Elements
Last week we created an application with a window and a few interface elements, such as
a checkbox and text editing area. Without writing a single line of code, we had a working
program. Let's continue where we left off and expand on that theme. Open up last week's
project (or create it from scratch again).
First, let's look at what happens when you select an interface object: the object is outlined
in your system's highlight color and handles (tiny black squares) appear at each corner.
These handles let you resize the object. If you drag an object from the inside, you can
move the object. If you drag one of the object's handles, you'll expand or shrink the size
of the object from that side. (When you are rearranging things, be careful you don't
accidentally drag and handle resize an object!)
(Note on Selecting: RB works like standard Mac applications in that you select an object
by clicking on it, and if you hold down the Shift key while clicking, you can select
several objects at once. You can also use the tip of the arrow to draw a selection rectangle
around objects and it will select all objects within the rectangle. When you do that,
realize that the object must be completely inside the rectangle -- sometimes, such as with
a static text object with a small amount of text, the visible portion of the object is smaller
than the full size of the object. Tip: If you press the Tab key while your window is
frontmost, RB will select the next object in your window. Each time you press the Tab
key RB will select the next object, in the order in which you added them.)
Let's select the EditField control we placed on Window1 last week and make it bigger.
Make it almost as wide as your window and the height of several lines of text. Notice that
when you change the size of the object, the width and height properties on the Properties
window reflect the change. If you want, you can simply type in numbers to specify the
exact height and width you'd like for the object.
Notice that when you move an object within the window, intersecting grid lines appear
and help your object snap to an invisible grid or to another object. This is great for
helping you align interface elements within the window and to each other. Here's what
that looks like:
REALbasic also gives you some powerful automatic alignment options. With more than
one object selected, go to the Format menu and the Align submenu. There you will see
several options such as "Align Left Edges," "Align Right Edges," and "Space
Horizontally" (there are also vertical versions of these same commands). If you select one
of these commands, the selected objects would be aligned on their left or right sides, or if
you selected the "Space Horizontally" or "Space Vertically" commands it will adjust the
objects to there is equal space between each object.
When you place interface elements on a window, REALbasic keeps track of how your
objects are positioned: you could, for example, place one element on top of another.
Sometimes this is inconvenient: you would like to move an object below or above
another. If you select one of the objects, you can use RB's rearrange commands (found on
the Format menu) to move that object forward or backward, or all the way to the front or
back of your window.
(Note: sometimes you'll need to move elements forward or backward simply because RB
makes it difficult to select one object behind another. If you're trying to select the
background object RB will keep selecting the front object when you click. You can
temporarily rearrange the objects to allow you to select the other object, then rearrange
the objects back to their original order when you're finished.)
EditField Properies
Let's select the EditField you created (probably called EditField1). With it selected, the
Properties window should show you something similar to this:
There are lots of settings here, but for now we are only interested one. Under
"Appearance" you'll find an item labeled "Multiline" which is probably not checked.
Check it. This will let your editfield accept more than one line of text -- essentially
turning your little edit box into a mini-word processor.
Try it! Run your program and play with the editfield. If you want, quit your program and
turn the "multiline" option off and rerun the program to see the difference. (You'll need to
quit your program to access the editfield's properties: you can't access an object's
properties while your program is running.)
Next Week
We'll add some code to our project so it will actually do something!
Letters
Our first letter comes from Benjamin Burton, who quotes last week's letter regarding
object-oriented programming and adds:
This is a letter you posted, and I'd just like to same I'm in the exact same boat. I've gone
from BASIC and Assembly on my Apple IIc to Pascal on a 286. I have some old projects
(and new ideas) I'd like to move into OS X, but I feel like REALBasic must be so simple
I'm missing something. I've purchased REALbasic: The Definitive Guide by Matt
Neuburg, and it just seems likes he knows too much, but not enough about
communicating.
Anyway, I wish your column would appear once a day instead of once a week. If it were
shareware, I'd pay the fee, so keep 'em coming.
-Ben
Thanks for the note, Ben! Matt's book is great and essential for anyone really interested
in getting the most out of REALbasic, but even he'll admit it isn't a tutorial. It's designed
as a comprehensive resource, and if you're ever stumped about how something works -or doesn't work -- in REALbasic, you turn to Matt's book.
For tutorials, turn to REALbasic University! Keep following RBU and in a few weeks
we'll have you producing your first real program.
Next, we hear from J. A. Russell:
I recently purchased RB 3.0 because of the advertising hype and a desire to extend my
very limited computer knowledge. As a person who has no programming experience, the
information provided by RealSoft is extensive, but of no help to someone, like me, with
no programming experience. I hope your tutorial will always keep this in mind.
I have seen other tutorial sites which started to help the complete new user, but soon
passed them by to deal with problems/questions sent in by more experienced users. I have
seen several of the, "I'm going to help you from step #1 to the end." Sites fail because
they quickly forgot that the site was started to deal with beginners, not the esoteric
problems that they recieve from more advanced users.
I understand completely, John! Keeping the right balance between "advanced enough to
be interesting and not so advanced it's intimidating" is a tough task, but that's my goal. I
want to challenge you, and help you learn, but I also have a real passion for bringing
programming to non-programmers. Your feedback on my success/failure on this is
important to me, so if I'm ever glossing over a complex subject or skipping a step you
don't understand, please let me know. I'd much rather receive complaints from advanced
users telling me I'm dealing with "the basics" than lose the "newbies."
Our final question comes from John Rahn, who includes a picture of his problem:
I know I'm missing something simple and basic...
I've written an application and it works fine - except for the fact that when I bring up my
"Settings" window, it is (sometimes) grayed out. The window is in front, and it is
accessable and all the controls are useable, but it is grayed out (screen capture attached):
What am I missing?
Thanks
JP Rahn
Wow, John, I hate to admit on my first technical question that I'm stumped, but I'm
stumped! Even though the dialog is frontmost and active, RB has it grayed out as though
disabled. Very strange.
I tried to recreate your situation myself, but it always worked correctly for me. To answer
your question I'll need more information: what window type is your "Settings" dialog set
to, and how are you calling it (dialog.show or dialog.showModal)? It might be helpful for
me to see some of the related source code, if you wouldn't mind.
Meanwhile, any of our bright readers have any ideas?
.
REALbasic University: Column 004
The Code Inside
Let's expand our program a bit. We're going to add a little bit of programming code -- just
a few lines -- to add a really cool feature to your program. The feature is called "Hot
Help." While the exact nature of "Hot Help" varies, it basically consists of textual
information displaying when you point at an object.
First, let's add a static text object: drag the "A" icon from the toolbox to your project
window. Position it near the bottom of the window. Since it isn't wide enough, either
click and drag one of the righthand handles to make the object wider, or select the static
text object and in the "Width" field on the Properties window, change it to 275.
Now, select the static text object, go to the "Name" property and change its name from
"StaticText1" to "infoDisplay" (I like to begin my objects with lowercase letters, but you
don't have to do that if you don't want to). When you have the statictext item selected, the
name field should look like this:
(Note: When you change the property of an object, it happens immediately. For instance,
there's no need to press Return to "save" the name change to your object.)
To add our feature, we're going to have put in some lines of code. Where do you type
code in REALbasic? The answer is cool (and similar to HyperCard): inside the object!
Begin by selecting one of your objects... let's try EditField1. Select the object and press
Option-Tab (hold down the Option key while pressing the Tab key). You should see
something similar to the following:
This is the Code Editor for Window1. On the left side you'll see a list of object types:
Controls, Events, Menu Handlers, Methods, and Properties. We'll be explaining these in
more detail in future columns, but for now, we're going to focus on the Controls section.
All of the interface elements you placed on Window1 are controls, so each of them should
appear within the Controls section.
(Each section has a disclosure triangle text to it, just like a List View of your files within
the Finder. You can reveal or hide the items inside the section simply by clicking on the
triangle. Objects within the sections may also have items inside them -- if so, they will
have their own disclosure triangles.)
In this particular case, we are looking at the events for EditField1: KeyDown, GotFocus,
LostFocus, SelChange, TextChange, MouseMove, MouseEnter, MouseExit, Open, Close,
DropObject.
Events are basically things that happen to a control. One of the ingenious things about
REALbasic is that code is not only encapsulated inside objects, but that the object itself
(a window or a control) is filled with events each with their own code. This separates
different types of code without you having to specifically differentiate between them
yourself.
For example, in traditional Mac programming, your main program is a loop that
constantly checks for events. Did the user click in the menubar? Did the user click in
your window? Is the mouse over a particular control? Did the user type on the keyboard?
Etc. With each choice, you'd send the computer off to a particular routine that handles
that choice.
Within REALbasic, events are sent to specific objects. An object can't have events that
don't make any sense to it. For instance, a popup menu does not have a keydown event
because a popup menu doesn't accept keystrokes.
All this makes your programming much easier: simply put in code within the event that
you need to handle. (This is why these events are often called "handlers," because they
handle that event.)
In the case of EditField1, when you pressed Option-Tab to open its Code Editor,
REALbasic automatically put you inside the handler for the KeyDown event. The
KeyDown event is triggered every time a user types anything inside EditField1. If you
happened to have a second editfield called EditField2, EditField1 would not receive
typing for it, and EditField2 would not receive typing for EditField1.
We don't want to add any code to the KeyDown handler; we're interested in the
MouseEnter handler. So underneath the EditField1 section on the left, click on the
MouseEnter event. The pane on the right should change to put your cursor within the
code for the MouseEnter subroutine.
Now we're finally going to type some code!
With your cursor within the MouseEnter subroutine, type "inf" -- your display should
change to look like the following:
Notice the gray "oDisplay"? That's REALbasic's "autocomplete" feature at work. You
typed the first few letters of an object and RB is about to finish typing the object's name
for you. The extra text is gray, because it's not really there -- it's just a REALbasic
suggestion. You can type something else if you want. Or, if the suggestion is correct and
you want to have RB finish typing it for you, press the Tab key. Now the full name
"infoDisplay" is displayed in solid (black).
Accessing Object Properties
We've already seen Properties at work. EditField1, for example, has Height and Width
properties. Properties can be settings for an object, but really they are just storage
containers. The "width" property of EditField1 is just a storage container for a number.
Eventually you'll learn how to add properties (storage containers) for objects you create
yourself.
What's important right now is that you understand how REALbasic let's you access the
properties of an object. REALbasic uses very simple syntax: object-name.property.
Translated into English, that means you have the object's name, a period, and then the
property name. For example, if you wrote EditField1.width in your code RB would
return the value contained in the "width" property of the control called EditField1.
In this case, we want to change the value of the "text" property of the static text control
called infoDisplay. By putting the property we want to change on the left side of an
assignment command (an equals sign), we are telling the computer to "put what's on the
right into the container on the left."
Make your code look like this:
infoDisplay.text = "This is where you type."
There! You have just written, in computer talk, "Put the phrase 'This is where you type.'
into the 'text' property of the infoDisplay object. Note that the text property only accepts a
string: a string is a programming term for text, as in a string of characters. A string can
contain numbers, but a number cannot contain letters. You tell REALbasic that
something is a string by enclosing the phrase in double-quotes.
If you had written:
infoDisplay.text = This is where you type.
or
infoDisplay.text = 4586
REALbasic would complain with a syntax error when you attempted to run the program.
(A syntax error means the computer can't figure out what you are trying to say; it's
similar to an error in grammar, except computers are so picky that the computer can't
understand anything if you don't feed it stuff in exactly the format it expects.)
We're going to reuse this code elsewhere, so let's select it and copy it to the clipboard
(Command-A to select all and Command-C to copy).
Now click on the disclosure triangle of CheckBox1 on the left side of the Code Editor.
We want to be able to see the events inside CheckBox1. Find the MouseEnter event, and
paste your code inside it. Since this code has the help phrase for the editfield, edit it so it
reads correctly for the checkbox control as follows:
infoDisplay.text = "This is a checkbox."
Perfect! Now find the MouseEnter event of PushButton1 and do the same thing. Change
it to read:
infoDisplay.text = "This is a push button."
Guess what? We're finished! First save the program (Command-S or Save from the File
menu). Then run the program (Command-R or "Run" from the Debug menu) and see
what your program does. Try pointing the arrow cursor at the push button, the editfield,
and at the checkbox. See how the text at the bottom of the screen changes? Cool, eh?
You've just added "Hot Help" to your program! With no loss of blood, too.
The final program, showing hot help in action:
Now the program is a bit limited. You'll notice, for instance, that when you first launch
the program the label at the bottom of the window says "Label:" which is rather ugly, and
when you move your cursor outside of an object the help label still displays the old text.
Ideally the help text should go away when you point away from an object. Can you figure
out how to do that? I hope so, because that's your homework assignment.
Next Week:
We learn more about REALbasic syntax.
Letters
I found a couple letters I'd neglected in the initial onslaught, so let me deal with them
first. Mike Ziegler writes:
I am considering switching from Hypercard to RealBasic. Is it possible to convert
existing hypercard stacks to realbasic?
There's no way that I know about. REALbasic is not HyperCard, though it is easy to use.
HyperCard is very different in terms of how it is structured, with everything on "cards"
(instead of standard Mac windows). Theoretically someone could write a tool in
REALbasic that would "run" HyperCard stacks or convert them RB, but I don't know that
anyone has done that.
If you're interested in a HyperCard-like product, look at MetaCard or SuperCard.
Next, we hear from Alvin Chan:
Good morning, I've used RealBasic, but how powerful is it compared to VB6 which I use
now? Is it capable of handling databases of different formats like Active X and does it
have APIs function calls too?
I do like RealBasic's interface better than VB6 except for the menu facility where VB is
easier to use.
I'd like to migrate from VB6 to C++ then Java. What vendor is the best for C++ or Java
and the ones that big companies like Apple use?
God bless,
Alvin Chan
Thanks for the note, Alvin. I haven't used Visual Basic, so I can't really compare the two,
but I'd assume RB is as powerful, at least on the Mac platform, though I have heard that
VB has some features not matched with RB. For instance, there is no Active X on the
Mac, so that wouldn't apply. RB does have some database capabilities (either using it's
own built-in format or external databases in SQL, ODBC, or Oracle) and there are plugins such as Paradigma Software's Valentina. You'll have to explore these yourself and see
if they'll meet your needs.
REALbasic does support Mac OS function calls and people have done some advanced
things with RB by making use of those.
As to your C++ and Java question, you're getting out of my league (I have little ambition
to explore that territory, which is exactly why I use REALbasic ;-), but as far as I know
the best (only?) tool is Codewarrior.
Finally, we get back to John Rahn's question from last week, where his project included a
puzzling active window that was "grayed out." I attempted to duplicate his situation and
couldn't, so John sent me the source code for his project. I did some digging and finally
figured out the error.
Meanwhile Paul Harvey (I doubt it's the radio commentator ;-) wrote in with the same
problem:
I have also had the situation where a frontmost window in realbasic is greyed out! I've
not identified the source, but I have windows that are 'hidden' that are scratch areas for
processing editfield data. I don't know if one of those is perhaps taking the frontmost
position, even though we can't see it! However, even clicking on the greyed out window
does not change the greyed out status. Sometimes clicking on another document window
then back to the greyed out one solves it.
Paul is right on track, because that's exactly the problem. John's project had numerous
hidden windows, and some of those were of the "floating palette" variety. Sometimes
those hidden windows would be frontmost and sometimes they wouldn't, explaining why
his "Settings" window was sometimes grayed out even while it was active.
The way I finally tested this was to write a little routine. I added a Popupmenu control to
the main window of his project. Then, within the MouseEnter handler, I entered the
following code:
dim i as integer
me.deleteAllRows
for i = 0 to windowCount - 1
me.addRow window(i).title
next
What this did was fill the Popupmenu with the names of the active windows in order,
from front to back. Remember, an active window is a window that is "alive" (i.e. loaded
and running) and is not necessarily visible on the screen.
Since I placed this code within the MouseEnter handler, every time I moved the pointer
into the Popupmenu's area the window list was refreshed. As I played with his program I
periodically went and checked the current window listing. I quickly noticed that
whenever the "Settings" was grayed out it was not the frontmost window and whenever it
was correct it was the frontmost window. That made me realize the other hidden
windows were interfering with REALbasic's understanding of which was the frontmost
window and causing it to draw incorrectly.
The solution is simple: close, don't hide windows when you are finished with them.
Closing gets rid of the window while hiding it just makes it invisible. For instance, John
displays a "splash screen" when his program is launched, but because he never closed the
window it was still around, and sometimes it was frontmost, even though it was invisible!
To close John's splash screen, I simply added a self.close command within his
window's MouseDown handler (replacing his hide command).
Closing an unused window also saves memory, because an active window is still using
memory. Since the window is part of your REALbasic project, you can always bring it
back -- it will just get loaded from disk instead of RAM.
Another important thing to understand about windows: any reference to a window
activates it (brings it into memory). The window may or may not be visible, but just
referring to it makes it active. In John's case when his program was launched he would
read in items from a preference file and set variables in a hidden window with those
settings, effectively activating the window though it was invisible. A cleaner approach is
to store the setting in a global variable which your window can read when it is opened.
(For instance, to set the state of a checkbox, in your window's Open handler put
checkBox1.value = globalCheckBoxValue where globalCheckBoxValue is a global
variable containing the setting.)
That's all for this week. Keep the questions coming! If I didn't get to your question this
week, I'll deal with it sooner or later.
.
REALbasic University: Column 005
Helpful REALbasic
REALbasic is a fairly simple language, and as it is a form of BASIC, one of the oldest,
simplest computer languages, it isn't hard to learn the, uh, basic (sorry) syntax. RB uses
traditional BASIC terms such as DIM, FOR, IF, ASC, MID, etc., though occasionally the
syntax might be slightly different. If you're familiar with BASIC you'll learn REALbasic
quickly, but even if you've never programmed a microwave oven to cook popcorn, you'll
find learning RB isn't difficult.
The actual language of REALbasic is limited to a few dozen commands and functions;
but those commands are just the beginning of what you can do with REALbasic.
Remember, REALbasic is an object-oriented programming environment. Objects are
powerful because an object inherently knows how to do certain things -- it has abilities
that distinguish it from another object.
For example, a push button control knows how be pushed. (It's a pretty simple control.)
But push buttons have other abilities (many shared with other objects) that are less
obvious, such as the ability to change its size, label, position, and enabled state. These
settings of a control are its properties. Much of what you do in programming is work with
these settings to reflect the current state of the program. For instance, you might set a
push button's state to disabled (not usable) when it's inappropriate for that button to be
active.
REALbasic includes dozens of pre-defined objects, each with unique and shared
properties. Learning the capabilities of all of REALbasic's many objects is a challenge,
but vital if you want to make your program do what you have in mind.
Remember from last week's lesson that you access an object's properties by typing the
object's name, a period, and the property name. Many properties are simple enough:
width sets or gets an object's width, while height does the same with the object's height.
But others can be confusing.
For instance, a popup menu's text property returns the text of the current setting (the text
of the chosen menu). But a bevel button control, which can have a menu, uses caption
for the button's text and menuvalue to return the number of the selected menu.
So while every REALbasic object is similar, they are not exactly the same. Another
problem: some properties are read-only (you cannot set or change them), while others
only want numbers, or perhaps a true or false (boolean) setting.
Fortunately, you don't have to memorize all those property names. REALbasic features a
built-in help command which can show you the object and a list of its properties and their
settings. With many commands, there is even sample code showing how the command
can be used.
Exercise: Go to the Window menu in REALbasic's IDE and choose "Reference" (or
press Command-1) to bring up REALbasic's online language reference guide.
Learning to use REALbasic's online help is essential to becoming a capable programmer.
Even after years of using REALbasic, I find myself constantly turning to online help just
to make sure I've got my syntax correct. It saves me lots of time and aggravation -- ten
seconds of preventative medicine is easier than discovering some code won't compile, or
spending hours trying to figure out why a routine isn't working only to eventually realize
you accidentally reversed the parameters of the inStr command.
REALbasic's help is simple to use. A list of topics are in a column on the left, and when
you select one, its text appears in an area on the right. There are two navigation modes:
the list at left can display every topic alphabetically, or it can display help items grouped
by topic. You choose between the two by clicking "Alphabetical" or "Themes" at the top
of the list.
Syntax Illustrated
Being able to find the command or function you need help with is one step, but you still
need to understand what the help text means. Programming syntax can be complicated;
it's vital you understand what REALbasic is telling you.
Let's take a look at that inStr command. It's a function (a function returns a result) that
returns the position of one string inside another. That's a fancy way of saying it finds one
sequence of characters in another. Here's REALbasic's Help on the subject:
What does all that mean? It may look intimidating, but it's not bad once you get the hang
of it. The first bit tells you the syntax of the command. In this case,
result = InStr([start,] source, find)
shows you how the command is supposed to be used. The items in between the
parenthesis are parameters for the inStr command.
The bit after the syntax explains each of the parameters. Result is an integer (a whole
number). In other words, the command is going to tell you at what position the search
string was found. If you searched for "fox" within "The quick brown fox" the inStr
command would return a 17.
Notice how the word "start" above is enclosed in square brackets? That tells you that the
parameter is optional. If there are no brackets around the parameter, you must include it
or REALbasic will generate a syntax error. In this case, start is a number, and represents
the starting point of the search. You might have noticed that inStr always finds the first
occurrence of an item. What if you needed to find the second or third occurrence? The
answer is you'd start the seach after the first find (you'd have to save the value of the first
search and search again, using that saved value as the basis of the second search).
The final two parameters for inStr are "source" and "find" -- both are strings (text).
Source is what you are searching within and find is what you are searching for.
Therefore, the following is a valid search:
inStr("The quick brown fox", "fox")
Of course if you just type the above into REALbasic, you'll get an error because you
aren't doing anything with the result of your command. Since the command returns a
number, you must do something with that number: store in a variable, or display it. Try
putting this in a pushbutton and running your program and clicking the pushbutton:
msgBox str(inStr("The quick brown fox", "fox"))
(The above calls the msgBox routine which displays a message, and it uses the str
function to convert the number returned by inStr to text, since msgBox only displays
text.)
You may notice that many of the REALbasic help screens have a "Notes" section after
the syntax explanation. This is important stuff! For instance, what happens when inStr
can't find the string you are searching for? You could test it to see, like this:
msgBox str(inStr("The quick brown fox", "dog"))
But if you read the notes you'll see REALbasic says, "If the find string is not found
within the source string, 0 (zero) is returned."
Ah! Good info to know! Now your program can check the result, and if it's zero, know
that the string was not found.
But keep reading. The notes continue, "InStr is case-insensitive." What does that mean?
That means inStr("The quick brown fox", "fox") and inStr("The quick brown
fox", "FOX") will both return 17 -- inStr doesn't care if the search string is upper or
lower case, or if the case of the search and find strings don't match. Most of the time this
is good, as you don't have to explicitly check for both situations, but if you want casesensitivity, inStr isn't the command for you.
Below the Notes section is an Examples section. Here you'll find some sample code
which can be helpful as you see the command "in action."
Notice how the block of code has a gray outline around it? That means you can drag that
chunk of code into your program! It's a great way to experiment with REALbasic
commands without having to do a lot of typing. Once the source is in your program, you
can edit it as you like.
After the Examples area, there's a "See Also" section which lists similar commands to the
one you're looking at. With inStr, for example, the "See Also's" include Mid, Left, and
other string functions (functions that manipulate strings). Notice how those are in blue
and underlined? They are hot-links -- click on one to jump you to that command. (You
might also find them elsewhere on the page -- like the inStrB reference in the Notes
section.)
Object Syntax Illustrated
The inStr function we just looked at is a simple command. A control object like a
listbox is much more complex.
Go to the listbox help within the online guide. (The simplest way is to type "list" while
the guide is open: REALbasic will select the first item in the list starting with the
characters you type.)
You should notice a few differences between this instruction page and the inStr one
(besides the fact that this one is much longer). First, notice that because the listbox
control is an object, it has a "Super Class" section. That's basically its mother -- as a
control, it is based on "RectControl," a generic control class. What that means is that it
has inherited all of the characteristics (properties) of a RectControl -- stuff like "width"
and "height" properties. It's important to know that, because those properties aren't listed
in this help window, but of course they are valid for using with a listbox.
Next, you'll see a list of properties. These are settings you can change. Some, like
"initialValue" are listed in bold, meaning they cannot be changed while the program is
running. Some you can never set, but others you can set before running the program via
the Properties Window when the object is selected.
After the Properties listing, you find an Events listing. Events are things that happen to an
object. For instance, it might be opened, closed, or clicked on. Remember our little "hot
help" program? We used the "mouseEnter" event to display an appropriate help message
when the cursor was pointed at an object (a control). You won't see a mouseEnter event
listed here, because mouseEnter is an event inherited from Mom (rectControl).
Note: you don't access an object's events programmatically, by coding;
listbox.gotFocus generates an error. (This is only true of the events REALbasic
provides -- with your own objects you can call your own events.) Instead, an object's
events show up within the object's code window. There you can put appropriate code for
each event. The listing within the help window is not to show you the code you can type,
but what parameters different events generate.
Finally, if you scroll down past Events, you'll see Methods. Methods are like procedures
and functions in other languages. Since REALbasic is completely object-oriented
(everything is an object), your subroutines are always methods (the method of an object).
Methods do things. Some methods need a parameter or two, some don't. You call a
method the same way you access a property. listbox1.deleteAllRows is a valid
command (assuming an object named "listbox1" exists). If a method requires parameters,
you can put those after the method (either enclosed by parenthesis or not). Both of these
are valid REALbasic statements:
listbox1.addRow "This text will be added as a new row"
listbox1.addRow("This text will be added as a new row")
(If there were more than one parameter required, you'd separate them with commas.)
The Notes and Examples sections of powerful controls like the listbox are essential
reading. Some of the examples use skills or features you haven't used yet, so they can be
confusing, but playing with the examples is a great way to learn. We'll be exploring the
listbox and other controls as we continue with this series.
Further Help
REALbasic's online help isn't comprehensive or perfect: there are mistakes, typos, and
even a few commands missing. There's also a lot it doesn't cover or fully explain. But
don't fret: there are other resources available to you.
REAL Software also provides REALbasic documentation in PDF format. There are three
documents: Language Reference, Developer's Guide, and the Tutorial. (If you want to try
the tutorial, you'll also want the tutorial files.)
The Language Reference document is essentially identical to online help: it details every
REALbasic command.
The Developer's Guide offers more of an explanation of techniques and methodology for
programming in REALbasic, which is great, but it would be much more useful with
abundant sample code and more examples.
The Tutorial takes you through a step-by-step process of building your own SimpleTextlike word processor. Unfortunately, in the interest of speeding you through the project, it
leaves out a lot of explanation of what you are doing and doesn't explain alternative
methods. It basically says, "Do this, put that there, type this in, and you're done." For
experienced programmers, it's a great demo, but if you've never programmed before, it
can leave you puzzled about why what you did worked (or perhaps didn't, if you messed
up).
(The approach I'm taking with REALbasic University includes more hand-holding and
explanations of why we are doing what we're doing. It's slower, but you'll learn more.
And for those advanced students who wish for a faster pace, just skip the explanations.)
If REAL Software's documentation isn't enough (and it probably isn't), there's Matt
Neuburg's excellent REALbasic: The Definitive Guide, published by O'Reilly. It's a
volume packed with just about everything there is to know about REALbasic. I use it
whenever I find I'm entering unfamiliar territory. (For example, I've never had a need to
do anything with sockets; if I do, I'll read the section in Matt's book before trying
anything.)
Important Note: Matt's book is not a tutorial -- it's a reference volume. Like its title, it is
"The Definitive Guide." But it won't teach you programming.
If all that isn't enough help, don't forget REAL Software's REALbasic Network User
Group mailing list. It's an invaluable resource for posting questions and receiving quick
answers. Sometimes you'll even get technical answers from one of the programmers at
REAL Software! There's also a search engine of the list archives -- it's a good idea to
check there before you post and see if anyone has already answered your question.
(If you're not familiar with an internet mailing list, it's basically an e-mail exchange
where messages are sent to all members. Mailing lists work best if you set up a filter in
your e-mail software to direct a list's messages into a particular folder. The REALbasic
list tends to be very high volume, with over a hundred messages per day, so you don't
want them cluttering up your regular e-mail. Just remember, you don't have to read every
message! I usually only have time to browse the messages with topics I find of interest.
You can also subscribe to the digest version, which clumps multiple messages together
into a single large e-mail.)
And that's not all! If you really want to get the goods on REALbasic, begin exploring the
hundreds of websites devoted to it. There are sample programs, free code, classes and
routines you can use in your own software, REALbasic plug-ins (which extend
REALbasic's capabilities), and tutorials. Your best bet is to start and the REALbasic
Webring and go from there (at press time 107 sites were listed, including my own). Many
sites link to others which aren't on the webring.
Next Week:
We start on our first "real" programming project! We're going to write a simple but useful
utility. It should take us several lessons, but we'll learn about REALbasic while doing,
instead of me just lecturing.
Letters
Our first letter comes from Ryan Goudelocke, who writes:
Marc,
I'm glad to see all the positive feedback you've gotten for RBU - here's some more! RB's
docs are fine if you just want to check out particular controls etc., but overall program
design is pretty much left up to the user. Unless you're willing to buy and read Matt
Neuberg's book, it would be difficult even to get started. Your column is without doubt a
big step in the right direction.
Question: It's sometimes unclear when objects which are linked to particular windows go
out of scope, and how they can best be accessed by functions living in other windows.
RB's behavior, as far as I can tell, is different from both all-in-memory languages (like
orig. BASIC) and from C++, wherein all classes and their (public) functions are globally
available. Later on in your series this might be a helpful topic to explore - it seems like
there are a zillion ways of storing/transferring information between classes and their
windows (I guess this applies to modules as well), but which is best?
Anyway, as I said, keep up the good work!
Thanks for the encouragement, Ryan! I've very pleased with the progress and response of
REALbasic University so far, and I hope it grows and becomes an essential part of many
people's week.
Regarding your question, I will certainly explore that issue in depth in future columns.
For those who are just learning REALbasic, let me briefly explain Ryan's question. He's
talking about problems with the scope of a variable. Let's say you've got a subroutine
with a variable called Myvar. Myvar is local to that subroutine only -- other routine's that
attempt to access Myvar will generate an error, because they don't know that variable
exists. A window can have it's own variables, local to anything inside that window,
including subroutines inside that window, but a separate window won't know about those.
Other variables can be made global -- they are accessible everywhere in the program, by
any window, by any routine.
The problems Ryan is talking about is when you have a dialog box that asks the user a
question, what is the best way to get their answer back to the routine that opened the
dialog box? If you store the answer inside the dialog box (in a local variable), it goes
away when the dialog box is closed. If you store it in the calling window (or routine), the
dialog box doesn't know about it, and can't access it to set it.
The simplest method is to use a global variable. For instance, you could have a string
variable called gTheResult and the dialog box could put the answer inside that. If the
user canceled the dialog, you'd set gTheResult to nothing (gTheResult = "").
However, using global variables can waste memory. For a simple dialog like the above,
it's probably fine (especially if you reuse gTheResult for multiple dialogs), but what if
your program is some accounting software and the dialog box is supposed to edit a
customer record: do you pass a copy of the entire customer record to the dialog? What if
the dialog needs to access multiple records, or the entire database? Passing a duplicate
copy of the whole database is certainly not efficient, yet there could be complications if
the dialog box is allowed to modify the original.
I remember as a REALbasic newbie I didn't worry about it much but came up with my
own hastily glued together solutions and later regretted it. As a matter of fact, my ZWrite uses five or six different methods for passing info between windows -- not good.
While it doesn't necessarily affect the user, it makes updating more difficult, because
sometimes I find I've effectively painted myself into a corner: whatever system I used to
pass info has to change if I want to add a new option to a dialog box. (But I've learned
from my mistakes: with each Z-Write update, I clean up some of this poor code.)
We'll explore this in a future column: there's certainly many ways to do this, and none of
them are completely wrong or always right.
Our final letter for this week comes from Jules Steinhausen, who writes:
Having struggled with Think Pascal, then Codewarrior, where the simplest prog error
would crash or freeze my Mac making development a slow process, I welcome
REALBasic. HyperCard etc are fine, really flexible and easy to program but oh so slow.
Enough of the prattle.
My Questions: 1. The tutorial makes you use an App class as the basis for its text editor. I
have built a working word game that just has a window as its basis - as the window is
visible throughout the game is this OK or should I re-structure my game.
2. How can I get it to read MS Word files? I've tried adding the type MS8W (I think) to
the File types in the appropriate box but the Generic Find file dialog box does not display
them for selection.
PS. At 55 its hard to learn new tricks, but as an old friend of mine (Ed Reppert) used to
assure me, Understanding Object Orientated structure is a 'Road to Damascus' job it
comes 'one day' in a blinding flash. I live in hope.
Hey, Jules, great to see you tackling new challenges! You're never too old to learn new
tricks. New tricks keep you young!
To your questions:
1) The tutorial uses the App class simply because that allows you to have multiple
documents. When you only have REALbasic's default window as the window for your
program, that window is your program. Closing it closes your program (it doesn't actually
end anything, but there's really nothing else the user can do -- there isn't even any menu
active except for Quit). With an App class that class is your program -- when it ends your
program has quit. Since the App class is running even with the main window closed, it
can respond to user commands (and enable appropriate menus). We'll talk about this in
depth in a future column.
2) Word files are a proprietary format owned my Microsoft. They don't release the specs,
though some companies (like DataViz) have reverse engineered the format enough to
create their own translation software. Reverse engineering a complex format like Word is
no easy task: that's why most people use Rich Text Format (RTF) as an exchange format.
RTF is a text-only format that describes complex documents and is used to move
formatted text between word processors. I myself recently released RTF-to-Styled Text, a
freeware program that converts RTF to text so that my Z-Write users can import Word
documents (Word can export in RTF format). RTF-to-Styled Text is Open Source (I used
Belle Nuit Montage's RTFParser library), so you can download the REALbasic source
code on my site.
Hope that helps everyone. That's it for this week -- keep the letters coming. It's great to
see what you all are doing with REALbasic!
.
REALbasic University: Column 006
The Problem
My day job is working at a printshop where I receive files from clients every day. Many
of these files are created in programs I don't use, or perhaps the job comes from a PC or
via the Internet. Either way, I've got files with generic icons that won't open when I
double-click on them. Somehow I need to tell my Mac that "ad.jpg" is a JPEG file and it
should be opened with Adobe Photoshop.
The trick is that Mac's have secret Type and Creator codes attached to all files. These
codes are simply a series of four characters. Most are simple, like text files are of Type
"TEXT" (capitalization is important with Type and Creator codes). If you double-click on
a file with "R*ch" as the Creator code, your Mac will try to open the document in Bare
Bones Software's BBEdit text editor (if you have installed on your computer). If wanted
the file to open in SimpleText, instead, you could change the Creator code to "ttxt" and
you'd be set.
These hidden codes are visible in many technical programs or utilities, such as ResEdit.
But most of these are a pain to use, and overkill for my needs. There are also shareware
programs out there that let you see and modify a file's Type and Creator codes, but the
programmer in me is inherently cheap and I love to reinvent the wheel. So I'm going to
write my own utility for changing Type and Creator codes.
REALbasic to the Rescue
This kind of problem is ideal for REALbasic. It's a simple problem, but challenging
enough to be interesting. As we explore solutions we'll learn about several aspects of
REALbasic, such as creating a user interface, classes and data structures, working with
files, dialog boxes, and more.
Program Design
The first step in writing any program is to do a bit of planning. I know, I know. If you're
like me, you prefer to just dive right in a start programming, but a little thinking ahead
can save us a lot of headaches later. So let's stop and think about how we want this
program to work. What are our needs? What should the program look like? What would
we feel comfortable working with?
Let's start by listing a few essential requirements of our program.
1. Simple. Type and Creator codes can be confusing. We don't want to be confused.
We want something simple and quick, almost transparent. We shouldn't have to
worry about saving a file or messing with dialog boxes. In fact, we may want the
actual Type and Creator codes to be hidden -- the program could display the kind
of file we're converting to and that's all we need to see.
2. Powerful. I frequently find I need to change the same types of files over and over
again. It would be ideal if the program could memorize various Type and Creator
codes so I could just choose "Quark XPress" and make some files Quark
documents. It also should support multiple files -- what if I have a whole folder of
JPEG images I want to be owned by Photoshop?
3. Elegant. The program should have a clean interface and be easy to use. It should
be easy to add, remove, and change saved items.
That's a pretty good start, but it's rather general: almost all programs should be simple,
powerful, and elegant. What we need now are some specifics. Exactly how is our
program going to accomplish its task? What is it going to look like? How will it
remember Type and Creator code combinations?
Let's try envisioning the program at work. Remember, we're brainstorming here: anything
goes, and nothing's finalized yet. We haven't written any code, so we're free to change
anything. It's important to come up with several scenarios, as with programming, there
are many ways to do every task, and some are better than others. It might even be a good
idea to get out a pen and paper and sketch these out so you can visualize them better.
Concept #1
Okay, there's a menu at the top with a list of file types (Quark XPress, Photoshop JPEG,
etc.). Only one of these can be checked at a time. Then a menu command to "Select
File/Folder." When chosen, you are prompted to select a file or a folder. When you do
that, those files are changed to the kind you indicated. There'd be other menu commands
for adding, removing, and editing saved items.
Advantages: simple and clean; not even a window.
Disadvantages: can only see file type chosen when menu is pulled down; can only
convert an entire folder (not ten items in folder); uses the Select File dialog which is a
pain to use, especially for multiple files.
Concept #2
Instead of menubar menus, let's use a window with a popup menu with a list of file types.
The user just selects one. There's a convert button which converts all files/folders that are
dragged to it. To add an item to the File List, just drag a file of that kind to the popup
menu. There'd be menu commands for editing/deleting file types.
Advantages: simple; handles multiple files well, and doesn't use the Select File dialog.
Disadvantages: since both the popup and convert buttons accept dragged files, it might be
confusing which is which.
Concept #3
There's a listbox control on a window. Files/folders dragged to the list are displayed.
Next to each filename is a file type icon and file type name as a popup menu. The user
can easily change the file type for each file by choosing a new one from the menu next to
it. There'd a convert button under the list so when all the files were in place, one click
would change them all. This way a person could change multiple files to multiple kinds at
the same time!
Advantages: powerful; visually good (displays file type icon and lists files to be
converted).
Disadvantages: complex -- the ability to change multiple files to different file types at
once might be too powerful; difficult to program (REALbasic doesn't have a built-in way
to get a file's icon); RB's listbox won't let us include popup menus inside it.
Concept #4
A variation of Concepts 2 and 3. We'll have a popup menu of File Types at the top.
There's a list of files to be converted and a "convert" button. If a user doesn't want a
particular file converted, they can selected it and remove it by pressing the delete key. All
files would convert to the same File Type. There'll be a menu item for editing the File
List, but you could drag a file to the popup to quickly add it to the list.
Advantages: simple and clean; visual difference between large list box and small popup
list of file types minimizes confusion; actual Type and Creator codes hidden until editing;
ability to remove individual added files is powerful.
Disadvantages: semi-complicated to program as multiple dialog boxes are needed.
So, there we go. That gives us a few options to choose from. Concept 3 is obviously the
most powerful, but it's rather complicated; overkill for the simple utility we want.
Concepts 1 and 2 would be easy to program, but they wouldn't be as elegant as Concept
4. Concept 4 won't be much more work than the others, but offers the perfect compromise
between usability and ease of creation. So it's decided: we'll develop Concept 4.
All that's left is to think up a cute name for our program. We could go with the obvious,
something like FileTyper, but let's be more creative. How about something memorable
and a little silly... since changing a file's Type and Creator is a bit like a certain surgical
operation, how about GenderChanger?
Next Week:
We design GenderChanger's user interface.
Letters
Our first letter this week is a compliment from Jean-Yves Bosc, who writes:
Thank You very much for your the REALBasic courses on the net. I am a database
developer and was interested in trying out REALBasic. I am very grateful and impress by
your effort for providing online lessons. That's it... Thank You Very Much again
p.s.: I am a soccer fan as well.
Thanks, Jean-Yves! Obviously a man of taste and distinction. ;-) Seriously, I hope that
RBU will help you realize your goals.
John Rahn, who earlier wrote about a problem with active windows being grayed out,
writes:
Here's a quick question: Is there a way to FORCE the cursor into a selected EditField?
Example: A window that has several EditFields, like a "Preferences" window, etc. Before
the user can close the box, one EditField MUST have a value keyed into it.
The user attempts to close the window with that EditField empty, and a Message Box is
displayed telling the user that the box must have something entered into it. Then upon
closing the Message Box, the cursor is automatically placed into the EditField in
question.
Sure, John! Just use the editField.setFocus method where editField is the name of
an editField. It moves the cursor to the editfield you name.
Next, Dennis writes with some comments and a question:
Hi, Mr. Zeedar.
First, I'd like to thank you for the way cool REALbasic tutorials. As a REALbasic
newbie, and as someone who's not much more advanced than a newbie when it comes to
programming in general, I've found your tutorials to date to be nicely tailored to my skill
level. I especially like the way that you clarify things (e.g., other readers' feedback) for us
newbies, and generally make learning REALbasic much easier than would be the case
were we to rely on the REALbasic documentation alone. Already, I've created a coollooking (though almost completely useless for the user) application which has taught me
a nice bit, and which would've taken me months to create with CodeWarrior or the like.
I have one specific comment and one general one. First, how are global variables created?
I've searched through the documentation, and have discovered that global _constants_
can be stored in modules, but I couldn't find any specifics on global variable creation. (In
case you care, I'm thinking of creating one of those match games, wherein one turns over
playing cards until he/she finds a match. It seems to me that I'm going to need a global
variable or two in order to 'tell' one playing card object whether or not another such
object has been turned over.) Second, I think that a lot of people -- especially hobbyists
like myself -- would receive much enjoyment from creating a game like Pong, or Break-
out. Perhaps you could do tutorials on this in the future, or at least go into sprites in a
much more comprehensive way than the REALbasic documentation does.
Thanks again. I encourage to you continue your good work. As other readers have said, I
look forward to each of your tutorials, and wish that there could be a new one every day.
:)
-- Dennis
Thanks for the feedback, Dennis. I'm glad you like the pace I've set -- I worried that it
would be too slow for some and too fast for others. It's tough to strike such a delicate
balance.
As to your question, you create a global variable simply by adding a property to a
module. Modules are inherently global, so anything you put in a module: a constant, a
property, or a method, is global and accessible anywhere in your program.
As a beginning programmer it can be difficult trying to decide when to use a global
variable versus a local one. In general, use a local variable when the information is linked
or part of something else, like the current window. For instance, in a game, you may have
a window that opens when the user starts a new game. The score of that game would be
local to the window. But the list of high scores you don't want to disappear when the
window's closed, so those should be stored in a global variable.
Sometime you just learn through experience, doing it the wrong way and realizing your
mistake later in the project. Some beginning programmers just make everything a global
variable simply because that's easier, but remember, globals eat up memory: they are
always around, wasting storage. For a small app that may not be a problem, but for a
more complex one, it could be bad.
Finally, it's interesting that you mention both a card game and an arcade game, because
those are both projects I thought might be appropriate for future tutorials. In fact, I have a
version of Pyramid that's almost finished that I thought I might go into after we're
finished building GenderChanger.
That's it for this week, keep the letters coming!
.
REALbasic University: Column 007
Starting A Project
Today we're going to start our first useful REALbasic program. You'll want to have
REALbasic up and running while you work through the tutorial. GenderChanger is going
to be a simple utility for changing the normally invisible Type and Creator codes, useful
if you want some JPEGs to open in PhotoShop instead of the Mac OS default
"PictureViewer."
We'll begin by launching REALbasic and choosing "New Project" from the file menu.
This will give us an empty project window. The first thing I do after starting a project is
to save it -- I am paranoid about saving no matter what program I'm using.
Let's create a new folder called
(you create that special F by holding
down the option key and typing an F) -- and inside it we'll save our project as
(the pi symbol is obtained with option-p). Again, these are just
conventions, and you don't have to use them, but they do make it easier for you to
identify your project's folder and the actual project file itself.
Okay, now we are ready to begin!
Working Visually
This first part of creating our application is going to be fun and easy -- we'll get to typing
in actual code next week. For now, we're going to work visually, defining the various
windows our application will need.
First, note that the default project REALbasic created for you automatically includes a
window called Window1. That's great. We'll use it. Double-click on it to open it up. Now
the default size of the window isn't quite right for our purposes, so let's adjust it. You can
either drag the resize widget in the bottom right corner of the window to enlarge it, or
with the window active, go to the Properties palette and change the width to 358 and the
height to 300.
Let's also, as long as we're here, change the title of the window to GenderChanger.
(The title field is found on the Properties palette. You should be able to select and
change the text next to the label.)
Next, make sure that the Visible checkbox is on, CloseBox should be off, GrowIcon on,
and ZoomIcon can be either on or off.
Go ahead and save your project.
Note: since we're going to be using a number of REALbasic objects from the Toolbox, it
might be helpful for you to brush up on our Toolbox tutorial from lesson 2. Click the
thumbnail to enlarge it. This screenshot is from REALbasic 2.1 -- if you're using RB 3.0,
the Toolbox looks a little different, but the basics are the same.
Let's go to the Toolbox and grab a "GroupBox." It's on the left side, four up from the
bottom:
Click and hold down the mouse button and drag it to the window. Don't worry where you
put it -- you'll adjust it in a moment. Just stick it anywhere for now. It always comes in
the wrong size anyway.
A GroupBox is a neat object. It's like a 3D-style rectangle, but with a built-in text label in
the upper left corner. You've no doubt seen GroupBoxes used in lots of Mac programs,
usually to group a set of related controls in a dialog box. For instance, the Mac's Remote
Access control panel (used to dial in to an Internet Service Provider) has a "Status"
GroupBox show connection statistics which is separate from the Setup GroupBox, which
has fields for you to enter your login name, password, and the telephone number to dial.
With the GroupBox selected, you can look on the Properties palette and see that
REALbasic has named our GroupBox GroupBox1. That's fine. Now let's change some
settings. Make your GroupBox have the following settings:
Now let's drag in a ListBox (three up from the GroupBox). A ListBox shows a list of
items, similar to the Finder's List View of files. Lists are very powerful as you'll see as
we develop GenderChanger. For now, just drag it in position and give it the following
values:
Next let's drag in two StaticText items (at the very top with an "A" icon). Put one of
these in the upper left of Window1, and the other inside GroupBox1 but at the bottom left
of ListBox1. (Notice how the objects automatically jump into position to align
themselves to the edges of the other objects?)
Make the settings of the two StaticTexts look like this:
That second StaticText object includes a long bit of text: too much to show in the above
screenshot. See the little square with the three dots on the far right of the text property?
Click that to bring up a text editing dialog -- a mini-word processor, easier than typing a
lot on that single line of type -- and type in the following:
Select a File Type from the menu. Drag files/folders to the list and click Convert.
(What we're creating here is a label with instructions for the user.)
Okay, we're almost ready for a break -- just a couple more objects. We need a popup
menu: it's in the middle of the Toolbox, right next to the ListBox icon. Grab one and drag
it just to the right of StaticText1. Give it these settings:
And add a Convert button:
Excellent! If you did everything correctly, your completed Window1 should similar to
this:
If yours looks different, go back and make sure you gave your objects the same settings
as mine. (Some things, like the fonts you use, don't really matter, though you may find
you need to make some objects bigger to accommodate larger text.)
Menus
Let's switch from messing with windows and add some menu commands to our program.
REALbasic makes working with menus quite easy (at least most of the time). Our
program will only have a couple menu commands, so this won't take long.
You'll notice that REALbasic included a Menu object inside your project. You'll find you
can't delete it -- all programs need a menubar. If you double-click on it, you'll see what
appears to be a Mac menubar inside a window. Click on the Apple Menu to drop it down.
See the blank item underneath it? (If you look carefully, you'll see there's a small gray
rectangle inside the blank area.)
Click on that to select it. Now you are ready to add a menu command. Just type in "About
GenderChanger..." (no quotes) and press Return. Notice that the Properties Palette has
changed. REALbasic automatically named your menuitem AppleAboutGenderChanger.
That's the name of the menu -- we'll need to refer to it later, to enable it and make it do
stuff. I don't like such a long name, so let's shorten it. Go to the Name area of the
Properties palette and delete the end so the name reads just AppleAbout. Much better.
GenderChanger will only need one other menu command, which we'll put on the file
menu. This is going to be the command for editing the stored list of file and creator types.
So click on the File menu (inside the Menu window) to drop down the menu. Click on the
blank area underneath the "Quit" menuitem and type "Edit File Types..." (without the
quotes) and press Return. Once again, REALbasic uses the full text of the menu for the
name. Shorten it to FileEdit.
Since this is an important command, let's add a command key shortcut for this menuitem.
Let's use "E" (for Edit). With the FileEdit item still selected, look at the Properties
palette. Near the bottom is a field labeled CommandKey (under Behavior): next to it type a
capital E.
Now you're essentially done, but let's fix a couple things for aesthetic reasons. It's a
standard Macintosh practice that the very bottom item of the File menu is Quit, so our
program looks rather dumb with an "Edit File Types..." item underneath the Quit item. So
drag it upward until it's above the Quit. Excellent!
But it's still not perfect -- Quit and Edit File Types are two very different functions. They
really shouldn't be right next to each other. So let's add a divider! Click on the blank area
at the bottom of the menu (you're adding a new menu item) and type a hyphen ("-").
When you press Return it will change to a standard menu separator (gray line). Much
better. Now just drag it up so it's between the Edit File Types and Quit menus and we're
done.
(If your File menu looks like the image below, close the Menu window and go on.
Otherwise, fix your mistake.)
Homework
You can run your project and you'll see how everything looks, including the menus, but
don't be surprised that the program doesn't do much. All we've defined is some of the
interface -- we haven't told the program to do anything yet. We'll get started with that
next time.
For now, you've got some homework. The "Edit File Types" command we created above
is going to need a custom dialog to allow us to edit the file types. Instead of me walking
you step-by-step through the process of creating the window, I'm going to show you the
finished dialog and let you try to figure out how to make yours match mine!
To get you started, go to the File menu and choose the "New Window" command. That
will add a new window into your project. You should be able to figure out the rest:
I'll explain this in more detail next week, so don't despair if you can't complete it
perfectly. But it should give you a nice exercise to do some exploring on your own.
Next Week:
We'll finish the interface of GenderChanger and begin typing some code.
Letters
This week's question comes from Anders Wahlby. He writes:
I am working on a CD catalogue application to list and edit the entries in the CD Remote
Programs database. My desire is to be able to find the entry for an Audio CD that is
currently inserted in the computer. How do I read the Table of Contents on a Audio CD?
Are there any functions in shared libraries that I can access with a declare function
statement for Audio CD properties? I know there is a program called NetCD by Toby
Rush, but I would like to do it for myself. Any ideas?
Keep up the good work!
Wow, that's a bit of an advanced question. It so happens I know nothing about CD stuff,
so I turned to the master: Mr. Toby Rush himself, who created NetCD in REALbasic.
Toby was kind enough to send me the following answer to Anders' question:
I've actually made my CDDB classes available in an informal and probably-not-100%bug-free state. They're available here.
They don't have any documentation, but they should be unlocked and commented.
Anyone may use them for free in any project, commerical or otherwise, as long as I am
credited.
However, there is another caveat: these only work for the original CDDB, not the new
CDDB2. I am actually working on similar classes for CDDB2, and I hope to have them
complete in the next month or so. When they are done, I will make them available as
shareware or freeware via my web site. Gracenote, the company that owns the CDDB, is
pressuring all developers to move to the CDDB2 and has stated that the original CDDB
will be "turned off" at some point in the future.
If the person who wrote you would like to really "do it himself," then good for him! :)
He'll want to check out Apple's technote DV22, which covers everything about audio CD
access in system 9 and before. As for Mac OS X, that involves using the IOKit, and I
haven't even figured that one out yet. :)
If he has any further questions, feel free to put him in touch with me at this address.
Thanks!
Wow, Toby, thanks for a comprehensive answer! I knew it was complicated, but I wasn't
sure of all the technical details involved. Anders has quite a challenge in front of him, but
you've given him the start he needs.
That's all for this week. Keep writing and don't forget to visit the new RBU MacBoards
discussion area.
.
REALbasic University: Column 008
Adding Dialog Boxes
Last week we started programming GenderChanger, our utility to manage invisible Mac
OS Type and Creator codes. I left you with homework: to create this dialog box.
I'm sure you recreated this perfectly, but just to be on the safe side, here are the properties
of the various objects in the dialog. Make sure yours match.
Note that I've changed the names of a few of these objects: be sure your object names
match mine, or else you'll run into problems down the road when I refer to objects by
name in RB code.
Next, we're going to add one more window to the project. This will be the rename dialog,
for when a user chooses to rename a file type entry.
Go to the File menu and choose "New Window." Set the window's properties to match
this:
Now you'll have to add several elements to the window. Drag over two pushButtons, an
editField, and a staticText. Set their properties to match the following:
Now there's one more element which looks a little strange in the IDE: it's the gray square
in the upper left. What is it? It's a canvas. A canvas is a special kind of REALbasic
object that represents a drawing area. Within a canvas you can draw whatever you want.
Since the drawing instructions are entirely REALbasic code, the IDE doesn't know what
to display -- so it displays a gray rectangle.
For now, don't worry about it. Just drag a canvas (it looks like a sunset picture) from the
Tool palette to the window and set its properties like this:
Creating a Data Structure
Excellent! We're making terrific progress. We've got all of our user interface elements in
place. The only thing left is the actual code that makes everything work!
To start, we've got to establish a data structure for our program's data. A data structure,
conceptually, is simply how our program's information is going to be organized.
Remember, if our info isn't organized our program won't be able to find it to work with it!
The most common data structure for a program is to use an array. An array is a special
kind of data structure: it's like a repeating variable. Remember, a variable is just a
container. An array is like multiple containers, all linked the with the same name. Each
separate container in the array has a unique index (a number).
Like many other programming languages, REALbasic lets you create arrays and you
access each variable by putting the variable's index in parenthesis after the name like this:
arrayname(12) would refer to the 12th element in the array.
Let's use an example. Let's say you had a list of names. You could store each name in a
separate index of an array like this:
dim theArray(5) as string
theArray(1)
theArray(2)
theArray(3)
theArray(4)
=
=
=
=
"Steve Jobs"
"Steve Wozniak"
"Billy Joel"
"Michael Owen"
theArray(5) = "Clint Eastwood"
The first line of the above code tells REALbasic to set aside memory for an array of five
string elements. The next lines assign names to each of the elements of the array.
If you told the program to use theArray(2) it would return the name of the co-founder
of our favorite fruit company.
In less powerful languages of the past, arrays were limited because the size had to be
established before the program compiled. The above "dim" command, which sets the size
of the array, could not be changed while the program was running. Thus arrays were
useless in situations where you didn't know the quantity of data in advance. (An
alternative was to use an array but put a limit on the amount of a particular kind of data.
Old software had frequent arbitrary limits because of that. For instance, for
GenderChanger, we could make it so the program couldn't handle more than 100 file
types.)
REALbasic, however, is a modern language: it allows you to shrink and expand the
number of items in an array on the fly. That's good, because arrays are convenient, useful
data structures that are easy to program, but we don't want our program limited simply
because we want easier programming. With REALbasic we get both: easy programming
and the size of our arrays is limited only by available memory.
In the case of GenderChanger, what kind of data are we going to need to store? We want
to save Type and Creator codes of various kinds, right? Each of those will need a unique
name as well. Thus there are three pieces of information we'll need for each file kind:
•
•
•
Name (string of any length)
Type (four-character string)
Creator (four-character string)
This is perfect for an array structure, since we'll have a series of file kinds to save. But
then we run into a problem: each element of an array can only contain one item. We have
three items for each element in the array. How do we do that?
Well, there are a number of approaches. One approach is to put all the info for all three
pieces of data in the array element. By using a unique separator between each piece of
info, you can later figure out which is which. For instance, fileArray(3) =
"FreeHand;FH80;AGD3" would be a way to set the third item to the name "FreeHand",
Creator "FH80", and Type "AGD3".
Another approach is to use three separate arrays (one for each piece of info). Since the
index of each element is the same in all three arrays, we know that name(10) matches up
with fileType(10) and fileCreator(10). As long as the index numbers don't get out of sync
for some reason, we have access to our data.
The problem with both of those approaches is that they're complicated and easily broken.
What if you suddenly decide you need to save a fourth piece of information with each
record? What if the user types a semicolon as part of the file kind's name? With three
arrays, you've got to do every step three times, once for each array: it'd be easy to forget
in one instance and your program wouldn't work right (and you'd have a devil of time
figuring out where the bug was).
A better approach is to use REALbasic's object capability. Instead of having an array of
strings, we'll have an array of a special object we invent. Our object will contain three
properties: a name field, a Type field, and a Creator field. This way we'll have an
efficient single array to work with, yet our data is nicely structured, and our code is easy
to understand.
To do this, we first need to invent (define) our new object. Objects in REALbasic are
called classes (every object belongs to a particular class). So go to the File menu and
choose "New Class" to add a "Class1" object to your project. Then select Class1 and
change its name to "fileTypeClass".
Now double-click on fileTypeClass to open its window. While classes, like any other
object, can have events, menu handlers, etc., the only thing we're concerned with are our
object's Properties. To add a property -- with fileTypeClass as the frontmost window -we go to the Edit menu and choose "New Property". That should display a dialog similar
to the following:
Type in MacCreator as string in the editfield and press return (or click OK).
Do this process twice more, adding properties of "macType as string" and "name as
string". Your finished fileTypeClass window should look like this:
Excellent! You've just created a new kind of object, fileTypeClass, and you've added
three properties (containers) to that object. Next week we'll refer to that object in our
code, creating an array of fileTypeClass items.
Adding Startup/Shutdown routines
Our final task this week is to add an application class to our program. What's an
application class?
Well, by default, the Window1 that REALbasic creates when you create a new project is
your program. Close that window and your program is done. That's because you haven't
created an interface to the application itself.
Remember, REALbasic is object-oriented: it wants to send messages to your application,
but your application doesn't have an object. You need to create one. (I personally think
REAL Software ought to make new projects automatically have an application class, but
they don't; I'm not sure why.)
There are several benefits to your program having an application class. If your program
allows multiple documents, for instance, you'd have to have one (otherwise closing the
last document would end your program). In our case, we want GenderChanger to load a
list of saved file types when launched and to save the updated list when shut down. Since
an application gets launch and shut down messages (a simple window does not), this will
be easy to add once we've got an application class.
Creating an application class is simple: go to the File menu and choose "New Class" (just
like you did when creating fileTypeClass). This time, you're not only going to change
the name of the new object to "app" but you're also going to change the "Super" property
to "Application" (choose "Application" from the popup menu next to the Super property).
Your application class object's properties should look like this:
That's it for creating the object: now we're just going to add some code in the "Open"
(launch) and "Close" (shut down) events of the application object.
Open the app object. Click on the "Events" disclosure triangle and select the Open event.
On the right, in the code editing area, type "openFile". Your window should look like
this:
Good. Now click on the "Close" event and type "saveFile" in it.
That's it!
(Right now your program won't compile -- it will complain that "openFile" is unknown.
That's because we haven't written that routine yet. That will be for next week.)
Next Week:
We add load/save routines for our data, and begin completing other aspects of
GenderChanger's code.
Letters
This week's letter comes from Jerome Guelat, who writes:
Hi,
Many thanks for this new tutorial, I'm a beginner with REALbasic and I appreciate your
good work. Everything is very comprehensible for a newbie like me and it's quite
interesting. However I have a "vocabulary" question: what's the meaning of routines and
subroutines, and what's the difference between these two words?
Could you put a definition in the glossary?
Excellent question, Jerome, and thanks for the feedback. I've been trying to keep things
simple for newbies and was wondering if I was succeeding (once you've mastered
something, remembering how your brain was before you had that knowledge is tricky).
Glad to know it's working!
As to your question, they are pretty much the same thing; at least I use them
interchangeably. Technically I guess you could call a subroutine an entire procedure (or
function or method) while a routine could be a few lines that are part of a larger
subroutine. I draw this distinction simply because a routine could be considered an
algorithm that completes a small task while a subroutine, by definition, is a stand-alone
method. But that picking nits: for all practical purposes, these are the same thing. (If a
routine is more than a couple lines, you'd be advised to put in into a subroutine.)
Glad to know you find the glossary useful! Keep sending in questions and suggestions for
terms you'd like me to explain.
REALbasic University: Column 009
Adding a Module
Last week we created a data structure for GenderChanger. Remember we made a new
class called fileTypeClass? But as of yet we have not assigned any storage element for
items of that class. All we did was define the class -- we didn't do anything with it.
So the first thing we're going to do today is create an array structure of type
fileTypeClass.
But where do we put this property? If we place it inside Window1 the information is local
only to Window1. We want all aspects of the program to be able access this info easily, so
let's create the array as a global variable. To do that, we must create a module.
A module is a special kind of REALbasic object. It has no interface -- the user does
interact with it. Instead, a module is simply a collection of code, like a library: it can
contain constants, properties, and methods. Properties and constants belonging to a
module are global -- any part of your program can access them. Methods within a module
are also global: any routine can call them.
Because a module is inherently a group, it is ideal for code that you plan to reuse in
multiple programs. For instance, you could create a standard help system and reuse it all
your programs. To export a module, just drag it from the project window to your hard
drive. To import a module into a project, do the reverse.
Modules are also the only place you can create constants: named items who's values are
set when your program launches and can't be changed. For instance, you could use a
string constant and put the name of your program's preference file in it. Why not just
"hard code" in the filename? Why use an inflexible constant instead of a variable?
Well, let's say you have several places in your program where you access your preference
file. If you "hard code" the name "GenderChanger Prefs" by typing it in each location,
what happens when you release GenderChanger version 2 which uses a different filename
and format? You have to search through your entire program and replace
"GenderChanger Prefs" with "GenderChanger 2 Prefs". If you happen to miss one and it
opens the old format preference file, your program won't work correctly and might even
crash. By using a constant, the name is set in only one place, making it easy to change.
You could do the same thing with a variable, of course, but that's more work: you have to
remember to set the variable to the appropriate value when the program is launched.
Constants are powerful and highly recommended. Use them for all sorts of things. For
instance, if your program has a preset limit for something, such as the length of a
filename or the number of items that are supported, use a constant instead of a hard value.
That way if in the future you need to change that value, you can easily change it in just
the one location. Like the Mac OS limits filenames to 31 characters, but Windows and
Mac OS X have 255 character limit.
REALbasic also lets you set a constant to be one value on Macintosh and a different
value under Windows (remember, the professional version of REALbasic lets you
compile your program as a Windows application). You also use constants when you
localize your program (translate it to another language). REALbasic lets you assign the
constant a different value for each language. If you use a constant for any text your
program displays you can simply select a different target language when you compile
your program and RB will use the text you input for that language. (You do have to
provide the translated text, of course. REALbasic isn't magic.)
Tip: by convention, programmers name constants and global variables in a certain
fashion to remind you that they are a constant or a global. A lowercase "k" precedes the
name of a constant and I use a lowercase "g" before global variables. As this is a
convention, nothing too bad will happen if you don't follow it, but it is a good idea as it
makes reading your code considerably easier.
So let's add some globals and constants to GenderChanger! First, we'll add a module. Go
to the File menu and choose "New Module." This adds Module1, a dumb name. So
change it to globalsModule. (To change the name, select the module in your project
window and edit the name field in the Properties window.)
Good. Now open it up by double-clicking. You'll see three sections on the left side:
Constants, Methods, and Properties. Let's add a constant first. Go to the Edit menu and
choose "New Constant." You'll see a dialog that looks similar to this:
Where it says Name, type in "kPrefName" (no quotes, capitalized the way I did it). Make
sure the Type popup menu is set to String, and then type in "GenderChanger Prefs" in
the Value field. Click okay. You've just added a constant that is the name of
GenderChanger's preference file!
Now we're going to add two global properties. From the Edit menu, choose "New
Property" and type "gDialogReturn as string" in the dialog box prompt. Add a second
property as "gTheFileList(0) as fileTypeClass" and that's it.
What you've just done is add an array variable (gTheFileList) which is of type
fileTypeClass. You've made it a size of zero, because we aren't sure how many
elements it will need. Instead of setting the size in advance, we'll dynamically grow the
array as needed.
The other property, gDialogReturn, is a simple string variable which we'll use to send
information to and from our dialog boxes. (That way we'll be able to tell if the user
cancelled the dialog, for instance, or made a change. More on this later.)
Your globalsModule should look something like this:
Adding Load/Save Routines
Okay, we've done the simple part. Now we've got to create two important methods. These
will load and save our program's data. Since neither of these methods have any
parameters or return values, this is easy. Simply go to the Edit menu and choose "New
Method." Name the first one "openFile" and do it again for the second, naming it
"saveFile". (You can create methods at any time, of course, but I find it easier to create a
bunch at once instead of one at a time as I need them.)
Now we get to code! We'll start with saveFile. Why? There's a good reason, in fact: it's
always best to create your save routines first, then base your open routines on the save
routine. (Essentially open and save routines are the reverse of each other.) If you create
your open routine first you have nothing to test it with! By creating the save portion first,
you can save some data, then see if your program will open it correctly. Also, you'll
sometimes find while creating your save routine that you forgot an important detail that
needs to be saved and going back to modify your open routine is prone to error.
First, let's think about what saveFile is going to do. (This will help us decide what
variables the routine will need.)
All our data is going to be saved in GenderChanger's preference file: it will automatically
be retrieved on launch and saved on quitting with no interaction from the user. So first
we'll need to find our preference file. Then we'll need to write our data to it. Finally, we'll
close the file.
But what format will we use to save the data? Our data structure that we defined last
week is a series of fileTypeClass objects. Each fileTypeClass object has three elements
(properties):
•
•
•
Name
Creator
Type
The simplest file type is an ordinary text file, so let's use that. And since REALbasic has
file commands for reading and writing a line of text at a time, it's convenient to use a
single line of text as the basis for each record of our data. Each line will therefore contain
a file name, creator, and type. Like this: StuffitSIT!SIT5.
But look how they're all scrunched together. How will we know where one ends and the
other starts? Simple: we'll use a delimiter, a unique divider character. Now it's important
that it be a character the user can't type -- otherwise if they used that character in the
filetype's name we would think that signaled the end of the name. It can't be a return
character, because we're already using that: each line is a new record, remember, and
return signal the end of a line. How about a tab character? That is perfect: REALbasic by
default won't let a user type tabs in a dialog box so the user won't be able to insert one
when naming an entry.
Knowing exactly how our data will be formatted is important: in fact, with a simple
format such as GenderChanger's, we can even manually create a facsimile of what our
pref file's data might look like. (The image below is in BBEdit; tabs are shown as
triangles.)
Cool. We now have a good idea of what our routine is going to do, so let's figure out
what variables we'll need. REALbasic uses a special type of object called a folderItem
to represent files, so we'll need one of those. We'll also need a textOutputStream object,
another special REALbasic object for text going to a file.
Tip: you can find out more about the powerful folderItem object within REALbasic's
online language reference help.
What else will we need? A string object for each line of the file, while we build it, and
some integers for the loop through the data structure. I can't think of anything else, so
let's get going.
REALbasic uses the DIM command to set aside storage for a variable. Remember when
we created the gDialogReturn variable earlier? Essentially we were doing the same
thing, except without the DIM command (RB infers that it was there). The syntax is the
same: the name of the variable, the keyword "as", and the data type.
Let's create some variables. Go to the saveFile method. (Make sure you're in the right
one: you don't want to reverse the two so your open routine tries to save a file!)
In the right hand panel type the following code:
dim
dim
dim
dim
f as folderItem
out as textOutputStream
theLine as string
i, n as integer
Cool! Now that we've defined the variables our routine will use, we can start working
with them. What was the first thing we said this method was going to do? Look for our
preference file, right?
But how do we know where that is? It's in the user's Preferences folder, yes, but where is
that? On the current Mac OS, the Preference folder is inside the user's active System
Folder. But what if the user's running our program on a different, future OS? What if
they're using a new ability like OS 9's multiple users feature where different users have
different Preference folders?
No problem. The brains at REAL Software created a special function which checks with
the Mac OS to find out where the user's Preference folder is located. All we have to do is
use the preferencesFolder command, which returns a folderItem set to the user's
Preferences folder, no matter where it is located!
GenderChanger's preference file is inside the Preferences folder, so it is a "child" of that
folder. We can use REALbasic's child method to return the folderItem of the file we
want. Here's what that looks like:
f = preferencesFolder.child(kPrefName)
Remember, preferencesFolder returns a folderItem... therefore it is, in effect, a
folderItem. So we can use a folderItem's child method by simply using the .child
syntax. The child method requires the name of a file, so we pass it the preference file
name constant we created earlier. The whole portion to the right of the equals sign returns
a new folderItem that represents our preference file and assigns it to the variable f.
Simple! (Amazing that one line of code does all that, eh?)
Now that we've got a folderItem representing our file, we need to open it right? Not so
fast. What if the folderItem was bad for some reason? It's rare, but it could happen. If
we've got a bad folderItem and we try to work with it, our program will have a nasty
crash! We don't want that, so let's do some error-checking to make sure the folderItem
is good before we work with it.
If REALbasic can't create the folderItem item properly, it sets it to a special kind of
value called nil. Nil means nothing. It's like the item is dead, or even better, nonexistent. (When you first define an object variable like a folderItem but don't put
anything in it, the default value is nil.) So we can check to see if the folderItem is valid
by checking to see if it's nil. If it is, we skip the portion of program that works with it
and return an error message.
Note: only object data types are nil upon creation. Other data types are simply empty.
Like a string is "" and an integer is 0 (zero).
Here's the code:
if f <> nil then
end if // f = nil
beep
msgBox "Unknown error. Couldn't save the preferences file!"
Our if statement will only let a valid folderItem pass through: otherwise the above
beeps and displays an error message.
Note: the // tells REALbasic that all the text following that on that line is a comment,
not code. (You can also use a single quote, like '.) It is a good idea to add comments to
your code as that makes it easier to figure out what you were doing later. In the case of
the above, it helps associate the end if command with the start of that command, so
when there are multiple end ifs you won't be confused about which is which.
So if we've got a valid folderItem, what do we do with it? Well all we need is for it to
generate a textOutputStream -- a special RB type that is used to write to a text file. We
earlier defined out as a textOutputStream object, so now we set it to a text file. To do
that we use a folderItem's createTextFile method which returns a... yes, you guessed it,
a textOutputStream object. This opens the text file for writing.
Put this line under the earlier if statement:
out = f.createTextFile
Now if there's an error creating the text file -- say the hard drive is write protected (or
anything else rare but possible) -- out will be set to nil. So let's check for that before
continuing.
Add this after the previous line:
if out <> nil then
end if // out = nil
Good. Now we've checked for potential errors, so our next thing is to actually save the
data. Our basic methodology for that is to loop through our data structure, writing one
line of text for each record of data.
To do that, we'll need a loop structure. A simple for/next loop makes the most sense,
but we have to know the size of our loop first. How many records are in our data
structure?
Fortunately that's not a problem for REALbasic. We can use the uBound command to find
out the size of our array.
Important sidenote: the uBound command does not technically return the size of an
array. It tells you the number of the last element in the array. In other words, if index 5 is
the last element in your array, uBound will return a 5. However, that is not the size of the
array: arrays in REALbasic have a zeroth element, so the size (the total number of
elements) is always one greater than the uBound value. In the case of GenderChanger,
we're not using the zeroth element of the array so the uBound value and the size are the
same, but that is not always the case. Remember that! (See the Letters section following
this column for more on this subject.)
So we'll assign variable n to the uBound of our array gTheFileList. Then we'll start a
for/next loop that will count from 1 to n.
Explanation: A for loop counts from one value to another. Each time through the loop it
assigns the current count to the variable you used. So in the code below, i starts off as 1
the first time through, then it's 2, then 3, etc. All the code between the for and the next is
repeated each time through the loop. (Extra credit: what happens when the count values
are nonsensical, such as "count from 1 to -1"? Nothing! The whole loop is just skipped.)
n = uBound(gTheFileList)
for i = 1 to n
next
Bonus Question: why do we assign the uBound value to n instead of just counting from 1
to uBound(gTheFileList)? Think about it. (The answer is at the end of this tutorial.)
Now that we've got our loop, we just need to organize each record of our data into the
appropriate save format. Remember how we decided to separate each element with a tab
character? All we have to do now is put together a string with each piece of information
separated by a tab.
Here's the code (this goes inside the for/next loop):
theLine = gTheFileList(i).name + chr(9)
theLine = theLine + gTheFileList(i).macCreator + chr(9)
theLine = theLine + gTheFileList(i).macType
You see that we put the name into our theLine variable, then append the creator and type
elements, separated by tab characters. We use the + operator to combine the elements.
Note that to append to theLine, we include theLine in the stuff on the right of the
equals sign. That's a way of saying to the computer, "Take theLine add this new
information to it, and put in all into theLine." (Observant readers will see that the first
line does not append but sets theLine to the value. If we didn't do that, theLine would
include the contents of records from previous journeys through the loop.)
The chr function accepts a number and returns the ASCII character that matches that
number. Since tab is ASCII number 9, chr(9) is a tab character. (ASCII is an
international computing standard matching letters with numbers. Remember, computers
don't actually work with letters -- everything's a number to them. Some characters, like
tab or return, don't have a visible equivalent.)
Now that we've got our theLine variable filled with our data, we need to save it. We use
the writeLine method of our textOutputStream object, out. Add this after the above and
before the next command.
out.writeLine theLine
Excellent. We're almost done. There's only two more things that need to happen. One, we
need to close the file after we're finished with it. The close command must come after
the next command of the for/next loop (otherwise the file would be closed each time
through the loop). Two, we need to use REALbasic's return command to signal that the
method is at the end. (If we didn't add that, the routine would always beep and give you
the error message. With most routines, the return is implicit at the end of the routine.
For functions, you use the return command to return the appropriate response.)
So after the next command, insert the following two lines:
out.close
return
That's it! We're done. Here's what the whole routine looks like in the editor so you can
make sure you've got it perfect (this is from REALbasic version 3; yours may be
colorized differently):
Well, guess what? We've run out of time! That's right, this was a long lesson, so we'll
save the openFile routine for next time. Actually, openFile is pretty simple as it's just
the reverse of saveFile. Why don't you try to write it yourself?
(It's safe: though you're working with files, you're not writing anything, only opening.
Whenever you write information you potentially could overwrite an existing file.)
Here are a few hints to help you out:
Hint #1: textOutputStream becomes textInputStream Hint #2: You'll need the
redim and new commands to allocate space in the gTheFileList array and to initialize
each fileTypeClass object before you can use it. Hint #3: Use the nthField command
to parse the input lines.
Enjoy your homework assignment. I'll give you the complete code and an explanation
next time.
Next Week:
We finish the openFile routine and begin coding the interface so our program actually
does something.
Answer to Bonus Question: Speed. The uBound command takes a few milliseconds of
time to execute. When you're doing a loop, the uBound command is called each time
through the loop. Those milliseconds add up and slow down your routine. (In
GenderChanger's case, it's irrelevant as the loop is very short, but doing it this way is a
good programming habit.)
Letters
This week we've got several letters all on the same topic. Ryan Goudelocke, Dennis, and
Ron all pointed out an "error" (of omission) in my explanation of arrays last week.
Dennis wrote:
I'm glad to see GenderChanger as our first project; I'd been thinking of putting together
such a program before you even brought it up! Regarding this week's column (#8), unless
I'm missing something, you probably will want to edit the part about arrays, so that it
jives with what the RB 3.0 developers guide says in its "Using Arrays" section, on page
169:
"You create an array by specifying the number of elements (values) of the array in
parentheses. The number of values that you specify in the Dim statement is actually one
less than the actual number of array elements because REALbasic arrays always have an
element zero. Such an array is sometimes referred to as a zero-based array. For example,
the statement: Dim aNames(10) as string creates a string array with eleven elements."
Thus, Marc, when you wrote "...arrayname(12) would refer to the 12th element in the
array", "12th" actually should have been "13th". Also, your example array (with Steve
Jobs et al.) contains a total of six, not five elements.
Thanks so much for the columns!
Actually, my explanation was limited on purpose: I hadn't really wanted to get into the
complex zero-based array issue immediately, though I probably should have mentioned
it. It's something that's quite confusing, especially to new users. However, in this week's
lesson we did learn to use the uBound command, which opens up that particular bottle of
worms, and it can be confusing if you don't understand how arrays in REALbasic work.
In short, all REALbasic arrays have a zeroth element. But here's the tricky part: you don't
have to use it. If you use it, your arrays all have an extra element. That's not a problem as
long as you're aware of it. The key is to be consistent: either use the zero element or
don't. (I generally don't, but there are exceptions.)
Even REAL Software isn't consistent. Some functions in REALbasic are zero-based and
others are one-based. For instance, the listBox.List method is zero-based, but
folderItem.item is one-based. So watch out: always know if the array you are working
with is one- or zero-based.
Finally, as to terminology, I prefer to refer to the fifth element as index number 5 of an
array. Yes, technically that's incorrect: it's really the sixth element (five plus zero). But
that's confusing to me. I think of the zeroth element as the zeroth element, the first as the
first, the second as the second, etc. If I started doing it the other way my head would
quickly start hurting something awful!
So just be aware: I do realize that arrays are zero-based, but you'll find me referring to the
zeroth element directly, if that's important. If I say the "first" element, I'm talking about
index 1, not zero. I'll do my best to make this very clear when it becomes an issue in
future columns.
Meanwhile, keep sending me your column suggestions, questions, and corrections! (We'll
probably be needing more of the latter as things get more complicated. ;-)
REALbasic University: Column 010
Finishing the Open File Routine
Last week I left you with a challenge: to write GenderChanger's openFile routine
yourself. I gave you a few clues, but in case you weren't able to figure it out, we'll go
over it right now. It's fairly simple as it's just the reverse of saveFile.
We start the routine by defining the variables we'll use. Notice that while most are
identical to saveFile, I changed "out" to "in" and made it a textInputStream instead of
a textOutputStream.
dim
dim
dim
dim
f as folderItem
in as textInputStream
theLine as string
n as integer
Next, we open the file for reading with the openAsTextFile method and write the main
loop. The line that gives us the preference file's folderItem is the same in both routines,
as is our error-checking code (the if-then statements).
But in this case, we use a while loop instead of a for-next loop. That's because we don't
know in advance how many items have been saved in the file (so we don't know how
high to count). Our while loop keeps reading until it reaches the end of the file. The EOF
property is a boolean variable that is set to true when there is no more text in the file.
We use the not operator to invert the EOF value: we are in effect saying, while EOF is
false, keeping doing the loop.
f = preferencesFolder.child(kPrefName)
if f <> nil then
in = f.openAsTextFile
if in <> nil then
n = 0
while not in.EOF
wend
in.close
return
end if // in = nil
end if // f = nil
Just like in saveFile, once we're finished with the loop, we close the file and issue a
return command, which kicks us out of the openFile routine.
But the "meat" of the routine is the code that actually reads and processes the text. There
are several parts to this code.
Since we don't know in advance how many records have been saved in the file, we've got
to dynamically allocate more space in our array as we need it. Since our file format is
such that each line in the file is a record, we increment n by one each time through the
loop (n represents the number of records we'll have). Then we redim our array with the
larger size n. Redim is a special command that reallocates space (either more or less) on
the fly, while a program is running. The dim command must come at the start of your
program, before any code, while redim can be used anywhere (but only on variables that
have previously been dimmed). Another key difference between the commands is that
dim cannot be defined with a variable (like n): it must be a hard coded value (like 10 or
216).
Put this inside the while-wend loop:
n = n + 1
redim gTheFileList(n)
theLine = in.readLine
After we redim the array, we read in the next line of text from the file and save it in the
theLine string variable. Then we're ready to process it and store the record in the array.
But hold on: remember that our array is of our custom type, fileTypeClass. That means
each record in our array is an object. Objects do not exist until you create them with the
new command. If you don't new an object before attempting to use it, REALbasic will
crash with a Nil Object Exception error. Essentially, RB is saying, you attempted to use
a non-existent object, and since it doesn't know how to deal with such an absurd situation,
it just quits.
Add this line of code next:
gTheFileList(n) = new fileTypeClass
Once you've told REALbasic to create the object, you are free to put stuff inside the
object. In this case, the object is one element of an array, so we've got to remember in use
the index number of the correct element of the array. Since we're just adding to the array,
this is the last element, or n.
Now each object in our array has three properties: name, macCreator, and macType. We
want to set these to the correct value, but how do we obtain those from our theLine
variable? Remember, it's a line of text, like this, with the fields delimited by tabs (here
I've used a * to represent each tab):
FreeHand*FH80*AGD3
How do we grab just the first part, which represents the name field? Easy: we use a
REALbasic function called nthField.
NthField needs to know the string you are looking into, the delimiter you used (in our
case, a tab character), and the field number you want. It then returns just the portion of
text that matches the field you wanted (with no delimiter -- those are removed).
For instance, if you ran this code:
msgBox nthField("FreeHand*FH80*AGD3", "*", 2)
REALbasic would display "FH80" in a dialog box. (Try it.) That's because I told
nthField to return the second field from the "FreeHand*FH80*AGD3" string using "*"
as the delimiter.
Tip: you can use nthField to retrieve the individual words in a sentence by passing it a
sentence with a space as the delimiter. Note that this won't work with multiple paragraphs
as there isn't a space character between paragraphs.
NthField is a very powerful command, but it can be slow when working with huge
strings (like thousands of records or megabytes of data). There are better methods for
large databases. But for our purposes, nthField is ideal.
Sidebar: nthField's brother is a function called countFields, which only takes the string
to be searched and the delimiter as its parameters, and returns the number of fields in the
string. (In the above, it would return 3.) Tip: use countFields with a space delimiter as
a quick-and-dirty word count method.
Here's the next bit of code we'll use for GenderChanger. Remember, the chr function
returns a string character of the ASCII code we pass. Since a tab is ASCII code number 9,
chr(9) returns a tab character, which is what we are using as our delimiter.
gTheFileList(n).name = nthField(theLine, chr(9), 1)
gTheFileList(n).macCreator = nthField(theLine, chr(9), 2)
gTheFileList(n).macType = nthField(theLine, chr(9), 3)
The final bit of code is the same as saveFile; we just change the error message
displayed (this goes after the "end if // f = nil" line):
beep
msgBox "Unknown Error. Couldn't open the preferences file."
Here's what the full routine looks like (in REALbasic 3):
Does yours match? If not, fix the errors. If so, let's continue with the other parts of
GenderChanger.
The Code for listBox1
We're now finished with globalsModule: we added a constant, two properties, and two
methods to it, which is all we'll need. So let's move to Window1. Open the code editor for
Window1 by holding down the Option key and pressing the tab key while Window1 is
selected in the project window.
In the left pane, you should see an item called Controls with a triangle next to it. Click
the triangle to expose the contents (which are a list of controls in Window1). Click the
triangle next to listBox1. Now click the Open method of listBox1. You should have
text cursor in the right pane, ready for typing code.
Code for Window1.ListBox1.Open:
me.heading(0) = "File Path"
me.heading(1) = "Creator"
me.heading(2) = "Type"
me.acceptFileDrop("anyfile")
A control's open code gets executed when it is opened (right after the window using the
control is opened). This is where you put code to initialize a control (in this case,
listBox1). Whatever you do here happens before the control is displayed -- thus you can
change any of the control's properties before it comes into being, so to speak.
Here we are telling listBox1 to create three heading buttons with the names "File Path",
"Creator", and "Type". (The heading property is only effective if the hasHeading setting
has been set to true.)
Note that we use the me function instead of naming listBox1. This makes the code more
portable: it will work for any listbox, not just listBox1. The me function returns the
name of the control it is inside. It will not work inside a method, since a method is not a
control. (For a method to change a control's properties, it must name the control
explicitly.)
The final line of the open event is critical: we are telling listBox1 to allow files of any
kind to be dropped on it. This will enable the user to drag files from the Finder to the
listbox to add them to the convert queue.
Now click on listBox1's keydown event. What we are going to do here is very simple:
we're simply going to allow the user to delete an item from the list.
We check for two things: that the user pressed the delete key (ASCII number 8) and that
a line in the listbox is selected. We check for the latter by examining the listIndex
property of listBox1: since that property always returns the current selection, if it's not 1 we know that a line is highlighted (-1 means there is no selection).
Code for Window1.ListBox1.KeyDown:
if key = chr(8) and me.listIndex > -1 then
me.removeRow me.listIndex
end if
If the user has pressed delete with a line selected, we simply remove it with the
removeRow method.
Now click on the dropObject event. This is where most of the work is done.
What we want to do here is add the list of files dropped on the listbox to the listbox.
Since listBox1 is divided into three columns, we'll have to add three elements to each
line.
First, we check to make sure a folderItem object has been dropped: error-checking,
again. If so, we start a do loop loop. The loop continues until all the dropped files have
been processed.
Sidebar: What's the difference between a do loop and a while-wend loop? A while
loop has the condition that must be met at the beginning, while a do loop has it at the
end. That means with a while loop the code inside the loop will never be executed if the
condition is untrue, while a do loop will execute the code once, and then stop, if the
condition is false. In this case, that's what we want, because if we get to the loop we
know we've got at least one file and loop only repeats if the user dropped more than one
file.
Remember, obj is the name of the object that was dropped on the control (in this case,
one or more files).
Code for Window1.ListBox1.DropObject:
if obj.folderItemAvailable then
do
me.addRow obj.folderItem.absolutePath
me.cell(me.lastIndex, 1) = obj.folderItem.macCreator
me.cell(me.lastIndex, 2) = obj.folderItem.macType
loop until not obj.nextItem
end if
Since the first column is the file's path, we'll add the file's absolutePath property to a new
row of the listbox. Then we use the cell method to set the contents of the other columns.
The cell method requires the number of the row as well as the column number we are
changing. Since we've just added the row, we can use the lastIndex property to find out
the number of the row just added.
Important Note: Listbox columns are numbered from 0, so the second column is number
1, the third number 2, etc. This can be very confusing, so remember it!
The contents of the second and third cells are set to the creator and type of the dropped
file. These aren't used by GenderChanger, but only displayed for the user's information.
Sidebar: Confused by the multiple dots in the above "obj.folderItem.macCreator" line?
Don't be. Remember, periods indicate a sub-element of an object. (A sub-element can be
a property, method, or another object.) Since obj.folderItem is a folderItem object and
macCreator is a valid property of a folderItem object, the code is good. Technically
every object is sub-element of another one, all the way back to the "root" object, the
application itself. So multiple periods might look confusing, but it's the way OOP works.
Our loop is finished with the loop until command, which checks the status of the
nextItem property. NextItem returns true if there is another file in the queue. Just
checking nextItem causes it to throw away the previous folderItem and set obj's
folderItem property to the next file in the queue. Since the loop stops repeating once
nextItem is not true (i.e. false), when there are no more files in the queue, the loop
ends.
The Code for PopupMenu1
Click the disclosure triangle for PopupMenu1 to reveal its contents and click the change
event (if it's not already highlighted).
The change event happens whenever the user changes the selected item from the popup
menu. For instance, say there are two items on the menu, "One" and "Two" and it
currently displays "One". If the user changes it to "Two", then the change event would
execute.
Important Note: The change event is not called when the user reselects the same item.
In the above example, if the popup is set to "One" and the user checks the menu and
settles on "One" again, the change event does not happen. This can cause problems for
you if you want the popup to do something each time the user selects an item.
In our case, we're going to have a convert button which, when clicked, will change the
file types of the dragged files to whatever file type is selected in PopupMenu1. If
PopupMenu1 is not set to a file type, we want to disable the button.
Our code is simple: we just check to see if PopupMenu1's listIndex setting is -1. If it is,
we disable the button. If it isn't, we enable the button.
Code for Window1.PopupMenu1.Change:
if me.listIndex = -1 then
pushButtonConvert.enabled = false
else
pushButtonConvert.enabled = true
end if
Save your project. We're done for this week. Quick Quiz: can you think of a shorter
(more condensed) way to write the above PopupMenu1.Change code? (Answer after the
letter.)
Next Week:
We continue with GenderChanger by adding code to support menus, and write our main
file type changing routine.
Letter of the Week
This week's letter comes from Hayden Coon, who asks about OOP (Object-Oriented
Programming).
Dear Prof. Zeedar,
I find your pace slow but your articles are very good indeed: examples of good teaching
and clarity. I, myself, am a long time (started in 1956/7 with IBM 650 machine language)
programmer but needless to say, in the old fashioned (non-oop) style. I am following
your lessons in the hope that I shall one day become more comfortable with oop. My
native tongue is actually 'Common Lisp' which now has been taken over by CLOS (lisp's
version of oop). I have also worked in Assembler, C++, Pascal, and, of course, classical
Basic. I am a research biologist who uses programming to help with my research (I am
interested in artificial life, automata, etc.) simulations, mainly.
You are already doing it, but I hope that there will be more "asides" aimed at people like
me who are dinosaurs in this age of mammals: top-down proceduralists in an age of OOP.
I frankly am bewildered by the apparent obtuseness of oop model - it doesn't seem to fit
anything that I want to do -- at all. I spend my time blundering about trying to figure out
how "they" do simple things like querying the user for "input" on the fly... Where is the
counterpart to the INPUT, DATA, READ commands? I don't want to just mash a button
with some canned response - I want the program to assess what it is doing, its progress
and ask for my insights for choices based on answers to what to try next. It is hard, for
me, to do this with oop. You migth say, OK, it is there - "you just mash the appropriate
button/select the appropriate thingy from a list," to which I respond: "I don't see how (that
is my failing) but why do you PREVENT me from being able to do this IF I WANT TO?
There are countless other examples, people like me (and I know others) are "lost in" and
not "helped by" the oop way of thinking. Thank you, and keep up the good work; I shall
be trying to appreciate it...
Hayden Coon, Sebago, Maine
Thanks for the nice letter, Hayden. I trust the "Professor" part was tongue-in-cheek. I
have little formal training in programming, though I have been at this a while and taught
myself a few things (usually the hard way). It sounds live you've got plenty of traditional
programming experience.
From your letter, it seems to me you are confusing two concepts: OOP and non-modal
(modeless) programming. The two are very different, but somewhat intertwined. Let me
explain.
In the "old" days (i.e. as recent as the 1980's), computers were mostly modal. That is to
say, they operated in modes. When a computer program was in an "input" mode, nothing
else could be done: it only recognized input of the kind it was expecting at that moment.
For example, when I first got into computers I used a word processor called WordStar.
Now WordStar, while not being WYSIWYG, had features similar to most of today's
word processors, including the ability to copy and paste blocks of text. But there was no
mouse so you couldn't select (highlight) a patch of text. Instead, you had to go the start of
your block and press a certain key command. Then, using the arrow keys, move the
cursor to the end of the block and type an "end block" command. This was very awkward
and modal: if you did anything else after the first "start block" command, WordStart
would forget the start block and you'd have to start over from the beginning.
Almost all computer programs were written like this. When you were in a certain mode,
you could do nothing else until you exited that mode. When Macs came along, they
brought a new concept to the table: user-driven programming. (It's also called eventdriven programming.)
A Mac program, in general, has no modes. It sits there, waiting for you to do something.
You can do anything: choose something from a menu, type, move a window around,
click a button, switch to a different program. You, the user, are in control.
From a programmer's perspective, this is wildly different. Traditional modal
programming is very linear: step 1, step 2, step 3, step 4, etc. With Mac, or non-modal
programming, any of those steps could happen at any time, whenever the user decides to
do that function. If you're used to writing programs the other way, it's a big mind-shift. I
know I had trouble in the late 80's when I first tried Mac programming after years of
using a PC and writing modal programs.
The key is to think like a user, not like a programmer. How are you going to use your
program? For instance, you mention wanting to generate a user response on the fly. With
traditional programming, you force the user into a linear structure. You can do this on a
Mac simply by displaying a modal dialog: nothing else happens until the user handles the
dialog. But from the Mac perspective, this isn't always the best choice. Mac users don't
like this: it feels unnatural and forced. Mac users aren't used to thinking modally -- they
often will do steps out of order, like bringing up an Open File dialog and remembering
they need to change the name of the file, and switching to the Finder to change it. A Mac
program thus must be flexible to allow the user some degree of freedom.
A user-drive program can still force the user down a certain path -- you just do it subtly.
For instance, it's a reality that you can't check spelling until you open a document. Macs
force you to remember this by disabling ("graying out") the "Check Spelling" command
until a document with text in it is available. Sometimes commands aren't even visible
until a situation is present that is appropriate for those commands (contextual menus are
an excellent example).
In your case, you don't say exactly what you are attempting to program, but I get the idea
of what you want (biological simulations, perhaps?). What I would suggest is that you
break your task into smaller pieces. Instead of thinking of the entire project as a linear
process from one to ten, break it up into smaller chunks that don't depend on user input.
For example, let's say you are doing a biological simulation. The user first must click a
"start" button. Perhaps this brings up a dialog with some choices the user must set. Once
that's done, the first part of the simulation processes, perhaps with a progress bar or
graphical display so the user can monitor what's happening. When the program reaches a
fork -- a point that requires user interaction, it would stop and display the results so far. If
it's a simple fork, the user could click A or B, or perhaps enter in more data that's
required. But instead of being modal, you're still a user-driven program: the user can quit,
switch to another program, start the process over, or do something else. (Obviously some
of your program's options are disabled, e.g. a graph feature can't be used until data has
been generated for it to draw a graph.)
Does this make some sense? OOP itself has nothing to do with event-driven
programming except that OOP makes writing event-driven programs easier. In OOP,
objects know how to do things by themselves. In REALbasic, for instance, a listbox
knows how to add a row of data to itself. That means your program is just a bunch of
objects the user can interact with -- a huge shift from having a "main" routine that is your
program.
In non-OOP Mac programming, however, while you have a "main" routine, all it does is
check for events. Did the user click the mouse? Ooh, call the "mouse clicked" routine,
which would see if the mouse was clicked on a window, on the menubar, on a button, etc.
So even though non-OOP programming might seem less intimidating, it really is the
same thing, just without objects, and with more manual labor.
The shift to user-driven programming is a big one, especially if you're very used to the
traditional methods. For some tasks, the traditional method is fine: performing a
calculation, for example, will generally always be done in a linear fashion. But any
program that requires user interaction should be written to be modeless: give the user the
power and control over their destiny. Users prefer it, are comfortable with it, and resent
programs that don't allow them control.
So, Hayden, that's about the best I can do without knowing exactly what you are
attempting to do and the problems you are encountering. Feel free to send me a real
world example of your situation and I'll see what I can do to help you with it.
Answer to Quick Quiz: Programmers often compete to see who can write a routine in
the fewest lines (or even characters). It can be a fun challenge, but it's also a valuable
skill, though in the real world, sometimes more readable code is better than more
compact code.
Here's the answer. It looks a little strange with two equals signs in the same equation, but
it does make logical sense (at least in terms of Boolean logic).
pushButtonConvert.enabled = not (me.listIndex = -1)
REALbasic University: Column 011
The buildMenu Method
Last week we started working with Window1. Today we shall finish with it. We'll start by
opening the code editor for Window1 (Option-Tab while Window1 is selected in the project
window). Go to the Open event.
Type in "buildMenu" and that's it. That's the name of a method we're about to add.
BuildMenu will fill PopupMenu1 with the names of our saved file types.
Go to the Edit menu and choose "New Method." Name it buildMenu and press return.
The code for this method is fairly basic: we simply count through all of the records of our
data array, and add each record's name to PopupMenu1.
Code for Window1.buildMenu:
dim i, n as integer
popupMenu1.deleteAllRows
n = uBound(gTheFileList)
for i = 1 to n
popupMenu1.addRow gTheFileList(i).name
next
Note there are two key things about this routine. First, note that I've included a call to the
deleteAllRows method. This deletes all existing rows of the indicated popup menu.
Why is that important? Because if buildMenu was called multiple times, each call would
add items to the popup, not replace them.
The second key thing I'll put to you in the frame of a question: why do we use a method
for filling listBox1? Why not put this inside PopupMenu1's open event? That's because
the user's going to potentially be adding and changing these items. If we didn't do this,
we'd have to compare each item to the updated list and see if the user changed the name
or added a new one. It is much simpler just to rebuild the list from scratch. By making
buildMenu a method, we can rebuild the menu with a single command any time we want.
The returnItemNumber Method
Now we'll create another method, this one a little more complicated. You see, when the
user chooses an item from our PopupMenu1, we can ask REALbasic to tell us the text of
what the selected menu. In other words, we can find out the name of the record. But that
doesn't tell us the index number of that record in our data array.
This routine will search through our data list and return the index number of the record
we are looking for.
Create another new method and call this one returnItemNumber. Don't press return yet,
because this time we need to add some more stuff. On the "Parameter" line type in "name
as string" and on the "Return Type" line put in integer. What we are saying here is that
we're going to send returnItemNumber a name and get back a number. Go ahead and
press return.
Our code for returnItemNumber simply scans through the entire database (our array of
file types), comparing each record name with the name sent to the method. If they match,
it returns the current index number. (Note that the return command terminates the loop
and the method.)
Code for Window1.returnItemNumber:
dim i, n as integer
n = uBound(gTheFileList)
for i = 1 to n
if gTheFileList(i).name = name then
return i
end if
next
Good. Now we're going to write the main code for GenderChanger, the one that actually
changes the type and creator codes.
The Code for PushButtonConvert
Open the code editing area for PushButtonConvert's Action event. This is the code that
will be executed when the button is clicked.
What we do here looks complicated, but it's fairly simple. As always, we first define our
variables. In this case we need a folderItem variable and a couple integers for our loop.
Our loop, in this case, is not through our data array, but through the files the user dropped
onto listBox1. So we set n to be the number of items in the list, and we count through
them.
(Notice that this is where we use the returnItemNumber function. We pass it
PopupMenu1's text and set theItem to the result.)
Partial Code for Window1.PushButtonConvert.Action:
dim i, n, theItem, theCount as integer
dim f as folderItem
theCount = 0
theItem = returnItemNumber(popupMenu1.text)
n = listBox1.listCount
for i = 1 to n
f = getFolderItem(listBox1.cell(i - 1, 0))
if f <> nil then
end if // f = nil
next
msgBox "Changed " + str(theCount) + " files!"
For each item in the list, we attempt to create a folderItem from the saved file path.
(Remember, we put the file's path into the first column of each row.) Since a listbox's
items start at zero, we use i - 1 as the row number, and 0 as the column number for the
parameters of the cell method. We pass that path to the getFolderItem function which
returns a folderItem object, if it can. If it can't (the path is bad), f is set to nil, so we
check for that before continuing.
Once we've got a valid file, we make sure it exists. Then we... hold on a second. If a
folderItem is valid, doesn't that mean the file exists? The answer is an emphatic no.
Since you use a folderItem to create new files, a folderItem can indeed point to a nonexistent file. But if you attempt to modify a non-existent file, your program will crash. So
just because f is not nil does not mean the folderItem is good: we must check that the
file exists before working with it.
Once we've got a good folderItem pointing to an existing file, we can change the file's
creator and type codes. Our variable theItem is now set to the index of the record
matching the one the user chose, so we can use theItem as the index for our data array
and retrieve the appropriate type and creator codes.
(Insert the following after the "if f <> nil then" line.)
Partial Code for Window1.PushButtonConvert.Action:
if f.exists and theItem > 0 then
f.macCreator = gTheFileList(theItem).macCreator
f.macType = gTheFileList(theItem).macType
theCount = theCount + 1
end if
Excellent. For each file we convert we add to our theCount variable, and our final bit of
code is simply to let the user know that we're finished changing the codes on the dropped
files. (Add this at the end, after the next command.)
msgBox "Changed " + str(theCount) + " files!"
Here we convert the integer theCount to a string, using the str function, and let the user
know how many files we changed.
Guess what? GenderChanger, while not finished, should be usable now. We haven't
added code for our menus, and the dialog boxes are non-functional, but the basic program
should work at this point. If you'd like to try it, feel free.
You should be able to Run the program (choose "Run" from the Debug menu) and drag
files to the listbox. (You won't be able to convert any files at this point, as you can't add
file types until we finish the Edit Types dialog box.)
If your program won't run for some reason, you get to enjoy the fun task of debugging.
Check your code carefully, and compare it to mine from this and previous lessons. Make
sure you've followed every detail -- programming, unfortunately, is all about tiny details.
Enabling Menus
There are three things you need to do to get a menu working in REALbasic. First, you
have to add and name a menu with the menu editor. (We did that back in lesson 007.)
Second, you must enable the menu within a window's enableMenuItems event. Third,
you have to add a menu handler and put in code to make the menu do whatever it's
supposed to do.
Let's start this process for GenderChanger by enabling the menus. Within Window1's
code editor, toggle the Events triangle so you can view the contents, and find the
enableMenuItems event. Put in the following code.
Code for Window1.EnableMenuItems:
appleAbout.enabled = true
fileEdit.enabled = true
Notice that all we did was set our named menu objects' enabled properties to true.
Since they default to false (disabled), you only have to enable the menus that need to be
active.
Note: You don't have to worry about the copy, paste, and cut commands on the Edit
menu. REALbasic handles enabling, disabling, and even action events for these menus.
You can override them if you want, but generally they work automatically.
While it doesn't apply to the simple menus we need for GenderChanger, you could put in
code here to check for certain situations before you enable a menu. For example, in a
word processor, you could make sure that some text is selected before you enable a menu
that changes the selection to uppercase. That's because the enableMenuItems event is
called every time the user clicks in the menubar (i.e. before a menu drops down). This is
also where, if appropriate, you'd put checkmarks next to menu items.
Tip: REALbasic sometimes gets confused and doesn't update menus properly after a
dialog box is closed. You can solve this by forcing menus to update by calling
enableMenuItems yourself -- simply include an enableMenuItems call in your code. For
instance, after your dialog box is closed, put in an enableMenuItems command to refresh
the menubar.
Moving on, let's add two menu handlers. Go to the Edit menu and choose "New Menu
Handler". On the popup menu that's displayed choose "appleAbout" and click okay to
insert that handler. Repeat the new menu handler command and this time choose
"fileEdit" from the popup. Press return.
You should now have two new menu handlers in your Window1's Menu Handlers
section. Click on the first one, appleAbout. Put in the following code.
dim s as string
s = "GenderChanger 1.0" + chr(13) + "Copyright 2001 by REALbasic
University"
msgBox s
This will display a very simple dialog box with the indicated text. Quiz: Can you figure
out what the chr(13) does?
Next, go to the fileEdit menu handler. Here we're going to do two things: we're going
to display the Edit Types dialog box, and we're going to call the buildMenu routine to
rebuild the PopupMenu1 in case the user added or changed the list of memorized file
types.
editDialog.showModal
buildMenu
Now your menus will be active and will even work when you run GenderChanger.
However, since we haven't added any code to the dialog boxes yet, they won't do much.
In fact, you can't even close a dialog box once it's opened because we haven't added code
to make the cancel button work!
Next Week:
We finish GenderChanger by completing the code for the rename and edit file types
dialog boxes.
Letters
This week we have a critical letter, from J. Russell. He writes:
Mr. Z.:
You are already losing me. You have a great idea and your column was what I had hoped
would really help me master REALbasic. However, you are doing the same thing that
REAL basic does in their tutorial, not explaining what is really going on, just telling you
to type some stuff into a window. Perhaps, I am one of those individuals who needs more
handholding than most, but I don't believe this is true. The question section of your
articles more than proves my point. The questions are not addressed to current items you
are addressing, but deal with items far in advance of what you have discussed.
I really want to learn how to use REALbasic, but your column has quickly gone beyond
the scope of newbies and has settled on dealing with mid/hi level users of REALbasic. I
believe if you surveyed the people who originally signed up for your tutorial you would
find more than 50% of the newbies have quit your column because they are as lost as I
am.
Respectfully,
J. Russell
Here was my response:
Hi John!
I'm sorry to hear the column has gone over your head. As you can no doubt appreciate,
REALbasic has attracted both intermediate and beginner programmers, people with no
programming experience at all, and others who know traditional techniques but just want
to understand how REALbasic itself works. Attempting to satisfy and interest these
diverse groups is a delicate balance.
Let me explain a little of what I'm attempting to do with REALbasic University. First,
understand that programming is somewhat complicated: there's no way around that. It's
going to take time to absorb much of what programming is about. Your early
programming attempts are going to be fraught with error, trials, and tribulations. That's
reality. But RBU is your resource: I'm here to help get you started, point you in the right
directions, answer your questions, alert you to potholes and deadly pits, and hopefully
make the process less frustrating than it can be. It won't happen overnight or in a single
lesson, but gradually, you'll learn.
The approach I'm taking with REALbasic University is two-fold. Every couple of months
I'll walk you through, step-by-step, the creation of a real program, like a game or utility.
Gradually these will increase in complexity. In between programming projects, however,
I'll be writing about other aspects of REALbasic. I'll discuss programming techniques and
concepts, review or alert you to various RB resources, explain a particular issue (such as
graphics programming or getting your RB app to work in Mac OS X), or discuss
algorithms something else related to REALbasic.
An important difference between these two types of columns: the general columns will
often be oriented more towards beginners, explaining fundamentals, while the
programming project columns will be geared towards completing the project. I will try to
explain what's going on during each step, but sometimes I'll omit issues or gloss over
something complex simply because I don't want to overwhelm and distract you from the
main purpose, which is completing the programming project.
(A good example is the recent discussion of arrays in Lesson 8. I omitted the concept of
zero-based arrays -- arrays that start with an index of zero -- but later addressed that in an
answer to a letter in Lesson 9 because people had thought I was making a mistake. In
reality I was just trying to keep things from being too complicated for the average reader.
I still feel I haven't really covered them in the detail I ultimately want to, but I plan to do
so a future column.)
I could do RBU where we don't do any real programming projects but I just explain
various programming concepts, but I know that I find it much more interesting to actually
write a program that does something, instead of discussing theories.
As to my explanations within the programming project columns, I'm torn between
explaining things too much and slowing down the progress, to going too fast for
beginning users. As to how I succeed or fail in that, I need you to tell me! Your letter
above is excellent, but to be truly helpful, I need you to specifically tell me what you
aren't understanding. You mention I lost you, but you don't say where or what you didn't
understand.
One thing to remember: it's easy to lose the "big picture" while in the middle of a
program. You focus on a single routine and you forget the purpose of the whole thing. I
can see this happening a little with GenderChanger. As we focus on each step, the overall
view of what we are doing (and most important, the why) is getting lost. Don't worry: I
have a "wrap up" column to come which will go back over GenderChanger and explain
how it works.
As to the letters covering advanced issues, I answer the letters I'm sent. If there's
something you don't understand, write me! That's what the letter section is for. Some of
the readers who have written asked more advanced questions so I answered them. Send
me some "beginner" questions and I'll answer those. The letters section is a generalpurpose area for any REALbasic-oriented questions -- they don't have to be about the
current project.
Let me conclude by addressing your comment regarding me "just telling you to type
some stuff into a window". To a certain extent, I agree I'm doing this. It's not something I
particularly like, but I also don't like putting in two pages of explanation for why you are
typing in two lines of code. Sometimes you'll just have to trust me, type the code in, and
understand that I'll eventually explain it in a future column. (Remember, this is our first
programming project. After we've done several of these, certain aspects will be clearer as
your understanding grows.)
My own programming experience began in the 1980's when I used BASIC. I remember
spending hours typing in pages and pages of programming code from Compute! books
and other magazines. Many times a single typo would sabotage my efforts, requiring
hours of painful debugging for me to figure out what had gone wrong. There was no
handholding there: I was completely on my own. There was no explanation of what the
code did: I had to figure that out for myself. I remember those first dark years very
clearly: everything was a fog. I had no idea what I was doing, and it was extremely
frustrating. But every now and then, something worked. It was like magic. The little Tic
Tac Toe game would beat me, or the new command I discovered did what I thought it did
and was really cool. My programming ambitions grew as I became more confident. I
must have started hundreds of programs in those days, and only a few actually worked or
did anything useful, but I was learning and having fun. It was great!
REALbasic is a huge leap forward from those days, absolutely unbelievable. But there
are certain similarities that I consider important. For instance, I could just give you the
completed GenderChanger project. You'd have to do nothing but run it, maybe poke
around inside and change it a little. But by making you type it in (or copy and paste it),
you have to do a little thinking, make some decisions, understand -- at least a little -what's going on. I realize it can be confusing, intimidating, and frustrating, but that's part
of the learning process. Learning is always a bit painful. If it's too easy you won't
remember it. In that sense, I want you to be frustrated, I want you to experience a little
pain. I want you to wonder, "Why I am doing this? Why is this here? Why doesn't that
work when I do this?" Have patience -- eventually things will be clear. Don't expect
everything to work for you first time, instantly, without a little effort. Programming can
be tremendously frustrating. I've spent hours and hours of painful, horrible effort trying to
get a single feature of a program to work correctly. When it finally does, you feel
wonderful, victorious, simply because it was so difficult.
Finally, let me just reiterate that I'm here to help. Tell me what I'm not explaining
something clearly, and I'll do more. Unlike an interactive classroom setting where I can
see your face and determine that my explanation is working or not, here I can only see
what you tell me.
-- Marc
So, what do the rest of you think? Am I going too fast, too slow, or just right? I am
explaining what we are doing enough, or do you need more detail? Are others of you
lost? Are the columns too long, too short, or about the right length? What am I leaving
out? Would you like more screenshots or less? Write and tell me! Without your feedback,
I don't know if I'm succeeding or failing.
Let me also add that I don't like to think of users in terms of "beginner," "advanced,"
"intermediate," etc. Programming is a strange science: an expert at one type of
programming can be a complete novice in another. I know that over my years of
programming I've ran into "experts" who knew less than I did (and I knew nothing) and
"hobbyists" who knew far more than I'll ever know (especially about math, where my
knowledge takes about 6 bits of storage ;-). Trying to categorize a person's technical
knowledge is almost impossible. So you'll rarely find me labeling my lessons as
"advanced" or "beginner" because those terms mean different things to different people.
What I think of as advanced you may think of as elementary, and vice versa. It all
depends on the subject matter and your experience with it.
I also think that labeling lessons tends to intimidate people. "Oh, that's 'intermediate' and
I'm only a white belt. I'd better skip this lesson." Nonsense. There's room for everybody
in the club. In REALbasic University we'll do some fun, simple stuff and some complex,
advanced projects, but I won't tell you which is which until after we're all done! So just
dive in and have fun and don't worry about it being too complicated: it will make sense
eventually.
Are you confused by the folderitem object? I exchanged notes with Dennis, who was
puzzled by something in last week's lesson. He writes:
I ran GenderChanger after completing this week's changes. How is it that there's now a
preferences file in my Preferences folder? I see no code which creates it. I see only that
code which beeps and complains if there isn't already an extant preferences file.
My response that the saveFile routine gets called when the application is launched still
did not explain everything for Dennis, as he didn't understand what in the saveFile code
actually created the file.
I'm still a bit confused, though. I don't see how saveFile can create a preferences file
when there isn't already one. That is, if there's not already a preferences file in place, the
only code from saveFile which gets executed lies outside of the "if" statement, namely:
f = preferencesFolder.child(kPrefName)
beep msgBox "Unknown error. Couldn't save the preferences file!"
I'm missing something, right? The "f= pre..." statement doesn't actually create the file,
does it?
I wrote back with a more complete explanation:
Ah, I see what confuses you. I should have explained this better. Folderitems are a
confusing thing until you figure them out (then they are easy ;-).
A folderitem is not a file -- it's just REALbasic's way of representing a file. It's a
REALbasic object that represents a file. When you create a folderitem you are not
creating a file, you're simply creating an object that points to a file (or a potential file, as
it might point to a file you haven't created yet).
Once you've got a folderitem you can tell REALbasic to do things with the file it points
to, like .createTextFile (which creates a new text file or erases the old one if it already
existed).
In the saveFile routine, the "f =" line creates the folderItem, not the file. By checking to
make sure it's not nil we're not checking to see if the file exists or not, we're checking to
make sure the folderitem object exists (i.e. was created properly). A valid folderitem does
not mean the file exists -- it just means that the file _could_ exist, if created. In other
words, REALbasic has the ability to work with the file if you choose to do so.
For instance, the line:
f = getFolderItem("Hard Disk:dgfgdsa.text")
would probably fail on most computers because they don't have a volume named "Hard
Disk": it is an invalid path and therefore an invalid folderitem and thus f would be set to
nil.
In the case of GenderChanger, f is going to be valid except in extraordinary
circumstances, so the routine will nearly always go on to the code inside the if
statement. It is there that we hit the "out = f.createTextFile" line: that is the line that
actually creates the file. In fact, it creates it if it exists or not! If it's already there, it is
rewritten anew (the old one is erased).
Does that make more sense? The key is understanding that a folderitem is NOT a file. It's
a bit confusing: I remember I was puzzled for a while when I first started with
REALbasic. A good analogy is to think of those robot arms scientist uses to handle
radioactive material behind a glass shield. The robot arms are completely controlled by
the user, but the user never touches the radioactive material. A folderitem is like those
robot arms: you, the user, never directly touch a file. You tell REALbasic to do
something with a property or method of a folderitem, and then REALbasic does that to
the actual file. The process of creating the folderitem is simply a way to associate robot
arms (a folderitem) with the right batch of radioactive material (a file). That way when
you manipulate the arms (folderitem), the right material (file) is changed. If you don't
create the arms properly (a valid folderitem object), then the arms don't exist (are nil).
Hopefully that analogy clears things up a bit.
REALbasic University: Column 012
The Rename Dialog
This week we finish our first REALbasic project, GenderChanger, by adding code to the
two dialog boxes, renameDialog and editDialog.
We'll start with the simple renameDialog. This is called from within the Edit Types
dialog when the user double-clicks on a memorized item to change the item's name.
First, we'll go to renameDialog's Open event. Type in "editField1.text = gDialogReturn"
for the code. What this does is put the contents of the global string variable,
gDialogReturn, into the dialog's editField. Why do this? I happen to know that when
the dialog is opened, gDialogReturn contains the name of the current file type, so we're
just putting in the current name in the editing field.
Next, go to renameDialog's canvas1 object, to the Paint event. Here we're just going to
draw a standard Macintosh icon (called a "note" icon) in the canvas. (Remember, a
canvas is just a drawing area object.)
All of REALbasic's powerful drawing methods are part of the graphics object. In the
Paint event, which gets called any time the object needs to be redrawn (such as when the
window is covered by another window), we are given a graphics object g. RB has a
built-in drawNoteIcon method which we'll use, passing the drawing coordinates of 0 and
0 (horizontal and vertical dimensions).
g.drawNoteIcon 0,0
That's it! Simple, eh? We'll get into more complex graphics with future RBU projects.
For now, move on to EditField1 and find its TextChange event.
The Code for renameDialog.EditField1.TextChange:
if len(me.text) = 0 then
pushButtonOkay.enabled = false
else
pushButtonOkay.enabled = true
end if
All we are doing here is making sure that EditField1 contains something. We use the
len() function to check the length of the text in the field, and if it's empty, we disable the
okay button. Since this code gets executed every time the text is changed (it is called the
TextChange event for a reason), we can be sure that the second the last letter is deleted,
the okay button will be disabled.
Bug Watch: Ideally we should be doing a lot more here. We should make sure that the
user doesn't type in the name of a file type already used. Remember the
returnItemNumber method we wrote last week? That routine assumes that each name is
unique. If two file types have the same name, only the first will be found and used. So we
should really prevent the user from typing in a duplicate file name here, but I'll leave that
as a GenderChanger improvement you can make yourself.
We're not quite done with renameDialog, but almost. There are still two other pieces of
code we need to add. When the user clicks either the Cancel or Okay buttons, we need to
somehow remember that, and we need to close the dialog box. So add the follow code to
the appropriate Action events of the Okay and Cancel buttons.
The Code for renameDialog.PushButtonOkay.Action:
gDialogReturn = editField1.text
self.close
The Code for renameDialog.pushButtonCancel.Action:
gDialogReturn = ""
self.close
What we've done here is save the contents of editField1 -- the new name -- into our
gDialogReturn when the user clicks the Okay button. If the user clicks the Cancel
button, we set gDialogReturn to nothing (""). Thus the routine that called
renameDialog will be able to test gDialogReturn and tell if the user canceled or not.
The self function refers to the parent window the object is on. Since that's
renameDialog, self.close is a way of sending a close command to renameDialog
(renameDialog.close would also work, but it's less generic).
The Edit Types Dialog
This is the most complex part of GenderChanger. Remember, we are trying to create a
fairly sophisticated user interface. We want the user to be able to drop new files into the
dialog to add those file types, the user needs to be able to delete existing file types, and be
able to rename a file type. We're also going to give the user the power to edit a Type or
Creator code by typing in a code manually. We'll learn some nifty new REALbasic stuff
in this process, so be ready!
Let's start with the easy one. Open the editDialog Code Editor (option-Tab while
selecting it in your project window). Expand the Controls section and double-click on
PushButtonDone (which should put you in the button's Action event). Put in the
following code.
The Code for editDialog.PushButtonDone.Action:
listBox1.headingIndex = 0
saveData
self.close
This button is clicked by the user when they are finished editing file types. So we want
three things to happen: the list to be sorted, the data (along with any changes) to be saved,
and the dialog box to close.
First, we're going to make sure that the list is in alphabetical order. Since the listbox's
first column (column zero) contains the file type's name, we can sort by the first column
to alphabetize the list. Our listbox has header buttons which automatically sort by that
column when the user clicks on one of those buttons. We can simulate the user clicking
in the first column by setting listBox1's headingIndex property manually to zero.
Next, we call saveData, a method we haven't written yet, but for now we'll assume it will
save all the changes made within the dialog box.
Finally, we close the dialog with the self.close command.
Now let's create that saveData method. Go to the Edit menu and choose "New Method"
and use "saveData" as the name.
I'll give you the entire code for saveData and then explain what it does.
The Code for editDialog.saveData:
dim i, n as integer
n = listBox1.listCount
redim gTheFileList(n)
for i = 1 to n
gTheFileList(i) = new fileTypeClass
gTheFileList(i).name = listBox1.cell(i - 1, 0)
gTheFileList(i).macCreator = listBox1.cell(i - 1, 1)
gTheFileList(i).macType = listBox1.cell(i - 1, 2)
next
If you've been following GenderChanger so far, the first few lines should be familiar.
We're basically setting up a loop: each time through the loop we'll save a separate record
of data (file type information).
We set n to the number of items in the listbox. Then we redim (reallocate) space in our
array. This sets the number of items in our array to n (not counting the zeroth array
element which we don't use). This way if the user added items, they are included. If the
user deleted some items, they are not included in the new array. Rebuilding the entire
data structure from scratch is not always the best method (it wastes time), but for our
simple purposes it works fine.
Then we use a for-next loop to count through the items in listBox1. For each record,
we initialize a new fileTypeClass object, and then assigns the contents the data in the
listbox row. It's probably easier to see this visually. Here's what listBox1 looks like with
a few items added.
Notice that there are three columns, Name, Creator, and Type, and four file types have
been added. The first row has the name "FreeHand", the Creator "FH80", and the Type
"AGD3". (If you assigned that file type to a file, it would show up as a Macromedia
FreeHand 8 file.) This 3 x 4 grid is very much like a spreadsheet: it has 12 cells, with
each cell containing an important piece of data (in string, or text format).
REALbasic lets us access each of these cells independently using a listbox's cell
method. The cell method requires two pieces of information: it needs to know the row
and column of the cell you want to access. That's easy info: our i loop variable represents
the current row (since each record is on a separate row), and we know which column
contains which kind of information.
However, listbox rows and columns are always numbered from zero, so since i starts at
one, we'll need to subtract one from it before passing it to the cell method. Columns are
also numbered from zero, so we can therefore figure out that Name = 0, Creator = 1, and
Type = 2. That means listBox1.cell(0, 0) would return "FreeHand" and
listBox1.cell(2, 2) would return "SIT5" (StuffIt 5's Type code).
Tip: The cell method returns our data in string format, which is exactly what we want,
so we don't need to do any conversion: we can just put the contents of the cell into our
data object. (If we had a number field, we'd have to convert the string returned to a
number using the val function.)
So each time through the loop, we create a new data object and fill it with the contents of
each line of listBox1. When the loop is finished, we're done. Since we created
gTheFileList as a global array, editDialog is free to access it, and we don't have to
worry about passing the new data structure back to the main application: the data is
global.
Okay, that takes care of saveData. Let's move on to listBox1, which has several pieces
of code. First we'll go to the Open event. This is where we initialize listBox1. It's critical
because this is where we add the file type data so that when the user brings up the dialog
box, listBox1 is filled with the current list of file types.
Here's the complete code for the Open event. It's explained below.
The Code for editDialog.ListBox1.Open:
dim i, n as integer
me.heading(0) = "Name"
me.heading(1) = "Creator"
me.heading(2) = "Type"
me.columnType(1) = 3
me.columnType(2) = 3
me.acceptFileDrop("anyfile")
n = uBound(gTheFileList)
for i = 1 to n
me.addRow gTheFileList(i).name
me.cell(me.lastIndex, 1) = gTheFileList(i).macCreator
me.cell(me.lastIndex, 2) = gTheFileList(i).macType
next
The first bits of code are simple. We dim some variables we'll need and name the column
heading buttons appropriately. Then we have an interesting command:
"me.columnType(1) = 3". What does that do?
Well, REALbasic supports different kinds interfaces inside a listBox. Remember how I
compared our listbox contents to a spreadsheet? Well, imagine that different cells in the
spreadsheet could contain more than just text. How about a checkbox? (For instance, I've
got an invoicing program that uses a checkbox for a yes/no "Tax" field.)
A checkbox is option 2. Changing "me.columnType(1) = 3" to "me.columnType(1) = 2"
would result in the cells of the second column being checkboxes instead of text fields.
Here's what the Edit File Types dialog looks like with the second column set to be a
checkbox field:
Obviously, that's not what we want for this application, but it's cool that RB lets you do
that. In our case, we want to use another option, 3. The 3 setting tells REALbasic that you
want the cell to be inline editable. That's a fancy way of saying the user can type in the
cell. That's right: you can edit right in the cell, just like it was a cell in a spreadsheet!
By setting our second and third columns as inline editable, the user will be able to type in
file type codes manually. This is a cool option for the power user, who knows Type and
Creator codes, but there's no interface to confuse the average user.
Quick Quiz: Sharp-eyed readers will notice that we have made columns 1 and 2 editable,
but not column 0. Since column 0 is the file type's name, it seems natural to be able to
edit that. So why not make it editable? The answer comes from experience. Inline editing,
while powerful, can be confusing. If a user clicks on a cell on a row, for instance, the
entire row might be selected (depending on where and how they clicked), or the cell
might be enabled for editing. It's very hard for a user to figure out how to select the entire
row when that's what they want. In our case, we want the user to have the ability to delete
a row by selecting it and pressing the delete key, so selecting rows is important. If all
three cells of a row were editable, the user would be clicking on the row trying to select
it, and the program would keep putting the cursor in the cell for editing. By keeping the
name column uneditable clicking on it selects the row. Clicking on a Type or Creator cell
opens the cell for editing. Easy enough for the user to figure out the difference. By using
a separate rename dialog box we avoid the row selection problem while still keeping
things simple enough for the user.
The next line of code in our Open event enables listBox1 to receive dropped files of any
kind. Why do that? Well, we want to give the user an easy way to add file types to the
list. This way, when this dialog is visible, files of any type can be dropped onto the
listbox to add them. Want to add a Quark XPress document type to your list of file types?
Just drop any Quark file on listBox1 and it's added!
The final section of code is another loop. First we use uBound to find the size of our data
structure, then we loop through our data array. For each record, we first add a new row to
listBox1 putting in the name of the saved file type (which gets placed in column 0), and
then we put the Creator and Type codes in that same row, but in column 1 and column 2
respectively.
Now you may have noticed in the above dialog where I was typing inside the cell I wrote
more than 4 characters. Remember, Type and Creator codes must be exactly four
characters. What happens when the user types in more or less than four characters?
Ah, we'd better put in a solution for that problem, shouldn't we? Let's go to the
cellAction event of listBox1. This event happens whenever the user finishes editing a
cell (by pressing return or enter, or clicking to a different cell). We're given the Row and
Column of the cell the user edited, so we can use that info to grab the current contents of
the cell.
Once we've got the contents (assigned to string variable s), we can check the length of
the string with the len function. If there are too many characters, we trim it to four with
the left function.
The left function takes a string and a number: it returns that number of characters
starting at the left side of the string. For instance, left("hello", 4) would return
"hell", the four leftmost characters of "hello".
We can put that result back into the cell to effectively trim what the user typed in.
But what if the user didn't put in enough characters? Simple: we pad the string with
spaces! Here's the code.
The Code for editDialog.ListBox1.CellAction:
dim s as string
s = me.cell(row, column)
if len(s) > 4 then
me.cell(row, column) = left(s, 4)
elseif len(s) < 4 then
me.cell(row, column) = left(s + "
end if
", 4)
See what we did in the second instance? We still used the left function to make sure we
had four characters, but we added four spaces to whatever the original cell (s) contained
to be sure we had at least four characters for the left function to use. That way if s was
"t" then it would become "t " or if s was "the" it would become "the ". (If s is
completely blank, it would become " ".)
Let's move on to the KeyDown event: the code we put here gets executed whenever the
user types a key while listBox1 has the "focus" (meaning it is the active control). All we
want to do here is see if the user pressed the delete key, and if so, delete the current row.
Tip: A delete key is chr(8), or ASCII character number 8. How do I know this? Well,
from years of experience. But if you don't believe me, load this project into REALbasic
and run it. It will display the ASCII number of any character you type.
The Code for editDialog.ListBox1.KeyDown:
if key = chr(8) then
me.removeRow me.listIndex
end if
Quiz: There's a serious bug in the above. Can you figure out what it is? I'll reveal the
answer next week. There's a free Z-Write license to the first person who explains not
only the error, but gives me the solution as well!
Now go to the DoubleClick event. This is where we'll put the code that will allow the
user to change the item's name.
Remember renameDialog which we created a couple lessons ago? This routine will
bring up that dialog box so the user can change the name.
We pass the current name (obtained from the name cell of the current row) to
renameDialog via our global gDialogReturn variable. We call the dialog with the
showModal method, which is a modal dialog, meaning that GenderChanger stops doing
anything until the dialog is dismissed. (REALbasic windows also has a show method,
which opens (displays) a window, but doesn't freeze all other action.)
When the user is finished with the dialog and clicks Okay or Cancel, control is returned
to our code immediately following the showModal method. Our first thing is to check to
see if gDialogReturn is empty or not: if it is, we know the user clicked Cancel, so we
don't change the name. If gDialogReturn has some text in it, we assume that's the new
name and set the name cell to the contents of gDialogReturn.
The Code for editDialog.ListBox1.DoubleClick:
dim i as integer
i = me.listIndex
gDialogReturn = me.cell(i, 0)
renameDialog.showModal
if gDialogReturn <> "" then
me.cell(i, 0) = gDialogReturn
end if
Note: I've mentioned this before, but there is a potential problem here if the user gives
two file types the same name. We really should check to see if the new name matches any
existing names before returning from renameDialog. That's something you can do to
improve GenderChanger if you're so inclined.
Okay, we're almost done, believe it or not! We need to add the code for handling the
dropped files. So go to the DropObject event.
Here we are passed a obj variable of class dragItem. DragItem is a special REALbasic
object that can contain a variety of data. It has built-in methods of accessing the various
types of data. In our case we are only interested in files dropped (not text or pictures, for
instance), so we'll check for an available folderItem.
Note: When working with dragItem data, you always want to check data availability
first, because the object may not contain the data you expect. For instance, we are
expecting a user to drop files on our listbox, but if the user drags a picture from a word
processor onto our listbox, the DropObject event happens, executing our code. If we
blithely assume that a folderItem is available and it is not, our program will quit with a
Nil Object Exception error (meaning that you tried to work with an object [a folderitem]
that didn't exist).
Once we know there's a valid folderItem available, we're free to use it. Since we don't
know what kind of file it is (in terms of a textual description, such as "Quark XPress
document") we'll just use the name of the file dropped for the file type name. The user
can change the name later.
So we add a row and put the folderItem's name on it. Then, using the row we just added,
we put the folderItem's Creator and Type code into their respective cells.
The Code for editDialog.ListBox1.DropObject:
if obj.folderItemAvailable then
do
listBox1.addRow obj.folderItem.name
listBox1.cell(listBox1.lastIndex, 1) = obj.folderItem.macCreator
listBox1.cell(listBox1.lastIndex, 2) = obj.folderItem.macType
loop until not obj.nextItem
end if
Notice how we put this within a do loop until loop? That's in case the user dropped
more than one file. Remember, obj.nextItem will be either true or false: true if there
is another file to be processed. Just checking obj.nextItem sets obj.folderItem to the
next folderitem dropped.
Running GenderChanger
Guess what? GenderChanger is almost ready to run! If you've tried it already, you found
it almost works, except for one thing: you can't drag files to the main listbox.
To enable file drag-and-drop in a REALbasic program, you must do three things:
•
•
•
Tell the control to accept file drops (of the correct kind)
Do something with the dropped files
Define a file type in REALbasic's File Type dialog
The first two we already did. Our DropObject event knows what to do with a dropped
file, and in our Window1.listBox1's Open event we put in this line of code:
me.acceptFileDrop("anyfile")
That line tells listBox1 to accept files of type "anyfile" to be dropped on it. But what
kind of file is type "anyfile"? REALbasic doesn't know because we haven't told it yet.
Go to the Edit menu and choose "File Types" (near the bottom). Click the "Add" button.
In the dialog that comes up, put in "anyfile" for the name and "????" for both the Creator
and Type fields, like this:
Click Okay to close both dialogs and save your project. If you Run (Debug menu, "Run"
option) GenderChanger, it should be fully functional, allowing you to drag files to the
main dialog.
Of course, until you add file types to the data structure, GenderChanger won't be of much
use: the File Type popup menu will be empty. So launch GenderChanger, press
Command-E to open the Edit File Types dialog, and drag some files of various kinds of
the listbox.
Here's an animation of GenderChanger in action:
Once you've defined one or more file types, you can close the Edit File Types dialog and
try changing some test files. (I'll emphasize this point strongly: until you've confirmed
your program is working correctly, always test it on practice files, not irreplaceable real
files!)
Here's an animation of GenderChanger converting a BBEdit file into an Acrobat Reader
PDF file type.
As you can see, the Finder immediately updates the file with the icon appropriate to the
new file type!
Note: Remember, GenderChanger only changes the file type, the way the Mac OS sees
the file, not the contents of the file. If you double-click on the BBEdit file I changed to a
PDF type, Acrobat Reader launches but complains it couldn't open the document. Of
course, if this was a real PDF file that had been incorrectly saved as a BBEdit text file,
changing the type would enable me to open it in Acrobat Reader.
The Complete GenderChanger Project
Next week we'll summarize what we've learned with GenderChanger. In the meantime, as
per a reader suggestion, I'm going to pass on the complete REALbasic project file for
GenderChanger. (It's in REALbasic 2.1 format, so users of both the current and the older
version of RB can work with it. You'll need StuffIt Expander to decompress it.)
If you haven't followed every step of our lessons, having the full project will enable you
to see exactly what I did to create this program. Explore it, run it, experiment with your
own enhancements, and have fun!
Next Week:
Now that we've finished GenderChanger, we'll compile a standalone version you can
distribute to others, discuss potential problems and improvements that you can resolve,
and we'll reveal our quiz prize winner and solution.
Letters
This week's letter is a question from Ernie Peacock, who writes:
Marc,
This is perhaps a dumb question -- apart from some SuperCard scripting years ago, I'm
pretty much a newbie.
I noticed that you had (for example) most of the save method nested inside an error trap.
Couldn't you just as easily have tested if f=nil, provided the error message, and then gone
on with the method?
My old brain struggles to follow along when there are several conditional statements on
the go at once. If it's just a matter of style, I'd find a clunkier one-thing-at-a-time
approach more helpful. But perhaps there's good reason for doing it your way . . .
Thanks for good work you're doing -- you've rekindled my determination to learn to
program.
P.S. A printer-friendly version of these tutorials would be a boon to those of us with little
iMac monitors.
Thanks for the note, Ernie. Here at RBU we don't consider any question dumb!
You are correct that either method would work. I've gotten used to the style I'm using, but
even I've gotten confused when miles of code is enclosed by long if-end if
conditionals. You could simply put this at the beginning of the routine:
if f = nil then
msgBox "Error! Couldn't open file!"
return
end if
Note that you must include the return command or else execution would continue after
the message is displayed. The return will stop the routine after the error message and go
back to whatever part of the program called this routine.
There are many methods and styles of programming; one of the things I want to do at
RBU is show different ways of doing the same thing. So I might use your method in a
future project just to show it in action. There's often no performance difference between
different methods of coding, but sometimes one method is easier to debug than another,
or it may be that one way just suits your brain better.
As to a print-friendly format, someone else suggested including a PDF file of each lesson
and I'm looking into a method to do that.
Have an idea for a future project for RBU? Have a REALbasic question? Send in your
questions & comments!
REALbasic University: Column 013
Compiling a Standalone GenderChanger
Up to now you've only been able to test GenderChanger within the REALbasic
environment. While that's great for testing, it isn't especially useful if you want to send
Grandma a copy of your new program.
REALbasic has the ability to create "standalone" Macintosh (and Windows, more or less)
applications. That means anyone with a Mac can run your program and they don't have to
have REALbasic or know anything about REALbasic.
There are several steps to compiling a standalone application. First, if this is an app you
plan on distributing (say as freeware or shareware), you need to define a unique Creator
code for your program. Apple maintains a database of Creator codes and you can register
yours to make sure no one else is using the same code. (Codes are exactly four characters
and are case sensitive. Apple reserves all-lowercase codes for themselves, so your
Creator needs to have at least one capital letter in it.)
Note: What would happen if two programs used the same Creator code? Nothing too
horrible, but basically your Mac would become confused. If you clicked on a document
belonging to application A, it might launch application B instead. Application icons
might become switched as well. Using unique codes ensures that every application has a
unique "name."
I've gone ahead and registered "GdCh" as the code for GenderChanger. To set that, go to
the Edit menu and choose the "Project Settings" option. Type in "GdCh" as the Creator
code:
Once you've set your application's Creator code, you're ready to compile.
Note: you could compile your app without a Creator code, but it can cause problems if
you later want to assign a custom icon to your program as the Mac sees your program as
a generic application. For your program to have a custom icon, you must have a unique
Creator code.
Go to the File menu and choose "Build Application". This brings up the following screen:
The options you see here let you set various characteristics of your program. Here you
name your program, set the app's memory partition size, compile for 68K or PowerPC
processor, etc. For GenderChanger our needs are modest, so we'll just set a few of these.
Check the "Macintosh" checkbox as the kind of app we want to create. (There's no point
of creating GenderChanger for Windows as Windows doesn't use Type and Creator
codes.) For the name field, type in "GenderChanger".
Since GenderChanger doesn't do anything that's processor intensive, it doesn't need to be
a PowerPC app: 68K will give us plenty of speed. Compiling for a PPC can be faster for
intense applications, but PPC apps require more memory and are much larger on disk.
Feel free to try compiling GenderChanger as both versions and comparing them.
Note: you can't create both 68K and PPC versions at once -- you have to first do one
version, then the other. You can check both boxes, but that just puts 68K code in with the
PPC code (increasing the size of your compiled program even more) to create what's
known as a "Fat" application. That means the app would run on any Mac, PowerPC or
68K. If you compile for 68K your app will run in emulation on PowerMacs, but a PPC
app won't run at all on 68K Macs.
Make sure you give your application enough memory. For GenderChanger, we don't need
much. The default is 1024, but I don't like to use minimums, especially with RAM as
cheap as it is these days. I go with 2024, which should be plenty.
Note: Unfortunately REALbasic doesn't give you an easy way to find out if your program
needs more memory: your app will just crash if it runs out. You just have to experiment
to find out the optimal settings for your program. Err on the generous side.
Next, you'll want to set some version information for GenderChanger. This is the info
that will be displayed with the Finder's Get Info command.
Since GenderChanger's not quite polished enough to be considered completely finished,
I've left it as a "development" version (1.0d). In your own programs, keeping the version
numbers up-to-date is highly recommended. Change the version number when you make
any change to your program, and keep a list of changes somewhere (I use comment lines
in my application's Open event).
Note that there's also an icon setting with a graphic next to it. If you wish, you can select
the image and paste in a replacement which will be used as GenderChanger's icon. (You
can use any graphics program to create the new picture, or use ResEdit's icon editor.) If
don't change the icon, your application will use the generic REALbasic cube icon.
Once you've finished setting all your information, click the "Build" button. REALbasic
will give you a progress bar showing the compiling process and in a few seconds you'll
have a new double-clickable application on your hard drive. Try it!
A cool feature: REALbasic will remember the settings in the Build Application dialog,
so you only have to enter all that stuff one time. In the future you can just change the
version number and recompile.
Answer to Last Week's Quiz
Last week's lesson included some buggy code with which I challenged readers to find the
error. Guess what? No one won the contest! I don't know if people couldn't find the error
or weren't interested in the quiz, but I'll reveal the answer here.
The problem was in the code for handling the delete key in the listbox in the Edit File
Types dialog box. The idea is that when the user presses the delete key, it deletes the
selected file type. Here is the original buggy code:
The Code for editDialog.ListBox1.KeyDown:
if key = chr(8) then
me.removeRow me.listIndex
end if
It looks fine on the surface, but there's a tricky thing we forgot: we never check to see if
the user has selected a line in the listbox! If you run GenderChanger with the above code
and press he delete key in the Edit File Types dialog box with no file type selected in the
listbox, GenderChanger will quit with an "out of bounds" error.
That means you attempted to access (work with) an array element that doesn't exist (it's
out of the array's range). Remember, the rows in a listbox are stored in an array. When
the user has no row selected, listBox1.listIndex is set to -1. By trying to run the
me.removeRow me.listIndex command, you are telling REALbasic to remove row -1:
there is no such row, and thus the error.
The fix for this is simple. Look at the code we used in lesson 10 for the listbox on
Window1.
Code for Window1.ListBox1.KeyDown:
if key = chr(8) and me.listIndex > -1 then
me.removeRow me.listIndex
end if
See the difference? It looks very similar, but in addition to checking to make sure the key
pressed was the delete key, we also make sure that listIndex is greater than -1, ensuring
that we won't attempt to delete a non-existent row. So the bug-free code should look like
this:
The Code for editDialog.ListBox1.KeyDown:
if key = chr(8) and me.listIndex > -1 then
me.removeRow me.listIndex
end if
Fix that and GenderChanger won't crash if someone presses the delete key in the Edit File
Types dialog without selecting a row.
GenderChanger Overview
Now that we've finished GenderChanger, I thought I'd take a few minutes and look back
at what we've accomplished. This was REALbasic University's first project and while it
went well, I've learned a few things writing the column that I hope will make future
projects even better.
We first started designing how GenderChanger would work in lesson 6. Now that you've
got a working GenderChanger of your own, it might be good to go back to that lesson and
see if I chose the correct design. Would a different interface make GenderChanger work
better? Are there improvements you could make to GenderChanger to fix its flaws?
Hopefully you can see how critical is to plan your application carefully: the way an
application is designed to work is critical to a successful program.
In lessons 7 and 8 we built the user interface for GenderChanger, the windows and
controls the user would interact with. You can easily see how the design tied in with the
user interface. Because of the planning, it was easy to know what windows and interface
elements we'd need. Often it's easier to combine the two stages, sketching potential
interfaces out on paper as you design them. Once you've got them designed, creating
them in REALbasic is easy, though it can be a little tedious.
In lesson 9 we actually started to type in code, and that continued in lessons 10, 11, and
12.
Learning how to code is the biggest challenge of learning programming. If you're familiar
with other languages, learning REALbasic is only a matter of understanding the RB way
of doing things, its unique syntax, and its bugs and idiosyncrasies. If you're completely
new to programming, learning coding will take time: there are too many aspects of
programming for me to explain it all at once. But as we explore different aspects of
REALbasic and create a variety of programs, you'll learn multiple techniques and how to
program different types of software.
For instance, look at all we learned while creating GenderChanger:
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
How to start a REALbasic project
Adding windows and controls to a project
How to set a control's properties
Adding and working with a REALbasic module
Constants
Methods
Arrays
A simple array-based data structure
How to create our own object class
How to load and save a program's data in a preference file
Delimiters
Error prevention
How to support dropping of files within your program
Working with different kinds of controls, such as popup menus and listboxes
Displaying and closing dialog boxes
Passing information between a dialog box and your main program
How to compile your program into a stand-alone Mac application
In retrospect, there were things I should have done differently. First, GenderChanger was
too complex of a project for our first. I should have picked something simpler. Second, I
should have explained some basic programming terms and techniques -- things like
loops, if-then statements, etc. -- before starting on a real project.
On the other hand, lectures are boring: building a real application is more fun and you
learn by doing. In the future, I'll try to balance lectures with projects a little better; let me
know how I do.
Based on reader feedback and what I've learned in the writing process, I'm making some
improvements to future REALbasic University columns:
•
•
•
•
•
•
•
Project code. Each column will include the REALbasic project file of that week's
lesson so you don't have to code from scratch. While typing in code can be a good
learning experience, it's also tedious and can be confusing, especially if you make
a mistake.
More screenshots. I originally wanted users to be able to select and copy code, so
a screenshot of code wasn't practical, but seeing the code the way it looks inside
REALbasic is much easier for the new user. I think visually, and my explanations
will be better if I can show you rather than tell you.
Working code. One of the biggest problems with GenderChanger was that the
way I presented it you couldn't really run it until the very last lesson. That's not
how real programming works: you usually get your program partially working
and gradually improve it. While that's messier, the benefit of that approach is that
you get the reward of seeing your creation grow before your eyes. I'll keep this in
mind for future projects, making sure the program will at least partially work as
we go along.
More visuals. I'm an oxymoron. Though I'm a word person, I think visually. I
find I can explain things better with visuals and I'll use more diagrams, drawings,
and animations in the future.
Sidebars. While I don't want to break my columns into multiple documents, I
may occasionally include a link to separate sidebar page explaining a particular
technique or issue in detail that advanced users may wish to skip.
More example programs. My original idea was to teach a technique when that
technique came up as part of a project. But one drawback to that is that a single
project only uses that technique in a specific, limited fashion. By using a nonfunctional example program I can demonstrate multiple aspects of a technique.
For instance, GenderChanger showed a limited version of drag-and-drop -- files
could be dropped onto the listbox control. But there are many other aspects of
drag-and-drop that GenderChanger doesn't explore.
Start with a completed application. One problem with GenderChanger that I
realized in retrospect is that it's an odd sort of utility: it deals with an invisible
technical side of the Mac that's difficult to visualize and it's hard to understand
what GenderChanger is supposed to do. I noticed this most clearly last week when
I included animated GIF images of GenderChanger in action: it made
GenderChanger's purpose much clearer. I should have included a completed,
compiled version of GenderChanger right at the beginning so readers would be
able to run the program and see how it works. Then, over the course of several
lessons, I would explain how the program was created. For future projects that's
what I intend to do and I think it will be better as readers will not only have a
visible target, but be able to compare our code to the finished application.
All that said, I'm pleased with the progress that REALbasic University is making. I don't
intend any one of these lessons to stand on their own but work together as a cohesive
whole: in a year or so, the collection of columns would make an excellent tutorial for a
new user.
Though it's a lot of work, I'm enjoying the process, and it's helping me to be forced to
analyze why I make the programming decisions I make. Please continue to let me know
how I'm doing and keep me on my toes!
Next Week:
For a change of pace, I'll review Erick Tejkowski's new REALbasic for Dummies book.
Letters
This week I reprint part of an email exchange I had with Gary Trazanka. He wrote me
saying:
First, I definitely enjoy the classes on RBU, all tutorials are helpful and to have one go
through the whole process is a bonus. Thanks.
Now for my question: Converting Strings to Doubles and Doubles to Strings. I am
working on a conversion program (inch to feet, feet to inch, etc).
How do I set up the fields, which should be doubles and which should be strings?
When I try making the Doubles into strings (ex. STR(NumToConv) it displays a zero for
the value. Also how do I get the Doubles and Strings to display in the same edit field (ex.
12 Inch)?
I responded:
I'm not sure I understand... editfields are _always_ strings. They cannot contain any other
value. But that shouldn't be a problem for you as you can convert doubles to strings and
vice versa.
I'll answer your query in more detail in the column, but for now, check out the format()
command in the online help.
Format() converts numbers to strings just like the str() function, but lets you define how
the number is converted. For instance, can display five digits past the decimal point using
the following:
editField.text = format(theNumber, "#######.#####")
That apparently was enough for Gary, because he responded with:
Man, Thank YOU!
My background in programming consisted only of VB on the PC and I have been trying
to switch to RB beacause I love the Mac so I imported the program over and started
going through it.
The difference was, I could add a number to my VB editfields by adding .value or if I
wanted strings just adding .text to the editfield label.
As soon as you said that editfield could only contain strings it hit me why my STR and
Val switches weren't working right.
10 minutes later and it was working.
Interesting, Gary. I've never used Visual Basic (VB) as it's PC only, so I wasn't aware
that it has a way to store a number in an editField. In REALbasic, editFields can contain
only string data (text). Without using VB I can't really say if its method is better; I
personally love the control that the Format command gives me.
Look at the REALbasic help file to see a detailed look at the Format command, but
briefly, it lets you designate a special placeholder string that represents the way the
number will be converted to a string. Within the placeholder string, the # sign represents
a single digit: that digit will display it it's present. A "0" also represents a number, but
will put in a zero if the number if not present.
So msgBox format(450.000, "#####.###") displays "450." while msgBox
format(450.001, "#####.###") displays "450.001".
But if you use zeros instead of # signs, msgBox format(450.000, "00000.000")
displays "00450.000", the zero digits are displayed whether or not they are actually part
of the number.
You can also include symbols and special characters. For instance, let's say you want a
number to print as money, with a dollar sign and two places past the decimal.
This displays "$3560.30":
msgBox format(3560.3, "\$#####0.00")
If you also wanted it to include commas at the thousandths position, change it to the
following, which would display "$3,560.30":
msgBox format(3560.3, "\$###,##0.00")
For very large numbers, format is essential: look at the difference between what format
and str do. Put this code in a button, run the program, and click the button:
dim n as double
n = 450343029420.93005
msgBox str(n)
msgBox format(n, "###,###,###,###,###,###.#####")
The above code first displays "4.503430e+11" and then "450,343,029,420.93005":
mathematically the same numbers (at least to a degree of precision), but visually quite
different.
Experiment with the Format command: it's very powerful and you need it to properly
convert doubles (numbers with decimals in them) to strings (text).
Have an idea for a future project for RBU? Have a REALbasic question? Send in your
questions & comments!
REALbasic University: Column 014
Review: REALbasic for Dummies
Those of you reading this column are probably not dummies, but you might find Erick
Tejkowski's new REALbasic for Dummies book helpful in your quest to master
REALbasic programming.
There are now two books devoted to REALbasic, and it is difficult to review Erick's book
without comparing it to Matt Neuburg's REALbasic: The Definitive Guide. The two
books couldn't be more different.
Matt's book is more like an encylopedia: it covers every topic in depth, but is short on
tutorials and example code (it does not come with a CD, though code samples can be
downloaded from Matt's website). It's ideal as a reference, especially when you run into a
development snag. It's targeted at the professional REALbasic user and experienced
programmer. (The current version of Matt's book deals with REALbasic version 2, but
Matt has finished an update which covers version 3 and should be available soon.)
Erick's book is geared toward the first-time user or REALbasic hobbyist. It covers a wide
variety of material, including several advanced topics, but doesn't pursue any of them in
depth. It's excellent as an introduction, but a reader may find themselves frustrated when
they attempt to move on to a real-world programming project.
For instance, the examples included on the CD are all rather simple demonstration
programs; the most complex could be written by a moderately qualified user in an hour.
That shouldn't discourage you, though, as the examples are excellent for experimentation
and learning. Each example focuses on a specific technique or concept, which is great if
you need to learn about how dialog boxes work, or menus, etc.
For example, I have never had the opportunity to work with sockets (a special
REALbasic object for dealing with Internet connections) and Erick's example is ideal for
me to play and experiment with using sockets for the first time.
However, if you're wanting to go deeper, and develop a real program, Erick doesn't lead
the way. His examples are not real-world, but of the "Hello World" variety.
(A "Hello World" program is the traditional program created by newbies as an example
of real working program, though it does nothing more than display "Hello World". In
REALbasic, you can create a "Hello World" program by putting msgBox "Hello World"
inside the Open event of a new project's Window1.)
For instance, one program shows the different window types REALbasic can create.
Useful for the newbie to see the different types, but not something you'd actually do in
real life.
This calculator is an example project from the RB for Dummies CD. Note that it's
buggy -- its math ignores numbers past the decimal point.
The structure of REALbasic for Dummies is appropriate for the beginner. Erick
introduces the REALbasic environment in the first chapter, then proceeds to step through
windows, interface elements, object-oriented programming, variables, control structures,
etc.
Erick's style is informal, similar to most Dummies books. The layout is excellent, and
there are lots of screenshots, bullet points, and step-by-step instructions. But actual
detailed explanations are rare.
When explaining windows, for example, Erick mentions custom WDEFs, which let you
create windows with unique characteristics (like a round window, or even a doughnut
shape). Erick explains WDEFs are a special Window Definition file, and he even
includes a screenshot of one, but he gives no instructions or hints on how to make these.
Why bother bringing them up if they aren't going to be explained? (At minimum, I'd
expect a link to a web resource with more detail.)
The same is true in the AppleScript chapter: Erick explains how to incorporate an
AppleScript into a REALbasic program (something moderately useful -- his example is
using AppleScript to call the Mac OS' speech synthesis system, which REALbasic doesn't
directly support), but doesn't even mention something far more useful (and desperately
needing documentation): making your REALbasic program scriptable.
The book does serve as an excellent introduction into REALbasic programming,
providing a buffet of treats for you to sample. Erick provides examples of REALbasic
QuickTime movie manipulation, sound, graphics, sprite animation, working with files,
Internet access, and working with a database.
If you're not sure what REALbasic can do for you, the examples may help to inspire you.
They'll certainly help you understand the basics quickly. But note that REALbasic for
Dummies is not a tutorial: each chapter simply presents concepts with a few examples,
but Erick does not do step-by-step hand-holding.
One nice touch: occasionally the book includes small snippets of code that are very
helpful for new users. Such code bits can be reused in many situations and save the
beginning user gobs of time from figuring out something that seems like it should be
simple but in reality is tricky if you don't know what you're doing. For instance, in the
chapter on windows, Erick includes code to center a window on the main display.
In general, I would say REALbasic University so far falls in between REALbasic for
Dummies and REALbasic: The Definitive Guide. If RBU seems advanced to you,
Dummies should help you through the bumps. On the other hand, RBU and The
Definitive Guide make an excellent combination for the intermediate user who finds
Matt's book a little intimidating. (If you haven't bought Matt's book yet, you might want
to wait until the new edition is published this summer.) Both books are well-written; they
simply target different audiences. Some users might like to have both books.
Next week: We explore one of the best uses of REALbasic programming: writing
software for personal use (a.ka. "in-house" software).
Letters
This week's letter comes from Sir Steven:
How can you get a number from an Editfield? Say you want the user to input some
numerical information (such as age) so you can do some calculations with it. Whenever I
try it says type mismatch:
dim Number as integer
number = editfield1.text
I wrote back:
This is something that's very confusing to new users. It's hard to get your mind wrapped
around the concept of fixed data types. Humans, when we think of math, think of
numbers. Text, even when it has numbers in it, is text. So why can't computers work the
same way? Unfortunately, they don't. Most programming languages are pretty strict that
numbers are not text and vice versa.
But most programming languages include routines to translate between data types. That's
what you have to do for the above. The command for changing a number into text is
str() and text into a number is val(). So the following is valid:
dim number as integer
dim text as string
number = val(editField1.text)
text = str(number)
Does that make sense? Also look at last week's letter where I explain the Format
command, which is similar to str(), but more powerful.
.
REALbasic University: Column 015
Programming for the Rest of Us
If you've followed our first REALbasic tutorial project these past few weeks, you've seen
not only how easy Mac programming can be, but also a glimpse of the benefits of that
power.
But now that you're a programmer, what are you going to do with REALbasic? Not
everyone is interested in writing shareware, or even being very professional in regards to
their programming. Some just want to create a game, or maybe a simple tool to help them
in their day job.
It might come as a surprise, but eighty percent of software in the world is not
commercial. That's right: the shrinkwrap boxes and CDs you buy account for only a
fraction of the world's software. Most software is created for in-house use: custom
applications for unique circumstances in a single company. (Remember the fuss over
Y2K? The real worry was not with commercial software, but critical in-house
applications with date problems.)
Most likely you're in a similar situation. If you do any kind of repetitive task, you might
see if REALbasic can help simplify your life. I've created dozens of "in-house" type
programs myself. These are ugly, unfinished, and badly programmed, but they work. And
they've saved me gobs of time. To inspire you, I'm going to go through a few of these, so
you can see how to make REALbasic a part of your life.
Example One: Pantone Converter
My full-time job is as a graphic designer at a printshop. In design, we use a color
matching system created by a company named Pantone. Pantone colors are an
international standard: if you specify Pantone 485 in New York or Nairobi, you'll get the
same bright red.
However, a great deal of printing doesn't use spot colors (custom ink mixes), but process
inks (cyan, magenta, yellow, and black, or CMYK) to print full color. By combining
various percentages of the four process inks, you can simulate any color. The accuracy of
this simulation varies, however, especially when you attempt to match a Pantone spot
color.
Pantone has figured out the best process percentages for its custom colors, and it prints
these calculations in a color guide. Unfortunately, the guide I have at work doesn't
specify the percent as a number, but uses a letter code where each letter matches an
arbitrary, pre-defined percentage. For instance, o = 0%, i = 34%, and z = 100%. Since
there are four process colors, a four-letter code makes up the values of the four-color
combination (i.e. "ozit" = C34, M100, Y0, and K76).
Yeah, translating "ozit" is a lot of fun. There's a table at the front of the book for
converting these letters back to numbers. But Pantone, in their infinite wisdom, doesn't
use the standard CMYK order that the rest of the world does: their table follows the
YMCK order. Since all my Mac software wants the numbers in CMYK order, I'm
dreadfully confused trying to figure out what percentages Pantone has in their guide. Just
getting the numbers for one color takes several minutes.
Anyway, a number of years ago, while I was struggling with this idiotic system, I
suddenly thought of REALbasic. (This was in the days of REALbasic 1.) I fired it up and
in less than twenty minutes, I had a program that accepted a four-letter sequence and
translated that into the correct percentages for each color. It displayed these colors in the
CMYK order I was familiar with, and I no longer had to mess with that Pantone
conversion chart. Very cool!
(For those smarties out there who are thinking, "Hey, Photoshop converts Pantone colors
to process automatically!" you are correct. However, back when I wrote this program,
different programs used different values for the conversion, which caused color-matching
problems. Today they all seem to match Pantone's guidebook, so I rarely bother with my
program, but it still works fine.)
Example Two: Z/Web
For years I've tried to maintain a personal website, but never had much luck. The main
thing I wanted was for it to be updated regularly -- there's nothing more boring than a
static site. But what kind of content could I create daily? I finally hit on an idea and in
August 1999, Z/Web was born.
It's essentially what today is known as a weblog: a reverse chronological list of short,
frequent postings. The subject matter I selected was mini-reviews of books and movies.
(Since then I've added soccer games.) This gave me something new to post almost every
day, which was what I wanted, and even if no one else finds my online journal
interesting, it's a nice record for me. (I hate not being able to remember that movie I
watched last week, so now I can look it up on the web. ;-)
Initially Z/Web had only a few postings a week and it was easy to update by hand. (For
the sake of speed -- I hate waiting ten minutes for Golive to launch -- I hand-coded in
BBEdit.) But gradually, as I developed an archive of old material, keeping track of it all
was awkward for me as well as for readers. What the site needed was a way for someone
to browse the archives easily: alphabetical listings, listings by month, a complete list of
all posts, etc.
Knowing nothing about weblogs and software that's already out there, I naturally turned
to my best friend, REALbasic. I have no idea how much time it took -- I worked on it
here and there -- but eventually I created a custom program in REALbasic which acts as a
database for all my Z/Web postings. It allows me to add and edit postings, and with the
push of a single button, it converts everything to HTML for easy uploading to my site.
[Click for full view]
What's really cool is that the program automatically generates all the archive pages and
maintains links between everything. It's really quite sophisticated, if I'm permitted to say
so.
First, it creates the main page -- index.html -- which includes all of the current month's
postings in reverse chronological order. If the current month has less than 20 posts, it
includes enough of the previous month's post to make up the difference so that the home
page doesn't look barren with only a few entries at the beginning of a month.
Actually, the main page isn't completely created by Z/Web: it uses a pre-created template
page and merges the new listing into that. That way I can easily change the header and
footer of the page (by modifying the template) without having to recompile my program.
Z/Web also creates an archive page -- news.html -- which includes links to all the
archived posts. The archives themselves are generated in several formats. There are the
alphabetical listings, one HTML page for each letter of the alphabet. These include a
table of contents at the top of each page, with each item listed by title, so you can quickly
jump to the item you want.
Also included in the archive are monthly pages -- a full month's postings in one file -- and
a complete list of all posts. These latter items are not the full posting, but simply the title,
hot-linked to the full posting stored in the alphabetical file.
All this sounds complicated, but the net effect from the user's point of view is that
clicking the "publish" button generates all those pages in a few seconds. Since most of
the pages don't change, I only have to upload the new monthly listing, index.html,
news.html, completelist.html, and whichever alphabetical files have changed. (I'm
thinking of eventually making Z/Web upload the changed pages to my site automatically,
instead of me using a separate FTP program.)
So how does all this work? The program has a data structure holding all of the entries in
the database. I created multiple sorting routines to sort by title, date, etc. I have separate
routines for each type of page that needs to be created. The "Publish" function loops
through these, building each page in a string variable. The postings are sorted as
appropriate for that page type, and then added one-by-one to the page. Since the postings
themselves always look the same, I have a routine that accepts a posting as a parameter
and returns the posting formatted as HTML. Finally, the HTML pages are saved on disk.
Example Three: DVD DB
I'm a movie buff, and I've got a fair collection of DVDs that friends are always wanting
to borrow. To make it easier for me and them, I put a list of my DVDs on my website.
(Warning: it's a large table that takes a bit of time to load.)
While this list looks complicated to update, it's simple for me: again, I wrote a program in
REALbasic to help me maintain the database and export the HTML. The most difficult
part is finding the info to add to the database.
From that single database, DVD DB is able to export a variety of lists in HTML format.
What's unusual about DVD DB is that I used XML as the format for the database. That
meant I had to create my own XML interpreter: an interesting challenge. Mine isn't a fullfledged XML interpreter, just a basic one, but it should be adaptable for use in other
programs.
From the XML data file, the program exports multiple HTML files with the movies
sorted by various categories.
Example Four: RBU
Ever wonder I how create the HTML for this column? The answer, once again, is
REALbasic. I wrote two programs, RBU Glossary and RBU Converter.
RBU Glossary maintains the RBU Glossary page: each term is an item in a simple text
file database. RBU Glossary creates the HTML file, automatically formatting each
definition as appropriate, and adding the hot-linked Table of Contents at the top of the
page. Simple and slick, and only took a few hours to write (perhaps a few more for
debugging).
A more complicated program is RBU Converter. I write the basic text of my columns in
Z-Write or BBEedit, manually adding some basic HTML tags like bold and italic. I also
add some custom tags, like <head> and <TT>. These are converted by RBU Converter
into complex FONT tags, complete with size and color attributes.
The actual program is rather boring. Here's what it does to text, however. The first image
is the before, the second the way after completely converted to HTML.
BTW, the manual coding of my HTML isn't completely manual: I created a One Click
palette with buttons for the most common HTML commands I use.
Extremely ugly, but it works. I'll soon have to recreate these buttons as a REALbasic
program because OneClick doesn't work under Mac OS X (as a native app, that is).
What's unusual about RBU Converter is I've learned from my experience with the
previous programs. I made RBU Converter flexible: it not only uses a template for the
basic structure, but the HTML start and end tags are separate text files. This means I can
change the look of RBU without recompiling RBU Converter!
Conclusion
I didn't detail the above programming examples to trumpet myself but simply to show
you how useful a tool like REALbasic can be.
In my experience releasing shareware, I discovered that I spend almost as much time
polishing and finalizing a program for release (writing documentation, web pages, press
releases, etc.) as I do originally writing it. What's cool about in-house programs is that
because they're just for yourself, they can be buggy, unfinished, and ugly. That saves
gobs of time -- the above little works have paid for themselves in time savings many
times over.
RBU Survey: What kinds of programs do you use REALbasic for? What kinds do you
want to create?
Next week: we'll create two programs, Fast Finger and Count Emails, that demonstrate
this kind of off-the-cuff programming.
Letters
This week we hear from Philip Mobley, who wants to create a help system similar to the
one included with REALbasic. He writes:
I am sure that there is a really simple way to do this, but I have not figured it out yet, nor
have I seen any tutorials on how to do this.
I want to create a reference window that behaves pretty much the same as the "RB
Library Reference." Discloser arrows, multiple "pages" of data, and keyword links to
other pages... basically the works. =)
Well, you're wrong about this being simple! It's quite complicated, though doable
(somewhat).
What you are wanting is something I've done, somewhat, with the help system of my ZWrite application. It requires a number of steps.
1. First you must decide in what format the data (the text) is going to be stored. For
instance, with Z-Write, the help file is an ordinary HTML file stored the application's
resource fork. That means Z-Write needs to know how to interpret HTML, which might
be overkill for your purposes, but for me it was best as it meant I don't have to recreate
my help file in multiple formats.
The text file must contain some sort of system for indicating to you what the sections are
(if you want folders in the section list), the actual pages of text, and what items are
keyword links to other pages (and the section they link to).
You could, for instance, just make up your own special file format, or you could use a
simple text file with special codes to "mark up" the file, just like HTML. For example,
you could put *F foldername* to indicate a folder name, and *P pagename* to indicate a
page of data. *L linkname* could indicate a keyword link to another page. It would be up
to your program, when it loads the data, to interpret your particular format and load the
data appropriately.
2. The second part of your problem is the actual interface. Here you need to create a
window in REALbasic and add onto it the controls you'll need. For instance, you'll need a
hierarchal listbox for the list of folders and pages, and a large editfield where you'll
display the text. You could also add other items, such a search function, and/or backforward arrows.
3. Finally, the third thing you must do is write all the code to support this system. You
have to write an initial routine which parses (translates) the text file into your help
system's data structure. Then you must load your list of pages into your listbox. Then you
must support actually displaying the appropriate page of data (loading it into your
editfield) when the user clicks on a name in the listbox. Finally, you must have a system
for handling it when the user clicks on a hotlinked keyword on a page.
Like I said, it's not easy: you're talking about a sophisticated, complex interface. The
good news it that if you encapsulate your help module correctly, you could reuse the
same system in multiple programs. That's what I've done with my help system: I use the
same code module for all my programs. (In fact, I'm in the process of rewriting this to
make it more portable, and I might include this as a future RBU project.)
REALbasic University: Column 016
Two Sample In-House Programs
Last week I described several REALbasic programs I created for personal use. Some of
those are fairly complicated -- too complex for me to explain how they work in a few
paragraphs (I don't want to dwell excessively on these minor programs, just give you a
taste of RB's capabilities).
I do have a couple very simple programs that I thought illustrate the power and
convenience of REALbasic. We'll go over these in detail in this week's lesson.
Fast Finger
A year or so ago I discovered ABC's Who Wants to Be A Millionaire and watched it
regularly for a while. (I finally gave it up as I just don't have time, but I still like the
show.) I even attempted to qualify as a contestant. Unfortunately, while my trivia skills
are above average, my speed skills are not. Give me ten seconds to put four ordinary
household appliances in alphabetical order and you'll get a blank expression.
I liked playing along with the show at home, but the "Fastest Finger" contest always
annoyed me: it goes so fast I have trouble measuring my own time, and by the time I've
figured out the third or fourth part of my answer I've forgotten what I picked for the first
part!
Of course it's unfair: the contestants have buttons they can push to pick to set the order of
their answers while I have to remember everything while keeping an eye on my
stopwatch.
So one night -- during the show -- I whipped open REALbasic and created Fast Finger.
This is a dirt simple program designed to do just one thing: record how fast you can push
four buttons. That's it. If you run it while you're watching the show, you can test yourself
against the other contestants during the "Fastest Finger" portion of the show.
It's not fancy and it's nearly useless, but it gave me something to do during the
commercials. It also demonstrates a few nice things about REALbasic. And if you're
lucky, it might even help you get on the game show!
Since the user interface of Fast Finger is a little complicated, I'm going to give it to you
all finished. That way all you have to do is put in a few lines of code (which I'll explain),
and you'll be all set to test your finger speed.
First download the following REALbasic project (you'll need Stuffit Expander to
decompress it):
fastfinger.hqx
Open the project in REALbasic (it's in RB 2.1 format, so it will work with REALbasic
2.1 or 3.x). This is what Window1 looks like in the IDE.
The project does nothing at this point: it's just an interface, with no code. First, let's add
an important property to the main window, Window1. We're adding it to Window1 because
we want all routines within Window1 to be able to access this property -- to be able to
look at it or change it.
Go to the Edit menu and choose "New Property" and type in "startTime as double" (no
quotes) in the dialog box that is displayed. Click Okay or press return.
The first code we'll add is to the Timer1 object. A timer is a special REALbasic object
that executes after a certain amount of time has elapsed. You set the amount of time, in
milliseconds, with the timer's period property. In this case I've preset Timer1 to 10
milliseconds: that means one hundred times per second, whatever code is in Timer1's
Action handler will be executed.
Select Timer1 on Window1 (open the window if it is closed) and press Option-Tab (hold
the Option key down while pressing the Tab key). That will open the Code Editor right to
Timer1's Action event.
Now put in the following (copy and paste it, if you want):
Source code for Window1.Timer1.Action:
dim theTime, secs, huns as double
theTime = microseconds
secs = (theTime - startTime) \ 1000000
huns = (((theTime - startTime) - (secs*1000000)) \ 10000)
Label.text = "00:" + format(secs, "00") + ":" + format(huns, "00")
What the above does is display a clock of the current time. We use REALbasic's
microseconds function to find out the current time to 1000th of a second. Then we break
that information into two pieces, the number of seconds since the countdown was started
(startTime) and the number of hundredths of a second since the countdown was started.
Finally, we display the time in a staticText object.
Now let's take a look at our pushButtons. You may have noticed that while we have four
visible buttons on Window1, there is only one Button in the Controls list of the Code
Editor:
Shouldn't there be four buttons listed?
No, there shouldn't. That's because I've used a special REALbasic feature: a control array.
Remember when we learned about arrays while builing GenderChanger? Well, a control
array is an array that contains controls. In this case, pushButtons! Just like with a regular
array, you access individual objects within the array using an index number. (Notice that
in the Control list Button() has parenthesis at the end: that's a reminder that it's a control
array.)
(Note that the Button() control array's index starts at zero, so the buttons are number 03, not 1-4.)
The benefit of a control array is enormous: instead of having four similar yet separate
buttons, each with identical code, I can have one button with a single set of code! I can
always tell which button the user has clicked by checking the Index variable that
REALbasic sends with all events.
In Detail
How do you create a control array? It's simple. There are a couple ways. With a control
selected in the IDE, go to the Properties palette and put a number in the Index field. The
Index field is normally blank, but if you put a number there, REALbasic assumes you're
wanting to create a control array.
After that if you Duplicate the control, REALbasic will automatically increment the
index number for the new control.
Another way to create a control array is to change the names of several identical controls
to match. REALbasic will ask if you're wanting to creating a control array. Say yes, and
RB will number the controls for you.
In this case, we're just going to pass the button pushed to a method (which we'll write in a
second) which will handle the push. All we have to put into the button's Action handler is
this (don't include the bold title, of course):
Source code for Window1.Button.Action:
doButton(index)
Next, let's go to Window1's Open method. All we're going to do here is make sure that
all four of our pushButtons are disabled when the program is first launched. Since our
buttons are a control array, this is simple.
Source code for Window1.Open:
dim i as integer
for i = 0 to 3
button(i).enabled = false
next
Now go to Window1's Keydown event. This method gets called whenever the user
presses a key. What we want to do here is mimic buttons being pushed with key presses.
In other words, the user can click button "A", type "A", or type "1" and all those actions
have the same result.
To do this we simply use a select case statement (see RB's help for more details) and
if the user's typed an appropriate letter or number, we send a command to our "doButton"
routine.
Source code for Window1.KeyDown:
select case uppercase(key)
case chr(13) // return key
if startButton.enabled then
startEverything
end if
case "1"
doButton(0)
case "2"
doButton(1)
case "3"
doButton(2)
case "4"
doButton(3)
case "A"
doButton(0)
case "B"
doButton(1)
case "C"
doButton(2)
case "D"
doButton(3)
end select
So what does this magical "doButton" routine do? Well, let's find out! Create a new
Method by going to the Edit menu and choosing "New Method". For the method name,
use "doButton" (no quotes). For the method's parameters, put "index as integer" (that will
allow this routine to receive the button number). Leave the return value empty, and click
Okay.
In the method's code area, type (or paste in) the following:
Source code for Window1.doButton:
if button(index).enabled then
button(index).value = true // make it look pushed
button(index).enabled = false // disable it
// Displays the buttons in the order you pressed them
answers.text = answers.text + button(index).caption + chr(13)
// Check to see if all four buttons have been pressed;
// if so, we stop the clock and enable the Start button.
if not (button(0).enabled or button(1).enabled or
button(2).enabled or button(3).enabled) then
timer1.mode = 0
startButton.enabled = true
end if
end if
Now go to startButton's Action method and type in "startEverything" (no quotes). This is
going to be a separate routine that initializes the program (resets the clock, turns on all
the buttons, etc.).
Create a new Method called "startEverything" with no parameters or return values. Here's
the startEverything code:
Source code for Window1.startEverything:
dim i as integer
startButton.enabled = false
for i = 0 to 3
button(i).enabled = true
button(i).value = false
next
answers.text = ""
startTime = microseconds
timer1.mode = 2
As you can see, it's simple.
Guess what? We're done! That's the whole program. Run it and you should be able to
start the timer, click the buttons, and see what order you clicked them and how long it
took, just like on TV. Here's what the program looks like in action:
If you don't feel like typing in all that code, here's the complete program, ready to
compile and run.
fastfingerfull.hqx
Count Emails
The second program I wanted to demonstrate is one I wrote one day while studying my
Stone Table Software customer list. I was looking at all the emails -- many from foreign
countries -- and I started wondering what percentage of my customers come from which
countries. Email addresses and domains aren't a surefire way to determine a person's
origin, but they can tell you a little.
So I sat down and wrote this little program which accepts a text file of emails (one email
per line) and sorts and counts them by extension (.com, .net, etc.).
To start, create a new project in REALbasic. Using the default Window1 that's created for
you, drag a listbox onto it. Make the listbox big so it fills all of Window1.
For ListBox1's properties, check the HasHeading option and put "3" for the number of
columns. For columnWidths, put "50%, 25%, 25%". (That will divide ListBox1 into
three columns, the first at a width of 50% and the others at 25%.) Your properties
window should look like this:
Option-Tab with ListBox1 selected to open the Code Editor. Go to ListBox1's Open
event. Here we're going to put in the following:
Source code for Window1.ListBox1.Open:
me.acceptFileDrop("text")
me.heading(0) = "Domain"
me.heading(1) = "Quantity"
me.heading(2) = "Percentage"
The above does two things: one, it sets ListBox1 to allow text files to be dropped on it,
and two, it names the listbox's headers.
While we're thinking about it, let's make sure our project has a "text" File Type created.
Go to the Edit menu and choose "File Types". If you see an item there named "text",
you're finished. If not, you'll need to add one. (Without the File Type being defined,
REALbasic wouldn't know what kinds of files to allow the user to drop on the listbox.)
Click the "Add" button, name it "text" and use "ttxt" for the Creator and "text" for the
Type. Click Okay twice and you're finished.
Now go to the DropObject event. This is a major part of the program. What happens is
the user drops a text file on the control, so first we check to make sure they dropped a file
and then we open it. We load up theList array with each line from the text file, then
close it. We call a few other routines -- stripList, sortIt, countDuplicates, and updateList - and then we're done.
Source code for Window1.ListBox1.DropObject:
dim in as textInputStream
if obj.folderItemAvailable then
if obj.folderItem <> nil then
in = obj.folderItem.openAsTextFile
if in <> nil then
// Erase the arrays
redim theList(0)
redim itemCount(0)
do
theList.append lowercase(in.readLine)
loop until in.EOF
in.close
stripList
countDuplicates
updateList
me.headingIndex = 1 // sorts by quantity
end if
end if
end if
We're going to need to create all those methods now, so let's get started. None of the
methods have any parameters or return any values, so they're simple. I find it easier to
create them all first, then go back and fill in the details later. So go to the Edit menu and
choose "New Method" a bunch of times and use the following names for the methods:
stripList countDuplicates updateList
When you've got all three methods ready, let's put in the code, starting with stripList.
It's a routine that deletes everything but the email extension from the email addresses.
The algorithm used is a simple one: it simply looks for the last phrase deliminated by a
period. Since all email address end in a dot-something, the text to the right of the dot is
saved and everything else is thrown away (stripped). This way our list of emails is paired
down to simple "com", "net", and other extensions.
If other words, this:
[email protected] [email protected] [email protected] [email protected]
becomes
uk com ch com
Source code for Window1.stripList:
dim n, i as integer
n = uBound(theList)
for i = 1 to n
theList(i) = nthField(theList(i), ".", countFields(theList(i),
"."))
next
In Detail
How does the above work? Well, the key line is the code in the middle of the for-next
loop. To understand it better, let's mentally run it the way the computer would, using a
sample address.
Let's say we have an address line "[email protected]" (theList(i) is equal to
"[email protected]earthlink.net"). That means the key line in the above is really saying this:
theList(i) = nthField("[email protected]", ".",
countFields("[email protected]", "."))
Process the end of it first. The code countFields("[email protected]", "."))
will return a number, the number of fields in the phrase delimited by a period. Since there
is only one period in the phrase, there are therefore two fields ("[email protected]" and
"net", everything to the left and right of the period). So replace the countFields code
with the number 2 (the result).
theList(i) = nthField("[email protected]", ".", 2)
So now our code is effectively saying, "Put field number 2 (the last) of
'[email protected]' into theList(i)." Since the second field is "net" we have
effectively changed "[email protected]" to "net"!
CountDuplicates is a little more complicated. It loops through the entire list and counts
how many of each kind exist. If it finds a duplicate, it does two things: it deletes the
duplicate from theList and increments the number stored in that item's count. (The
item's count is stored in the itemCount array.)
Note how we don't use a for loop for the outermost loop; I used a while loop instead. I
did this because the size of the loop changes: when we find a duplicate item we delete it,
reducing the number of items in the list. Using a while loop means that the computer will
keep repeating through the list until it reaches the last unique item.
Source code for Window1.countDuplicates:
dim total, n, i, j, found as integer
i = 1
while i < uBound(theList)
redim itemCount(uBound(theList))
found = 0
n = uBound(theList)
for j = 1 to n
// Check through
if theList(i) = theList(j) and i <> j then
found = j
end if
next // j
if found > 0 then
itemCount(i) = itemCount(i) + 1
theList.remove found
else
i = i + 1
end if
wend
// Figure out percentages
n = uBound(itemCount)
total = 0
for i = 1 to n
total = total + itemCount(i) + 1
next // i
redim percentList(n)
for i = 1 to n
percentList(i) = ((itemCount(i) + 1) / total)
next // i
The final part of CountDuplicates is where we calculate the percentage of each kind of
email. To do that we first need a total: not the number of elements in the list, but the total
number of emails. So we count through the itemCount array, adding up each item. Since
an itemCount element contains a zero if there's only one of that item, we add a one to it
to get the actual count.
We finish by initializing the percentList array to the size of our list, then set each
element in the array to the appropriate percentage. Note that we again add one to the
itemCount value.
Our final method for the program is the updateList method. This is used to display the
information in listBox1. It first deletes any existing rows in listBox1 (if you didn't do
that, dropping a second file on the existing listbox would display the wrong content), then
counts through the list of items.
For each item in the list, it puts the content in the appropriate column of the listbox. The
first item is in the addRow command: that's the ".com" or whatever was part of the email
address. Once we've added a row, we don't want to add another, we want to work with
the row we just added. So we use the lastIndex property which contains the index
number of the last row we added. We use the cell method to put the actual item count
(again, we add one to it) and the percentage into the correct columns.
Source code for Window1.updateList:
dim n, i as integer
listBox1.deleteAllRows
n = uBound(theList)
for i = 1 to n
listBox1.addRow theList(i)
listBox1.cell(listBox1.lastIndex, 1) = format(itemCount(i) + 1,
"000")
listBox1.cell(listBox1.lastIndex, 2) = format(percentList(i),
"00.#%")
next
Whew! Our program's almost done: we just need to add our arrays as properties to
Window1. So with the Window1 code editor window visible, go to the Edit menu and
choose "New Property". You'll do this three times, adding the following properties:
itemCount(0) as integer percentList(0) as double theList(0) as string
Save your program and run it. It should display a simple window with a listbox in it.
Oh no! You need some email addresses to test this, don't you. Okay, here's a list of
hopefully fictitious addresses I made up. (I tried to use a variety of extensions.) Save
them to a text file and you'll be able to drag the text file to the listbox. (Option-click on
this link to save the text file to your hard drive.)
[email protected] [email protected] [email protected] [email protected]
[email protected] [email protected] [email protected] [email protected]
[email protected] [email protected] [email protected] [email protected]
[email protected] [email protected] [email protected] [email protected]
[email protected] [email protected] [email protected] [email protected]
[email protected] [email protected] [email protected] [email protected]
After you drag the file to the listbox, your window should look similar to this:
Once again, for those who don't feel like typing in all that code, here's the complete
program, ready to compile and run.
countemails.hqx
Good. That's it for this week. I hope these little programs were helpful and you learned
something. For me they represent the real power of REALbasic: the ability to quickly
create a solution to a little problem without spending any money.
Next Week: We'll try a little animation by using sprites to create a simple twodimensional arcade game.
Letters
For those of you who responded to last week's survey on things you'd like to do with
REALbasic, thanks! I received many responses and I'll be publishing a summary in a
future column. If you haven't sent in your response, feel free to send it in now.
Our first question this week comes from Tim & Sarah Reichelt, who write:
Hi,
I'm sure there is an obvious answer to this, but how can I change the text color in a
listBox? I can't even change the color for the whole list and I want to be able to specify
different colors for different lines.
Interesting question! Odd that I've never ran into this, but there doesn't seem to be a way
to do what you want. REALbasic provides no method for setting the color of either
individual lines or the entire contents of a listbox. If this is something you really need,
bug REAL Software and get them to add the feature.
In the meantime, there are a couple potential workarounds, but like all workarounds, they
have limitations and disadvantages. First, you could check on the web to see if someone
has created a replacement listbox which lets you change the text color. There are plug-ins
to REALbasic which allow to create more sophisticated controls, and there are RB-made
classes which mimic standard controls. (A good place to start is the RB web-ring linked
at the top of this page.)
Another approach, if your listbox needs are modest, is to create your own listbox class.
It's a bit more complicated than I can detail here, but if you don't need listbox features
like multiple columns, editable text, folders, or pictures, it wouldn't be too hard to roll
your own using a canvas and a scrollbar control. Remember, a canvas is simply a
graphics area: you can draw lines of text in it, each in a different color if you want, and
use a scrollbar control to scroll the visible text. (If that sounds like it would work for you
and you don't find an existing solution on 'net, let me know: I may be interested in
creating this as an example project for RBU.)
I also received a letter from Sir Steven, who includes a slew of questions in a single
paragraph:
How exactly do you make global variables. You mentioned once that you could use a
method? Do you need to fill in all the edfields? How do you change its value? Are there
any other ways to use or make global variables. p.s. Those val() and str() prefixes solved
so many problems. I hadn't been able to find anywhere how do do that. -Learn to use the return key, Sir Steven! Just teasing. The answers to your questions, as
best as I can, are as follows.
How to create a global variable? Put the variable as a property inside a module. Modules
are inherently global and any property put in a module is available to all parts of your
program.
Use a method? A method is REALbasic's equivalent of a procedure/function/subroutine
in other languages. (By the way, if you put a method in a module, it too is global.)
Do you need to fill in all the edfields? I'm not sure what you mean by this; are you
referring to the fields on the Properties palette? If so, the answer is no: REALbasic will
use default (standard) values if you don't override them, so you only need to change the
ones you find necessary.
How do you change its value? Once you've created a global variable, you can change its
value simply by assigning it a new value (as in "gVar = 10"). Since it's a global, you can
do this anywhere in your program.
Are there any other ways to make global variables? No, that's pretty much it. If you
define a property that's part of an object (such as a window), the variable is local only to
that window. Of course you can still access it from another object by naming it directly,
so it's not inaccessible, just awkward. For instance, if Window1 contains a property called
myVar, then a routine in Window2 can access it by saying Window1.myVar.
Keep in mind that there are degrees of globalness: a property local to Window1 is
essentially global for Window1 (all routines inside Window1 can access the variable). That
might be all you need. A variable created inside a particular method, however, is only
available within that method: other routines cannot access it. Therefore a true global
variable must be created inside a module so that all routines can access it.
I hope that helps; if I misunderstood one of your questions, let me know and I'll try to
remedy it next time.
REALbasic University: Column 017
REALbasic Animation: Introduction to
Sprites, Part I
When I first started using computers back in the mid 1980's, programming graphics was
quite complicated. Even simple animation -- a static spaceship moving across the screen - was difficult. You had draw the graphic in one position, erase it, and redraw it another
place. If there was a background graphic behind the spaceship, that presented a whole
new problem, since erasing the spaceship also erased the background.
Another issue was timing: if you wanted several objects moving at the same time you
needed some sort of master drummer to keep everything beating in sync. I found out the
hard way that that heartbeat should be tied to something independent of the computer's
speed. An animated game I did worked fine until I put in a clock accelerator on my
computer that doubled the speed: I was surprised to discover my animation running twice
as fast as before and the game unplayable!
Even more ugly was the process of attempting to detect if two animated objects had
collided with each other. Collision detection may not sound difficult until you try to
program it, but there have been entire books devoted to the problem! It's not too bad
when your objects are square or rectangular in shape, but it gets really ugly when you've
got odd-shaped graphics, such as being able to tell when the spaceship crashes into the
radio antenna sticking up off the roof of the skyscraper of the cityline. (Unless you're a
math masochist, you don't even want to think about three-dimensional collection
detection.)
Of course professional game developers have software to make animation easier.
Fortunately for us today, REALbasic gives us some similar tools. First, we've got a
special kind of object called a sprite. What's a sprite? A sprite in an animated picture. It's
usually an icon, like that earlier spaceship. What's cool about sprites is that REALbasic
handles all the technical stuff of actually drawing, erasing, and moving the sprite: you
only have to worry about your program's logic, i.e. what the sprite is supposed to do.
For example, sprites have x and y properties that control where on the screen the sprite is
located. To move the sprite, just change the x and y properties!
Even better, REALbasic handles the timing problem as well. Sprites are drawn on
another special RB object called a spritesurface. A spritesurface is like a drawing
area for sprites. The spritesurface object has an event called NextFrame which gets
called whenever it is time to draw the next animation frame. That means if you update all
your sprites within the NextFrame event, all your objects will move in sync.
Take a game like the classic Space Invaders. Remember that? It had a horde of evil
spaceships in a grid formation trotting back and forth across the screen and slowly
descending toward your spaceship. To implement that in REALbasic, you'd create an
array of sprites (the evil ships), initialize their location (set where they appear when the
game is launched), and in each NextFrame event, you'd move the ships appropriately.
You could even get sophisticated and program different behavior for different kinds of
ships: your sprite array could contain additional custom properties such as the ship type
and your move code could move different ships in different ways.
Finally, REALbasic handles the collision problem as well, and it a very cool manner. For
each sprite you create, you can assign it a group code. The group code is an integer (a
number). If the integer is a positive value, the object will collide with any other positive
group object. If the object is set to group zero, it will not collide with anything. If the
object has a negative group value, it will only collide with negative group objects.
Your spritesurface object has a collision event: if any two objects collide, the
collision event is triggered. The two sprites that crashed into each other are passed to
the collision event as parameters, so you can see which two objects collided and decide
what action your program should take. For instance, in Shooting Gallery, which we'll get
to next week, I have a missile (bullet) sprite which shoots toward the target. If it hits the
target -- there's a collision -- the target explodes.
Shooting Gallery is a demonstration I wrote a few years ago when I wanted to learn about
sprites. It's simple, and the graphics are hilariously horrible, but it shows how easy it is to
create a shoot-em-up game in RB.
I'd intended to start on Shooting Gallery this week, but I got bogged down in finishing the
program. It seems REAL Software changed the sprite system considerably for
REALbasic 3.x so my old code didn't quite works as well as it did before. I decided that
an RB 3.x version of Shooting Gallery is important, since that's probably what most of
you are using. Unfortunately, RB 3.x sprite code is not completely backward compatible
so those of you using RB 2.x will need to either upgrade (use the RB 3.x demo) or I can
provide you with my original RB 2.x code if you're interested.
Walker
Since Shooting Gallery isn't quite ready for prime time, I thought a good introduction to
sprites would be a simple program called Walker. This just makes a stick figure walk
across the screen. This is really basic -- I have made no attempt to create realistic
animation and my stick figure is poor even by stick figure standards -- but you'll get the
idea.
Walker in action:
So first, create a new REALbasic project called Walker. Then download these three
graphics and save them as walker1.gif, walker2.gif, and white.gif (that's the empty white
box). (In most web browsers, you should be able to just drag them to your hard drive or
control-click on them and choose "Download image" from the menu that pops up.)
Drag walker1.gif, walker2.gif, and white.gif into your REALbasic project window.
They'll show up as picture objects with the names in italics (meaning that REALbasic is
linking to the originals: do not throw the originals away or you won't be able to run
Walker).
Now open up Window1. Drag a spriteSurface (
) onto it: size it to fill most of the
window, but leave a half inch of space at the bottom. Into that half-inch of space, drag a
pushButton. Set its caption to "Start".
For SpriteSurface1, there are only two critical properties you need to set. One is called
FrameSpeed. The default is zero, which means sprites animate as fast as your Mac will
move them, which is usually too fast. Set it to a more reasonable 2 or 3 (or even 5). You
can always change it later if you want the animation rate to be faster.
The other important property is the backDrop property, which you'll want to set to white
(the graphic you imported) from the popup menu. The white graphic is just that -- a white
graphic. REALbasic will automatically tile it, effectively giving us a white background
for our animation (otherwise our stick figures are on the default black background).
Walker just has two properties we'll define: open the Window1 code editor (double-click
on Window1) and choose "New Property" from the Edit menu and type in "dir as integer"
in the dialog. Do that again and put in "walkerSprite as sprite".
Now open pushButton1 and in the Action event put the following code:
walkerSprite = new sprite
walkerSprite = spriteSurface1.newSprite(walker1, 0,
spriteSurface1.height - walker1.height)
dir = 1
spriteSurface1.run
All we are doing here is initializing a new sprite and our spriteSurface object. We are
using the spriteSurface's newSprite method to assign a sprite into walkerSprite. The
first parameter is the picture the sprite will use (walker1). The second and third
parameters are the x and y coordinates (horizontal and vertical) of the sprite's initial
location. I've set it to zero horizontally (far left) and toward the bottom of the
spriteSurface (the height of the spriteSurface minus the height of the sprite). That way
the sprite will be walking along the bottom of the window.
The final line is the run command, telling the spriteSurface to activate. In earlier
versions of REALbasic, spriteSurfaces took over the entire screen and prevented you
from working with menus or other controls. With RB 3.x, we can constrain the
spriteSurface to a set area within which our animation will take place: much better. But
when the spriteSurface runs, it still sort of takes over the whole computer. The default
action of clicking the mouse button stops the spriteSurface. (There's a new RB 3.x
method of animating a spriteSurface that doesn't hog the CPU; we'll look at that later.)
All we need to do next is add some code the SpriteSurface1's NextFrame event and we're
done!
Code for SpriteSurface1.NextFrame:
if walkerSprite.image = walker1 then
walkerSprite.image = walker2
else
walkerSprite.image = walker1
end if
if walkerSprite.x >= me.width - walker1.width then
dir = 0
elseif walkerSprite.x <=0 then
dir = 1
end if
select case dir
case 0
walkerSprite.x = walkerSprite.x - 1
case 1
walkerSprite.x = walkerSprite.x + 1
end select
Believe it or not, that's it! That's the whole program. What does the above code do?
The first part basically toggles the sprite between two images, walker1 and walker2.
That's what creates the walking effect, since the two images are in different states of
walking.
The next part checks to see if the sprite has walked off the screen (in either direction, left
or right) and if so, changes our direction toggle (dir).
The final part moves the sprite: if dir is 0, we go left (minus). If dir is 1, we go right
(plus). Simple!
When you run Walker, you'll be presented with a blank white window. Click the "Start"
button to start the animation. Click the mouse button to stop the animation. Notice that
each time you click the start button you start a new walker animating: the old ones stop
moving, of course (they are effectively "dead"), but look how the new ones walk right
over the old ones!
Here's what Walker looks like with two walkers, one walking over the other:
Walker is very simple, but it should give you the idea: by putting together a series of
graphics you can make your sprite not only move across the screen but the sprite itself
can be moving as well! Imagine what you can do with that: a character which changes
based on the direction it is moving, a flickering candle, a puppy dog waggy its tail, etc.
If you want the complete Walker project, source code and graphics, it's available here.
Next Week: Marc continues his sprite introduction and provides the source and
explanation for Shooting Gallery, a simple sprite-based game.
Letters
In the next week or so I'll be publishing the results of the survey on uses for REALbasic:
I've gotten a wide variety of responses, so it should be interesting to see what your fellow
programmers are aiming to do with RB. If you still haven't sent in your response, please
do so this week.
Our letters this week all have to do with GenderChanger, the first RBU project. Glen
Foster writes that he found creating GenderChanger helpful.
Hi Marc,
I just recently came across Realbasic University Archives. I had purchased Realbasic
during the winter months to help kill the snow time but found nothing much that was able
to help me understand what and how things were accomplished in the program. I have
followed your description for the GenderChanger and completed it without too much
understanding, just follow instructions, I then dumped it and did it again with a bit of
understanding the why and wherefore. I have completed it 5 times now and each time
another light comes on and a bit more is understood. I really like the screen shots as some
of the terminology is unknown to a fellow who is well up in his seventies and pictures do
say more than a thousand words. Keep up the tutorials they are excellent.
That is great, Glen! I never thought about doing the same project more than once, but it's
a great idea. It's not that much work, and of course each time it gets easier, and things you
miss the first time or two you may notice and understand on a subsequent run. If any of
you out there found GenderChanger a bit of a challenge, try Glen's suggestion and do the
tutorial again: I bet you'll understand it better now.
Next, Philip Mobley offers an intriguing answer to the colored text in listbox problem
from last week and shares his discovery that creating GenderChanger helped him more
than he originally thought!
A possible answer for one of your readers questions (about the colored listbox text) is
there is a RowPicture method for ListBox controls. If the text is static, then maybe she
could use a picture instead of actual text.
I was assuming that there was a control for text color in the ListBox, but I guess not.
About RBU...
I re-read your column for the GenderChanger, and used quite a number of elements from
there. When I first read your column, I thought, "This is almost too basic." And when I
read your review of RealBasic for Dummies, I thought, "I guess I'm not going to buy that
book..."
Well, now I am 85% finished with a Gift Certificate Generating program (randomly
generated numbers). Since I do not have the Professional version yet, I am not able to
work with databases (although that would be the best). Because I am forced to work with
arrays, your GenderChanger program came in very handy as a roadmap for my own
program.
I went back and studied your GenderChanger in detail to understand every step and the
reasons your were using that code.
I realize now, how in-depth your columns are, and how they deal with a number of
fundamentals of programming. Keep it up. I can't wait for your next column.
Also, I may just go buy that Dummies book! =)
One good thing about programming books, Philip, is that you can always pass them on to
another deserving reader. Glad you are finding RBU helpful, though. That's the general
idea. I can't solve everyone's problems in just a handful of columns, but I picture this
column being an amazing resource after a year or so.
Your idea for using pictures instead of text in the listbox is a brilliant one! In fact, you
could even create these pictures dynamically, using the newPicture command. All you
need is a picture variable and you can draw into it, just the way you would a canvas
control.
Here's a little demo I did and it worked great! (It didn't even seem slower than adding
text, though I didn't test a listbox with hundreds of items.)
dim p as picture
dim s as string
// Make a new picture 200 pixels wide, 20 tall, and 8-bits deep
p = newPicture(200, 12, 8)
// Make sure newPicture call worked
if p <> nil then
// random color
p.graphics.foreColor = rgb(rnd * 255, rnd * 255, rnd * 255)
p.graphics.textFont = "Geneva"
p.graphics.bold = true
p.graphics.textSize = 10
s = "This is line " + str(listBox1.listCount + 1)
p.graphics.DrawString s, 92, 10
listBox1.addRow ""
listBox1.rowPicture(listBox1.lastIndex) = p
end if
The only odd thing is that with the drawString command I had to start drawing 92 pixels
over instead of at zero or else the text was cut off. I'm not sure if this is an RB bug or a
issue with how RowPictures are supported, or maybe I'm missing something else. (Feel
free to let me know if you've got an explanation for this behavior. I hate not
understanding why something doesn't work!)
Here's what the program looks running, after I've added some rows and scrolled the
listbox:
If you want the complete demonstration project, click here.
Finally, Martin Jean writes with a question on enhancing GenderChanger.
Marc,
First, I would like to thank your for your columns. I am a new user of RB with some
experiences with Think Pascal and HyperCard. I really enjoy studying REALbasic
University each week. Please, continue your good work!
After completing column 13 and playing with GenderChanger, some ideas came to me.
One of them is to modifiy the program to be able to drag a document from the Finder to
the GenderChanger icon (when the program is running or not). My goal is to enhance the
tool so that I can place an alias of it on the Finder (or in Finderpop items Folder) and
simply drop files for rapid identity changes.
How big is this to do? I read in Mark's book [I think he means Matt's book -- Ed.] this it
will involve the OpenDocument event of the App sub class, but I am still not enough
confident to go and make modifications of that levels without help!
When this will work, how about dropping an entire folder (or disk) to process its content?
Thank you again!
You're on the right track, Martin. But to get this to work there are a few not-so-welldocumented settings you must implement. If even one of them isn't done, it won't work. I
find the process confusing myself. (Part of the problem is that you can't test
OpenDocument events within the REALbasic IDE: you must compile the program to disk
and run that.)
First, your application must have a creator code. The creator code for GenderChanger
is "GdCh" -- you set it (if you haven't already) with the Project Settings menu item on the
Edit menu.
Second, you must check the icon checkbox within each File Type definition (also on
the Edit menu).
You must check the Icon checkbox if you want to be able to drop files of this kind
onto your application.
While you're in the File Types dialog, go ahead and add the folder file type, so folders
can be dropped onto GenderChanger (it should be available on the popup menu under
"special/folder"):
(If you want to be able to drop whole disks onto GenderChanger, add the "special/disk"
file type and check the icon checkbox. I really wouldn't advise this, though: I just can't
imagine you'd ever want to change every file on a whole disk!)
Once you've done that, go to the OpenDocument event of the App class of
GenderChanger. Here we'll add some very simple code:
if item.directory then
handleFolder(item)
else
handleFile(item)
end if
Item is whatever was dropped onto the program. Unlike the dropObject event we had to
handle as part of GenderChanger earlier, the OpenDocument event doesn't bring in a
bunch of files at once. Instead, it gets called once for each object dropped. That makes it
much easier to program (we don't have to call obj.nextItem): we simply check to see if
the user dropped a folder or a file and pass the result to the appropriate method.
Speaking of methods, we'll have two: handleFolder and handleFile. Both will have a
parameter of "f as folderItem". Go ahead and add them to the App object.
Here's the code for handleFile. Pretty simple. It does the same thing that the
dropObject event did as part of listBox1 (it adds the folderitem's path and creator and
type codes to the listbox).
window1.listBox1.addRow f.absolutePath
window1.listBox1.cell(window1.listBox1.lastIndex, 1) = f.macCreator
window1.listBox1.cell(window1.listBox1.lastIndex, 2) = f.macType
HandleFolder's a little more complicated. Since we know it contains a folder, we count
how many items are inside the folder and loop through each one. For each item we test if
it is a folder or a file, and then pass it to the appropriate method.
dim i, n as integer
n = f.count
for i = 1 to n
if f.item(i).directory then
handleFolder(f.item(i))
else
handleFile(f.item(i))
end if
next
Note that if it's a folder, we're effectively calling the method we're already inside! That's
known as recursion -- a routine calling itself. Recursion is powerful but dangerous: if
there are too many recursive calls your program can run out of memory and crash. Also,
your recursive routine must have a way out: in other words, at some point it must stop
calling itself so it can "unwind." Each method call must end and return control back to
the previous one until we're back to where we started and the program can continue. In
our case we have a finite number of calls because there's a finite number of files and
folders that can be dropped on GenderChanger (though if a huge folder or disk was
dropped, GenderChanger might run out of memory).
That's pretty much it: add the above code and make the above changes to the File Types
and you should have a GenderChanger that lets you drop a bunch of files onto the icon in
the Finder.
.
REALbasic University: Column 018
REALbasic Animation: Sprites Part II
Last week we introduced sprites and used them to put together a very simple walking
stick figure animation. This week we're going to start Shooting Gallery, a simple
carnival-style shooting game that is a little more complicated. Shooting Gallery isn't a
full-featured game, only a demo, but it should help you understand more how sprites
work. Most importantly, it demonstrates how to control multiple sprites moving at the
same time. I wrote it a few years ago when I wanted to learn about sprites. In the future I
plan to do a real, complete game using sprites and high-quality graphics, but this is good
for a start and it's a nice change of pace from boring utilities. ;-)
About the Game
Shooting Gallery is a simple game, though the internals are designed to support a more
complex one if you care to expand it. I spent about thirty seconds creating the "awesome"
graphics -- at least it looks that way. Just replacing my primitive graphics with more
sophisticated ones would dramatically enhance the game. The background is just a
picture I created in Bryce that I happened to have handy. I was originally going to draw
an carnival shooting booth for the background but got lazy and didn't bother (after all, I
was more concerned with figuring out sprites than making this a real game).
What is the game like? It's basically a bunch of ducks flying left and right across the top
of the screen (at various speeds and directions) and the user controls a shooter that moves
horizontally along the bottom. The shooter is moved with the left and right arrow keys.
One cool feature is that the shooter accelerates: that is, the longer one holds down the
arrow key, the faster the shooter moves (up to a maximum speed).
When the user presses the "fire" key (the space bar), a missle shoots vertically from the
user's shooter. If it collides with a duck, the duck explodes. If it misses all the ducks and
goes off the screen, the missle is gone. The player can only have one missle in the air at
once. There's currently no limit on the number of missiles the player has, but in a real
game it might be appropriate to give the user a limited number of shots.
All sprites wrap. That doesn't mean they chant rhyming lyrics at high speed but that if
they go off one edge of the screen they emerge on the other side.
The game stops when the player has terminated all the ducks (the normal option is ten
ducks, but that can be changed by the programmer).
Starting the Project
To begin, launch REALbasic 3.x and create a new project (choose "New" from the File
menu). Open up Window1, the default project's window. Go to the Properties palette:
there are a few settings we need to change.
First, check the HasBackgroundColor checkbox and make sure BackColor is set to
white (if it's not, click on the colored rectangle and choose white in the color picker that
pops open). Next, change the window's title to "Shooting Gallery" and check the
FullScreen option. Window1's Properties should look something like this:
Now, from the Tools palette, drag a spriteSurface (
) onto Window1. Stick it
anywhere -- its location and size doesn't matter since we'll be setting those with code.
One spriteSurface1 setting you do want to change is the FrameSpeed property: be sure
to set it to 1 or 2, not 0 (zero means the animation rate will be as fast as possible, which is
usually too fast).
Guess what? That's it for our interface. Well, we'll add a few menu items, but that's it for
an interface for Window1. The game itself takes place entirely inside the spriteSurface!
Before we add the menu items, now would be an excellent time to save your project. Be
sure to put it inside a project folder: we'll be incorporating graphics and sounds for this
game and those always reside external to your REALbasic project file. That means every
time you open the project, REALbasic will look for those graphic and sound objects: it's
easiest to keep them in the same folder as your project file.
Open the Menu object in your project and go to the Apple Menu icon. In the white space
under it type in "About RBU Shooting Gallery..." and press Return. With the new item
selected, change its name to AppleAbout (shorter and easier to work with).
Next, add two items to the File menu. Add "New Game" and set its CommandKey
property to "N" and then add a separator line. (To add a separator line, click on the blank
menu item and type in a hyphen ["-"] for its text.) Finally, drag the items to rearrange
them: we want the New Game option first, then the separator, and finally the Quit item.
Your final menu should look like this:
We just have one more menu we need to add: we'll create an additional menu called
"Options" first. Click on the blank rectangle to the right of the Edit menu and type in
"Options" and press Return. Underneath that select and type "Sound On" and press
Return.
That's it for the interface! Now for some code.
Beginning to Code Shooting Gallery
The first code we'll add is a custom class. Go to the File menu and choose "New Class."
Select the Class1 object that appears in your project window and change its name to
duckClass. Now open it up.
What we are going to do is add several important properties to this class. By creating
multiple instances of our own class of duck we can remember details about each specific
duck (such as whether it's alive or dead, in the process of exploding, etc.).
Add the following properties to duckClass (to add a property, make sure duckClass is
open, then go to the Edit menu and choose "New Property" for each variable you want to
add):
Next we'll add a bunch of properties to Window1. Close duckClass and open Window1.
Add these properties to Window1:
What do all those properties mean? Most are self-explanatory, and a few I'll explain as
we go along, but briefly, here are what a few of them do. Direction helps us remember
which way the player's shooter is moving (left or right). numberOfDucks is a setting for
how many ducks the game will have. Score holds the player's score (100 points is
awarded for each duck shot and 25 points are subtracted for each missle fired).
MissleLaunched is a boolean (true/false variable) telling us that a shot is being fired,
while the shot itself is saved in the shot sprite. The player's shooter is shipSprite and
the current speed of the shooter is stored in speed. SoundOn remembers whether or not
sounds are active. The most important property, of course, is the duckSprite array: that's
where all the ducks are stored.
To start our coding, we'll go to the Open event of Window1 and put in the following:
// Set the spriteSurface size
spriteSurface1.width = islandsunset.width
spriteSurface1.height = islandsunset.height
// Center the spriteSurface on the window
spriteSurface1.left = (screen(0).width - spriteSurface1.width) \ 2
spriteSurface1.top = (screen(0).height - spriteSurface1.height) \ 2
// Create the sprites
initSprites
I've commented the code (REALbasic ignores text after an apostrophe ' or double slashes
//) so it should be clear what we are doing here: we're programmatically setting the size
and position of spriteSurface1 (that's why it didn't matter where you put it when you
dragged it onto Window1). Note: islandsunset is the name of the background graphic.
Note how we center the spriteSurface on the window: since we set Window1 to
FullScreen, it should be the same size as our main monitor. We use the screen object to
get the height and width of the screen (remember, the computer could have multiple
monitors attached -- screen zero is the monitor with the menubar on it) and do a little
math to calculate the center location.
In Detail
The formula for centering objects is simple (even for math-challenged me): you take the
width of the larger object and subtract from it the width of the smaller, then divide the
result by two. Try it: a screen of 1024 minus and object of 800 is 224. Divide 224 by two
and you get 112. If you set the left side of the smaller object to 112, it will naturally have
112 on the right side, meaning it is centered. You can use this same method to center a
dialog box or other window.
Next, let's enable the menu items we created a few minutes ago. Go to Window1's
enableMenuItems event and put in the following:
// Turn on our menus.
AppleAbout.enabled = true
FileNewGame.enabled = true
OptionsSoundOn.enabled = true
OptionsSoundOn.checked = soundOn
Note that we're doing something new here: we're setting the OptionsSoundOn menu
item's checked property to match the soundOn boolean. That means if soundOn is true,
the menu will have a checkmark by it. But if it's false, it won't!
Speaking of those menus, let's go ahead and put in some handlers to deal with them. Go
to the Edit menu and choose "New Menu Handler" and in the dialog that comes up, select
"AppleAbout" from the popup menu and click okay. In the AppleAbout handler that's
inserted, put in the following code:
dim s as string
s = "RBU Shooting Gallery 1.0" + chr(13)
s = s + "Copyright ©2001 by REALbasic University"
msgBox s
That's a bare bones About Box, but it's good enough for this little demo. We'll get into
fancier About Boxes in future lessons.
Add FileNewGame and OptionsSoundOn menu handlers. For the first one, put in this
code:
// Start a new game
initGame
SpriteSurface1.Run
Technically, this is the heart of the game: we initialize the game variables and start the
spriteSurface running. The spriteSurface runs until it is told to stop, either by the user
clicking the mouse (or pressing Return or Escape) or all the ducks have been shot. In
truth, however, most of the code that controls the game will be inside spriteSurface1's
NextFrame event, which handles updating the state of the game for each frame of
animation. That's the real heart of the game.
Now for OptionsSoundOn put in this:
// Toggle sound on/off
soundOn = not soundOn
That's pretty simple isn't it! Note that we don't have to set the checked state of the menu
here, just change our soundOn boolean. This confused me a little when I first got started
in REALbasic. I kept worrying that I was only changing the state here and what if the
user pulled down the menu before I could update it? But remember, we set the menu's
checked state in the enableMenuItems event. Since the menu doesn't actually drop down
for the user to see it until all of the code inside enableMenuItems has been run, the user
will never see the menu in the incorrect checked state. That's a good lesson to remember:
enableMenuItems is always called when the user clicks on the menubar but before any
menu drops down. That makes it the ideal place to set checkmarks, change menu names,
enable or disable menu items, etc.
That's enough for this week. Our program doesn't do much at this point, but we'll finish it
next week. Meanwhile, I've decided to give you the complete, finished project file, as
well as the graphics and sounds you'll need. That way you can actually play the game and
look at the code. Next week I'll explain the code so you understand exactly what's going
on.
Feel free to redraw my graphics between now and next week if you're so inclined: just
keep them a similar size and use the same names.
Next Week: We finish Shooting Gallery.
Letters
This week I decide to include letters I received as a part of the informal "REALbasic
Uses" survey I started a few weeks ago.
First, let me thank those who participated. I'm amazed and impressed at not only the
sheer diversity of ideas, but the ambitious size and scope of many of these projects!
While REALbasic University isn't going to create these projects for you (that would spoil
all your fun ;-), I hope that it will assist you in learning how to do it yourself. (Remember
the old line about "teach a hungry person to fish and they'll never be hungry again"?
Programming's the same way.)
I hope these project ideas will inspire you as much as they've inspired me. I'm also going
to use them as I plan future projects for RBU -- I won't create these specific projects, but
I will try to incorporate elements that will assist in building these kinds of programs.
Our first letter (these are in no particular order) is from Julian, who writes:
Since you asked.
a. Simple games, mainly word games. Word mastermind, scrabble (I know it's been done)
b. Word search program
c. Databases.
d. program to show the sequence of events at the battle of trafalgar.
Sounds good Julian: I'd love word games, so I might try to do one for RBU at some point.
Jim Martin writes:
I am a tennis pro who coaches some great young players. I often tape their matches and
analyze them later. I would love to make a program where I can log the score, who is
serving, where each ball was placed, and if the point ends on a winner or an error. I
would then like to print it all out and keep a record of what shots need to be improved
upon. Thanks for your course, and I look forward to each lesson. Keep up the good work.
Interesting idea, Jim! I'm not enough of a tennis person to write something like that, but it
doesn't sound too complicated. Basically an interface and a database of some kind (either
using one of RB's built-in database formats or rolling your own).
Next, Alexander Bennett writes from the land down under:
G'day Marc
Thank you for RBU and the very helpful comments which accompany your regular
tutorials.
In answer to your survey - I have written a Medical Records and Accounting System
which I use in-house to run a 3 doctor family medical practice in a small town in
Queensland, Australia. Those who have worked with information systems in the medical
industry will appreciate the size of this undertaking.
The system is written in 4D. It is a powerful but annoyingly idiosyncratic, dare I say
autocratic interface which again and again has limited my ability to create certain kinds
of interfaces. Properties of one kind of control are for unknown (to me) reasons not
implemented in others in the places where I expect they should be. 4D promotes its own
version of programming rather than true object-oriented programming. In addition, 4D
Inc and its affiliates are not very nice people when it comes to a low-budget small-time
programmer like myself - often not very helpful and VERY EXPENSIVE when it comes
to even minor upgrades.
My pedigree is in Hypercard and I spent many hours pushing and shoving it trying to
create a system which I used to run a solo medical practice in the late eighties and early
nineties. What I loved about Hypercard was the elegance of its structures and the freedom
it and its easily written XCMDs gave to the programming process. It was a sad day when
I finally resolved that Hypercard could not carry my database needs forward and that
Apple was no longer developing the programme.
I only recently discovered REALBasic. In contrast to my experience with 4D, the folks at
REALBasic have been simply excellent when it comes to answering my queries and
pointing me in the direction of appropriate resources - and I have not yet even purchased
the programme. The programme is affordable and seems imbued with an atmosphere of
mutual support which contrasts with the hard commercial edge of all my dealings with
the 4D community.
I have been playing with REALBasic these past twenty-eight days, when I really should
have been improving my 4D system. I have been pushing and pulling a few controls to
see if REALBasic will emulate the interface I have created in 4D. It took me a while to
get the hang of using graphics in REALBasic - but when I did - wow - so much more
programming power and so much more opportunity for the economic use of code is there
in REALBasic than in 4D.
My plan therefore is to spend the next six months or so porting my front end over to
REALBasic and continue to use the 4D Server (I have paid for several user licenses) via
the 4D plug-in. I expect the insights you continue to share with us to be very helpful in
this process. In particular I am keen to learn how to really crank up the canvas control
and to experiment with building an interface which retains its elegance on both Mac and
the dreaded PC. I am also looking to expand the internet capacity of my system so that I
and my colleagues are connected from with the interface rather than via "Explorer" and
"Outlook Express".
Keep up the good work.
Sounds like a great challenge, Alexander! I'm no database expert, but REALbasic is
certainly capable and possibly ideal for exactly what you're doing. Not only can it
interface with multiple database servers, you can even compile versions of your program
for Mac OS, Mac OS X, and Windows. A number of programs that incorporate clientserver technology have been written in REALbasic, so I could see you writing your own
system for connecting to your database over the Internet using your own server and
multiple clients. It'd be pretty sweet!
Your mention of the canvas control is a good reminder for me: at some point I want to do
a lesson on creating custom controls with the canvas class. That's one of the most
powerful things you can do with REALbasic, and while it's a bit advanced, it's not that
difficult.
Tom Nugent, Jr. writes:
Hi. I've been enjoying your RB-University columns -- thanks.
As to what I want to use RealBasic for, there are two categories. I have some ridiculously
large ambitions for what I want to do with RB, and so these are probably not directly
applicable for your column. Hopefully there's some useful tidbit in these, though, that
you could use for a column.
1) Graphics. a) Image stitching. I have a program called QuickStitch that takes multiple
photos and stitches them together by warping the individual photos. Problem is, it's a
Windows port and so it barely works on a Mac, and it's also been discontinued by the
company. I've been learning a bit about algorithms for taking multiple images and
combining them into a single larger image, but I'm pretty clueless about pixel-level image
manipulations in RB.
b) Data grabbing. There are a couple of programs out there that let you take a data graph
you've scanned in, and just click on various points to have it figure out what the original
data was. The problem with the existing programs is that some of them don't work on OS
8.6 or later, and the others don't have some functionality I want (such as being able to
zoom in and out on the image & scroll around in it while clicking, as well as being able to
do multiple lines per image). So here I'm going to have to figure out a coordinate system
for an image that translates as I scroll the image on screen and zoom in on it.
2) Scheduling. I'm working on trying to schedule events for a competitive event. Up until
now, I've just bounced things around in Excel. But it would be nice to have a program
that lets the user define or import events and event sizes, and then schedule various
rounds (ie. finals, semi-finals, etc.) into the various sessions (eg. afternoon, evening,
day1, day2), taking into account the different time lengths of the different rounds &
events. I'm envisioning a two-pass system, where you first do a rough pass on where
everything goes, and then fine-tune it to match your time constraints (i.e., putting 15
hours' worth of events into a 10-hour time slot isn't much good). And I'll need to put a
proper GUI on it, since the point of this is to make it easy on the user.
One of the big things with this project is the different data structures I'll need and ways of
manipulating them, since I want to be able to sort & find a time slot by event, by round,
and maybe by other criteria. Perhaps you can find a more tractable project that deals with
creating custom, semi-complicated data structures and manipulating (sorting, selecting,
finding, etc.) them?
Thanks!
Thank you, Tom! These are some great ideas, and certainly ambitious. The image
stitching program might be a bit too advanced for REALbasic: RB isn't great for pixellevel manipulation (it's slow). Some people have written REALbasic plug-ins in the C
programming language to handle specific image-manipulation needs fast, while creating
the program structure and user interface in REALbasic. That approach might be worth
looking into.
At some point I do want to do a lesson on graphics and scrolling: I've got a project I
wrote a while back that is a basic image viewer. I haven't yet added features like zoom in
and out, but that might be appropriate for a future RBU column.
Your scheduling issue is of personal interest to me: back in high school I was deeply
involved in competitive speech and debate. When our school put on a tournament, I
remember being amazed at how difficult to was to organize and coordinate all the
resources. It literally took months of planning. There were a limited number of
classrooms, multiple rounds of competition, over a dozen types of competitions (each
with different requirements and time periods), a limited number of judges, and of course
everything had to take place over a weekend. There were other complexities: for instance,
judges from our school could not be used to judge rounds in which students from our
school were competing.
I thought a computer scheduling program was exactly what the school needed, but though
I toyed with the idea, I never actually wrote it. Since then I've wondered off and on if
schools still do things that way and if there would be a market for such a program. I
should think there would be, if someone hasn't written it already, but it strikes me as an
enormously complex piece of software.
(My brother used to work for a company that wrote expensive scheduling software for
hospitals, and he told me a few times about how complex it was, managing doctors,
limited numbers of operating rooms and equipment, etc. It made my head hurt thinking of
all the permutations!)
You are correct that the data structure is all important for such a thing: I have some ideas
on this, and I'm planning a column specifically on data structures, so I may try to develop
an example program using your idea as the basis. We'll see!
John Walker writes about a project involving a database and PDF documents:
Hi Marc,
I've been trying to link a database FileMaker Pro to open a PDF to a specified page.
Sounds easy but is to complicated for me to solve. Any help would be appricated.
That's a tough one, John. I thought maybe an AppleScript could be made to open the
PDF, but Adobe's Acrobat Reader 4.0 doesn't appear to be very scriptable (I don't know
about the newest 5.0 version). Possibly a macro program like OneClick could be used to
jump to the particular page, but what you are talking about is a complex thing involving
multiple communication and scripting technologies interacting. If any readers have any
better ideas, let me know!
Rick Johnson writes:
Hi,
I enjoyed your piece on how you use REALbasic. I'm an illustrator at [a publishing
company] and most of the work I do is for Model Railroader magazine and the two
annuals, Great Model Railroads and Model Railroad Planning. I created a utility program
to help me scale objects for drawing trackplans (maps of people's model railroad layouts)
for the magazines. I can enter the scale of my drawing and the size of the object I want to
draw, either in its actual dimensions or measured in the scale of the model railroad. For
example, if I need to place a model structure that in real life measures 3" x 4", I simply
enter those dimensions and press return. My program gives me the resulting dimensions
in points and picas, which I can transfer to my illustration. If I need to draw something
like a turntable that I know measures a scale 75 feet in HO scale, I can select HO from a
popup menu, enter my measurements, and again it translates it into actual dimensions,
then to scale dimensions according to the scale of my drawing.
The neatest part, though, is that I added a canvas control I call the "drag strip" that allows
me to drag the scaled art into my Illustrator window, where all I have to do is rotate it and
color it.
I've also done a couple of shareware programs, most recently "NotePod" which keeps a
hierarchical list of text notes dragged from emails or other electronic documents or files.
I also wrote a utility app that parses an Adobe Illustrator file and extracts any embedded
EPS files or raster objects, which it rewrites as Photoshop TIFF files.
Finally, I wrote a small REALbasic app to generate the registration codes for my various
shareware programs, most of which are Adobe Illustrator plugins written in C with
CodeWarrior.
I can send screen captures if you're interested.
Very impressive, Rick! Your drawing scaler sounds a little like my ScanCalc program,
though yours sounds like it does more than just act as a proportional wheel. That "drag
strip" sounds particularly nifty! I'm curious how you did it, if you wouldn't mind sharing.
I thought at first your NotePod sounded like my Z-Write, but it's definitely more geared
toward popup reminders, text clippings, and to-do lists. It looks very impressive. I've
downloaded a copy and will be checking it out!
Finally, last but certainly not least, we hear from Edzard van Santen:
Dear Marc, Thanks for doing RBU. I am having a blast learning RB. past programming
experiences have been limited to using Basic to print labels, fiddling around with Macros
in Bill Gate's applications, and writing Filemaker scripts. My ultimate goal in this
endeavor is to write a simple PIM that will (a) serve as a db for my activities, (b) let's me
collect thoughts (few as there are) and notes. I've used various pieces of s... over the years
but am not really happy with anything I've come across. Most software doesn't "think"
the way I do. I've used Z-Write to develop presentations and manuscripts and enjoyed
using it. Again, thanks for doing RBU. I'm looking forward to learning.
All excellent uses for REALbasic! I'm amazed and impressed. This list shows that people
want and need the power of something like REALbasic -- real programming for the rest
of us.
Learning C and Codewarrior is just too complicated for most users who simply want the
ability to generate customized solutions, yet AppleScript and other languages aren't
powerful enough. REALbasic is that perfect solution in the middle.
Code on!
.
REALbasic University: Column 019
[Author's Note: Before we begin, let me apologize for the delay in this week's column. I
got suckered into helping my parents pack and move last weekend, so instead of writing I
was lifting heavy furniture. My whole week's been a quest to catch up!]
REALbasic Animation: Sprites Part III
This week we'll finish Shooting Gallery, a simple game using sprite animation. If you
followed last week's lesson, you've created a project, added a SpriteSurface control and a
few menus, and a custom class. Today we're going to write a few methods and put in the
code that controls the actual animation.
First, we'll begin with the initialization method, initSprites. This method is simple but
essential: it creates the shooter sprite, our duck sprites, and sets the default position for
the player's shooter.
Here's the code. Put this inside a new initSprites method (remember, to create a
method, Option-Tab while selecting Window1 to open Window1's Code Editor, then
choose "New Method" from the Edit menu).
dim i, x, y as integer
//
// This routine creates ten duck sprites. It must only be
// called ONCE per launch.
//
numberOfDucks = 10
redim duckSprite(numberOfDucks)
for i = 1 to numberOfDucks
// Creates a new duck sprite
duckSprite(i) = new duckClass
next // i
// Here we create the ship sprite and set its location centered
horizontally
// and 50 pixels from the bottom.
shipSprite = SpriteSurface1.NewSprite(shooter, spriteSurface1.width
\ 2, spriteSurface1.height - 50)
// The ship is group 0 (zero), which means it won't collide with
anything.
shipSprite.group = 0
What does this routine do? First we set our numberOfDucks variable to ten: by setting the
number of ducks with a variable, we can change this in one place throughout our entire
program. Feel free to experiment: change this to 2 or 50 or 100 and run the program and
see how it works.
The information about each duck is stored in an array, duckSprite. Each element of the
array holds an instance of a duckClass object (which contains the actual sprite, plus
properties remembering which way the duck is moving, the speed of the duck, etc.).
Since duckClass is an object, we must create a new one with the new command before
we can use it. We only want to do that once per launch of the application, so that's why
we've put it inside this method and used a separate initGame method for initializing our
game variables (which are reset with each game). Remember, we are not actually creating
duck sprites here, only duck containers which will eventually hold the actual sprite.
In Detail
In retrospect duckSprite is not a good name for that variable since each element of
duckSprite is not really a sprite: it's an instance of duckClass, our custom class, which
contains a sprite inside it. duckClassArray would have been more accurate.
You also may notice that our shipSprite variable is a sprite, but it's not really a ship:
gunSprite would have been a better name for it. That's what happens when you code off
the cuff. For this program it may not be a big deal, but for more complex programs, it can
be confusing. I highly recommend you take care naming your variables and methods, and
if you later see that one is inaccurately named, use REALbasic's Find-and-Replace
feature to change the variable name throughout your entire program.
Next we create shipSprite, a sprite that represents the player's shooter. The newSprite
method is part of the a SpriteSurface object: it associates the sprite with that specific
SpriteSurface (think of a SpriteSurface as a playing field for sprites). As part of the
newSprite command, we must tell it which picture to use for the sprite (in this case,
shooter) as well as the coordinates for its initial location (in this case, centered
horizontally and 50 pixels from the bottom of spriteSurface1).
Our final step is to assign shipSprite a group. Groups are a unique characteristic of
sprites: they allow us to set which sprites collide with each other. By setting shipSprite
to group 0 (zero), nothing will ever collide with it (if another sprite did touch it, it would
simply pass over or behind it and no collision event would occur). More on groups in a
minute, when we cover the ducks.
We're finished with initSprites, so let's create a new method, initGame. This routine
is called whenever the player chooses "New Game" from the File menu. It resets all of
the game's variables. For instance, ducks are randomly positioned and set to visible and
the score is set to zero.
dim i, x, y as integer
//
// This routine initializes all the sprites with their starting
// settings (location, speed, direction, etc.).
//
// Close any open sprites
for i = 1 to numberOfDucks
if duckSprite(i).theSprite <> nil then
duckSprite(i).theSprite.close
end if
next // i
for i = 1 to numberOfDucks
// Sets the horizontal and vertical position of a duck
x = rnd * spriteSurface1.width
y = ((rnd * 5) + 1) * 40
duckSprite(i).theSprite = SpriteSurface1.NewSprite(duck, x, y)
// Speed is a random number from 1 to 6 (zero to 5 plus one)
duckSprite(i).speed = round(rnd * 5) + 1
// Set other defaults
duckSprite(i).visible = true
duckSprite(i).exploding = 0
// We assign each duck to its own group. The group
// number corresponds to the duck's index in the
// array of sprites.
duckSprite(i).theSprite.group = i
// Here we randomly pick a direction (left or right) for the duck
x = round(rnd)
if x = 0 then
duckSprite(i).direction = true
else
duckSprite(i).direction = false
end if
next // i
// Initialize score and other settings
gameOver = 0
score = 0
speed = 1
direction = 1
buildup = 1
drawScore
The first thing we do is close any existing duck sprites. Closing a sprite is basically
killing it off (setting it to nil). Note the critical difference between a duckSprite
element and the actual duck sprite (duckSprite(i).theSprite): killing a duck's sprite
does not kill the duckSprite object. Once again, this is where poor naming of a variable
makes things confusing!
Why are we killing off our duck sprites? Well, we can't be assured that the player has
shot all the ducks before starting a new game: this loop ensures that all ducks have been
truly killed off so we can create new ducks for a new game. If we don't do this, there'd
still be old ducks on the screen when the new game started! The old ducks wouldn't move
because we would be moving the sprites associated with the new ducks: the old sprites
would still be there, but we wouldn't have a way of controlling them since our
.theSprite property now points to the new sprites.
(If you want to see this, comment out the .close line by putting an apostrophe ' in front
of the line and running the program.)
Look at it this way: assigning a newSprite to .theSprite does not replace the existing
sprite, it merely means that .theSprite refers to the new sprite, not the old one. The old
sprite -- if it hasn't been closed -- still exists.
Our procedure for doing this is simple: we have a for-next loop that counts through our
ducks and looks at each duck's sprite to see if it exists or not (is nil or not). If the sprite
does exist, we close it.
Note that the first time the user chooses "New Game" all the sprites are nil anyway, so
this routine does nothing in that situation.
Once all our sprites are dead, we can create new ones. This is simple: another loop
through the numberOfDucks, and each duck is assigned a random location and a duck
picture. We also assign each duck a random direction (left or right), a random speed
(from 1 to 6 -- it's the number of pixels the duck moves with each frame of animation),
and we make sure that the duck is visible and is not in the process of exploding (we set
the .exploding property to 0).
Now here's a problem. When two sprites collide, REALbasic just tells use the two sprites
involved, not which duckClass object was shot. But the duckClass object is where we
keep all our info about the duck: so we need a way to get from the actual sprite shot to the
duckClass object. How do we do that?
The answer is simple, and even a little clever. We could have created a custom sprite
class with an extra property that would tell us the index number of the duckSprite array,
but instead I used the sprite's built-in group property. We assign each duck a unique
group. The group number matches the duck's index value (within the duckSprite array):
since we can always find out the duck's group value, that will allow us to easily tell
which duck was shot. For example, if duck number 7 was shot, its group value will be 7,
and thus I know that duckSprite(7) was the one shot.
Convenient, eh? We're using the group property to serve a dual purpose!
However this does create a small problem: since all positive groups that have different
numbers collide with one another, we'll need a check in our collision routine that will
ignore duck collisions (ducks crashing into each other). That's simple enough -- we've
just got to remember to do that.
After we've initialized our ducks, we set some default values for the game and the
player's "ship" (the shooter). That's it!
The final method we'll add is the drawScore method. This one is very simple: we just
draw into the graphics port of Window1. We first use clearRect to erase the existing
score, then we set our font, color, and text size. Then we draw the score.
// Write the score on the screen.
window1.graphics.clearRect(0, 0, 200, 48)
window1.graphics.foreColor = rgb(0, 0, 255) // blue
window1.graphics.textFont = "Geneva"
window1.graphics.bold = true
window1.graphics.textSize = 18
window1.graphics.drawString("Score: " + str(score), 20, 48, 200)
Coding the Animation
The most important part of Shooting Gallery is what happens in SpriteSurface1. Once
we've started spriteSurface1 (with the .run method), it runs until it is stopped (either
by the user clicking the mouse or the game being over). All the animation and game logic
occurs within spriteSurface1.
First, let's initialize the background picture. Put this code in the spriteSurface1.Open
event:
// I programmatically set the backdrop because REALbasic
occasionally
// forgets the backdrop I had set in the IDE (via the Properties
palette).
me.backdrop = islandsunset
This just sets the background of spriteSurface1 to islandsunset. As I explain in the
comments, I did it with code because for some unknown reason, REALbasic kept
forgetting the backdrop when I did it via the Properties palette. I'd run the program and
the ducks would be flying on a black background!
One of the most important parts of spriteSurface1 is what happens when two sprites
collide. Go to the Collision event and put in the following code:
//
// if we are here, two sprites have collided.
// We must figure out which two, and do
// what's appropriate.
//
// Is a missle launched and is one
// of the colliding sprites the missle?
if missileLaunched and s1.group = -1 then
// Set the second sprite to an explosion graphic,
// then hide the duck sprite (it's dead), and start
// its explosion count going (so we can let the explosion
// stay on screen for a half second).
s2.image = explosion
duckSprite(s2.group).visible = false
duckSprite(s2.group).exploding = 1
// Add to the score
score = score + 100
if soundOn then
boom2.play
end if
// Get rid of the missle sprite
shot.close
missileLaunched = false
end if // missileLaunched
You'll notice the spriteSurface1.Collision event passes two sprites to us: s1 and s2.
Those are the two sprites that collided. So we need to examine them to see what's
happened.
First, we check our system-wide boolean missleLaunched. We set that to true when we
launch a missle, and to false when the missile either explodes or goes off the screen, so
if it's false (not true), we know there is no missle in the air. If there's no missile flying,
we can just forget about a collision -- the only other collision that could occur would be a
couple ducks crashing into each other and we don't care if that happens.
The primary collision we're wanting to check for is that of a missle crashing into a duck.
So we also check to see if the first sprite is a missile. We do that by examining its group:
if it's a negative one (-1) we know it's a missile. If it's not, then we know that while a
missile is in the air, the collision reported is two ducks and we can ignore the collision.
If s1.group is a -1 and a missile has been launched, we then know that the player has
shot a duck! What happens when a missile hits a duck? Well, several things. First, we
change the graphic of the second sprite -- the duck -- to an explosion. We also set the
duck's visibility to false. (Note that the sprite doesn't become invisible when we do this:
the visible property is our own custom property that's part of duckClass and doesn't do
anything. We just use it later to tell the difference between ducks that have been shot and
those that haven't.)
Next we set the sprite's exploding property to 1. Why do we do that?
In Detail
Well, when I first wrote Shooting Gallery, my duckClass didn't have an exploding
property. When the duck was hit, I simply changed the image to an explosion and then
deleted the sprite. That's where the problem happened: depending on your SpriteSurface's
frameSpeed setting, your animation is probably being drawn at 30 frames per second or
faster. That means nextFrame is being executed 30 times per second!
At that speed, the explosion graphic was only displayed for a fraction of a second before
the sprite was deleted! It was literally too fast for your eye to see it. As far as the player
was concerned, a shot hitting a target meant that both sprites just vanished. Not good.
My solution was to add an integer variable -- the exploding property -- to each duck so I
could keep track of how long it had been since it had been hit. On first hit, exploding is
set to 1. On every frame after that exploding is incremented by one. I decided 30 frames
was good: after 30 frames (one second at 30 fps animation) the explosion disappears
when the sprite is deleted.
It's details like this one that make a game more playable and fun. It's more work for you
as a programmer, but you can't think of that: you must look at things from the player's
perspective. A target that simply vanishes is not as much fun as one that explodes over
several frames.
In fact, you could make Shooting Gallery even better by making the explosion more
elaborate. For instance, you could add an extra graphic to the explosion with the flames
in a slight different position to make it look like flickering flames. If you wanted to go all
out, you could create a series of graphics so the explosion starts out small and grows and
slowly fades. Or maybe you're the violent type and you want to see the duck explode into
a puff of feathers!
Either way, you'd use the exploding property as a counter to keep track of where you are
in the explosion animation sequence.
Once we've set the sprite's new characteristics, we increment the player's score and, if
appropriate, play the explosion sound.
Our final step is to get rid of the missile sprite by closing it, and setting the
missileLaunched variable back to false.
Okay, that's all we've got time for this week. The complete, finished project file with all
the required pictures and sounds is available for download, but I'll explain the final bits of
code in detail next week.
Next Week: We'll code the most complex part of Shooting Gallery -- the nextFrame
event -- and finish up the project.
Letters
Ryan and I exchanged a couple letters regarding Shooting Gallery. He tried to run the
program in Mac OS X and it didn't work. I pointed out that it worked fine in OS 9 and
that it was probably due to bugs in the sprite system under Mac OS X (I'd heard there are
a few bugs). He wrote back to say:
Marc,
You must be right, there must be an RB or OS X bug involved with that crash. It ran just
fine on 9.1 for me (couldn't test in Classic - the RB 3.2.1 Carbon application doesn't have
that "Open in Classic" checkbox for some reason). Maybe something to do with replacing
the sprite images when the SpriteSurface redraws? Hopefully that'll get fixed; doesn't
bode well for RB sprite animation.
One suggestion: in the RB 3 docs (the lang. reference) it notes the new, improved method
of animation: calling Update repeatedly, instead of just handing control to the Surface
and watching for a keypress. That sounds like it would allow more precise flow control,
but might it also solve this problem? Maybe the sprite updating is different when Update
is called than the automatic Run updating. Maybe RB has deprecated Run, and now
wants you to use an empty Timer even when you don't need to relinquish control. Just an
idea.
REAL Software's suggestion to use a timer to control animation is apparently the new
direction for sprite animation. I suspect that the old way is buggy and less reliable under
Mac OS X. Probably using update would fix the problem you ran into.
A timer certainly gives the programmer more control and flexibility since other things
can be happening in between frame animation events (currently when you give control
over to your spriteSurface with the Run command, it takes over completely).
I thought about demoing the update animation technique, but I figured I'd leave that as
an exercise for the reader. For one, it wouldn't be hard to do (just add a timer with a
spriteSurface1.update command and don't explictly call spriteSurface1.run), and
for another, Shooting Gallery was an existing project I already had completed using the
old method.
If people are really interested, let me know and I can post a version of Shooting Gallery
using the new update method.
.
REALbasic University: Column 020
REALbasic Animation: Sprites Part IV
Our program is really coming along, but now we've got to write the most complicated
part: the NextFrame event. This is the core of the program: all this code will be executed
at 30 frames per second (if you set spriteSurface1's frameSpeed to 2).
Before I give you the complex code, let me explain a bit about what we need to do in this
routine. Our basic goals are the following:
•
•
•
•
•
•
•
•
Move all "living" ducks
"Wrap" around any ducks that have moved off the screen
Kill off any explosions with an exploding value greater than 30
If there's a missile moving, move it
Move the player's ship if the player is pressing the left/right arrow key
Fire a missile if the player presses the space bar
Stop the game if the user presses the Escape or Return key
Stop the game if there are no ducks left
Whew! That's a lot of stuff to do. You can see how this could get complicated in a more
sophisticated game!
It's not so bad if we take each step piece by piece, though. Here's the first part of the
code:
dim i, j, x, y as integer
//
// This is the program's main routine. Here is where the
// ducks move, the user shoots a missle or moves the
// ship, etc.
//
j = 0
for i = 1 to numberOfDucks
// Is the duck alive (visible)?
if duckSprite(i).visible then
j = j + 1
// Here we move the sprite right (+) or left (-) and wrap it
around
// if it's moving off the screen.
if duckSprite(i).direction then
duckSprite(i).theSprite.x = duckSprite(i).theSprite.x +
duckSprite(i).speed
if duckSprite(i).theSprite.x > me.width then
duckSprite(i).theSprite.x = 0
end if
else
duckSprite(i).theSprite.x = duckSprite(i).theSprite.x duckSprite(i).speed
if duckSprite(i).theSprite.x < 0 then
duckSprite(i).theSprite.x = me.width
end if
end if // duckSprite(i).direction
else
// We get here if the sprite is hidden. We check to see if it's
// in the process of exploding, and if so, increment the
explosion
// counter. When the counter is greater than 30, we terminate
// the sprite.
if duckSprite(i).exploding > 0 then
duckSprite(i).exploding = duckSprite(i).exploding + 1
if duckSprite(i).exploding > 30 then
duckSprite(i).exploding = 0
duckSprite(i).theSprite.close
drawScore
end if
end if
end if // visible
next // i
This first part is just a big loop through every duck. We check to see if the duck's visible
(alive) or not, and if it is alive, we move it. We move it left or right according to the
duck's direction property, and the amount we move it -- the number of pixels -- is the
speed value.
After we've moved the duck, we check to see if we moved it too far so it's off the screen.
If so, we move it to the opposite end so it "wraps around" and starts over from the other
side.
If our duck is "dead" (hidden), we check to see if it is exploding. (If the exploding
property is greater than one, we know it's exploding.) We increment the exploding
property. If it's greater than 30, then enough time has passed for the explosion to be seen,
so we terminate the sprite and set its exploding property to zero.
Our final step is to draw the score after the explosion has gone away.
The next bit of code directly follows the above:
if j = 0 then // all ducks shot, game over
gameOver = gameOver + 1
if gameOver > 30 then
me.close
end if
end if
// Here we move the missle. Note it moves 5 pixels at a time, much
// faster than the ducks.
if missileLaunched then
shot.y = shot.y - 5
// Has the shot gone off the screen? if so, terminate it.
if shot.y < 0 then
shot.close
missileLaunched = false
end if
end if
First we see if the all the ducks have been shot. If so, we stop the execution of
spriteSurface1 -- that's the me.close line-- there's no point in continuing if there are
no ducks to shoot.
Next, just like we move the ducks with each frame of animation, we now move the
missile (if there is one). The ducks move only horizontally while the missile moves
vertically, so for the ducks we added or deleted numbers to the x property. With the
missile, we work with the vertical -- y -- property, and we only subtract since the missile
only moves upward. Since we want the missile to move quickly, we move it 5 pixels at a
time. (Feel free to change this and see how that changes the gameplay for you.)
Finally, we check to see if the missile has gone off the top of the screen (obviously
without hitting anything). If it has, we delete it and set missileLaunched to false.
This routine is a long one, but breaking it into a few steps helps. This final part handles
player interaction.
The SpriteSurface object is a bit strange: since it's running in its own little world, you
can't use a window's normal KeyPressed event to see if the user pressed a key. Instead
you must ask SpriteSurface if the user has pressed a particular key. But even then
SpriteSurface is strange: you can't just ask it if a particular ASCII character has been
pressed -- you have to instead pass it a hexadecimal keycode.
Keycodes are unique to keyboards: the French keyboard thus uses slightly different
keycodes for certain letters than the English keyboard!
Here's a chart showing the hexadecimal keycodes for the English keyboard:
[Click for full view]
Here's the code for the user interaction section:
//
// This portion handles user interaction.
//
// Left arrow pressed.
if spriteSurface1.keytest(&h7B) then //left
if direction = 1 then
// Buildup is used for acceleration: the longer
// the user holds down the key, the faster
// the ship moves (to a max speed of ten).
buildup = buildup + 1
if buildup / 10 = buildup \ 10 then
speed = speed + 1
if speed > 10 then
speed = 10
end if
end if
else
// We were going the other direction, so down
// we switch and reset speed and buildup values.
speed = 1
buildup = 1
end if
// Move ship to the left; wrap if off screen.
shipSprite.x = shipSprite.x - speed
if shipSprite.x < 0 then
shipSprite.x = me.width
end if
direction = 1
// Right arrow pressed.
elseif spriteSurface1.keytest(&h7C) then //right
if direction = 2 then
// Buildup is used for acceleration: the longer
// the user holds down the key, the faster
// the ship moves (to a max speed of ten).
buildup = buildup + 1
if buildup / 10 = buildup \ 10 then
speed = speed + 1
if speed > 10 then
speed = 10
end if
end if
else
// We were going the other direction, so down
// we switch and reset speed and buildup values.
speed = 1
buildup = 1
end if
// Move ship to the right; wrap if off screen.
shipSprite.x = shipSprite.x + speed
if shipSprite.x > me.width then
shipSprite.x = 0
end if
direction = 2
// Space bar pressed.
elseif spriteSurface1.keytest(&h31) and not missileLaunched then
//space (fire missle)
// Create a new missle sprite and start it going.
missileLaunched = true
x = shipSprite.x + (shooter.width / 2) - (missile.width / 2)
y = shipSprite.y - missile.height
shot = SpriteSurface1.NewSprite(missile, x, y)
shot.group = -1
if soundOn then
zap2.play
end if
score = score - 25
drawScore
// return or Escape pressed.
elseif spriteSurface1.keytest(&h24) or spriteSurface1.keytest(&h35)
then //quit
me.close
end if
Most of this code is very similar to what we've done before: we move a sprite by adding
and subtracting to its x and y properties, and we wrap it around the screen if it goes too
far.
One new twist is the buildUp variable. Again, this is subtle detail that I wanted my game
to implement. Basically, I wanted the movement of the player's ship (or gun) to
accelerate: the longer the player holds down the arrow key, the faster the ship moves (up
to a point).
Now I could have just incremented the speed value, but since this happens every time
nextFrame is called (at least 30 times per second), that would happen almost instantly
(less than a second). So instead I use a second value, buildUp, and I increment that
during each frame of animation. When buildUp is evenly divided by ten (every ten
buildUps) I then up the speed one notch. Thus in one second (assuming 30 fps
animation), the speed only goes up by 3. Since the maximum speed is 10, it takes 3.3
seconds to achieve maximum speed. The result is a gradual acceleration of the player's
ship.
If the player changes direction, speed and buildUp are set back to 1. The flaw (bug?) in
this implementation is that the above code makes no provision for resetting the user's
speed if the arrow key is released. The player can therefore go to maximum speed and as
long as the player doesn't change direction, always move at maximum speed.
In Detail
In REALbasic, the slash acts as a divide command. So 5 / 10 = 0.5. The backslash is
also a divide, but it's an integer divide: it returns only the integer portion of the result (it
ignores everything to the right of the decimal point). Therefore 5 \ 10 = 0.
So to see if buildUp is evenly divided by 10, I use the following if statement:
if buildup / 10 = buildup \ 10 then
Our next bit of code check to see if the player pressed the space bar to launch a missile.
This is only allowed if a missile isn't already in the air: a more complex game could allow
the user to shoot multiple shots at once. Then you -- the programmer -- would have to
keep track of multiple shots (probably with an array of missiles, similar to the current
array of ducks).
To launch the missle we simply create a new sprite and set its initial location. Once it's
created, our previous code will automatically move it starting with the next frame of
animation.
Finally, we finish by checking to see if the user terminated the game by pressing either
the Escape or Return keys. Neither key quits the program -- they just halt
spriteSurface1 and abort the game in progress. (Obviously, in a real game, you'd want
to confirm this with the user before stopping.)
Well, guess what? We're finished! That's it for Shooting Gallery. I hope you enjoyed it
and learned a little about sprites. Like I promised, we'll do something more elaborate in
the distance future, but this should get you started and help motivate you into writing
your own animation-based game.
The complete, finished project file with all the required pictures and sounds is available
for download.
Experiment with it: change some of the variables and try different things. Enhance it,
make it play better. For instance, how about letting the player's "ship" (gun) move
vertically as well as side to side? What about making the ducks drop bombs and/or dive
downward? How about different kinds of ducks? Instead of the game ending when all the
ducks are shot, why not advance the player to a tougher level?
There's plenty more to do: I've just given you a step in the right direction!
The Week After Next Week: Next week I'm off on vacation, so have a great Fourth of
July holiday. When I come back, we'll have a look at REALbasic Basics. Some of our
tutorials have been a bit advanced, so we'll have something special for the newbies in the
audience.
Letters
Remember how last week I mentioned there was an alternate method of animating a
spriteSurface -- calling its update method repeated via a timer control? Well Sarah
tried that and wrote:
Hi Marc,
I tried implementing the Timer for updating the sprite surface instead of running it but it
didn't help. It was also a lot slower than running the spritesurface - I had to double the
speed of the ducks and the shot to make it playable even with the timer supposedly
calling the spritesurface update every millisecond! The OS X crash still happens when
some ducks are shot (not every duck). It seems to happen when a collision occurs but
before it gets to the collision handler. The program also crashes sometimes when I move
the ship.
Now to my question: how can you know that the shot will be s1 & the duck will be s2 in
the collision handler? Shouldn't you be testing both s1 & s2 for their groups?
Interesting info, Sarah. Thanks for the update. I can only surmise that sprites are
problematic under Mac OS X for now, though I'm sure REAL Software will eventually
fix the problems. I'll have to play with the update method myself at some point: I like the
idea of the better control, but Timers are notoriously unreliable for precision timing.
(To see what I mean, start a Timer doing something every second, like moving a
staticText. Then pull down the Edit menu of your running program. The Timer doesn't
fire. See? Timers don't get called when the Mac is too busy with other stuff. Of course
that may only apply under Mac OS -- OS X could be different.)
As to the second part of your question, you got me! One certainly shouldn't know which
of the two sprites is which: theoretically either could be the missile. But in practice, it's
always s1. Why? I don't know. But when you switch the two around and check for s2
being the missile, it never is. It could be something along the lines that the missile sprite
was created more recently and therefore it is the one that's listed first.
I did notice that bug and thought about changing it, but the deadline was looming and I
half wondered if anyone would notice: I ought to give your sharp eyes a prize! While the
way I did it works, it's not a good programming technique. Never depend on a fluke of
your programming environment, even if it consistently works. The bug or oddity might
be fixed in a new release of REALbasic and then your program will suddenly start
crashing and you'll be driven nuts trying to figure out why.
Christoph Scherer also wrote in with a sprite issue:
Hello,
Any idea if the "update" method for the spritesurfaces would prevent an error like that in
my game-prototype ??? When missiles were fired at the enemy and some are left over,
they home in on the next dude (as wanted) and all, but they won´t collide. I set a
debugging-breakpoint into the "special action/tab" check in the "nextframe" event, just to
look what the sprites exactly are when they should collide. Guess what: all values, even
group, priority and stuff are correct, so they could collide technically...
I´m getting really desperate. These stupid errors are frustrating, especially when you
looked everywhere...
I wish I could be of more help, Christoph. I downloaded your prototype game -- the
graphics are very impressive -- but I couldn't really tell much from that. I'd probably need
to poke around in your actual source code to see if I saw anything. I did notice that the
game was extremely crash prone on my Mac OS 9 system. That could be because it's a
Carbon version, which also could be part of the problem. Do you have the same problems
in a straight PPC version? (It's been my experience so far that Carbon RB apps tend to be
slower and buggier than PPC ones.) Your application wouldn't let me enlarge the
memory partition, so I couldn't tell if the problem was low memory.
It could also be that your game is just too complicated: how many sprites did you have
going at once? Maybe if there are too many REALbasic doesn't handle the collision issue
well.
If anyone else has ideas for Christoph, let me know. It also might be fun to point out
some great REALbasic animated games: if you have any you've written or know about,
send me the URL and I'll post a link to it next column.
.
REALbasic University: Column 021
REALbasic Basics
Though I briefly covered a little about REALbasic's interface items in previous columns,
I soon dove in to our first project, GenderChanger. I did not intend to neglect beginning
users, but simply cover all the bases. For the next few lessons, I will focus on some of the
most basic aspects of REALbasic, demonstrating how to use the various interface
controls built-in to RB.
REALbasic Version 3.2
REALbasic is a program that is upgraded regularly: REAL Software releases beta and
alpha versions as frequently as every few days. That's excellent for full-time developers,
who can afford to live on the cutting edge. The rest of us are usually better off relying on
finished releases. (Features in pre-releases are not finalized and may change or not be
supported in final releases.)
For this and future columns, I'll be using REALbasic 3.2, the latest finished release as of
this writing. In the past, I was using older versions. The differences are mostly cosmetic
in that the look of the IDE has changed, but there are a few new commands and features
(such as the changes to the sprite system mentioned in our sprite tutorial). One key issue
with using a current release is that older versions of REALbasic cannot open RB 3.2
project files -- which means you'll need at least 3.2 to open REALbasic University
projects from now on.
(It also means you need to be careful when upgrading: once you convert your older
project to a newer version of REALbasic, it's difficult to go backward. RB 3.2 offers a
"Save as RB 2.1" command, but if you use any of the new version's features, the project
won't compile in the old version.)
Basic Interface Elements
REALbasic includes a number of interface objects we haven't had a chance to use in an
RBU project. For instance, we've used listboxes and editfields, but not tab panels, sliders,
or progress bars. So let's take a look through some of these elements and learn how to use
them.
I've created a REALbasic project, RBU Demo, which you can download and use to
explore all of the demonstrations we'll cover in this series. This is what the project looks
like within the IDE:
As you can see, there's lots going on. But the project doesn't really do anything except
show how to work with various RB interface elements.
To guide us on our tour, here's a screen shot of REALbasic's "Tools" palette:
The first six items are so simple they don't require much explanation: lines, circles,
boxes, and text labels. These are graphical interface elements that don't usually interact
with the user. (That's not to say they can't: you could, for instance, write a simple drawing
program using these elements, but you're limiting yourself to objects REALbasic can
draw. It'd make much more sense to use a canvas where you can draw anything you
want.)
Of the next three, the placard, imagewell, and canvas, the latter is the most important.
I'm honestly not even sure what the first two are for: a placard is simply a 3D rectangle
that's either pushed-in or popped-up, and an imagewell can only display a picture. Both
features can be done with a canvas, which gives you much more flexibility.
To demonstrate a little of what a canvas can do (we'll have a more in-depth look at a
canvas in a future column), we'll have it display a picture. But as long as we're doing
this, let's take advantage of the situation to demonstrate a couple other controls as well:
scrollbars.
Scrollbar controls work to allow you to display a portion of content that is too large to
fit the display area. In our example, we'll display the large island sunset picture from last
column's Shooting Gallery project in a very small canvas. To do this, we'll need two
scrollbars, one for the vertical dimension, the other for the horizontal. I've named them
vScrollBar and hScrollBar.
I position them below and to the right of my canvas object. When the program's running,
it will look like this:
Scrollbars are fairly simple controls. Other than size and position, there really are only
three key properties you need to set: minimum, maximum, and value. Those set the range
the scrollbar will cover. The value setting is the current position of the "thumb" (or
"elevator"). If value is equal to minimum, the thumb is all the way to the left (or top on a
vertical scrollbar). If value matches the scrollbar's maximum, it's all the way to the right
(or bottom).
To scroll a picture, you simply draw your picture in your canvas, offsetting the
coordinates by the horizontal and vertical amounts of the scrollbar thumb (the value
setting). Each time the user scrolls the picture, you redraw it with the new coordinates.
It's simple!
There are just three parts to this process. First we need to initialize the scrollbars. For
some applications, this would work best in a method, since the size of your picture may
change while the program is running (i.e. if the user is allowed to open a new picture). In
our demonstration case, we'll just put our initialization stuff in window1's Open event:
Code for Window1.Open:
// Here we set the size of the picture's scroll bars
// to the size of the picture
hScrollBar.maximum = islandsunset.width - hScrollBar.width
vScrollBar.maximum = islandsunset.height - vScrollBar.height
// Center the picture
hScrollBar.value = hScrollBar.maximum \ 2
vScrollBar.value = vScrollBar.maximum \ 2
Our project has a picture called islandsunset dragged into it: we therefore get its size
and use that to set the maximum scroll position of our scrollbars.
In Detail
Notice, however, that we subtract from that size the size of the scrollbar: why do we do
that?
For instance, couldn't we just say:
hScrollBar.maximum = islandsunset.width
vScrollBar.maximum = islandsunset.height
Doesn't that seem to make sense? But if we do the above, we'll cause something strange
to happen. The code will allow the user to do this:
See how the user scrolled past the end of the picture to display white? That's rather odd
and ugly.
Remember, maximum represents the largest number of pixels we can scroll the picture. If
we set it to the full width of the picture -- say 1000 pixels -- and the user scrolls all the
way, the picture will move 1000 pixels. But we draw the picture starting at the upper left
corner -- that means we'll be drawing the picture -1000 pixels to the left of the canvas:
the canvas itself will display nothing!
This is an important and often confusing point. Perhaps the following diagram will
explain it more clearly.
As you can see, our scroll area is not the full size of the picture, but the size of the picture
minus the size of our canvas. This will allow the user to scroll and see the entire picture,
but never more than the picture.
After we've initialized our scrollbars, we must add a bit of code to the scrollbars
themselves. Here all we want to do is tell the picture to redraw if the scrollbar has been
moved. Scrollbar controls have a valueChanged event, so all we have to do is put this
line of code inside that subroutine:
canvas1.refresh
(Remember, you have to do this for each of the two scrollbars.)
What does canvas1.refresh do? Simple: the refresh command tells canvas1 to
redraw itself. That's it!
In Detail
Have you ever had a dialog box pop up and cover up what you were working on? Did
you notice that when the dialog box goes away, the stuff behind the dialog is redrawn?
Mac programming traditionally has a refresh event which is a program's way of saying
that all or some portion of a window needs to be redrawn.
REALbasic usually handles refreshing your program's content automatically. (All of RB's
built-in controls automatically refresh when appropriate. You generally don't need to
worry about it.)
In the above case, you are simply telling canvas1 that it needs to be refreshed.
But what does canvas1 do when it redraws itself? Let's look at the code inside the Paint
event.
g.drawPicture islandsunset, -hScrollBar.value, -vScrollBar.value
Unbelievably simple, isn't it? Yet those few lines of code have given us a picture inside a
frame which we can scroll around.
How does the above code work? Well, g is canvas1's graphics object, i.e. the place
where stuff is drawn. A graphics object has a number of drawing commands, such as
routines to draw lines, rectangles, circles, etc. One of those is the drawPicture method.
The drawPicture method is powerful (and thus complicated), but we're only using it at
the simplest level here. (For instance, you could use extra parameters for drawPicture to
resize the picture you are drawing.) In our case, we're working with just three parameters:
a picture object and the horizontal and vertical coordinates of where to draw the picture.
Our picture is islandsunset, an image we dragged into our project. For our drawing
coordinates, we use the negative of our scroll positions. Remember, (0, 0) represents
the upper left corner of canvas1. Thus negative numbers draw the picture further to the
left or up, revealing more of the right and bottom of the picture. The farther we scroll, the
larger the scroll value, thus the further left (or up) we start drawing the picture and the
more right and bottom of the picture is displayed. Simple!
Disclosure Triangle
Now that we've finished with our canvas example, let's move on. We've covered the
editField in previous columns, so we won't discuss it here. Instead look at the two
triangle objects on our Tools palette. What are they?
The first is a black triangle and is known as a popupArrow. It's an extremely simple
control: the only thing you can set is its location and the direction the triangle is pointing.
What good is it? Well, you can use it for several purposes. For instance, if you have a
floating palette, the triangle can indicate a popup submenu like this:
(This is from Z-Write, but such menus are found in many programs, such as Adobe
Photoshop.)
A more common control is the disclosure triangle. Many programs use this to show
or hide advanced options. Hiding advanced options can be less confusing.
Apple's Remote Access control panel uses a disclosure triangle control to toggle
between simple and complex modes.
But how do programs hide and reveal portions of the interface? If you're new to
programming, the answer isn't obvious, but it's remarkably simple.
In our demo project, we've got a disclosure triangle which will hide or show the
"music" portion of our demonstration program.
RBU Demo with a portion of the interface shown or hidden.
How does the disclosure triangle do this? Well, actually, it does nothing in and of
itself. But since we can detect whether the triangle is pointing up or down, we can change
the state of our interface accordingly. To hide the bottom portion of the window, all we
have to do is shorten the height of the window! The controls are still there -- they don't
move -- but since they are off the bottom of the window, they aren't visible!
Here's the code for disclosureTriangle1's Action event:
if window1.height = 300 then
window1.height = 255
musicLabel.text = "Show Music Player"
else
window1.height = 300
musicLabel.text = "Hide Music Player"
end if
All this does is check to see what is the current height of the window and toggle it. Then
we change the name of our staticText item to "show" or "hide" as appropriate. Simple,
but it works great!
Well, that's enough for this week. Next week we'll learn about BevelButtons,
ContextualMenus, a QuickTime movieplayer, and other exciting elements.
Next Week: REALbasic Basics II
RB Quick Tip of the Week
If you're loading thousands of items into a listbox it can take quite a while to fill it. To
greatly speed up the process, set it to invisible while filling it.
Try this: create a new project, drag on a listbox, a checkbox, and a psuhbutton. In the
Action event for the pushbutton, put the following:
dim i, tt as integer
if checkBox1.value then
listBox1.visible = false
end if
tt = ticks
listBox1.deleteAllRows
for i = 1 to 3000
listBox1.addRow "Line " + format(i, "000#")
next
listBox1.visible = true
msgBox "That took " + str(ticks - tt) + " ticks!"
Now run the program and click the pushbutton with the checkbox checked and
unchecked. See the difference in time? (Remember, one tick is 1/60th of a second.) When
the checkbox is checked, the listbox will be set to invisible while it is filled, otherwise it
will be visible and much slower.
Time, in ticks, to fill a listbox that's visible (top) and invisible (bottom).
Letters
Our letter this week comes from Ben, who doesn't like the Watch cursor that's displayed
in RBU's Shooting Gallery sprite example.
i've noticed that when a spriteworld is running, the cursor is a waitwatch. that seems
somewhat unelegant. is there a way to just tell the cursor to go away, as in a game in
which a sprite is controlled by the mouse? thanks
Well, Ben, REALbasic doesn't have a way to directly hide the cursor (that's normally
handled automatically by RB). But you could try this: create a blank cursor in ResEdit
and drag it into your project. To make this easy for you, I've created one called
blankCursor and you can download it here.
Simply put in a line that says me.mouseCursor = blankcursor where you want the
cursor hidden (such as within the spriteSurface object). That should do the trick! (Don't
forget to reset the cursor back to arrowCursor when you're done.)
(BTW, another method that might work is to disable wait cursor using the
DisableAutoWaitCursor pragma directive. See RB's online help for more.)
.
REALbasic University: Column 022
REALbasic Basics II
Last week we learned how to display a picture in a canvas and use scrollbars to allow the
user to scroll the picture. This week we'll continue exploring more of REALbasic's builtin interface controls.
The Slider Control
If you're interested in programming, you've probably heard about Cocoa, Apple's
programming environment for Mac OS X. Cocoa is based on the programming
environment for the NeXT computer, which was renowned for being easy to use. Many
years ago I went to a computer show and got to see the brand-new NeXT computer in
person. I even took a little time to play with programming it, and to my surprise, I found
it incredibly simple (even with no documentation or assistance).
The task I set out to do is something that seems like it should be simple: create a slider
control that changes a number setting when you drag the slider. Such a control is easier to
use than typing in numbers, and it's a very common Mac interface. With traditional Mac
programming, however, creating that is a very complicated thing. On the NeXT,
however, it was ridiculously simple: I didn't even need to write a single line of code! I
just dragged a slider to a window and linked it to a text field.
When I discovered REALbasic, one of the first tests I did was to try to duplicate the same
slider control. While REALbasic wasn't quite as easy as that NeXT implementation
(RB required a few lines of code) it was so easy it convinced me to purchase REALbasic.
If you look in RBU Demo, the project I started on last week, you'll find an area called
"Slider Test". Select the slider1 control and go to it's Open event. Here we're just going
to put in the following line:
me.value = 75
All this does is move the slider's thumb to the 75 position (as though the user dragged it
there). It does this even before the control is displayed, so the user won't see the change
from the default zero position.
In Detail
Here's a question for you: since you can set a slider's value via the Properties Palette,
why set it manually in the slider's Open event?
The answer is that we want the slider's value to be reflected in the contents of
editField1. The contents of editField1 are set whenever slider1's value changes, but
if you set the value via the Properties Palette, slider1's ValueChanged event never
happens. Thus, when you launch the program, editField1 would not contain the proper
number until the user drags the slider. You could preset editField1's content to the same
number as slider1's value, but that means you're manually setting the value in two places
and so you must be careful to make sure they match. The easiest way is to just set
slider1's value programmatically, via code.
Now go to slider1's ValueChanged event. Here put in this code:
editField1.text = str(me.value)
This converts the number of the value of slider1 to text, and places that text into
editField1. (Remember, me represents the current control, which is slider1, so
me.value is the same as slider1.value.)
Now the above is all you need to set the text of an editField (or staticText) to a
number that matches the setting of a slider control. As I mentioned, it's pretty simple.
But this is only a one-way link -- slider to editField. How about bi-directional link:
editField to slider? That way if the user types in a number into the editField, the
slider will change accordingly?
It's not that difficult, but it does require a little more code. Within the Code Editor, find
editField1 and go to the TextChange event. Put in this:
// Convert contents to a number
me.text = str(val(me.text))
// Set slider1 to this number
if val(me.text) > 0 then
slider1.value = val(me.text)
end if
Our first step looks a bit odd: what we are doing is making sure that the contents of
editField1 are really a number. We convert whatever text is there to a number with the
val command, then convert that back to a string (text) and set editField1 to that text.
Why do that? Well, if there's not a number there, or some text that's not part of a number,
this will effectively get rid of it and only save the valid number portion. For instance, if
"3b0" is the contents of the editField, it will be converted to just "3".
Our next step simply sets slider1's value. We first check to make sure the number is
larger than zero: that's because if the val function doesn't work for some reason, it
returns a zero. Since the range for slider1 is 1 to 2000, a zero isn't allowed. If it's not a
zero, we set slider1.value to the number.
So now our controls are linked in two directions: typing in editField1 sets slider1 to
that value, and vice versa. But it does look ugly to allow the user to type in letters in a
field that only allows numbers. Let's fix that.
Go to editField1's KeyDown event and put in this:
dim n as integer
n = asc(key)
// Allows delete key and cursor keys to pass through
if n = 8 or (n > 27 and n < 32) then
return false
end if
// Only allows numbers
if (n < 48) or (n > 57) then
return true
end if
Wow, what does all that mean? First, we convert key, which is the character the user
typed, to a number using the asc function. (We don't have to do that, it's just easier to do
math on a number.) We save this number as n.
Now we could just see if the user typed a number and if they didn't, don't allow the
typing. But there are flaws in that algorithm. That's because the user could have typed
some important "invisible" characters. For instance, the delete (backspace) key, or
perhaps an arrow key to move the cursor. Those are real characters, just like an "a" or "7"
or "%". If we block those characters, the user won't be able to delete mistakes or move
the cursor without the mouse!
So the algorithm we use is this: first we check to see if they typed a valid invisible
character. If they did, we allow that to pass. Otherwise, if what they typed isn't a number,
we block it.
How do we do this? Well, we check to see if n is either an 8 or a 28, 29, 30, or 31. The
delete (backspace) key is ASCII number 8, while the arrow keys are numbers 28 through
31.
In Detail
How do I know what ASCII values match the delete and arrow keys? Well, I know it
from experience and memory, but you could also find out for yourself. Just drag an
editField onto a blank RB project, option-Tab to open it's KeyDown event, and type this
in:
msgBox str(asc(key))
That will display a dialog box with the ASCII number of the key you typed. Write down
the numbers of the keys you need to know.
Note that once we've decided that a character typed is a valid one, we must allow it. We
do this with the return false line. How does that work?
Well, the KeyDown event is a function: it returns a boolean value. The REALbasic default,
if you don't specifically return a value, is false. The KeyDown event interprets a false as
being that you didn't handle the character yourself and you want the character to pass
through and be displayed in the editField. Thus, a return true would block the
character, but a return false allows the character.
As always, when a function encounters a return command, it stops executing at that
point and returns to the calling point. In other words, it won't process any more of that
routine. Thus when it hits the return false, it's done with the KeyDown event.
To block the non-number characters, we simply check to see if the typed character is less
than 48 or greater than 57 (48 is a zero and 57 is a nine). If it is, we return true to
block it. If the character is a number, it won't get blocked, and when REALbasic reaches
the end of the routine, the default false is returned, preserving the number typed.
Extra Credit
By the way, this doesn't block everything: we haven't blocked what a user could paste in
from the clipboard. Since RB handles the clipboard automatically, the user could
conceivably paste in letters.
But our method of resetting editField1 to whatever number it contains fixes that,
though not as elegantly and not allowing pasting if the clipboard contains letters. For
extra credit, can you figure out how to do that?
BevelButton Controls
Let's take a look at another REALbasic control, the BevelButton. It's a rather odd
control in some ways: it's a cross between a PopupMenu and a PushButton. Depending on
how you program it, a BevelButton can take on multiple appearances and functions.
In the BevelButton Test area of RBU Demo, I've placed four BevelButtons with code to
demonstrate their features.
The first is functionally a simple PushButton. Except it includes an icon. You can
programatically set the position of the icon and the caption. BevelButton3 is similar,
except that it has no icon and is a "toggle" button: it stays down when you push it, then
pops up the next time you push it.
BevelButton1 and BevelButton3 include no programming code: they are defined
entirely via their settings on the Properties Palette.
BevelButton2, however, includes some code. It is set to have a popup menu, so we use
the button's Open event to define the menu items and set what happens when the user
chooses one of them.
BevelButton2's Open event is very simple:
me.addRow "Bevel 0"
me.addRow "Bevel 1"
me.addRow "Bevel 2"
This just adds three menus, named as shown. When the user chooses one of these items,
we then set the "bevel" of the button to the appropriate amount (0, 1, or 2). This is the
code for BevelButton2's Action event:
me.bevel = me.menuValue
Neat, eh? All this does is set the button's bevel property to 0, 1, or 2. The effect is
immediately viewable by the user.
Notice that BevelButton2's menu drops down below the button? BevelButton3's menu is
displayed to the right of the button. That's a setting from the Properties Palette.
For BevelButton3, I thought we'd make it do something a little more useful, like display
a list of fonts installed on the machine. So the Open event loads the popup menu with font
names:
dim i, n as integer
n = fontCount - 1
me.addRow font(0)
for i = 1 to n
me.addRow font(i)
next
fontCount is a special REALbasic command that returns the number of fonts installed.
The font function looks like an array, and in a sense it is, though it's one built in to
REALbasic. It basically wants the index number of a font and it returns a string. So
font(0) returns the first font installed, font(1) the next, etc. Because font starts at
zero, we must subtract one from fontCount to end up with the proper index for the last
font. (The command font(fontCount) would return an error while font(fontCount-1)
would be okay.)
What happens when a user chooses a font from the menu? Simple: we change the font of
the BevelButton to match the font selected!
Put this in the Action event:
me.textFont = me.list(me.menuValue)
Again, me is the current control (which is BevelButton4). MenuValue is the number of
the menu choosen, say menu 27 (the 28th font -- remember, the first is zero). A
BevelButton's List is a zero-based array of text items which contain the text of the menu
items (i.e. the font names).
So what the above convoluted mess does is translate the number of the menu chosen to
the actual text of that menu. Since that's the font's name, we can use that to set the
textFont property of the BevelButton. It's really fairly simple, but we must do it this
way because a BevelButton only tells us the number of the chosen item, not the text
value. (PopupMenu controls have properties that give us both: ListIndex, the menu
number, and Text, the menu text.)
That's enough for this week. We've got plenty more basics to cover in our next segment,
where we'll learn about tabPanels, contextual menus, and QuickTime movies.
Next Week: More REALbasic Basics.
Letters
This week's letter is from Stefaan Degryse, who writes:
Hello Marc,
Well your 'Quick Tip of the Week' for the last column (faster listbox fill-up) looked very
promising so I tried it out.
But I discovered some things which make me wonder.
If you run the example project and leave the checkbox unchecked then you get a high
time, but no where near 261 ticks (I got around 35 ticks). When checking the checkbox it
indeed goes faster (about 16 ticks).
But now it comes. Uncheck the checkbox again and the listbox fills up as fast as when the
checkbox is checked.
If you do the procedure the other way around (first checked and then unchecked) you just
will get the same results.
Conclusion. It seems that RB does some initializing of the listbox when it's filled for the
first time.
But not all is 'lost' though. It's a great trick for multi-column listboxes where one typically
uses the following code-snippet
ListBox1.AddRow some stuff for column 0
ListBox1.Cell(ListBox1.LastIndex, 1) = some stuff to put in listbox
ListBox1.Cell(ListBox1.LastIndex, 2) = some more listbox thingies
Anyway thought you might wanna know this, and keep them RBU articles coming ;-)
BTW I tested this on a G4 at home and an iMac at work with RB 2.1.2 and RB 3.2
Interesting results! I got the same when I retested: I'm not sure what's going on. The
initial fill-up does take longer, but once the box is filled it's faster. Very odd. I have
noticed some strange listbox stuff with REALbasic. For instance, putting a listbox on a
floating palette type window takes longer to fill than on a regular window! Why that
would be, I don't know, but I ran into that slowdown and this fix while attempting to
optimize one of my programs.
Thanks for the feedback, Stefaan. Keep those letters coming! I may not answer your
question immediately, but I'll hopefully get around to in a future column. (If you don't
want your correspondence published, just be sure to indicate that when you write.
Otherwise I'll assume it's fair game.)
REALbasic University Quick Tip
To stop automatic listbox column sorting in a listbox that has headings, put return true
in the sortColumn event.
Before, a user could sort the column; now
they can't.
.
REALbasic University: Column 023
REALbasic Basics III
Last week we worked with Sliders and BevelButtons. This week we explore TabPanels,
GroupBoxes, Contextual Menus, and learn how to add a QuickTime movie to your
REALbasic project.
The TabPanel Control
When you've got to present many options to a user -- for instance, a zillion preference
settings for your program -- you could group releated items and create a separate dialog
box for each type preference. But that's confusing for the user, since they must search
through multiple dialog boxes to find the option they want to change.
A better system is to use a TabPanel control: it allows the user to jump between multiple
sets of interfaces all in the same dialog. A good example is Apple's Appearance control
panel:
By selecting the appropriate tab at the top, only interface items relating to that category
are displayed.
For our sample project, RBU Demo, we've got a TabPanel already created with three
panels and various items on each panel.
Working with TabPanels within the REALbasic IDE can be confusing. Generally they
work just like they do when the program is running: clicking on a tab switches to that
tab's contents. But if you have a control already selected on another tab, it "shows
through" as an invisible selected item.
In the above picture I've got the StaticText from the first tab selected. That's confusing,
since it's not on this tab. My advice is to always deselect all objects before you switch
tabs so you don't accidentally modify the wrong item. (If the StaticText in the above
were a little lower, I might think I had the StaticText in this tab selected.)
Another problem with TabPanels is that they aren't entirely reliable. Bugs mean that
some controls can bleed through and affect others. For instance, our example has a
QuickTime movie on the third tab, and if you play the movie and then switch to a
different tab, the movie continues to play on top of the wrong tab:
In general, use TabPanels for simple interfaces and don't try to get too complicated. In
our example, we show how to programatically set the tab that's displayed. In TabPanel1's
Open event is the following line:
me.value = 2
That simply sets the TabPanel to the third panel (remember, panels are numbered starting
at zero).
The LittleArrows Control
The first tab of our example just has a simple StaticText and a CheckBox. But the
second tab shows how to use a strange little control called LittleArrows.
LittleArrows are used by the Mac OS in many places where it's easier to click to
advance or reduce a number rather than type. For instance, in the Date & Time control
panel:
Our example's going to do something similar, except with a larger number and a
StaticText. When the user clicks the down arrow, we'll reduce the number. When they
click the up arrow, we'll increase the number. (To make it even more fun, we'll increase
or decrease the number by 10 when the Option key is held down while clicking.)
Here's the code for the Down event:
dim n as integer
n = val(staticText1.text)
if keyboard.optionKey then
n = n - 10
else
n = n - 1
end if
staticText1.text = format(n, "-00#")
staticText1.refresh
First this converts the contents of staticText1 to a number. Then we check to see if the
Option key is down. If it is, we subtract 10 from the number. Otherwise we subtract 1.
Our final step is to put the new number back into staticText1. We use the format
function instead of str because we want the number to have leading zeroes (that way the
number is always the same number of characters).
We also refresh staticText1 to force it to draw the update. Why do that? Well, if you
don't force it to update, the new number won't be drawn until the user lets go of the
mouse button. When I tried that, the number incremented to several thousand in the
fraction of a second while I clicked down the button! This way after each increase or
decrease, the staticText is redrawn, showing the user the current value.
The Up event code is identical except it adds instead of subtracts.
Adding a QuickTime Movie
The third tab of our TabPanel is where we'll place a tiny QuickTime movie. I've created
a small movie and included it with RBU Demo. Just drag it into your RB project to add it.
Using the Properties Palette, I've preset the movie controller to automatically be ready to
play my movie, "simple.mov".
But having a built-in movie isn't always enough. Wouldn't it be nice if the user could drag
in their own movie to play it? How about an MP3 music file? Sure, why not!
First, let's define two types of files so our program knows how to work with them. Go to
the Edit menu and select "File Types...". In the dialog that comes up, click the "Add..."
button.
On the right of the first field is a popup menu. You should be able to select the predefined
QuickTime movie file type like this:
Selecting it fills in the fields for you automatically. Click okay and do another one, the
MP3 file type.
If you don't remember doing this in our previous lessons, all we are doing is naming
some Mac file types. That way REALbasic knows what kinds of files our program will
work with. Our next step is to tell our movie controller that it should accept file drops of
this type.
Go to the Open event of moviePlayer1 and put in:
me.acceptFileDrop("audio/mp3")
me.acceptFileDrop("video/quicktime")
Next, go its DropObject event and put in the following:
if obj.folderItemAvailable then
me.movie = obj.folderItem.openAsMovie
me.play
me.refresh
end if
All this does is see if a file has been dropped on the control. If so, it tries to open it as a
movie and then play it. The refresh command is important because it makes sure the
controller is resized if needed.
This allows the user to drop QuickTime movies or MP3 files onto the control to play
them. It's not super sophisticated, but it works just fine. Going from this to writing your
own MP3 player is not a huge leap!
Adding a Contextual Menu
Another great interface tool of modern computing is the ContextualMenu. These are
hidden menus that popup when a user clicks the mouse while holding down the Control
key. Ideally they live up to their name and are actually contextual: that is, they change
depending on the user's current situation.
REALbasic has a pretty good implementation of contextual menus, and they're easy to
use. The basic process is to drag a ContextualMenu icon onto your window, create some
code in a mouseDown event that dynamically creates the menu, and add code to the
menu's Action event for whatever the menu's supposed to do.
For our example, since we don't actually have anything particularly significant for the
menu to do, we simply want something that demonstrates the dynamic nature of the
control. So what I decided to do is change the menu text according to what area of the
window the user clicks upon. For instance, if the user Control-clicks within the "Scroll
Test" area, the contextualMenu will be "Scroll Test item".
To do this, we need some way of tracking where the user currently is. This isn't difficult,
but it does require a few steps. First, we must create a new Window1 property, cmenu.
This is a string variable which will contain the text that will become the contextual menu.
So with Window1 open, go to the Edit menu and choose "New Property." Type in "cmenu
as string" in the dialog that pops up and click "OK".
Now we need to establish a default value for cmenu, so let's add this to whatever code's
already in Window1's Open event:
// Default contextualMenu1 menu item
cmenu = "Window item"
Next, we must change cmenu as the user moves the mouse around. This is easily done
since the only controls we're concerned about are the GroupBox controls. There are four
of them, and I built them as a control array (meaning that they all share the same
code).
Go to GroupBox1's MouseEnter event and put in the following:
cmenu = me.caption + " item"
Since the me function is always the current control, it will be whichever GroupBox1 the
user is entering. So cmenu will be set to that GroupBox's caption, plus the string " item".
Next, put this in GroupBox1's MouseExit routine:
cmenu = "Window item"
This is basically just a default value: if the user clicks outside any GroupBox, the
contextual menu will just say "Window item".
Since we want our menu to work anywhere inside Window1, we'll put our contextual
menu code inside Window1's MouseDown event:
if isCMMClick then
// Build the menu
contextualMenu1.deleteAllRows
contextualMenu1.addRow cmenu
contextualMenu1.open
end if
This first checks to see if the user Control-Clicked: if they did, then we build
contextualMenu using the contents of cmenu as the text for the menu, and open it (which
displays it).
That's it! If you run RBU Demo now, you should be able to Control-Click to bring up a
contextual menu where the contents of that menu will change depending on where you
click.
In Detail
Did you notice that we put no code within contextualMenu1? That's because, for this
example, we're not really doing anything with the menu, just displaying it. In your own
program, you'd obviously want to do something when the user selected one of the menu
items.
To do that, you'd add some code in contextualMenu1's Action event. For instance, you
could use a select case construct like this:
select case item
case "Window item"
msgBox "This is the default menu item."
case "Scroll Test item"
msgBox "You chose the Scroll Test menu!"
else
msgBox "You chose menu " + item + "."
end select
See? It's pretty simple. Note that if your contextual menu is duplicating a menubar
command, you'll need to put your menu handler's text in a method so that you can call
that method from both the contextual menu's Action event and the menu handler
(otherwise you have to duplicate the code which makes it difficult to edit).
That's enough for now. Next we'll finish up with RBU Demo and move on to some other
RBU Basics, like how to incorporate classes others have written into your own projects.
Next Week: More Basics, including instructions on using pre-built classes.
REALbasic University Quick Tip
To stop a listBox heading from staying pushed down, put
me.headingIndex = -1
in the listbox's sortColumn event.
Letters
This week we hear from Jimmy Isaacson:
Dear Marc:
First let me say that I really enjoy your REALBasic lessons. They are paced just right for
my hectic schedule. Now I have a question and a suggestion.
First, I got the image scrolling to work but as it scrolls there is an annoying blink while
painting successive images. In an effort to confine the redrawing to just the portion of the
image that is affected by each step in the scrolling process, I changed the call to the
drawPicture method to the following:
g.drawPicture islandsunset, 0, 0, hScrollBar.maximum,
vScrollBar.maximum,
hScrollBar.value, vScrollBar.value, hScrollBar.maximum,
vScrollBar.maximum
But it still blinks when scrolling. Can you suggest a change or addition to the program
that will stop the blinking?
Now for the suggestion. While reading the portions of the lesson that are printed on a
solid yellow or red background rectangle, my eyes become very tired and I have a strong
after-image when the background color returns to the normal white. Could you perhaps
frame those important sections in color instead of printing them on solid and very bright
colors?
Thanks again for what you are doing with the REALBasic University.
I think the scrolling flash you are noticing is just a limitation of REALbasic. I played
around with things a bit to see if I could minimize it, and I saw no improvement when I
had RB just redraw the changed area. What I did notice is that the flashing only happens
when you're using the arrow buttons of the scroll bars. When you drag the "thumb" of the
scroll bar, the picture scrolls perfectly smoothly, with no flashing. That makes me think
that the problem is not with the scrolling code or RB's ability to redraw quickly, but with
how frequently RB updates the picture when you're holding down the mouse button to
press an arrow button.
(If anyone else has a solution, let me know!)
Thanks for the suggestion about the colored boxes, Jimmy. I'll see what I can do. (I'm
experimenting with the idea of creating my HTML formatting with Cascading Style
Sheets, so I'll implement your idea when I do that.)
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
REALbasic University: Column 024
REALbasic Basics IV
This week we finish our "Basics" column -- I hope you're finding these helpful. We've
explored how to use a variety of controls, so there should be something for everyone. For
the music lovers out there, today we'll look at the NotePlayer control, and in the process,
we'll learn about progress bars and other controls.
Creating Music with REALbasic
I'm not a musician, so don't expect this tutorial to generate real music: we'll just use the
NotePlayer control to generate some random sounds. But it will demonstrate how to use
the control; if you're musically inclined, you could use my example as a starting point to
generate a song.
We'll need several interface elements for this example. As before, the complete sample
project, RBU Demo, is available for downloading. If you're creating this from scratch,
you'll need to drag several items onto Window1: we'll need a pushButton, a
progressBar, some chasingArrows, a notePlayer, and a Timer. Arrange them at the
bottom of Window1, something like this:
Note that it doesn't really matter where you put the notePlayer and Timer, as those are
invisible controls (the user won't see them). REALbasic just gives you icons so you have
something visual to look at, and to allow you to put code inside the controls.
Once you've got the basic controls, let's focus on PushButton1. Set its Caption property
to "Start Music" and put this code in its Action event:
// Disable self
me.enabled = false
// Reset the progress bar
progressBar1.maximum = (rnd * 500) + 100
// Show chasingArrows1
chasingArrows1.visible = true
// Start a "song" playing
timer1.mode = 2
First, we disable the button. We do this because we don't want the user to be able to push
it again while the "song" is playing. Next, we initialize the progressBar to a random
maximum value: that's going to be the length of the song.
Then we make chasingArrows1 visible. That's pretty much all you can do with chasing
arrows: when they are visible they spin, otherwise they do nothing. You use them to
show that a process is happening. They're useful for a process that will take a while but
you aren't sure how long. (If you have a way of calculating the length of the process, a
progress bar is better since it gives the user an idea of how much time is left. But chasing
arrows are better than nothing, because they at least show the user the computer isn't
frozen.)
Finally, we start timer1 by setting its mode property to 2. A timer that's set to mode 0 is
turned off. If you set it to 1, it will be execute the code in the timer just once and then
turn itself off. With mode set to 2, it keeps calling itself over and over, as often as you've
set the period property. (The period property is in milliseconds, so 1000 is 1 second. For
this example, I have it set to 100, which is ten times per second.)
It's interesting to note that both NotePlayer1 and progressBar1 have no code inside
them: they're just controls we set programatically (via code), but don't need to interface
with the user. So though we added a bunch of controls, the only other one that needs code
is Timer1. All it has is an Action event, so put this there:
// Set the notePlayer1 to a random instrument
notePlayer1.instrument = (rnd * 127) + 1
// Play a random note
notePlayer1.playNote(60, 60)
// Increment the progress bar
progressBar1.value = progressBar1.value + 1
// if we're at the end, stop
if progressBar1.value = progressBar1.maximum then
// Stop note playing
notePlayer1.playNote(60, 0)
// Turn off timer
me.mode = 0
// Hide chasingArrows1
chasingArrows1.visible = false
// Renable start button
pushButton1.enabled = true
// Reset the progress bar
progressBar1.value = 0
end if
This is the part of the program that actually plays the "music." It really isn't much of a
song -- we just play middle C in a random instrument. (See REALbasic's online help for
information on which number corresponds to which instrument.)
The playNote command needs two parameters: the note (a number from 0 to 127 where
60 is middle C) and what RB calls the "velocity" (0 to 127) and is how hard the key is
pressed (sort of how loud it is -- change it to 127 and run the program and you'll see). If
you want, you could make the program randomly choose a note. Replace the first 60 with
round(rnd * 127) like this:
notePlayer1.playNote(round(rnd * 127), 60)
The next line increments progressBar1's value property: that's where the bar graph of the
progress bar is currently located. The progress bar control itself takes care of all the
percentage math and drawing of the bar -- all you do is set the current position and the
maximum amount. In our case, the maximum represents the total number of notes we'll
play, so we increment value by one for each note played.
Finally, we check to see if we're at the end of the "song" (value = maximum). If we are,
we stop playing the note, turn off Timer1, hide the chasing arrows, enable the "Start
Music" pushButton, and reset progressBar1 to zero.
In Detail
Actually using a NotePlayer control to create real music is a little more complicated.
You could just put in a series of playNote commands, each with the appropriate note
number and volume, but that's not very intuitive (it also produces very simple music).
A better approach would be to write your own music creation program. It might give you
a piano key interface and "record" the notes you play for the length you hold down each
key. Then later you could play back your complete song. The song data would be in a
format that your program could decipher (perhaps an instrument number, a note number,
velocity number, and a length of time number), so you could save it and play it back later.
If people are interested, I might consider doing a program like that for an RBU project.
Let me know!
That's it for RBU Demo. Feel free to tear it apart, change values and see what happens,
and experiment with it. Since it demos many of REALbasic's controls, it's a good
learning project. There are a few advanced controls, such as sockets, serial control,
and databaseQuery object that I didn't demonstrate. We'll have separate advanced
tutorials in the future for those.
REALbasic Aesthetics: the Look of Code
Since today's tutorial is such a miscellaneous one, I'm going to take this opportunity to
talk about something I've wanted to mention but hadn't found the appropriate
opportunity: the aesthetics of code.
As you know, REALbasic automatically formats certain aspects of your code for you. For
instance, it automatically indents loops and if-then constructs, and it colorizes
keywords. But there are many other aspects to how your code looks that REALbasic
gives you free reign. For instance, you're free to add extra spaces, enclose parenthesis
around single parameters (or not), and name your variables (properties) as you wish.
If you're writing code only for yourself, it doesn't make that much difference how you
format your code, but if others are expected to read what you write, it's best to follow
some standards. Every programmer should develop a particular "style" and follow that
style consistently. Which style you follow isn't as important as being consistent:
inconsistency leads to confusion. Even if others aren't going to see what you write, it's
best to write clear, well-formatted code so that you yourself can understand it. You might
think that's not a problem, but believe me, if you come back to a program a couple years
after you wrote it, you'll be glad for those comments and formatting.
I'm going to detail the style I use; feel free adapt it to your own needs. You're certainly
under no obligation to do what I do, though I find it helps make my code more readable.
Case
I don't like uppercase letters -- they draw undo attention to keywords and variables. It's
also not necessary for keywords since REALbasic colorizes those. So the style I use is to
keep all REALbasic code in lowercase. For variable and procedure names, I begin them
with a lowercase letter but uppercase the other words in the name.
For instance, I'd write myVariable, theVariableName, and gThisGlobalVariable just
like you see them.
Uppercasing the second, third, and fourth word in a name is a good practice since it
makes those words stand out, and you can't use spaces in a variable name. Another
method is to use an underscore instead of a space to separate words, like
the_variable_name or the_computer_score (you could keep things lowercase if you
did that).
Be consistent with your use of case -- it makes your code significantly easier to read,
especially when it comes to the names of routines and variables (your mind will think
playerName and PlayerName are different but to REALbasic they are identical in
function).
Names
Speaking of variables, there are some standards for how to name them. I'd recommend
you take some time when naming your variables: come up with names that accurately
represent what the variable is, and try not to name items with similar names (that's
confusing and it defeats REALbasic's auto-complete feature).
For instance, theList() is a vague name for an array, but theAddressList() is much
better. Don't be afraid of using long, descriptive names for your variables: REALbasic
finishes the typing for you, so you never have to type more than the first few letters
anyway.
The same logic applies to procedures and functions: I like to name functions things like
returnTitle() or convertStringToHTML().
There are also some standard conventions you should use when naming variables. For
instance, it's a long-standing tradition that global variables are proceeded by a lowercase
"g". Another one is the preceed constants with a lowercase "k". The following are
"proper" names for variables (assume the first three are global):
gScore as integer
gTheDate as date
gPreferenceArray(6) as string
const kPi = 3.14
const kPar = 72
const kPicasPerInch = 6
You can also make up your own conventions for naming variables. For instance,
preceeding strings with an "s" or arrays with an "a". One of my personal ones is the
define "hot help" strings with an "h" like hSaveButton as string.
Another technique I use, especially appropriate for complicated object-oriented code, is
to define custom classes with the word "class" in the name. For example, let's say I'm
writing an accounting-type program that's going to keep track of clients and the projects
done for each. By defining the following custom classes, it's easy to understand what
information each client record will contain:
clientRecordClass:
company as string
contact as string
address as addressClass
phone(0) as phoneClass
history(0) as projectClass
notes as string
projectClass:
projectStartDate as date
projectEndDate as date
projectName as string
projectNotes as string
projectCosts as costClass
phoneClass:
phoneNumer as string
phoneKind as string
addressClass:
line1 as string
line2 as string
city as string
state as string
zip as string
country as string
costClass:
projectedBudget(0) as costItemizationClass
actualCosts(0) as costItemizationClass
costItemizationClass:
itemName as string
itemCost as double
itemQuantity as integer
itemNotes as string
Because all of the above are classes, they are only definitions of a structure and contain
no data. The actual variable (property) that would contain the data would be named
something else (like clientRecordArray(0) as clientRecordClass) -- but naming
the classes the way I do makes it much clearer which items are classes (definitions) and
which are actual data.
Spacing and Blank Lines
Spaces and blank lines don't add much size to your code, but the definitely make it much
easier to read. Which of the following code snippets do you find more pleasing to the
eye? (The example code is from the REALbasic online help.)
dim i as integer
dim WindowTitle as string
if WindowCount>1 then
for i=0 to WindowCount-1
WindowTitle=Window(i).Title
ListBox1.AddRow WindowTitle
next
dim i as integer
dim windowTitle as string
if windowCount > 1 then
end if
for i = 0 to windowCount - 1
windowTitle = window(i).title
listBox1.addRow windowTitle
next
end if
I like to put spaces between variable assignments ( i = 0 ), between mathematical
elements ( i - 4 * 7 ), and between parameters and items within parenthesis:
graphics.fillRect(x, y, w, h). The extra spaces make code look a lot less dense
and it's easier to find things.
Blank lines between sections of code help divide things: notice how the for-next loop
above looks nicely grouped together. It's obviously not necessary for such a simple
routine, but when you've got a long, complex bit of code, a few blank lines make a big
improvement.
Comments
Commenting code is something we can all do strive to do better with. The key rule of
thumb to remember: you can never have too many comments. Even if your comments
are longer than your code, that's not too much. When you come back to a project a year
or decade later, you'll appreciate those notes. (This is especially true if you're an amateur
programmer and apt to make strange coding decisions that years later will be
incomprehensible.)
REALbasic supports three types of comments: // or ' or rem are valid commands that
cause the compiler to ignore everything else on that same line. (Rem is rarely used any
more, but included for BASIC compatibility.)
I have a personal style choice in that I use // for "real" comments and ' for temporarily
hiding code. For example, let's say I'm trying to debug a routine and get a particular
drawing routine to work. My code might look like this:
// Draw player's score
' staticText1.bold = true
' staticText1.textSize = 24
' staticText1.text = str(gScore)
g = gameWindow.graphics
g.textFont = "Geneva"
g.textSize = 24
g.bold = true
s = str(gScore)
x = g.left + g.width - g.stringWidth(s) - 10
y = g.top + g.textHeight + 10
g.drawString s, x, y
On the first pass of the program, I just had it draw the score into a staticText object,
but later I hid that and wrote the code draws it directly into the window's graphics port.
That let me do a rough approximation of what I wanted quickly, to get the program
working, and add in a more sophisticated routine later. Often I'll save the old code, at
least for a while, because I know it works and I might need to refer to it or even switch
back to it if my new code runs into trouble.
Another good comment habit to get into is to include a description of what a method does
at the start of the routine. This can be helpful for you in the future: you can quickly
determine the purpose of a particular routine without having to decipher the code.
For example:
dim index, numRecords as integer
//
// This method sorts the clientRecord() array by
// the client's LAST NAME.
//
numRecords = uBound(clientRecord)
(Notice how I combined this with the previous suggestion on spacing? I placed blank
lines before and after the description, as well as putting in blank comment lines to
highlight these important comment lines even more.)
Another advantage of adding a routine description comment is that since you can write
the description of what a routine is supposed to do before you actually know how to
program that, your thoughts are more organized and structured for when you begin
writing the actual code.
REALbasic lets you place comments at the end of a line of code like this:
x = (screen(0).width - x) \ 2 // This centers the drawing point
But I prefer not to do that as it makes lines too long. See how much cleaner this looks:
// This centers the drawing point horizontally
x = (screen(0).width - x) \ 2
Getting used to incorporating comments in your programs is a difficult but extremely
valuable skill: I highly recommend you start commenting all your code today.
That's it for our little lesson in coding etiquette. I hope you learned something and
develop some better programming habits as a result.
Next Week
We'll learn about incorporating a custom class into your own project, as well as how to
create your own custom class.
RB Quick Tip of the Week
Did you know you can drag objects from your project window into the Code Editor
window? For instance, if I drag Window1 into my Code Editor, REALbasic will "type in"
Window1.show for me! Drag a sound and it inserts in the name of the sound file with a
".play" after it.
This trick doesn't work for every kind of object, but it's fun and quick when you need it.
Letters
Our first letter this week comes from rcfuzz, who writes:
Hi, I just got the program and i can't figure out how to make an array global. Any help
would be appreciated. Also, how can you make edit controls return intergers instead of
strings.
To make an array (or any property) global, put it inside a module. Just go to the File
menu and choose "New Module" to add a module within which you can add properties.
As to the second part of your question, I'm not sure what you mean by "edit controls" -- if
you mean editFields, they only contain text, but you can convert the text to numbers with
the val function.
Next, we hear from Tauseef, who has a complicated graphics question.
Dear Sir,
I'm trying to develop a map application using realbasic using a vector map. Since I'm new
to Realbasic I am not very apet at going about the task. I'm facing a problem in making a
distance finder tool while the map and its different overlays have been plotted in the
window. I have used a line control and I hook its one end to my reference long lat and the
other end moves along with the Mouse move event. But when the mouse moves the
previous line position persists and the map becomes cluttered. If I refresh the window
map in the mouse move event handler the screen flickers a lot when the line is moving.
Please advise me a technique so I can go around this problem and also please advise me
on some resource that can guide me in multithreading using realbasic.
You're on the right track, Tauseef, but drawing a dynamic line like that is a bit tricky.
First, I wouldn't use the window's MouseMove event -- I'd incorporate the line drawing
into the canvas where the drawing is taking place. Best of all, create your own custom
canvas class that knows how to draw the map and what to do when clicked upon (draw a
distance line).
I've created a simple list demonstration application to show this: you can download the
project file here. This program simply draws a dynamic line from where you first click.
As you move the mouse around, the line is redrawn to the new cursor position. It looks
like this animation:
(I put those shapes in the background just so there'd be something for the line to move
over.)
Note that when you're in the REALbasic IDE there's a lot of flickering, but once you
compile a stand-alone application, there's little flicker.
As to how the program works, there are several parts. First, I created a custom canvas
class which I called canvasClass. Then in mouseDown event, I put the following code:
firstClick.x = x
firstClick.y = y
return true
This just saves the click point into a custom pointClass variable I created. By returning
true (instead of the default false), the mouseDrag event is allowed to occur. So in
mouseDrag I put the following:
if lastClick.x <> x or lastClick.y <> y then
lastClick.x = x
lastClick.y = y
me.refresh
end if
This basically just says, "If the cursor's moved since the last time we checked, set the
endpoint of the line to the current cursor position." Then we tell the canvas to "refresh"
(redraw) itself.
That, then, is the heart of the matter: the redraw portion of the canvas. In Paint event, we
must draw everything that the canvas might ever need to draw. So first we draw our
series of background objects in red, then we draw the line in black.
(Since firstClick is an object of a custom class, it must be created with the new
command before it is used, so I check to make sure it has been created before I attempt to
use it by making sure it isn't nil. If firstClick isn't nil, we draw the line from the
firstClick point to the lastClick point.)
// Draw a few objects so that we have a background
g.foreColor = rgb(200, 0, 0) // red
g.fillRect(40, 40, 50, 50)
g.fillOval(200, 200, 10, 50)
g.fillRect(400, 100, 25, 120)
g.fillOval(50, 200, 50, 50)
if firstClick <> nil then
g.foreColor = rgb(0, 0, 0)
g.drawLine(firstClick.x, firstClick.y, lastClick.x, lastClick.y)
end if
One important thing to remember is that items in your Paint event are drawn in the order
you write them, so you must draw you background stuff first, and draw your dynamic
line last, or else the line might end up behind your shapes!
We'll actually explore this more when we create our custom class next week: I want to
demonstrate how to create a custom class that allows the user to select an object.
As to your question about multi-threading, I don't know of any specific resource for that,
though both REALbasic books (you can find links to them on the RBU banner at the top
of this page) explore the topic. I plan to cover threads at some point in my tutorials, but
not in the near future.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 025
A New Look
You should notice some changes to REALbasic University this week. In my efforts to
better myself, bought a book on Cascading Style Sheets (CSS) and I've revamped RBU
by redoing my HTML as CSS.
(If you aren't familiar with CSS, it's a cool enhancement to HTML that allows you to
define a sheet of display styles for your content. Thus you can easily modify the look of a
web page or entire site simply by editing your style definition file.)
There are many benefits to CSS:
•
•
•
•
•
•
•
•
•
•
The column will have a more consistent appearance.
Pages are easier to maintain and update.
It's possible to change the display of text without changing a page's HTML.
I can make site-wide changes to every RBU column in a few seconds, simply by
editing my style definition file.
No more <font> tags.
Pages are typically at least a third smaller, meaning faster downloading.
Text is marked up in a more logical manner based on content, not appearance.
More compatible with modern web browsers.
The new format is cleaner and easier to read.
This display of source code is enhanced by colorizing keywords and comments,
just like it appears within REALbasic.
Example Changes
While the underlying HTML code that makes up this page has changed considerably, for
the reader you should only notice a few display niceties:
•
•
•
•
Headlines are now in "RBU blue" with a rule across the top (and there are various
sized headlines);
source code is now framed in a box to differentiate it from the surrounding text
more, and keywords and comments are colored like in REALbasic;
the RBU Quick Tip of the Week and In Detail sidebars are now framed with a
colored border instead of being filled with color;
finally, words that are defined on the RBU Glossary page are hotlinked like this
instead of a normal blue underline.
Potential Problems
I put off learning CSS for a long time because initially few browsers supported it (and the
few that did supported it badly). Nowadays almost everyone has a modern web browser
that supports CSS, so I decided it was time to make the move.
It is possible that those of you with older browsers (or using less common browsers) may
experience a few incompatibilities with the new format: please let me know if you see
anything unusual. CSS works in such a manner that browsers that don't support it should
simply ignore the extra code, but the bigger problem is that not all browsers support CSS
fully or in the same manner. I've tried to keep the tags simple to avoid problems and I've
tested this on the most common browsers without incident, but to see the pages as they
are truly supposed to look, use a current browser (such as IE 5 for the Mac).
At this time I've only got this column in CSS format, but I'll be converting all the older
columns after I'm assured that this format works for the majority of my readers. Please
send me feedback (positive or negative) on this format change!
I'm also open to improvements -- the combination of CSS and my RBU-to-HTML
program makes formatting so easy it is possible for me to enhance the layout in other
ways (for instance, differentiating between off-site links and RBU links, or highlighting
project download files in a special manner or color). Let me know what you think is
important.
And now, enough talking about myself -- on with today's lesson!
Using pre-built Classes
One of the best things about a modular programming language is that it makes it easy to
reuse portions of code. For instance, if you've written a routine that sorts an array of
integers, you can just reuse that in another program saving you the time of "reinventing
the wheel."
REALbasic is a modular programming language, and it allows you easily move routines
from one program to another. If you put your code inside a module, you can drag that
module to your desktop to create a file of it. Then that file can be dragged into another
project: instantly, all those routines are available to the other program!
In Detail
You must be careful when you're creating routines that you want to be able to reuse. The
routine must be completely independent of the rest of your program: it cannot know
about controls in windows or your data structure. It also gets complicated if the routines
depend on custom classes or other definitions within the original program; if those aren't
also transferred to the new program, your routines won't compile.
For example, let's look at the sort routine I mentioned a moment ago. If your program has
a unique data structure (say an array of objects), your sort routine would have to know
about that structure in order to sort it. That makes moving the sort routine to another
program and reusing it difficult. One workaround would be for your sort routine to work
on a simple data format (such as an array of strings or integers) and any program that
calls that routine would have to temporarily convert your special data structure to that
simple array format prior to being sorted, and of course, reverse the process after the sort
is complete.
Of course, a better way is to make your data structure an object that sorts itself (that's
what object-oriented programming is all about).
Object-oriented programming takes code reuse to another level. If you create your objects
correctly, it's possible to just move an entire object into another program and reuse it as
though it was a built-in object!
That's where stuff gets really cool. It's easy to take an existing REALbasic object, such as
an EditField, modify it, and then distribute the modified object as a new kind of
EditField. For instance, many people have released "numbers only" EditField objects,
which you can simply drag into your own projects and gives you an EditField that only
accepts numbers.
These objects are called custom classes, since they are customized versions of an existing
REALbasic class of object (such as an EditField or ListBox).
There's gobs of REALbasic code floating around on the 'net, just waiting for you to use it.
This week's lesson is going to show you how to do that.
We'll start with a few classes and projects I've released myself. These are all available for
downloading from my REALbasic page. I wrote these years ago and decided to release
them into the REALbasic community since I gained and learned so much from what
others shared.
(My page has some good information about these classes on it, so it's worth reading, but
to make it easier for you, I've placed a download of all three projects here on the RBU
website.)
Calendar Popup
The first item is called Calendar Popup and is a simple way to allow a user to input a
date in your program. Instead of having a text-based interface, or perhaps tiny arrows to
click to change the date on digit at a time, this pops up a calendar where the user can
simply click on a date. It handles the years between 1500-2399 and it's Y2K compliant.
Calendar Popup includes a sample project which you can run and test to see how it
works. Calendar Popup is not a class -- it's a multi-part module. To use it in your own
programs, you'll need to import the following files into your project (drag them into your
project window):
•
•
•
•
•
calendaricon.pct
leftArrow.pct
rightArrow.pct
CalendarWindow
CalendarModule
The first three are simple graphics used by Calendar Popup (feel free to substitute other
artwork if you prefer). Then we've got the calendar window and the module which
contains the calendar code. If you'd like, put all these inside a folder inside your project
window so they don't clutter up your program.
Next, you'll need to create a BevelButton on the window where you want the Calendar
button to be (this is the button the user clicks on to bring up the calendar). Set its
properties to the following:
In the bevelButton's Action event, put this code:
CalendarWindow.showModal
if gReturnDate <> nil then
msgBox "You chose: " + gReturnDate.longDate
end if
The first thing this does is open the calendar window -- since it's opened with a
showModal command, your program comes to a halt until the calendar window is
dismissed.
Once the user's chosen a date, it is returned in the gReturnDate global variable. In this
example, we simply display it in a dialog box, but you can do whatever's appropriate for
your needs, such as putting the result into an EditField.
Since gReturnDate is a date object, we check to make sure it isn't nil before using it
(remember, an object may or may not exist so we must make sure before we use it).
Tip: if you set gReturnDate to a particular date before opening CalendarWindow, the
calendar will open to the date you passed.
That's all you need to do to add a calendar to your application! Isn't that cool? See, I did
all the work for you. You don't need to know anything about how I wrote the calendar
stuff, you just need to know what to do with the date that's returned.
Of course, since Calendar Popup is written in REALbasic, you can edit the code
yourself: make the display prettier, rewrite my code to make it faster, or fix any bugs you
find (please tell me about those so I can incorporate them into the project).
GrayEdit
While Calendar Popup is a simple module, GrayEdit is a more complicated custom class.
You know how REALbasic has an "auto-complete" feature that finishes typing variable
names and keywords for you? Have you ever wanted that feature in your own program?
Now you can!
GrayEdit is a special class of EditField that will finish typing text inside an EditField.
It's not designed to work with multiple items within a text editing screen like a word
processor, but for a single item inside a single field. It works best for auto-completing
items in a list, such as a font name.
Using GrayEdit in your own projects is fairly simple. Simply drag GrayEditClass into
your project window. After you've done that, you can select any EditField and change
its "super" (on the Properties palette) to "GrayEditClass."
Next, you'll need to initialize the EditField's grayList property. GrayList is an array of
strings that's part of the GrayEditClass definition. It contains the list of keywords that
the program will "autofinish" for you. You should initialize this list as soon as possible -when the EditField is opened is a good spot. If your list of keywords is dynamic, you'll
need to update grayList whenever your master list changes. Finally, you'll need to sort
grayList, since the keywords will be searched incrementally (and you want two
similarly spelled words to end up next to each other). Sorting is easily accomplished with
REALbasic's array sort command.
For instance, here is the initialization code for an EditField that auto-types font names
for you (we don't bother sorting it since fonts are alphabetical already, but if we wanted
to sort it we could add an editField3.grayList.sort line after the next.
redim editField3.grayList(FontCount)
for i = 0 to FontCount-1
editField3.grayList(i + 1) = Font(i)
next
Once the "GrayEditField" has been initialized, you don't have to do anything else. It will
just work for the user. By examining the EditField's .text property you can see what text
is in the field so you can use the information.
So how does GrayEdit work? Well, it's a bit complicated, but essentially it keeps track of
two strings that belong to the EditField: the "black" text and the "gray" text. The black
text is made up of letters the user actually typed, but the gray text is a suggestion by the
program. After each new letter is typed (or a letter deleted), GrayEdit scans the grayList
array to see if there's a match -- a match is if the letters typed match the first few letters of
an item in a grayList element.
If there is a match, the class puts the full text of the grayList element into the EditField
and colors the first few letters black and the balance gray. Finally, since colorizing the
text moves the cursor, it puts the cursor back after the last item typed.
So you can see that what we are doing is a bit of deception: the EditField actually
contains the entire word, but by coloring the suggested portion gray we make the user
think that area isn't real (final). As the user types, we're jumping around and inserting
new words and/or colorizing letters as appropriate. It's a bit crazy, but it works!
The trickiest part of GrayEdit is that for the deception to work, we can't allow the user to
place the cursor past the point where they've edited. Let me use an example to make this
clear. Say we're in our font-finishing EditField and we type "Mo" -- the field will add
"naco" in gray like this:
Monaco
The user's text cursor should be after the first "o" -- after all, that's as far as they've typed.
The gray text is supposed to be "vaportext". It doesn't exist yet so the user shouldn't be
allowed to move the cursor into that area.
Well, stopping that is fairly easy since we can check the SelChange event and tell if the
user moved the text cursor's position. We can just see if the cursor's past the last black
letter typed and if it is, move it back.
But then we run into a problem. Our own routines -- the ones that change the text's color - must move the cursor in order to select text and color it. That's a big problem since our
SelChange routine won't let us move the cursor where we want!
How do I solve this problem? Simple: I create a boolean property called
EditInProgress which allows me to distinguish between cursor movements of the user
and cursor movements I'm doing. My SelChange routine is wrapped around a check of
EditInProgress which means it won't change the cursor location if it's me doing the
moving, but it will if it's the user moving the cursor.
Take a look through the code in GrayEdit: I think you'll find it interesting and
educational. And of course, feel free to use it in your projects (just give me credit in the
About Box).
Listbox Column Resize
If you've used REALbasic's ListBoxes, you know they are very powerful. But they have
some key limitations, including some things that ought to be built-in. For instance,
headers that sort the columns when clicked on make sense, but it seems obvious that one
should be able to rearrange the order of the columns by dragging, as well as set the width
of each column by dragging.
Good news: this class does this. How does it do this magical thing? Well, it's not easy,
and it requires a little trickery.
The first problem is that to for me to intercept where a user is clicking and dragging, I
need to receive MouseDown events. Unfortunately, if you look at the ListBox control, it
does not have a MouseDown event. Therefore, my class is not a variation of a ListBox
control, but a special kind of Canvas!
Remember, unless you draw something in them, canvases are transparent: so a canvas
covering a ListBox doesn't interfere with the ListBox's functionality at all. That's great:
we can intercept MouseDown and MouseMove events without the ListBox needing to know
anything about it.
The disadvantage of this approach is that canvas must know which ListBox it is
covering: you must tell it before the user begins clicking. This initialization is very
simple.
First, drag the ResizeListClass2 class into your project (also drag in handCursor and
moveColumnCursor). Create a new Canvas and place it on top of your ListBox with the
exact same dimensions (width and height and position). Change the super of the Canvas
to "ResizeListClass2". Then, within the canvas's Open event, put the following:
me.theList = listBox1
Substitute listBox1 with the name of your particular ListBox. Here you are simply
assigning a ListBox to a property (theList) of the Canvas. This links them, and it's
critical: if you don't do this, the program will crash!
Since the Canvas we've created is a variation of a traditional Canvas, I'm free to modify it
as I need. Thus I've created several "user properties" and "user methods" -- these are
settings and routines that you, the user, are free to use just as though they were
REALbasic ListBox settings.
For instance, here are the User Properties you can change:
•
•
•
•
resizerZone -- integer. Represents in pixels the width of resize column area.
allowColumnDrag -- boolean. If true, user can rearrange column order
sortingOff -- boolean. If true, clicks in headers don't register.
dragDelay -- integer. Represents in ticks the delay after clicking in header before
user gets a grabber hand and can rearrange column order. Default is 30 (halfsecond). Any mouseup in less than 30 ticks is passed on as a mousedown. (Has no
effect unless allowColumnDrag is set to true.)
Here's a User Method you can use:
•
returnHeader(string) as integer. Since the user can now shuffle your columns
around, you need a way to find out which column is which. This routine accepts
the name of a column (the column title) and returns the current number of that
column. (This assumes all column names are different.)
The sample project that comes with ResizeListClass2 include simple checkboxes to set
these different settings, as well as a demo of the returnHeader method.
Finally, let me conclude by saying that this class isn't perfect: there are a few issues I
wasn't completely able to solve (and I haven't got back to finish it). There's more info on
my website about these potential problems. But I still felt like this was an excellent class
to demonstrate how to incorporate a third-party class into your own programs.
Next Week
We'll learn how to create your own custom class that you can share with others or reuse
in your own programs.
Letters
This week we hear from James, writing from right up the street from me in Santa Cruz,
who has a number of interesting questions.
Dear Marc,
Thank you for your great columns. They really do fill an important niche somewhere
between the two RealBasic books (which is the hope you had expressed in your review of
the two books).
My son and I are working on a MP3 player (using QuickTime), with playlists and a chat
component (and a file sharing component planned for the future). We have run into some
difficulties with the following items. We have consulted the two books, the archives of
your column, the RB documentation, and various RB-related websites, but have found
nothing that quite addresses the specifics of our problems. Anyway, here they are:
1. We have set creator and type codes for our application in the appropriate places in the
IDE. There also is a document icon for saved playlists. However, when we save a
playlist, invariably it appears on the desktop as a BBEdit icon, with TEXT as the type and
R*ch as the creator. Rebuilding the desktop has no effect. We have even added code in
the save routine to specify the MacCreator and MacType. This has no effect. We have
removed every BBEdit application from the computer. Still the same thing happens. Do
you know what's happening and why?
2. We use a static text box to display the elapsed time when a song is playing.
Unfortunately, there is some flicker when the field updates (which would be every
second). We have found techniques to reduce or eliminate the flicker in editfields, but
nothing for static text fields. Do you have any suggestions?
3. We are having a problem in writing an "open document" event (app class), so that a
double-click on a saved playlist will open the program and the playlist itself. The
program opens, but not the playlist. The playlist itself consists of two listboxes, a visible
one with the song titles and an invisible one containing the full path names of the mp3's.
We thought we could use the same routine we use to load a saved playlist from the open
application, adding some code for creating a new window, positioning it and showing it.
But that doesn't work. It might be important to note that when the application opens, the
only window that appears is a floating toolbar. A playlist window appears only after a
button is pressed on the toolbar.
4. The file menu is dimmed when we first launch the compiled application until a button
is pressed on the toolbar or there is a mousedown even in the menu bar. Since the quit
item is enabled by default, we thought that "file" would always appear as active in the
menu bar. We have tried putting "EnableMenuItems" in various places, but that hasn't
worked.
Sorry if we have overloaded you with questions.
By the way, we would definitely be interested in more coverage of the noteplayer in
future columns. A discussion of error handling would be great too.
Thank you for all your hard work.
Thanks for the kind words, James. Glad to help. I'll deal with your questions in reverse
order (simplest to most complicated).
#4. The File menu being dimmed is generally caused by displaying a temporary dialog
box (like a splash screen) and then closing it. What happens is that the menu disables
itself for the splash screen, then should be activated but it doesn't get refreshed properly.
The solution is what you suspected: add in an "EnableMenuItems" command. But the key
is that you must do this is the right place. Without looking at your code I couldn't tell you
exactly where this is, but it's probably right after you close a dialog box. Keep playing
with it and I'm sure you'll find the right spot.
#3. Creating an application that opens double-clicked documents is a little tricky. It
sounds like you're once again on the right track: you need a unique Creator code, and
your saved document needs a custom icon (without a custom icon RB won't link to the
document kind).
It sounds like you've also got an application class created (add a new class and set its
super to "application"), which is good. Double-clicked files should be passed to the
OpenDocument event: these are ordinary folderItems, so you should be able to send
them to your own open routine to open those files. (Ideally your open routine should
accept a folderItem as a parameter.)
Since the above is what you've done and it doesn't seem to be working, I'm not exactly
sure where the problem is located. The first, and most obvious thing I can think of, is to
make sure that you're testing this from a stand-alone compiled application. File-openings
like this will not work within the REALbasic IDE.
If you're doing that and it's still not working, it's time to do some debugging. The first
thing I'd try is put this code in the OpenDocument event:
msgBox item.absolutePath
Comment out any other code you have there. This should cause your program to display
the path of the file double-clicked. That will help narrow down the cause of the problem:
is it not getting the double-clicked file or is your program not opening the file correctly?
Let me know what you find out and maybe I can help further.
#2. The flickering of staticText boxes is legendary and long-time RB problem. My
solution is to simply use a Canvas (we'll name it displayCanvas). Create your own
method... say, updateDisplay that calls for a string parameter. Within that method have
it draw into displayCanvas like this:
sub updateDisplay(s as string)
dim g as graphics
g = displayCanvas.graphics
g.clearRect(0, 0, g.width, g.height)
g.textSize = 12
g.textFont = "Geneva"
g.drawString s, 2, 15
end sub
You may need to adjust the positioning of where drawString draws so that text isn't cut
off (it depends on the font you are using -- you can even use the .textHeight feature to
calculate the font's height for vertical positioning if you'd like your routine to be flexible).
So, instead of assigning a string to staticText1.text, just pass the string to
upateDisplay.
#1. Finally, your problem with saved files showing up as BBEdit files. I'll bet you're
using a TextOutputStream to save your file. The default for text files from REALbasic
is to save them as BBEdit files.
It's sounds like you're doing most things correctly: your application has a Type and
Creator and you've got a custom document icon defined in the File Types dialog. But it's
still not working. What really puzzles me is that you say you actually change the file's
Type and Creator codes manually and it still doesn't work; that's bizarre.
The only thing I can think of is that you're changing the codes too early: you must close
the TextOutputStream before you modify the Type and Creator codes.
Take a look at the following example (assume that f is a valid folderItem passed to the
routine):
dim out as TextOutputStream
out = f.createTextFile
if out <> nil then
out.writeline "This info is saved in the file."
out.close
f.macType = "TYPE" // Use your file Type
f.macCreator = "MYCR" // use your Creator code
end if
The above should work -- I've done it this way in many programs. An alternative is to use
a BinaryStream instead; there you specify the File Type when you create the file
(assume "myfilekind" is a File Type you defined):
dim b as binaryStream
b = f.createBinaryFile("myfilekind")
I hope that helps. Keep those letters coming! I may not answer your question
immediately, but I'll hopefully get around to it in a future column. (If you don't want your
correspondence published, just be sure to indicate that when you write. Otherwise I'll
assume it's fair game.)
.
REALbasic University: Column 026
CSS Update
Before we begin this week's lesson, let me thank those of you who responded to the RBU
layout change debuted last week.
So far everything is look good: I made a few changes to my CSS style definition file for
this week (which should also apply to last week's article if you reload it) based on user
suggestions, so hopefully the new format will work for everyone. I also fixed a bug with
my REALbasic program that converts my raw text columns to HTML and wasn't
marking keywords in program listings correctly.
As before, let me know if you notice incompatibilities or problems.
Creating Custom Classes I
As we learned last week, custom classes are an easy way to incorporate objects from
other programmers into your own projects. But how do you go about creating a custom
class for your own use? Well, today we're going to learn that!
In this lesson, we're going to create two different custom classes. The first is a simple
on/off switch. Next, we'll create a canvas that draws a grid with a box, and it lets you
select the box and move it around!
As usual, you can download the complete example project here, or follow along and
recreate it step-by-step.
If you're starting from scratch, create a new project and drag two canvases and one
EditField onto Window1.
OnOffClass
Our first demonstration is of a simple canvas class that displays itself as either off or on:
Clicking the canvas toggles the switch.
First, let's create a new class: go to the File menu and choose "New Class." This will add
class1 to your project window. Select it, and rename it on the Properties palette. Let's
call it onOffClass. Next, set the "super" to Canvas.
In Detail
What are we doing here? Remember, one of the key aspects of objects is the concept of
inheritance. If you have a master object, you can create variations that inherit all of the
characteristics of the original, except those you add or override.
The "super," in REALbasic terminology, is the master object. In this case we have set the
super to a canvas, so our new class will operate exactly like a canvas. It will have all of
the characteristics, properties, and methods of a traditional canvas. If we change those or
add new ones, our custom canvas will have those, but our modifications don't affect a
traditional canvas in any way.
When you drag a canvas object from the Tools palette onto a window, REALbasic by
default adds a traditional canvas. But if you wish, you can tell RB that the object is your
custom canvas class. That's what we're going to do next.
Select canvas1 on Window1 (if you don't have a canvas, drag one there). Set its super to
"onOffClass" and its width and height settings like this:
Now double click on your onOffClass class. That should open up a Code Editing
window. Here you'll want to add a property (go to the Edit menu and choose "New
Property"). Name it value as boolean.
Value is going to represent the state of the switch, either off or on.
Since the state will change when the user clicks on it, let's put the following code in the
MouseUp event:
value = not value
me.refresh
All this does is toggle the setting and redraws the control (since the state changed).
But notice that we put this in the MouseUp event, not the MouseDown: why did we do that?
That's because that's the way the Mac was designed to work: clicking a button doesn't
activate it. It may depress it, but the button doesn't change state until the user releases the
button. One advantage of that is that the slight delay gives us time to show the state
changing. In our case, we'll fill the whole button with black just to show that something's
happening.
Put this in the MouseDown event:
me.graphics.fillRect(0, 0, me.width, me.height)
return true
This simply fills the whole canvas with the default color (black), and then -- very
important -- we return true to allow the MouseUp event to register. If we didn't have
that line, MouseDown would default to returning false, and the MouseUp event would
never occur. (That's because by returning true you are saying you want to handle those
event.)
Once you've got that done, the majority of our code goes in the Paint event. This simply
draws the control. It checks the state and changes what it draws accordingly (green box if
on, red box if off).
dim s as string
dim x as integer
if value then
s = "ON"
g.foreColor = rgb(0, 155, 0) // Green
else
s = "OFF"
g.foreColor = rgb(230, 0, 0) // Red
end if
// Fills it with color
g.fillRect(0, 0, me.width - 1, me.height - 1)
// Draw the label
g.foreColor = rgb(255, 255, 255) // White
g.textFont = "Geneva"
g.textSize = 12
g.bold = true
x = (me.width - g.stringWidth(s)) \ 2
g.drawString s, x, g.textHeight
g.foreColor = rgb(0, 0, 0) // Black
g.drawRect(0, 0, me.width - 1, me.height - 1)
That's it! Now you can run the program and see that clicking on the control changes the
state (toggles the switch).
This is obviously a simple example, but you could make it more complicated. For
example, imagine a three-state switch, or a lever-type graphic. The cool thing is any
modification you make to your class, instantly changes every instance that is based on
that class. So you could have a program with dozens of these switches, then decide a
more 3D switch would look better, change the code in a single Paint event, and every
switch would have the new look!
Extra Credit: try adding the following lines to the Paint event to make the button more
3-dimensional.
g.foreColor = lightBevelColor
g.drawLine(0, 0, 0, g.height - 1)
g.drawLine(0, 0, g.width - 1, 0)
g.foreColor = darkBevelColor
g.drawLine(g.width - 1, 0, g.width - 1, g.height - 1)
g.drawLine(g.width - 1, g.height - 1, 0, g.height - 1)
BoxClass
Now let's try a more complex class. BoxClass is a type of canvas that lets the user select
and move a box around inside it. Obviously, that's not especially useful in and of itself,
but the concepts here are a good start toward a drawing program, or perhaps another type
of control with elements inside it that the user can position or resize.
First, let's again create a new class, rename it to "boxClass", and set its super to Canvas.
Now go to the canvas2 on your Window1 (add a new canvas if needed) and set its super
to boxClass.
Open up boxClass by double-clicking (Note: not canvas2) and add the following
properties:
As you can see already, this is a more complicated class: we need all these properties so
we can keep track of the box's location as well as various states within the class (such as
whether or not the box is selected).
Before our canvas starts, we need to initialize a few elements. Let's pick a random
location for the box, as well as for the box color. We'll also set the grid property to true
-- but of course you can change this when you run the program to see how it affects
things.
grid = true
boxLeft = (rnd * (me.width - 20)) + 1
boxTop = (rnd * (me.height - 20)) + 1
boxColor = rgb(rnd * 200 + 50, rnd * 200 + 50, rnd * 200 + 50)
Now that our canvas is initialized, let's write the Paint event, where everything gets
drawn. You'll see here that this is a somewhat complicated routine.
dim x, y, x1, y1 as integer
const gridSize = 15
// Draw grid
// (This is mostly here just to give us a background.)
if grid then
g.foreColor = rgb(200, 200, 200) // Light gray
x1 = me.width \ gridSize '- 1
y1 = me.height \ gridSize '- 1
for x = 0 to x1
g.drawLine x * gridSize, 0, x * gridSize, me.height
next // x
for y = 0 to y1
g.drawLine 0, y * gridSize, me.width, y * gridSize
next // y
end if
// Draws border
g.foreColor = rgb(0, 0, 0) // Black
g.drawRect(0, 0, me.width - 1, me.height - 1)
// Draw Box
g.foreColor = boxColor
g.fillRect(boxLeft, boxTop, 20, 20)
// Draw Selection handles
if selected then
g.foreColor = rgb(0, 0, 0) // Black
// Upper left corner
g.fillRect(boxLeft - 2, boxTop - 2, 4, 4)
// Bottom left corner
g.fillRect(boxLeft - 2, boxTop + 18, 4, 4)
// Upper right corner
g.fillRect(boxLeft + 18, boxTop - 2, 4, 4)
// Bottom Right corner
g.fillRect(boxLeft + 18, boxTop + 18, 4, 4)
end if
If grid is true, we draw a light gray grid pattern behind. How is it behind? Because it's
drawn first: the other elements drawn later will appear on top. Drawing order is critical in
controls like these, so keep that in mind.
Next we draw the border, and then the box. Finally, we draw selection handles. This step
is only done if selected is true, and since it's the final step, we know the handles won't
be covered by anything.
Overall, this is pretty simple stuff: ordinary drawing commands, though there is a little
math involved in figuring out exactly where to draw things. For instance, the selection
handles are drawn at each of the four corners of the box, but we shift them so they extend
outside the box a bit (that makes them easier to see).
Much more complicated is what happens when a user clicks inside the canvas. We have
to check for multiple situations: select and deselect the box, and move the box if the
user's dragging.
Here's the code for the MouseDown event:
if x > boxLeft and x < boxLeft + 20 and y > boxTop and y < boxTop +
20 then
// User clicked inside of box
xDif = x - boxLeft
yDif = y - boxTop
if not selected then
selected = true
me.refresh
end if
// Pass MouseDown to MouseDrag
return true
else
// User clicked outside of box
if selected then
selected = false
me.refresh
end if
end if
The outer if-then loop simply checks to see if the user clicked inside the box or outside
of it. If it's outside, we check to see if the box is already selected. If it is, we deselect it
(and force the canvas to redraw).
If the user clicked on the box, we do a couple things. First, we save our xDif and yDif
values. What are these? Well, since the box is drawn from the upper left corner, these
represent the difference between that position and where the user clicked. There's a
difference between the user clicking in the middle of the box or near the bottom. By
saving this difference we can allow the user to drag the box but the box's position stays
relative to the user's mouse cursor. (Otherwise the box would "jump" and the upper left
corner would be the user's mouse cursor location.)
Next, we select the box if it isn't already selected. We don't want a toggle, in this case,
because the user may be clicking on it to move it, not deselect it. To deselect it, the user
clicks anywhere not on the box.
Finally, we return true to allow us to process the MouseDrag event. Which is where we
are now:
if selected then
if (boxLeft <> x - xDif) or (boxTop <> y - yDif) then
boxLeft = x - xDif
boxTop = y - yDif
me.refresh
end if
end if
Since dragging is only permitted when the box is selected, we check for that first. Then
we check to make sure that the user's mouse cursor has moved: if the arrow isn't moving,
there's no need to draw the box in a new location.
If the user has moved, we simply draw the box at the new location and redraw the canvas.
Simple, eh? Give it a try. Run the program and move the box around. Select and deselect
it. Isn't that cool?
Adding New Events
Now here's something important about creating your own classes of existing objects.
You'll notice, if you open up Canvas2 (not boxClass), that there are missing events from
the Events list. What happened to the Paint and MouseDown events that canvases usually
have? They're gone!
This is an important aspect of inheritance: your class inherited those events and did
something with them (you put code in them) so they are, in effect, already used. You
therefore can't add more code to them.
Now in most cases this isn't a problem as you don't need to extend an event in a custom
class. For example, since boxClass automatically takes care of drawing itself, you
shouldn't need to access a Paint event to modify how it draws itself.
But what about an Open event? Isn't that how you initialize an object? What if you
needed to initialize some settings when Canvas2 was opened. Since you already used the
Open event in your class definition, you can't use it again and therefore have no way of
knowing when Canvas2 is opened!
But don't despair: REALbasic gives us an elegant solution. Within a custom class you
have the ability to create new events! These events can be called whatever you want, and
all you have to do to "activate" the event is put the name of the event on a line by itself.
For example, you could, within the MouseUp event, have code that checks for a doubleclick. (This is a little complicated as the two MouseUps must occur within a certain
period of time.) When your code determines that the user has double-clicked, you could
simply call your DoubleClicked event.
For extra credit, let's try it: add a firstClick as integer property to boxClass. Then,
in the MouseUp event, put this:
if firstClick = 0 then
firstClick = ticks
else
if ticks - firstClick < 30 then
DoubleClick x, y
end if
firstClick = 0
end if
Oh yeah, you'll need to add the DoubleClick event as well: go to the Edit menu and
choose "New Event" and call it DoubleClick. Give it the parameters x as integer, y
as integer.
That's cool and all, but you haven't really done anything with the double-click. So open
Canvas2 (not boxClass!), find the DoubleClick event, and put in msgBox "DoubleClicked!" there. Now when you double-click the box you should see a dialog box
telling you that you double-clicked.
But back to our original problem: since we used the Open event in our class definition,
canvas2 has no Open event. So let's just add one back in!
Go to boxClass and choose "New Event" from the Edit menu. Name it "Open". Now
your canvas2 should have an Open event. Put msgBox "test" in it and run the program
to see if anything happens. Nothing should: that's because while you've defined the new
Open event, you never called it (the event never happened). The solution is elementary:
add this code to the end of boxClass' Open event:
// Pass this event on...
open
Run the program and see that it works now. Basically the Open event in boxClass
happens, and it sends a message that its own "new Open" event occurred, which is the
event you see in canvas2. By the way, the new event didn't even have to be named
"Open" -- you could have called it "Initialize" or "Snogglegrass" and it would have
worked the same way.
Next Week
We'll create a variation of an EditField that includes built-in undo support. Now that's a
useful class!
Letters
Our letters this week concern RBU's switch to Cascading Style Sheets (CSS). First, from
Craig Brown:
Hey, Marc...
I think the new format using CSS looks terrific. Nice work. If you want a negative
comment or two, try viewing it with the iCab browser. Don't know if you've used iCab,
but it has a smiley face icon that turns into a sad face when it encounters HTML "errors."
Your column had 682 "errors." However, don't feel bad. I haven't visited a site yet
(except for iCab's own site, http://www.icab.de/index.html) which doesn't have a ton of
errors.
Thanks, Craig. I know a lot of people swear by iCab, but I've tried it a few times and it
really doesn't like any of my computers: it will work for a few minutes and then
completely lock up my system. I tried several versions of the program over a period of a
year and they all exhibited this problem. I assume it's a conflict with something on my
system, but I don't have the interest or time to track it down. (Until Microsoft starts
charging for IE, I'm happy with Explorer.)
Next, John writes:
Hi Marc
Just a bit of feedback on the new format - I think things are now even more legible. May I
suggest just one small change ? Have you though of making the Headings in a sans serif
typeface and keeping the body text in a serif typeface. For example Helvitica for the
headings and Times for the Body is a classic combination.
This stuff you are doing should be put together and published as a book - I am finding it a
very useful set of tutorials. I hope that one day you can cover the subject of printing
graphics at better than the default 72dpi.
Thanks John. As a graphic designer, Times and Helvetica are an anathema to me because
they are so overused. But your point is well-taken: the sans-serif headline and serif body
is probably a better combination (though I tend to prefer sans-serif for on-screen reading).
See what you think of today's change (I have specified extremely readable Georgia, a free
Microsoft font, for the body copy.)
Finally, Kevin writes:
Hmm. My initial impression of the RBU changes is that it looks nicer. I like the changes.
However, my browser (OmniWeb for MacOS X) doesn't support CSS completely, and it
appears that it doesn't support the background color part of the class definition. This is a
problem when I see a glossary term, because the glossary terms are colored white on a
different background. But because OmniWeb doesn't support the background part, I see
white text on a white background, so I can't read it without highlighting it. It would be
very helpful if the glossary terms were some color other than white.
Other than that, it looks great!
Thanks for the tip, Kevin. I didn't try this with OmniWeb (though I will). I changed the
text in glossary items to black -- hopefully that will work better with problem browsers
but still be readable for everyone else.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 027
Creating Custom Classes II
Last week we explored how to create your own custom classes that can be easily reused
in multiple programs. We built two examples of classes that were good for demonstration
purposes but aren't especially useful. Today we'll create a very useful EditField class
that will automatically (transparently) support unlimited undos!
As usual, you can download the complete example project here, or follow along and
recreate it step-by-step. (This example project includes the classes from last week as
well.)
UndoEditFieldClass
Have you ever had a dialog box with an EditField in it and thought wouldn't it be nice
if the dialog box supported undoing? Of course, adding undo capability isn't easy, and
who wants to fuss with all that?
Well, if adding undo to an EditField was just the matter of dragging a class into your
project and changing the super of your EditField to UndoEditFieldClass, your dialogs
might soon all have undo capability.
The good news is that it really is that easy.
We'll start by creating a new class (Edit menu, choose "New Class"). Let's name it
undoEditFieldClass and set its super to "EditField."
Next, add a property to the class. (Double-click on it then choose "New Property" from
the Edit menu.) Add undoString(0) as string.
This variable is going to store the undone text as an array of strings -- that will give us
unlimited undos.
Think about the basic steps of an undo system: we must keep track of what the user is
doing, save the old value, and restore the old value when the user chooses the Undo
command. So the first part of this -- watching what the user is typing -- is done in the
KeyDown event.
Go to the KeyDown event and put in the following code:
undoString.append me.text
All this does is save the contents of the text field to the undoString array. The append
method automatically allocates more space in the array, adding the passed text as the last
item.
Next we'll need a menu command for the Undo command. Since REALbasic
automatically creates a menu item for the Undo command we don't have to create that,
but as long as we're here, let's add a very useful "Select All" command to our class.
Open your project's Menu item and click on the Edit menu. At the bottom of the menu
that drops down is is a blank rectangle. Click on it and type a hyphen ("-") and press
return. That adds a divider line to the menu. Click on the blank rectangle again and type
"Select All" and press return. This time, while the Select All item is still highlighted, go
to the Properties palette and type an "A" in the CommandKey field.
Your finished menu should look like this:
Excellent. Now let's enable the menuitems. Put this into the EnableMenuItems event of
undoEditFieldClass:
if uBound(undoString) > 0 then
editUndo.text = "Undo Typing " + str(uBound(undoString))
editUndo.enabled = true
else
editUndo.text = "No Undo"
end if
if me.selLength <> len(me.text) then
editSelectAll.enabled = true
end if
What are we doing here? First, we're enabling the Undo menu command as appropriate:
if our undo buffer is empty (undoString has no items in it) we don't enable the command.
Next, we're changing the name of the Undo menu command to reflect which undo event
we are doing and the number of undos we have saved.
Finally, we check to see if all the text in the field is already selected. If it's not we enable
our Select All menu.
Pretty simple, as you can tell. Let's add those menu handlers now. Go to the Edit menu
and choose "New Menu Handler" and from the popup menu that's offered, select
EditSelectAll. Do that again and add the EditUndo handler.
Here's the very simple code for selecting all the text (put this in the EditSelectAll
handler):
me.selStart = 0
me.selLength = len(me.text)
This just puts the start of the selection to the very beginning of the EditField and sets
the length of the selection to the length of the EditField's text -- meaning that all the text
in the EditField is selected. (Remember, we're working inside a variation of an
EditField class: me refers to the current object, i.e. the current EditField.)
Now go to the EditUndo handler and type in this:
// Undo to saved values
me.text = undoString(uBound(undoString))
redim undoString(uBound(undoString) - 1)
// Move cursor to end
me.selStart = len(me.text)
This does three things. First it puts the text saved in the final item of the undo buffer into
the EditField -- basically going back to the most recent version saved. Then it reduces
the size of the undo buffer array (undoString) by one, deleting the last item. Finally, we
move the cursor to the end of the EditField.
Guess what? That's it! Our undoEditFieldClass is finished!
To try it, add an EditField to a window and put its super to "undoEditFieldClass". (If
this is in a different project, you'll have to drag a copy of undoEditFieldClass into the
project first.)
When you run your program, all typing in the EditField is remembered, so you can
easily undo your way back to your first character.
Note that this class isn't efficient: we save the text of the entire field every time the user
types, meaning that if the user typed a whole sentence, we'd have several dozen copies of
that sentence in memory. Of course for a simple EditField in a dialog box where the
user just puts in a few characters this isn't a problem: but you probably wouldn't want to
use this system for a word processor (or you'd limit the number of undos to a reasonable
10 or 20).
Now What?
Now that you've got the example project finished, you can drag the classes to your
desktop to create modules you can drag into future projects. So you'll be able to instantly
add simple undo capabilities to all EditFields in your programs!
To do that, just drag undoEditFieldClass into your project and then -- be sure to do this
-- set the super of all EditFields you want to have undo capability to
undoEditFieldClass.
You obviously could also do this with boxClass and onOffClass (from last week),
except they aren't quite as useful (though they are good starters for your own useful
variations).
Next Week
We begin our next major project, RBU Pyramid, an implementation of a classic solitaire
card game!
News
REAL Software released the latest upgrade to REALbasic last week, RB 3.5. This new
version adds some interesting features:
•
•
•
•
•
•
•
•
Microsoft Office Automation (allows you to control Office programs)
RbScript (lets your program run REALbasic code)
3D engine (work with 3D graphics)
Regular Expression engine (powerful search and replace)
DataControl (improved database control)
Microsoft Windows XP (RB will now compile for XP)
FolderItem dialog (support for custom open/save dialogs)
The Tips Window (tips within the RB IDE)
RBU's Take
I generally stay away from the RB beta releases so I haven't had time to thoroughly
explore these new features (you can download some demo projects from the REAL
Software website to see the new features in action). On the surface, these features sound
great, but in reality I wonder how often they'd be used.
The Office Automation feature requires you have Microsoft Office, and my reading about
it suggests there are a number of limitations (which will eventually be fixed). I can't quite
figure out what to use this for anyway -- I'm trying to purge my system of Microsoft
products, not become more dependent upon them.
Regular expressions is a much-requested feature, but I doubt it works with styled text
(which limits its appeal to me though you may like it).
RBScript, on the other hand, is intriguing. Theoretically you could use it to add a plug-in
type feature for your program (instead of adding AppleScripts to your program, plug-ins
would be written in REALbasic syntax).
Likewise, the 3D engine is an excellent addition, but I'm a bit intimidated by complex
graphics (especially 3D): we'll have to see how easy it is to use.
More disappointing is that long-standing bugs and limitations still exist; while I
commend RS for releasing frequent upgrades, I wish they'd fix known issues and enhance
existing tools (like the ListBox and EditField controls) before adding wild new features
that few people will use (like Office Automation).
On the whole, however, it's an excellent upgrade, and reasonably priced at $29.95 for the
standard version (though the professional upgrade is a more costly $89.95).
Letters
This week we hear from Cap, who has a question about creating games in REALbasic.
Hey Marc!
At first I've to admit that I'm not that kind of guy typing & coding around 10 hours a day
with RealBasic. Even though I try to ship my first "big" application in a month or so, and
I coded it within the last 8 months....
Moreover I visit forums & other websites, get newsletters about RB and test other
developers betas...
So, what's this all about? I found out that the whole RB-community (well, a great part of
it) turns to other tools when it's about coding games....C, Director, etc. Well, maybe it is
because of the beta status of RB 3D, but I think it's quite more...no matter which coding
tool you use, there are dozens of game examples out there (on mactech, for example). But
according to RB, there are only Strout's Sidescrolling Shooters and some fat FirstPerson-
Shooters Demos....(ok, maybe PacMan is somewhere out there....) Where have all the
times gone, where Netzee & Stratega (games even supported by GameRanger) were
developed with this cool RealBasic, making it possible to create such simple&cool
games. Hey, this is not about coding a second TombRaider or making strategy games like
Diablo....why don't we use the advantage of RealBasic according to more simple games?
Look at NetFungus (IMROM Software, GameRanger capable) or Airburst
(Aaron&Adam Fothergil)....I think this would be possible with RB, and if beginners
would see that, they wouldn't turn to other tools when it's about coding games! So, Marc,
if you search for some stuff for your RB University, just code some game examples like
Tetris or a board/card game...
Cheers,
Cap
Excellent timing, Cap! I'm not sure why people turn to other tools for games. Admittedly
REALbasic has problems with animation and its sprite engine has known bugs
(especially under Mac OS X), but unless you're creating a 3D shoot-'em-up, I don't see
those bugs as killer items. (And when you look at all the low-level optimization the C++
guys do to get a few extra frame rates out of those games, coding a 3D game in RB is
rather ridiculous.)
Personally, the kinds of games I want to write are simple dice and card games, logic
puzzlers, etc., which are ideally suited for REALbasic.
In fact, our next project -- which begins next week -- is a card game. It's a fairly
sophisticated version of Pyramid and it will probably take us a month or two to get
through, but I hope it will inspire others to use REALbasic for game programming,
especially for simple yet enormously entertaining classic games.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 028
RBU Pyramid I
It's finally time to start our next significant project. This time we won't be creating a
useful utility like GenderChanger, but a card game. The game I've chosen is a solitaire
game called Pyramid.
Like GenderChanger, my purpose in providing the program isn't just to give you source
code to browse, but to teach you programming techniques and explain how and why I
made the decisions I made. Thus this series will take a number of weeks to cover RBU
Pyramid. I also won't just throw a completed project file at you, but I will give you a new
project file each week or so, containing the code we've added for that week's lesson.
For today, we'll explore the rules of Pyramid and create our program's design.
Finally, to motivate you, I'm going to distribute the executable of RBU Pyramid, so you
can actually run and play the completed game. This will give you an excellent view of
where we are going and it should help you to see how the code we create relates to the
finished work.
Pyramid Rules
Pyramid is an addictive solitaire card game. It's just mindless enough that you don't have
to think too much, but there definitely are strategies and techniques to getting a higher
score.
I first discovered Pyramid on the Mac way back in the early 90's. I remember my brother,
a PC programmer, liked it so much he stayed up half the night writing a DOS version so
he could play it. A few years ago I found a Palm OS version and practically wore out my
Palm III playing it (my Palm's screen has permanent marks in a pyramid shape on it ;-).
RBU Pyramid, in fact, is loosely based on that excellent Palm OS version by James Lee.
My version works in a similar fashion, with a few enhancements.
To play Pyramid, you shuffle a regular deck of 52 playing cards (four of each card) and
deal them out (face up) in seven overlapping rows: one card on the first row, two on the
next, three on the one after that, and so on. The top row will have seven cards. The
leftover cards are placed upside down in a draw pile.
The object of the game is to clear the Pyramid. You get rid of cards by matching pairs
that add up to 13. The suit is irrelevant. Number cards are worth their face value, aces are
worth 1, jacks 11, queens 12, and kings are 13. Since kings are already worth 13, they
don't require a pair and can be removed by themselves.
The trick is that you cannot remove a card that is being covered by another card (unless
the overlapping card is part of the pair and no other cards are covering the beneath card).
If you wish to draw a new card from the deck, you place it face up next to the deck in the
first discard pile. There are two discard piles. The first discard may contain a maximum
of one card. When you draw a new card from the deck and there already is a card in the
first discard pile, that card is moved to the second discard pile covering up whatever card
is already there.
You may match any pair of cards from the pyramid or either discard pile.
In the RBU version of Pyramid, you are allowed two passes through the deck after which
the game is over (an alternate rule for Pyramid is to allow you just one pass, but I didn't
implement that in RBU Pyramid). To win you don't have to eliminate all the cards, only
those in the Pyramid. If you clear the Pyramid, a new one is created, and you can
continue to add to your score.
The game is over when you either can't remove any more pairs and you've exhausted the
deck for the second time.
Designing Pyramid
When planning a program, I usually go through two mental steps. The first is to put
myself in the user's shoes and envision how I'd like the program to work. I literally
mentally run the program and make sure the interface will work. (For larger programs, I
break it into smaller pieces and imagine how each part will work.)
The second step is to put on your programmer hat and figure out how you'd go about
programming that interface. Sometimes you discover that what you'd like to do
interfacewise just isn't possible because of limitations in your programming environment
(in our case REALbasic), limitations of budget (it will take too long and cost too much),
or limitations of knowledge (you don't know how). It's up to you to discover this as early
as possible in the development process. There's nothing worse than getting halfway
through and then realizing that you've effectively programmed yourself into a corner.
The trick, of course, is to find an interface that will work efficiently for your users, yet
isn't incredibly difficult to program.
For RBU Pyramid, I had the distinct advantage of playing similar games before, so I
knew how I wanted the interface to work. Because Pyramid simply deals with matching
pairs, I decided early on that dragging the cards was not a feature that was required.
Instead, the user would simply click on a card to highlight it, and clicking on the second
matching card would cause the two to disappear.
I'd played some versions of Pyramid that penalized the user for picking a second card that
wasn't a match: the program would beep an error and force the player to click on the first
again to deselect it, then they could continue. I decided that RBU Pyramid would work
differently. If the user's second card was an invalid match, the program would simply
deselect the first card and select the second. That eliminated the annoyance of having to
manually deselect the first card.
I decided on a few other enhancements to the traditional Pyramid interface as well:
keyboard shortcuts for drawing from the deck, a modifier shortcut for displaying valid
pairs (a la Eric's Ultimate Solitaire), sounds, changeable background patterns, unlimited
undos, and a special "safe" shuffling mode.
The latter was one of the features that inspired me to create my own version of Pyramid
in the first place. I'd been frustrated by many games in which the cards were dealt in an
impossible pattern. For instance, four aces near the top of the pyramid with the queens
near the bottom. Since aces and queens are a match to each other (1 + 12 = 13), I needed
to match them to clear the pyramid, but with all the aces covered, the game was
impossible.
This is an example of an impossible shuffle: the two 2's near the bottom of the pyramid
each require a Jack to be removed, yet there are three Jacks near the top of the pyramid,
meaning that a maximum of one exists within the Deck. Even worse, the top card is a 2,
requiring a Jack to match. What a mess! It's a Catch-22: the game cannot be solved.
With pure luck controlling the shuffle, impossible deals happen fairly often. I decided to
add a feature of a "safe" shuffling mode which wouldn't deal impossible shuffles. (I
wasn't totally successful with my algorithm, but it does prevent the majority of
impossible shuffles.)
Note that my safe shuffle doesn't prevent you from foolishly playing a card early that you
need later, but it will stop blind luck from creating an impossible situation.
I also decided to limit RBU Pyramid in a few ways. The version of Pyramid I based mine
on had several modes of plays (levels 1-4). Since I only play the easiest level 1 (which
gives you two turns through the Deck and you only have to clear the pyramid to
advance), that's all I bothered implementing. (Level 4 restricts you to going through the
Deck only once and to "win" you must clear all the cards, even those in the discard
piles!)
Playing Pyramid
Next week we'll get started programming Pyramid, but between now and then, may I
suggest you play RBU Pyramid so you understand what we are trying to do?
I've compiled four versions of RBU Pyramid. Please note that because my code isn't
finalized -- I'm still tracking down a few bugs in the undo system -- I'm releasing these as
betas and so I've set them to expire (to stop working) on November 30, 2001. Before that
date we'll have finished the program and released the final version. (Besides, every
program should have a beta-testing period before it goes into wide release.)
Download the version of RBU Pyramid appropriate for your system:
•
•
•
•
RBU Pyramid for Mac OS 68K (728K)
RBU Pyramid for Mac OS PPC (831K)
RBU Pyramid for Mac OS X (984K)
RBU Pyramid for Windows (600K)
While most of RBU Pyramid is finished and I'm not wanting to start over programming
it, I am open to feedback. Please let me know if you find any bugs or want to share a
comment.
Also, if you enjoy RBU Pyramid, feel free to share it with others, but let them know it's
still a beta release. When the program's completely finished, I plan to officially release it
as a freeware game to help promote REALbasic University.
Next Week
We begin programming our Pyramid card game!
Letters
James writes a follow-up to my response to his original letter:
Marc,
Thank you so much for your detailed and helpful replies to my questions. I thought you
might like to know what I did with your suggestions. I'll take them in the order in which
you wrote them.
-- File menu dimming: as you suggested, I kept "playing" with the code until I found
what I hope are the right places to put the "EnableMenuItems" command.
-- Double-clicking documents to open the application: I actually resolved that problem
before I read your response. I was trying to integrate an existing open file routine into the
open document event and I hadn't made the necessary adaptations. But it works now.
Thanks for the encouragement.
-- Flickering static text box: in this case, I followed your example closely and was quite
pleased with the result. I made some adjustments to account for the fact that the user can
change the window background (and hence the canvas background) and drew a filled
rectangle on which a placed the text. I liked the results so well that I changed all the static
text boxes that displayed changing text to your canvas-based method.
-- Files showing up as BBEdit files: you were exactly on target with your diagnosis of the
problem. I called the Type and Creator modification codes after closing the
TextOutputStream (yes, I was changing the code too early) and, as you said, it works just
fine. Now, I don't think I would ever have been able to figure that one out. Nowhere in
any of the RealBasic documentation, or the other sources I consulted, does it say anything
about BBEdit being the default for RB text files. Nor is there any discussion about where
to place the Type and Creator code to achieve the desired result.
Again, thank you for your invaluable assistance.
James Membrez
Thanks, James, glad I could help. In truth most nagging bugs have simple solutions, but
they can be difficult to find, and sometimes you just need a different viewpoint.
Next, Richard writes:
Hi,
I'm a bit late getting into the RBU tutorials, so this is a question about something a couple
of months old. In the Shooting Gallery game, when you set up the duck sprites you gave
them both a speed and a direction. Later on, when coding NextFrame, you test for the
direction of each duck before moving it. My question is, since speed is defined as an
integer, and integers can be either positive and negative, could you not simply give the
ducks a random speed between -6 and +6 (not including zero) and then simplify the code
for moving the ducks by leaving out the test for direction? This would mean just adding
the speed to the location and then testing whether the new location was off of either side
of the surface. Thanks for any reply.
Richard Becker
Hi Richard! Glad you've "joined" the RBU family. ;-)
Your point is a good one, and your idea certainly would work, though the "testing of the
direction" doesn't require much time to execute. Unless you had hundreds or thousands of
objects and speed was critical, I doubt it would make much difference (though some
empirical testing on your part, if you were interested, wouldn't hurt).
There are two reason I wrote it the way I did. First, it's easier to understand the code. That
might sound minor, but too often cleverness can come back to bite you a year or two later
when you're scratching you're head at midnight and trying to figure out how in the world
a particular routine works.
Second, since this was a demonstration of sprites, I wanted the keep the routine flexible.
The way it is written it wouldn't be difficult to add vertical movement to the "ducks." If
I'd written it the way you suggest, I'd be locked in to horizontal movement permanently
(without significant rewriting).
In short, look for efficiencies in coding, but not at the expense of clarity and expansion.
And most important, make sure the efficiency is worth the cost.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 029
RBU Pyramid II
Last week I gave you the opportunity to download and try the game we're going to be
developing as our next REALbasic University project. I hope you enjoyed exploring the
more-or-less finished game as preparation for the actual coding we're going to start on
this week. As always, the source code for today's lesson is available at the end of the
tutorial.
Getting Started
The two most difficult times you'll have when writing a program are the very beginning,
when you start with a blank file and it's a long while before anything's working, and
toward the end, when the program's 95% working but you need to track down a few
nagging bugs (and of course, by that time, your enthusiasm for the project is low).
The beginning of a program can seem overwhelming since you may not know where to
start. Another danger is that you'll just dive in without any thinking or planning, and you
might get a fair distance in and realize you're doing everything wrong and you must start
over. That can be frustrating and depressing, but sometimes there's no alternative. One
potentially excellent approach is to write a simple test program that does nothing more
than try out your data structure or interface. That can be invaluable in letting you know
you're on the correct track or not.
The key is that when you start your program, you should have a good idea of how you
will program it. Let's look at my thinking process for RBU Pyramid.
There were two issues I thought of when I opened a blank project and stared at that
empty gray window. First, I needed an idea of what my data structure would be like. At
this point it didn't need to be finalized, but I want at least an inkling so I could toy with it
and mentally run the program and see if I'd encounter any problems.
The second thought was to the interface; specifically, what sort of interface controls I'd
use.
At this point in the program's development everything's wide open: I could have gone any
number of directions. For instance, would I draw cards manually inside a canvas or
create cards in a graphics program like Photoshop and import them? My answers to these
questions would have significant ramifications.
Very quickly, I decided that it would be easier to draw the cards manually using
REALbasic. My rational was as followings. Drawing the cards externally, while it would
probably allow me to create better-looking graphics, would be more difficult to
incorporate into the program. There'd either be 52 pictures to draw, or I'd need to create
the basic card pieces (card frame, number, suit graphic, etc.) and assemble them
programmatically. If I kept my graphics simple, drawing a card shape into a canvas was
one line of code, and I could easily draw on a suit graphic and a colored number (or
letter). Besides, by putting my card drawing into a routine, I could easily upgrade it later
with better graphics if I was so inclined. Right now I was anxious to get started.
That decision made, I now turned to my data structure. The obvious choice was an array
of cards. I could see nothing wrong with that. But what kind of array? Would it be a twodimensional array: cards(suit, number) or would multiple arrays for each part of the
game be better (one for the deck, one for the discard, and one for the pyramid)? What
kind of information would be required for each card? How much storage space would I
be using? Would I be duplicating information and moving it around too much (i.e. from
one array to another)?
I also had to think about things like shuffling: the type of array I picked might make
shuffling more difficult. (In the case of RBU Pyramid, I already knew I wanted to
implement my "safe shuffle" routine, and though I had no idea how that would work, I
figured it would be best if the shuffle routine was as simple and obvious as possible.)
The solution I came up with is effective, I think. I would probably use the same approach
in future card games.
First, I created a new custom class called cardClass (choose "New Class" from the File
menu and rename the Class1 that's added to your project window). A CardClass object
will represent a single playing card. Here's how the class is defined:
As you can see, it's very simple: just a number and suit. That's it.
Then I added a module to my project (choose "New Module" from the File menu) and
renamed it globalsModule. Inside that I added a property ("New Property" from the Edit
menu): gCards(52) as cardClass. As you can see, this is a simple array of 52 cards.
Very simple.
I had decided that gCards would represent the "master" set of cards. Instead of shuffling
and moving card objects around, I would simply move integers. Each integer would be
the index of gCards, representing a card.
For example, let's say the user selected a card. The card they selected might be integer 7.
I would simply pass that 7 as the index to gCards to see what the actual card number or
suit was. Therefore the following syntax would be valid (assume i is the index):
msgBox "Card: " + str(gCards(i).number) + " Suit: " +
str(gCards(i).suit)
There are two key advantages to this. One, I only have to store a single number (from 1 to
52) to represent any card in the deck, and two, numbers are much easier to pass around
and shuffle than objects.
For this to work, of course, we can never change the order of gCards. That means we
need a different array to represent the actual cards in the deck (which we'll shuffle). So
let's add another global array like this: gTheDeck(0) as integer.
In Detail
Notice that I set gTheDeck to a size of 0 -- why did I do that? Shouldn't it be 52? Well,
gTheDeck is going to hold all 52 cards at the start of a game, but after we deal out the
pyramid, it will be smaller. It therefore needs to be dynamic (it grows and shrinks as
needed). Even though I happen to know it needs to be 52 at the start of the game, it's
better to keep it at zero as a reminder that it's a dynamic array and needs to be resized at
the start of the shuffle routine. (Note that we don't do this with gCards, since it's static
and will always be 52.)
Think what would happen if I defined it as 52 now, but later, after the user has played a
game, it is smaller, and the user decides to play a new game and the shuffle routine is
called. If the shuffle routine didn't set the size to 52, the program would crash, since the
shuffle must work with all 52 cards. That could be a strange bug to track down: your
program would work fine until you started a new game. You'd be scratching your head
wondering why!
So now we've got two basic data type created. Let's initialize them. Within
globalsModule, add a new method ("New Method" from the Edit menu) and call it
init. It takes no parameters. Here's the code:
dim j, i, index as integer
// *
// This routine should only be called ONCE per launch
// *
index = 0
gCards(index) = new cardClass
gCards(index).suit = 0
gCards(index).number = 0
for j = 1 to 4
for i = 1 to 13
index = index + 1
gCards(index) = new cardClass
gCards(index).suit = j
gCards(index).number = i
next // i
next // i
Our init routine sets up the 52-element gCards array. Our method for doing this is
simple: we have a nested loop which sets the number of the card (1 to 13, Ace to King),
and the suit (1 to 4) is established by the outer loop. Since our gCards array is a single
dimensional array, we increment a counter variable (index) each time through the loop
and use it as the index of gCards.
Note that though the gCards array, like all REALbasic arrays, has a zeroth element, we
don't use it. (Just to be safe I initialize it to zero.)
Next, we'll need a routine to shuffle the actual deck of cards we'll be using (the gTheDeck
array). So let's add a new shuffleDeck method and put in the following code:
dim i, j, k, t, n as integer
// Put unique card in each deck location
redim gTheDeck(52)
for i = 1 to 52
gTheDeck(i) = i
next // i
// Shuffle a random number of times
n = ceil(rnd * 10) + 5
for k = 1 to n
for i = 1 to 52
j = ceil(rnd * 52)
// Swap
t = gTheDeck(i)
gTheDeck(i) = gTheDeck(j)
gTheDeck(j) = t
next // i
next // k
Our shuffle routine is simple enough (for now). First we reinitialize the deck array to 52
and set each element to a different card number (the order doesn't since we're going to be
shuffling them around anyway).
I decided I wanted my shuffle routine to be a little more random than most (computers'
randomize routines are not truly random) so I shuffle the cards multiple times. To do this,
I compute a random value that's between 6 and 15 and shuffle the cards that many times.
(The ceil function rounds up to the next whole number, so the lowest number ceil(rnd
* 10) would return is one and the highest is ten. Add five to that to finish the
computation.)
The actual shuffle procedure isn't complicated. We simply count through the cards and
swap the current card with another picked at random. By doing this a few times, we end
up with a decent shuffle. (There are arguments in computer science circles on the best
shuffle routines, but for most situations I don't see a need for anything more complicated
than this simple method.)
Cool! We've now got our cards created and shuffled. All we need next is a way to display
them. Unfortunately, that's a bit complicated for the time we have left this week, so we'll
save that for our next lesson.
If you would like the REALbasic project file for this week's tutorial, you may download
it here.
Next Week
In part III, we'll create an array of canvases that will display the cards in a pyramid shape.
News
Matt Neuburg, author of REALbasic: the Definitive Guide, has posted an interesting RB
tutorial for children. The second edition of his book (covering RB past version 2) will be
published in September 2001.
Letters
This week we hear from Jerome, who has a "simple" question. I like those!
Hello!
I'm currenly working with RealBasic version 2.0/2.1.2 I'd like to know if there is a way (a
built-in method) to resize a picture? For example I load a picture from hard disk and, in
RB, this pict has one "height" and one "width" property, but these properties are not
modifiable. How could I resize this pict with RB methods (I'd like the program to do this
automatically) It is certainly not possible, but I'd like to ask you nevertheless
Thanks a lot and sorry for my English
Jerome
Your English is fine, Jerome. I know exactly what you are talking about. When I started
with RB, I was puzzled that I couldn't modify the width and height properties of a
picture: that seemed the natural way to resize it. But that's not how it works.
The only way to resize a picture is to draw it at a different size using a graphics object's
drawPicture method. Let's look at the parameters of that method:
Remember, the items between brackets [ ] are optional. Saying g.drawPicture p, 0, 0
is therefore usually about as complicated as you get. The rest of the stuff gets a little
hairy, but it's really not that bad. The third and fourth parameters are the new width and
height you are wanting to modify.
This is probably simplest to show you via an example program. I created a new project
and dragged a canvas and a slider onto it. The slider will be used to control the size of
the picture. Here's what it looks like running (I used a screenshot graphic for the picture):
You can download the entire project here, but I'll include the code in text form so I can
explain it.
This code goes into the canvas' Paint event: whenever it draws the picture, it draws it at
the percentage of size specified by the slider control.
dim x, y, w, h as integer
dim p as picture
// Set p to the picture you drag into your project
p = cardclass
// Set the new width and height to
// the percentage established by slider1
w = p.width * (slider1.value * .01)
h = p.height * (slider1.value * .01)
// Center the result
x = (g.width - w) / 2
y = (g.height - h) / 2
g.drawPicture(p, x, y, w, h, 0, 0, p.width, p.height)
Most of this code should be self-explanatory: the key is we get the percentage from the
slider (we multiply it by .01 to turn it into a decimal percentage) and multiply that times
the original width or height to create the new, resized width or height. The final line of
the routine passes that new size info to the drawPicture method, which draws the
picture at the new size.
Hope this will fill your picture resizing needs!
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 030
RBU Pyramid III
Last we established half of the equation of a working program: our data structure. This
week we create the interface to that data. Since we're writing a card game, that means by
the end of this lesson we'll be able to see the cards in our data structure.
Remember that I'd decided to draw the playing cards programmatically, within
REALbasic, instead of creating graphics externally? That means it's logical for me to use
a canvas object for each playing card in the pyramid. Since we're using REALbasic,
we're into object-oriented programming, so it makes sense that we shouldn't use an
ordinary canvas object, but a custom class.
Our custom class is going to be very simple: we just want to add a few properties to a
standard canvas object. Open last week's project file and let's begin. First, add a new
class ("New Class" from the File menu) and rename it cardCanvasClass. Give it the
following properties ("New Property" from the Edit menu):
Excellent. These properties represent the various states of a card in the pyramid. For
instance, card will tell us the card's suit and number, while booleans like highlight,
selected, and clickable will tell us if the card is highlighted, selected, or clickable
(playable). I'll explain them in more detail when we make use of them. For now, just trust
that they are needed.
Just because we defined a class of object doesn't mean we have any of those objects, so
the next thing we need to do is add one to our main window. Speaking of our main
window, let's rename it from window1 to gameWindow and give it the following
properties:
Now that our main window is established, drag a new canvas onto it and give it the
following settings:
Especially note the most important setting, the super property: it must be set to
cardCanvasClass. That's how we tell REALbasic we don't want an ordinary canvas but
an instance of our custom class.
We also want to be sure we set the index property to 0 -- that tells RB this object is part
of a control array.
Creating our cardCanvas object as a control array element has two primary benefits.
First, it makes it easier to have multiple duplicate objects (in this case, cards) on our
window simultaneously. Second, we can easily duplicate the "master" card object
programmatically instead of having to do it by hand within the IDE. Laying out a
pyramid of cards via programming is easy: doing it painstakingly by hand is a nightmare.
So the next thing we need to do is duplicate the cardCanvas and give us a pyramid
layout. How do we do that? It's not difficult. First we need to remember that there are
more cards on the screen than just the pyramid cards: we also have a Deck and two
discard piles as well. Let's create variables to hold all these cards.
Open globalModules, the module we added in the last lesson. Let's add the following
properties:
gPyramid(28) as cardCanvasClass
gDeck as cardCanvasClass
gTempDiscard as cardCanvasClass
gDiscard as cardCanvasClass
There are 28 cards in the pyramid layout, so we need an array of 28 cardCanvasClass
objects. Remember, that even though the Deck and main Discard pile can contain
multiple cards, only one card is showing at a time, so we really only need a single
cardCanvasClass object to act as the interface to the top card. (Whatever that top card is
can change, of course.)
Next, go back to gameWindow and open the Code Editor (option-tab while the window is
selected or double-click on the window). Add a new method called initBoard: that's
where we'll duplicate the card objects.
Here's the code for that routine:
dim j, i as integer
dim yOffset, xOffset as integer
dim index as integer
// *
// This creates the control array of the cards, duplicating the
single
// cardCanvas on the window and arranging the others in a pyramid
// shape.
//
// Only call this routine ONCE per launch!
// *
index = 0
yOffset = -20
for j = 1 to 7
yOffset = yOffset + 40
for i = 1 to j
index = index + 1
xOffset = (gameWindow.width - (cardCanvas(0).width * j)) \ 2
gPyramid(index) = new cardCanvas
gPyramid(index).left = xOffset + ((i - 1) * (cardCanvas(0).width
+ 5))
gPyramid(index).top = yOffset
gPyramid(index).card = index
gPyramid(index).row = j
next // i
next // j
// Deck of cards
yOffset = yOffset + cardCanvas(0).height + 20
gDeck = new cardCanvas
gDeck.left = 30
gDeck.top = yOffset
gDeck.clickable = true
// Temp discard pile
gTempDiscard = new cardCanvas
gTempDiscard.left = gDeck.left + cardCanvas(0).width + 20
gTempDiscard.top = yOffset
// Discard pile
gDiscard = new cardCanvas
gDiscard.left = gTempDiscard.left + cardCanvas(0).width + 20
gDiscard.top = yOffset
// Set initial values
cardCanvas(0).visible = false
What does this do? Simple: it duplicates multiple instances of the cardCanvas(0) object
on the main window. For each object, we establish its location on the window and, if
appropriate, set some of the settings for the object. For instance, the Deck is set to be
clickable since at the start of the game it's always full of cards.
After this routine runs, the window is will contain 32 instances of cardCanvas objects
(28 pyramid card + 1 deck + 2 discards + 1 master card). Our master object, of index
zero, is ignored for the rest of the game (note that we set it to invisible above).
Of course if you run the program right now, you won't see anything in the window. That's
because canvases are invisible by nature: they only display what you draw inside them.
So let's draw something inside them!
Within the gameWindow Code Editor, look inside the Controls section and you should find
a control called cardCanvas(). That's the code block for our control array of card
objects. Toggle that open if it's not already, and put this line of code in the Paint event:
drawCard(index, cardCanvas(index).selected)
Once again, REALbasic's object-oriented programming makes the complex simple. A
great deal is happening here. When gameWindow needs to be drawn, each of the 32 visible
cardCanvas objects will fire their Paint events, one by one. The objects all have the
same name (cardCanvas) since they're a control array, but they each have a unique
index. So we pass that index on to our drawCard routine. We also pass on the selected
state of the card.
Now we just need to write a card drawing routine. This is simpler than it sounds. Create a
new gameWindow method with the following settings:
Good. We can divide our card drawing routine into two parts: the Deck and the other
cards. That's because the Deck's cards are face down. The Deck also has a couple other
potential states: when it's exhausted the first and second times.
Speaking of the Deck holding multiple cards, the main discard pile can do the same
(though those are face up). We need to add a global property to store the contents of the
discard pile. Open up globalsModules again and add the following property:
gTheDiscard(0) as integer. That will give us a dynamic array representing the stack
of discarded cards.
Now we're ready to proceed with the drawCard routine. The first thing we do is check to
see if index equals 29. If it does, that means we're drawing the Deck, so we don't draw it
as a normal card.
Within the normal card drawing area, we have to check to see if index is either 30 or 31:
those are the discard piles. Since they could be empty, we must check for that and draw
an empty frame if there's no card there. Otherwise we draw the card.
If index is neither 30 or 31, then we draw the card.
Here's the code. (For now we're leaving the Deck card drawing blank.)
dim
dim
dim
dim
g as graphics
theCard, theSymbol as string
w, h, x, y as integer
cardColor, white, black, gray as color
w = cardCanvas(index).width
h = cardCanvas(index).height
g = cardCanvas(index).graphics
if index = 0 then
return
end if
if index <> 29 then
if (index = 31 and uBound(gTheDiscard) = 0) then
// Empty Discard
g.foreColor = rgb(0,0,0)
g.drawRoundRect(0,0,w,h,15,15)
else
if (index = 30 and gTempDiscard.card = 0) then
// Empty TempDiscard
g.foreColor = rgb(0,0,0)
g.drawRoundRect(0,0,w,h,15,15)
else
g.textfont = "Geneva"
g.bold = true
g.textsize = 14
theCard = str(gCards(cardCanvas(index).card).number)
select case gCards(cardCanvas(index).card).number
case 1
theCard = "A"
case 11
theCard = "J"
case 12
theCard = "Q"
case 13
theCard = "K"
end select
// suit color
select case gCards(cardCanvas(index).card).suit
case 1 // Hearts
cardColor = rgb(255, 0, 0)
theSymbol = chr(169)
case 2 // Spades
cardColor = rgb(0, 0, 0)
theSymbol = chr(170)
case 3 // Diamonds
cardColor = rgb(255, 0, 0)
theSymbol = chr(168)
case 4 // Clubs
cardColor = rgb(0, 0, 0)
theSymbol = chr(167)
end select
white = rgb(255, 255, 255)
black = rgb(0, 0, 0)
gray = rgb(200, 200, 200)
if reverse then
cardColor = rgb(100,100,100)
white = rgb(0, 0, 0)
black = rgb(255, 255, 255)
end if
if cardCanvas(index).highlight then
g.foreColor = gray
g.fillRoundRect(0, 0, w, h, 15, 15)
else
g.foreColor = white
g.fillRoundRect(0, 0, w, h, 15, 15)
end if
g.foreColor = black
g.drawRoundRect(0, 0, w, h, 15, 15)
g.foreColor = cardColor
g.drawString theCard, 6, g.textHeight + 6
g.textFont = "Symbol"
g.textSize = 30
g.bold = false
g.drawString theSymbol, 28, g.textHeight - 4
end if
end if
else
// Deck gets drawn here.
end if
The actual drawing the card is fairly simple. We first establish a string variable for the
card number (which could be a letter, if it's a face card), set the cardColor variable to the
appropriate suit color (red or black), and set theSymbol the appropriate suit symbol
(club, spade, heart, or diamond).
Next we define some basic colors: black, white, and gray. If reverse is true (the card is
selected), we invert those values.
Then we draw the background of the card. But here we do a check: if the card is
highlighted, we draw it with a gray background, otherwise it's white. (Highlighting is
different than being selected: we'll be adding a feature that will make RBU Pyramid
highlight playable matches when the user holds down the shift key.)
The final part of the code simply draws the suit symbol, card number, and card frame in
the appropriate colors. Simple!
Shall we try it? Run the program and what do you see... nothing! Huh? Why is that?
Well, there are a couple reasons. First, we never actually call our initBoard routine, so
no cardCanvas objects are created!
We can quickly fix that by adding this code to the bottom of our init method (inside
globalsModule).
// Set up the board
gameWindow.initBoard
That's better, but the program still doesn't draw anything! Oh yeah... we never actually
call init either!
Let's add a new class to the program ("New Class" from the File menu), rename it "app",
and set its super to application. Good. Now inside app's Open event, type "init" -- that
will run our init method when RBU Pyramid is launched.
Now your program should work. Sort of. It should display something like this:
Note that the discards are blank and there's nothing where the Deck should be, but we'll
take care of those details in the future. More significantly, the cards shown in the pyramid
aren't shuffled: but that's because we never call our shuffle routine!
There are other limitations as well: the cards can't be selected yet. That will give us a
good project for next week. At least we can now see our data: our imaginary array of
cards is starting to seem real.
If you would like the REALbasic project file for this week's tutorial, you may download
it here.
Next Week
In part IV, we'll shuffle the cards and make it so we can select them.
Letters
This week we don't have a REALbasic question, but a letter of encouragement from a
reader from France:
Hello,
I have currently no particular question, however I should like to thank you for the
columns. They are very interesting and I have learned a good lot thanks to them.
But above all, I want to tell you that I am very shocked and very sad because of the
terrible events that recently happened in USA. Believe that the French people is, as I am,
feeling close to the US people.
With all my sympathy, I wish you a great courage. Think that you are not alone in those
dreadful circumstances.
Excuse my bad English!
Sincerely yours,
Jacqueline
Thanks Jacqueline, for your kind words. In just one week the United States has grown a
great deal: our innocence was lost, heroes died saving the lives of others, and politicians
have set aside petty differences and united in an unprecedented manner. It's been an
amazing seven days.
For me personally, the most profound expression has been that of support from other
countries, especially the Europeans. America is such a large country we often ignore the
affairs of other countries, but to see U.S. flags and banners displayed at European soccer
matches this past weekend, UEFA matches postponed, and a minute of silence before
games was touching, perhaps all the more when I realize most Americans are ignorant of
these profound gestures.
My prayer is that this tragedy will produce some good; uniting the planet to defend
humanity will hopefully be the first step toward something more.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 031
RBU Pyramid IV
Last week we got our game to draw some cards, but they weren't shuffled and the player
can't select cards. We'll get that working today.
Shuffling and Dealing
Let's start by creating a newGame routine which will initialize the variables needed for a
new game. Open globalsModule (double-click on it) and add a new method (Edit menu,
"New Method"). Call it newGame and put in the following code:
// Distribute a fresh deck of cards
freshDeck
Okay, yeah, nothing much is going on here... yet. We'll be adding to this later. For
instance, we'll eventually need a variable to keep track of the player's score, and that
would be initialized to zero here. For now, though, we just want to get a bare bones
version of the game working. The key routine we need now is the freshDeck routine,
which generates a new, shuffled deck of cards.
Let's create it. Add in another method and call it freshDeck. Here's the code:
dim j, i as integer
// Build a full deck and shuffle it
shuffleDeck
// Establish cards on board
for i = 1 to 28
gameWindow.cardCanvas(i).card = gTheDeck(i)
next
// Remove those cards from the full deck
for i = 1 to 28
gTheDeck.remove 1
next
// Initialize all the cards
for i = 1 to 31
gameWindow.cardCanvas(i).clickable = false
gameWindow.cardCanvas(i).selected = false
gameWindow.cardCanvas(i).visible = true
gameWindow.cardCanvas(i).topCard = false
gameWindow.cardCanvas(i).highlight = false
next
// Erase the discard
redim gTheDiscard(0)
gTempDiscard.card = 0
gDiscard.card = 0
gDeck.card = 0
If you read through this code you can see it does more than just shuffle the cards: it
shuffles them, deals out 28 cards to the pyramid, builds the Deck and initializes the
discard piles, and finally sets several custom properties of the cards such as whether or
not the cards are visible, selected, etc. Overall it's fairly routine stuff, but very important.
All this stuff must be set up exactly like this at the beginning of any game, or things won't
work correctly.
Once we've done the above, we're almost done, except we must call our new newGame
method. Go to the Open event of the app class we added last week and make it look like
this:
// Initialize and start a new game
init
newGame
Excellent. You should be able to run the program now and if you've done everything
correctly, you'll see a screen full of shuffled cards this time:
Selecting and Deselecting
That's great, but we still can't select any of the cards. How do we get that working?
Well, the raw selecting and deselecting is easy: we actually did most of the work last
week in the cardCanvas' Paint event. That's where we check to see if a card is selected
or not (by examining our .selected custom property) and draw it shaded if it's selected.
In fact, we could just add these two lines of code to cardCanvas's MouseDown event to
enable selection/deselection:
cardCanvas(index).selected = not cardCanvas(index).selected
cardCanvas(index).refresh
Run the program and you'll see that yes, you can indeed select and deselect cards.
However, the process isn't intelligent, and it not only lets you select multiple cards, it lets
you select cards that shouldn't be allowed according to the rules of pyramid!
We've got to make the selection process more intelligent, and here things start to get a
little complicated. Before we begin, let's do a little forethinking about what we need to
happen. I've already written the code, of course, but this will give you an idea of the
thinking process that I went through to generate that code: it didn't just magically appear.
Visualize the game of pyramid. How does the selection process work? For starters, we
know that only the top-most cards are selectable. Next, we know that no more than one
card can be selected at a time: if a second card is selected it is either a match to the first
and both are removed, or it is an invalid match and the first card is deselected and the
second card remains selected (effectively switching our selection). If it's a match, we've
got to deal with removing those cards.
Then we've also got our Deck, which deals out a new card when clicked, as well as the
discard piles.
Let's think about these issues from a programming standpoint. The first problem, that of
only allowing the selection of the top-most cards, is fairly straightforward. We already
have a boolean (true/false) property for each of our cardCanvas objects which tells us if
a card is clickable or not. All we need is a routine that will scan through the cards and
set the appropriate cards to clickable or not.
To solve the second part of the problem, that of multiple selection, we'll need some way
to save the current (previous) selection. How else would we know that a card has been
selected or which card it was?
This is easily solved by adding a global property which contains the currently selected
card. Open globalsModule and add this property (Edit menu, "New Property"):
gSelection as integer
We'll also need routines that handle card matches, so let's add them now. Go back to the
gameWindow Code Editor and add three methods: cardMatch, kingMatch, and
discardMatch. For each method, include this parameter: index as integer.
For now, we'll pretty much leave these routines empty, but they give us a way to include
calls as appropriate within our selection routine. Just so we can see what's happening,
let's add msgBox "Match!" to the cardMatch method.
We're now ready to write our main selection routine. All this code goes into cardCanvas'
MouseDown event (replace the two lines of temporary code we used earlier):
// Card is clickable and is not the deck
if cardCanvas(index).clickable and index <> 29 then
cardCanvas(index).selected = not cardCanvas(index).selected
cardCanvas(index).refresh
// Previous card is valid and is not the same as the current card
if gSelection <> -1 and index <> gSelection then
// Are both selected?
if cardCanvas(gSelection).selected and cardCanvas(index).selected
then
// See if there's a match
if
gCards(cardCanvas(index).card).number+gCards(cardCanvas(gSelection).car
d).number=13 then
// Match! Is one of the cards a discard pile card?
if index <> 30 and index <> 31 and gSelection <> 30 and
gSelection <> 31 then
// No, so "delete" both cards and up the score
cardMatch(index)
else
discardMatch(index)
end if //
return true
else
// not a match, so we deselect old card
cardCanvas(gSelection).selected = false
cardCanvas(gSelection).refresh
end if // A match!
end if // both are selected
end if // gSelection <> -1 and index <> gSelection
// Delete King
if gCards(cardCanvas(index).card).number = 13 then
cardKing(index)
return true
end if // is a King
if cardCanvas(index).selected then
gSelection = index
end if
return true
end if // cardCanvas(index).clickable and index <> 29
This may look complicated and intimidating, but let's take it step-by-step. First, we
simply check to see if the card clicked on is clickable -- if it is, we continue to analyze
the selection to see what needs to be done. We also check at this point to make sure that
the user didn't click on the Deck -- that's a completely different problem we'll deal with
later.
Once we've got a valid click, we toggle the state of the current card (select it if it's
unselected, deselect it if it's already selected). Next, we do a quick check to make sure
that our saved selection variable, gSelection, contains a valid selection and is not the
same as the current card.
Finally, we make sure that both our previous selection and the current card are selected,
and if so, we check to see if there's a match. Finding out if there's a match is easy: we
simply add the numbers of the two selected cards to see if they total 13.
Once we know there's a match, we check to see if one of the matched cards is on a
discard pile. If it is, we handle that in our special discardMatch routine. Otherwise we
pass the match to our cardMatch method.
If the match isn't valid, we simply deselect the previous card.
Finally, we check to see if the player kicked on a King, and if so, we handle removing it.
Our final step is to set gSelection to the current card (if it's selected -- keep in mind it
could have been toggled off).
If you run the program now, you may be disappointed. Nothing much will happen: none
of the cards are clickable, and even though you put in code to handle matches, it doesn't
seem to do anything.
That's because none of the cards are clickable. Remember how we initialized all the
cards' clickable property to false in the freshDeck method? Since we've never done
anything to make any of the clickable, we now can't click on any of them!
The solution is we need to write a routine that will update the state of all the cards,
determining which are clickable. Add a new method (to gameWindow) and call it
updateCards. Here's the code (it's a little complicated):
dim row, column, index as integer
//
// Routine to update which cards are clickable.
//
// First initialize all cards to off status
for index = 1 to 28
cardCanvas(index).clickable = false
cardCanvas(index).topCard = false
next // index
// First row cards are ALWAYS clickable
for index = 22 to 28
cardCanvas(index).clickable = true
cardCanvas(index).topCard = true
next // index
// This checks for clickable cards underneath the
// top cards (valid if the top cards are gone).
index = 0
for row = 1 to 6
for column = 1 to row
index = index+1
if (not cardCanvas(index+row).visible) and (not
cardCanvas(index+row+1).visible) then
cardCanvas(index).clickable = true
cardCanvas(index).topCard = true
end if
next // column
next // row
index = 0
for row = 1 to 6
for column = 1 to row
index = index+1
// This checks for matches on top of each other (which are
removable)
if
(gCards(cardCanvas(index).card).number+gCards(cardCanvas(index+row).car
d).number=13) then
if (not cardCanvas(index+row+1).visible) and
(cardCanvas(index+row).topCard) then
cardCanvas(index).clickable = true
end if
end if
if
(gCards(cardCanvas(index).card).number+gCards(cardCanvas(index+row+1).c
ard).number=13) then
if (not cardCanvas(index+row).visible) and
(cardCanvas(index+row+1).topCard) then
cardCanvas(index).clickable = true
end if
end if
next // column
next // row
Yes, we are now getting into some complicated stuff!
The first parts of this routine are not too difficult: we first set all cards to unclickable,
then turn on the top cards of the pyramid.
Then we start getting messy. We have two checks we need to do. First, we need to find
the top-most cards and make them clickable. Remember, for RBU Pyramid, we never
actually delete a cardCanvas object: all we do is make it invisible. So we can check to
see if a card is top-most or not by looking at the cards above it and seeing if they're
visible or not. If they're invisible, then the card underneath is clickable.
To actually figure out which cards on are top requires a little math. To figure out the
formula, it's good to remember how our pyramid is built. Our pyramid structure is made
of cardCanvas objects which are numbered 1 to 28, like this:
Row
Row
Row
Row
Row
Row
Row
1:
1
2:
2 3
3:
4 5 6
4:
7 8 9 10
5:
11 12 13 14 15
6:
16 17 18 19 20 21
7: 22 23 24 25 26 27 28
If you study the above, you may see there's a relationship between the row number and
the card numbers on different rows. For example, the first row is row 1. If you add 1 to
the card number, you end up with the card on the next row (card 2: 1 + 1 = 2). This also
works on the second row: 2 + 2 = 4, and 4 is first card on the third row. Let's try it on the
sixth row. The first card is 16. If we add 6 we get 22, which is the first card of the seventh
row. It works!
This gives us a formula we can use to figure out which card is above the current card. If
we step through the cards in a loop, we can look at the cards on top and see if they are
invisible or not (that's the above not cardCanvas(index + row).visible [the left card
on top] and not cardCanvas(index + row + 1).visible [the right card on top]).
See? It's not so bad!
The second check is for the situation of where a matching pair overlap. Here the check is
similar, but we can't have more than one card overlapping and the pair must be a match
(total 13).
That should be about it, except that we need to call updateCards at the beginning of the
game. Go back to freshDeck and add these lines to the very bottom:
gameWindow.updateCards
gameWindow.refresh
There, that calls updateCards so the state of selectable cards get established correctly.
You should now be able to select cards and almost play the game.
As you can see, we've accomplished a great deal in the few lessons so far, but by now
you should begin to see the scope of what we are doing.
There's still tons to do, of course: we haven't done anything with the card matching
routines, so nothing much happens when there's a match. We also have been neglecting
to do anything with the Deck and discard piles. That gives us something to do for next
week.
If you would like the REALbasic project file for this week's tutorial, you may download
it here.
Next Week
We continue with RBU Pyramid, making it so card matches can be removed.
Letters
Our first letter this week is from across the Atlantic:
I am following the RealBasic University with pleasure but, as someone who has been
programming in BASIC since the 1980s (on Spectrums and Amstrads), I am having
difficult seeing the advantages of using custom classes rather than simple arrays.
To define a global array of, say, people's names in the 'traditional' way, you would write
something like:
[define the property in a module]
dim gName(10) as string
gName(1)= "John"
Using custom classes, you would need to write:
[create new class]
[define the property in the class]
dim gName(10) as nameClass
gName(1)=new nameClass
gName(1).name="John"
I can see the advantages for custom controls like editFields but, apart from grouping
properties/variables together in one area (which you could do with modules), there does
not seem an obvious advantage while there IS the clear disadvantage of extra steps and
lines of code.
What other advantages ARE there in custom classes? I'd be happy switch to using them if
I knew why I should.
Thanks,
Paul Thompson
Reading, UK
In your example, Paul, I'd use a simple array structure as well: you are correct that it's
much easier. The advantage of using a custom class is when you have multiple pieces of
information that benefit from being encapsulated into an object.
For example, which is easier to use:
dim
dim
dim
dim
dim
nameArray(0) of string
streetArray(0) of string
cityArray(0) of string
stateArray(0) of string
zipArray(0) of string
// Add a record
nameArray.append "John"
streetArray.append "123 Main Street"
cityArray.append "Springfield"
stateArray.append "MO"
zipArray.append "65807"
// Delete 8th record
nameArray.remove 8
streetArray.remove 8
cityArray.remove 8
stateArray.remove 8
zipArray.remove 8
personClass:
name as string
street as string
city as string
state as string
zip as string
dim personArray(0) of personClass // person data
dim n as integer
// Add a record
n = uBound(personArray) + 1
redim personArray(n) // add one
personArray(n) = new personClass // Create new object
personArray(n).name = "John"
personArray(n).street = "123 Main Street"
personArray(n).city = "Springfield"
personArray(n).state = "MO"
personArray(n).zip = "65807"
// Delete 8th record
personArray.remove 8
As you can see, the two programming methods are similar, except the first uses multiple
arrays while the second uses a single array of personClass objects. For some tasks, such
as adding a new record, the process is similar though a couple steps more complicated for
the custom class method. However, for other tasks, such as deleting a record, the custom
class method is far simpler requiring a single line of code.
This becomes even more of an issue the more complicated your data structure. With the
first method, it could be easy to lose track of an array's index: for example, if you forgot
to delete a record from the zipArray field, suddenly your zipArray isn't the same size as
the other fields and your data doesn't line up (i.e. the zip code for record 10 now lines up
with record 11 of the other fields). That can be hairy to debug!
If your data structure is at all complicated, the custom class method will soon prove its
value. For instance, in the above two examples, which would be easier to add another
element to the client data structure (such as home telephone or id number)?
And what happens if your data's sub-structure is dynamic? Let's say this database
structure we've created was a list of your salespeople and each had their own list of sales.
Since the selling data is different for each person, you can't use a static array like your
example. With the custom class solution you'd simple define a subfield, salesClass, like
this:
salesClass:
saleDate as date
amount as double
product as string
quantity as integer
Then you'd simply add that to your original personClass definition:
personClass:
name as string
street as string
city as string
state as string
zip as string
sales(0) as salesClass
Now there's a separate array of sales for each person. You can't even do this with your
method. (About the best you could do would be to use a multi-dimensional array with the
array's second dimension being the sales data, but that would require that everyone have
the same number of sales as the person with the most sales.) And I don't even want to
think about how complicated this could get if you had multiple dynamic data to keep
track of per person!
In conclusion, you do pay a slight price in complication using a custom class, but the
benefits can be considerable if your data structure is complicated. If it's simple, don't use
a custom class.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 032
RBU Pyramid V
In part IV we got RBU Pyramid to let the user select cards and even detect matches, but
we didn't do anything with those matches. Let's fix that today.
Removing Card Matches
Open your most recent version of the project and go to the cardMatch method within
gameWindow. Delete the temporary code there and replace it with the following:
// "Delete" the matched cards
cardCanvas(index).visible = false
cardCanvas(gSelection).visible = false
// Increase the score
gScore = gScore + 2
gSelection = -1
updateCards
Pretty simple, eh? This hides the two selected cards and increases the score by two (one
point for each card removed). When we're done we call updateCards to update which
cards are clickable.
Note that we have introduced a new global variable here -- gScore -- which represents
the player's score. Let's add that to globalsModule before we forget. Open
globalsModule (double-click on it) and add a new property (Edit menu, "New
Property"): gScore as integer.
Now let's tackle the more complicated routines. Back in gameWindow, go to the
discardMatch method. Remember, this is the match routine that's called when one or
more of the selected cards is on a discard pile. Why do we need a separate routine for that
contingency? Because we don't want to remove (hide) a card on a discard pile.
Here's the code for discardMatch:
// It's on one of the discard piles!
//
// Figure out which one is the discard and "delete" the other
// (We don't want to "delete" the discard canvases since they are
reused.)
if index <> 30 and index <> 31 then
cardCanvas(index).visible = false
end if
if gSelection <> 30 and gSelection <> 31 then
cardCanvas(gSelection).visible = false
end if
// if it's the temp Discard, we set its contents to zero
if index = 30 or gSelection = 30 then
gTempDiscard.card = 0 // erase card
gTempDiscard.selected = false
gTempDiscard.clickable = false
gTempDiscard.refresh
end if
if index = 31 or gSelection = 31 then
// It's the discard pile
// Delete top card of discard pile
gTheDiscard.remove uBound(gTheDiscard)
if uBound(gTheDiscard) <> 0 then
// Still some left, display top card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.clickable = true
else
// None left, empty discard pile
gDiscard.card = 0
gDiscard.clickable = false
end if
gDiscard.selected = false
gDiscard.refresh
end if // index = 30
// Increase the score
gScore = gScore + 2
gSelection = -1
updateCards
This routine has to do several things: if first hides the non-discard match (if any), and
then it uses the top card on the discard pile. If it's the temp discard pile, which can only
hold one card, it removes it and that's it. However, if the match is from the main discard
pile, it must remove the top card and set the discard pile to display the next card in the
pile. It does this by looking at gTheDiscard to see if uBound is not zero. If it isn't, then it
sets gDiscard.card (the actual cardCanvas object of the main discard pile) to the last
card in gTheDiscard.
What happens when a player clicks on a king card is very similar but different enough it
warrants its own routine. Put this code into the cardKing method:
// Is the King on a discard pile?
if index <> 30 and index <> 31 then
// not a on discard pile, so just remove it
cardCanvas(index).visible = false
cardCanvas(index).refresh
else
// It's on one of the discard piles
// Figure out which
if index = 30 then
// Temp discard pile
gTempDiscard.card = 0 // erase card
gTempDiscard.refresh
gTempDiscard.clickable = false
else
// It's on the discard pile!
// Delete top card
gTheDiscard.remove uBound(gTheDiscard)
if uBound(gTheDiscard) <> 0 then
// Still some left, display top card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.clickable = true
else
// None left, empty discard pile
gDiscard.card = 0
gDiscard.clickable = false
end if // uBound(gTheDiscard) <> 0
gDiscard.selected = false
gDiscard.refresh
end if // index = 30
end if
// Increase score
gSelection = -1
gScore = gScore + 1
updateCards
Note that we first determine if the king match is one of the discards or not. If it isn't, we
just remove (hide) it. If it is a discard, we handle it in a similar manner to the way we did
in discardMatch.
Good. Try running the program now: it should work and let you remove matches
(including kings). However, since we don't have the Deck working, we can't tell if that
code works yet since there are no discards. Let's get that Deck working!
Enabling the Deck
Getting the Deck working might not seem complicated, but it is. The Deck affects many
other aspects of the program, such as both discard piles. To get it going we'll need to add
some code to our existing routines as well as write some new ones. So pay close attention
as we'll be jumping around a bit.
First, we need to draw the card deck. Go to our drawCard routine and find the comment:
// Deck gets drawn here. Add the following code after that, but before the final end
if line.
// Empty Deck
if (index = 29 and uBound(gTheDeck) = 0) then
g.foreColor = rgb(0,0,0)
g.drawRoundRect(0,0,w,h,15,15)
g.foreColor = rgb(255, 255, 255)
g.textFont = "Geneva"
g.textSize = 36
g.bold = true
y = cardCanvas(0).height - (g.textHeight \ 2)
if gFirstTimeThrough then
x = (cardCanvas(0).width - g.stringWidth("R")) \ 2
g.drawString "R", x, y
else
x = (cardCanvas(0).width - g.stringWidth("D")) \ 2
g.drawString "D", x, y
end if
else
if reverse then
g.foreColor = rgb(0, 0, 0) // Black
g.fillRoundRect(0,0,w,h,15,15)
g.foreColor = rgb(0,0,0)
g.drawRoundRect(0,0,w,h,15,15)
else
g.foreColor = rgb(100, 100, 100) // Gray
g.fillRoundRect(0,0,w,h,15,15)
g.foreColor = rgb(0,0,0) // Black
g.drawRoundRect(0,0,w,h,15,15)
end if
end if
This drawing routine is simple. We first check to see if gTheDeck has cards left. If it
does, we simply draw the back of a card (a gray box). We draw the card back with black
if reverse is true (meaning the card is selected -- more on that later).
More complicated is what happens when the deck is empty: here we draw an "R" if the
deck is ready to be redealt, or "D" if we've exhausted the deck for the second time (no
redeal permitted).
How do we tell if the deck is on its second deal? Ah! Sharp-eyed readers will note that
we've introduced a new variable to track this. It's a boolean (either true or false) telling us
if we're on the first run-through or not. Open globalsModule and add this property:
gFirstTimeThrough as boolean.
That's the property we check in the above to tell if we draw an "R" or a "D".
Our next step in activating the deck is that we need to detect when a person clicks on the
deck. Detecting this means we need to modify some code we wrote earlier. Open
cardCanvas' mouseDown routine and scroll to the very bottom. Add this code:
// Clicked on deck
if index = 29 and (uBound(gTheDeck) > 0 or gFirstTimeThrough) then
deckAdvance
end if // index = 29 (clicked on deck)
This will detect if the user clicks on the deck and the deck has cards left. If so, it calls a
method called deckAdvance. Speaking of that, let's add that method now and put this
code in it:
// Is there an old selection?
if gSelection <> -1 then
// Deselect it!
cardCanvas(gSelection).selected = false
cardCanvas(gSelection).refresh
gSelection = -1
end if
// Are there are cards left?
if uBound(gTheDeck) > 0 then
gSelection = -1
// Is the temp discard empty?
if gTempDiscard.card > 0 then
// Move the temp discard card to main discard pile
gTheDiscard.append gTempDiscard.card
gTempDiscard.card = 0
end if
// Add one to temp discard
gTempDiscard.card = gTheDeck(uBound(gTheDeck))
// Remove last item from deck
gTheDeck.remove uBound(gTheDeck)
gTempDiscard.clickable = true
gTempDiscard.selected = false
gTempDiscard.refresh
// Redraw discard card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.selected = false
gDiscard.clickable = true
gDiscard.refresh
drawCard(29, true)
if uBound(gTheDeck) > 0 then
drawCard(29, false)
end if
else
// No cards left -- reset the deck if this is first time through
if gFirstTimeThrough then
resetDeck
gFirstTimeThrough = false
end if
end if // uBound(gTheDeck) > 0 (there are cards left in the deck)
updateCards
What does all this do? Well, first it deselects any existing card selection. Next, we check
to see if there are cards left in the discard pile. If there aren't, we reset the deck (moving
the discard cards back into the deck). If there are cards left, we deal out the top card to
the temp discard pile. If the temp discard pile already has a card on it, we first move it to
the main discard pile.
Finally, we draw the deck twice: once as selected (reverse mode), and once not. That will
give the player visual feedback for when they click on the deck.
As part of this code we are calling a routine named resetDeck. We won't add the code
for that until next week, but just so we can get this to compile, add a new method to
gameWindow called resetDeck (leave the contents blank).
If you run RBU Pyramid right now, it should work. You should be able to remove
matching cards, either from the discards and/or the pyramid, and the deck should advance
when you click on it. However, you'll note that the deck won't reset: we'll fix that next
week.
If you would like the REALbasic project file for this week's tutorial, you may download
it here.
Next Week
We'll get the deck to redeal, and we'll update the scoring system so that our pyramid
game is playable.
News
Matt Neuburg, author of REALbasic: The Definitive Guide, has written an interesting
article for O'Reilly on REALbasic for HyperCard users.
In other news, volunteers are hard at work translating REALbasic University into other
languages. Floris van Sandwijk has already translated the first few columns into Dutch,
and Kazuo Ishizuka is working on a Japanese version.
I'd like to publicly thank these two for their incredible work: translating technical
material like programming tutorials is tough. Please let them know you appreciate their
efforts! Also, if you notice errors, contact them directly rather than notifying me.
(If you are interested in translating RBU columns into another language, please contact
me for permission and instructions.)
Letters
Hi Marc!
I just went through your GenderChanger tutorial and thought it was one of the gretatest
tutorials ever. It was challenging but for me was easy to follow along. It taught alot of the
hidden things - like seting up your program- that most books don't do.
For example, you set an array's data type to a class - something that I wouldn't know you
could do from reading the manual and Matt Neuburg's book.
Anyhow, I decided to take this step a bit further with somwthing I'm working on (it's a
calcluator for a page imposition program(Preps) for prepress)
I have a global array using a data type based on a class I made. The array gets appended
here and there in the program. The array does seem to increase when I append it ( i test it
to see if the values are being sent - and they do get sent). Anyhow. I keep getting
NilException errors saying that an item in the array doesn't exists, when I know that isn't
so. (again, I test and the program does have values stored in the array but when I try to
use them elsewhere in code, the nasty NilException pops up.)
To be clearer, i got a gGlobalArray(i).sSheetSize array (gGlobalArray() as
cSheet -- .sSheetSize is a property of class cSheet) I just finished appending the array
then get the value of gGlobalArray.sSheetSize. Everything is fine until I save it into a
string like so:
dim theLine as string
theLine = gGlobalArray(i).sSheetSize
That's where the error happens. Do you have any suggestions? I'm completely stumped.
I'm using RB 3.5. Thanks for any suggestions!
Mike
PS. I really enjoyed your PageMaker scripting site. That too was a great site I LOVED to
go to. PageMaker is fun to script. I tried my hand at Applescripting Quark but for me it's
next to impossible. I don't know why. The handbook Quark has for it is awesome, and the
object model is easy to navigate around in, but the measuremnt system is bizarre. For
example, for a .5 in wide box the .5 isn't an integer or double. You have to go gymnastics
to convert it into a useable number then convert it back to a thing Quark understands.
Thanks for the letter, Mike. I haven't had much experience scripting Quark XPress -AppleScript's syntax is, frankly, bizarre. The language changes with each app you
attempt to script! Debugging is worse than frustrating: it's impossible. PageMaker uses a
proprietary language but I still find it easier than using AppleScript to automate InDesign
or Quark.
As to your REALbasic problem, I'm not positive without seeing more of your code, but I
suspect your problem comes from failing to instantiate your cSheet object. Remember,
you not only need to allocate space in the array for the object, but you must use the new
command to generate a new instance of the cSheet object.
For example:
dim n as integer
dim theLine as string
// Allocates more space in the array
n = uBound(gGlobalArray) + 1
redim gGlobalArray(n)
// Instantiates a new instance of a cSheet object
gGlobalArray(n) = new cSheet
// Now you can work with gGlobalArray(n) safely
You only need to new an object once, but since an array is made up of multiple objects,
you must do it for each object in the array. The rule of thumb: any class object must be
instantiated with the new command before you do anything with it.
If that's not your problem, there are some other possibilities:
•
You don't reveal above what data type .sSheetSize is, but if it's a class, it needs
to be instantiated with the new command.
•
Your index for the array is out of range (i.e. in your code snippet, i is not defined
so I don't know if it's valid or not).
It's an interesting problem: let me know if you figure it out. (By the way, since I work in
the prepress industry, I'd be curious to see your finished app if you're game. I don't use
Preps now, though I did at one time.)
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 033
RBU Pyramid VI
Last week we got the Deck working, allowing the user to click on it to draw out a new
card onto the temporary discard pile (moving any existing card on the temp discard to the
main discard). But while we laid the groundwork for the Deck to reset when empty, we
didn't actually implement the code to do this, so that will be our first task today.
Resetting the Deck
Open your RBU Pyramid project file and go to the resetDeck method inside
gameWindow. Copy and paste in this code:
dim i as integer
//
// Rebuild the deck
//
redim gTheDeck(0)
// if there's a card on the tempoary discard, add it to the bottom
of the deck
if gTempDiscard.card > 0 then
gTheDeck.append gTempDiscard.card
end if
// Move the discard pile back into the deck
for i = uBound(gTheDiscard) downto 1
gTheDeck.append gTheDiscard(i)
next
// Erase the discard
redim gTheDiscard(0)
gTempDiscard.card = 0
gTempDiscard.clickable = false
gTempDiscard.selected = false
gTempDiscard.refresh
gDiscard.card = 0
gDiscard.clickable = false
gDiscard.selected = false
gDiscard.refresh
gDeck.card = 0
gDeck.clickable = false
gDeck.selected = false
gDeck.refresh
The above is straightforward: we rebuild the empty gTheDeck array from the contents of
the main and temporary discard piles. Then we erase the gTheDiscard array (since it's
now empty) and set the contents of the two cardCanvas objects for the two discard piles
to nothing (making them unclickable and non-selected). Note that we refresh those
cardCanvas objects to make sure the new state is reflected in the display (i.e. if they
were selected, we force a redraw to draw them as not selected).
Simple eh? Run the program and click on the Deck until it runs out of cards and resets.
Go ahead, try it. I'll wait.
Uh oh. You encountered a problem, didn't you? The Deck didn't reset! What's going on?
Relax, it's just a little oversight. There's nothing wrong with our code. It's just that last
week we added a variable (property) that tells us if it's the first time through the Deck or
not. Since we never initialized gFirstTimeThrough to true, it defaults to false, and
therefore our resetDeck routine never gets called!
The solution is simple: open globalsModule and go to the newGame method. Insert this
code (don't erase what's already there):
// Initialize game variables
gFirstTimeThrough = true
Now try running the program again: this time it should let you click through the Deck and
display an "R" when the Deck is empty. Clicking on it again will refill the Deck from the
discards (watch them go blank) and the Deck is now full of cards again. When you click
to the bottom this time, however, it will display a "D" and be finished.
Fixing a Redraw Flaw
While testing the Deck just now, you may have noticed that the display of the Deck is
flawed. There's little feedback for when you click on the Deck. But wait a second... didn't
we include code for highlighting the Deck when it's click on? What happened to that?
Run the program and watch carefully what happens when you click on the Deck.
Depending on the speed of your computer, you may notice that the Deck does get
highlighted: but the highlight is so quick it's erased almost before it happens!
The solution is a simple delay between the initial highlight and the redraw. REALbasic
doesn't have a delay command, so let's add our own. Within gameWindow add a new
method (Edit menu, "New Method") like this:
Put in this code:
dim tt as integer
tt = ticks
while ticks - tt < t
wend
The ticks command returns the number of ticks -- 60ths of a second -- since your
computer was booted. Here we save the initial ticks value into tt and then do a loop
that continually checks to see of our passed value of t ticks have passed. By subtracting
the current ticks value with tt, we have the difference and that's what we compare to t.
In Detail
Note that this usually isn't the best kind of wait routine since it freezes all action on your
program until it is finished. A better way might be to use a timer control set to delay for
a specific amount of time. But in this case we want our program to pause, so this is
exactly what we need.
Now we just need to add code to call this wait routine between the drawings of the Deck.
Go to our deckAdvance method and find the code that begins with drawCard(index,
true). After the subsequent if line, put this code: wait(6).
The entire segment should look like this:
drawCard(29, true)
if uBound(gTheDeck) > 0 then
wait(6)
drawCard(29, false)
end if
Try the program now: you should see a brief display of a black Deck before it redraws
the gray card background. That's enough of feedback to let the user know they clicked on
the Deck (and prevent it accidentally being clicked too many times).
In Detail
How did I figure out that 6 ticks was the appropriate amount of delay? Trial and error: I
simply experimented with several numbers and tested them. Six seemed like a good
amount.
Feel free to try this: one of the best things about REALbasic is the way you can edit your
program while it is running. Just change the wait(6) call to wait(10) or wait(1). Hit
the run command to go back to the window and click on the Deck a few times, then go
back to your code (click on the code window in the background) and change the amount
again. You don't have to quit the program to do this. Keep doing that with different
values until you've got a setting that you like.
Adding Sound Feedback
Fixing that drawing flaw reminds me of the importance of feedback. One of the best
kinds of feedback is audio: wouldn't it be nice if cards made a sound when you clicked on
them?
Let's do that. For today we'll just add sound for the clicking on the cards, but we'll set this
up so it will be flexible enough to be easily modified for future sounds. Add a new
method like this:
Here's the code:
select case kind
case "click"
CLICK.Play
case "deck"
CLICK.Play
end select
This simply looks at the string passed and plays the appropriate sound. (I'm using the
same sound for both the Deck click and a card click.)
For this to work, you'll need to add a sound called "click" into your project file. I've
included this sound within this week's project file, or you can use your own sound. Just
drag the sound from the Finder into your project window.
In Detail
Are you wondering why we write a separate playSound routine instead of just using a
direct call to click.play?
There are two key reasons. First, this gives us the ability to easily add a check for a user
setting that turns all sounds off. If we didn't do it like this, we'd have to add that check
every time we attempt to play a sound.
Second, by not tying the sound file directly to each call for a sound to play, we have the
ability to easily change the sounds. For instance, I'm using the same click sound for the
Deck clicking, but it could easily be changed to use a card shuffling type sound.
We also could, potentially, add a feature that allows the user to pick the sounds that are
used from an internal library of sounds. With the above system we'd only need to modify
the code in the above routine to do play the sound the user specified.
If you run the program now, of course, it won't make any sounds. That's because we have
never called the playSound routine!
Go to the cardCanvas control and find the mouseDown event. After the first if
cardCanvas(index).clickable and index <> 29 then line, put this:
playSound("click").
In the deckAdvance method, insert playSound("deck") at the top (before any other
code).
Now you should have noise!
If you would like the REALbasic project file for this week's tutorial (including the click
sound), you may download it here.
Next Week
We'll do more with sounds (adding a sound off control) and get a basic scoring system
working.
Letters
Our first letter is from Christopher Hunter, who has a question about arrays.
I really appreciate your Real Basic university columns, they provide a kind of learning
that I have been missing for a while. (Long ago I learned to program in old-fashioned
BASIC from magazines with articles and programs like your RBU columns, but those
kinds of magazines are long-gone from the store shelves, and I gave up on programming
for a while, because while I could write programs in pascal and a little bit in C I could
never manage anything with a graphical user interface until RealBasic came along.)
I had started work on a program, with a lot of information to store, and your information
about custom classes has been a lifesaver, helping me keep track of lots of variables in an
organized way. I was particularly excited to see that I could put a custom class into a
custom class (as in your people/sales example). So I started creating the custom classes to
hold my data, and found that the program, instead of being able to refer to the data stored
in the sub-classes (my mechClass contains another class structureClass) and when my
program tries to alter a variable:
gMech.structure.endo=false
I recieve an error message that says "Invalid array method."
Do I need to somehow initialize the class (like I do with mech class "gMech = new
mechClass") somewhere?
It is declared in mechClass as per your examples "structure(0) as structureClass."
I hope that this question is coherent enough, I find this problem very confusing.
Ah, I see your problem, Chris. It's very simple: the property structure in your code is
an array. In your above line of code you aren't giving it an index.
The correct way to do it would be something like this, where i is a valid index:
gMech.structure(i).endo = false
That should solve your problem.
Here's a little chart of error messages and what they mean:
Error
Message
Meaning
Nil Object You're referring to an object that
Exception doesn't exist.
Solution
You must new the object before
referring to it
Out of
Bounds
Make sure that the index you use is
You are accessing an array with an
within the array's range (i.e. not greater
index out of range.
than the array's uBound() value).
Invalid
Array
Method
You are attempting to refer to the
Make sure you use an index of the
method of an object within an
array (i.e.
array when that object doesn't exist
arrayname(index).methodname).
(i.e. arrayname.methodname).
Next, Kevin writes with a suggestion:
Hello Marc.
I've just finished browsing your RBU pages and was interested in your 2 articles on inhouse programs. The one project that was most interesting for me was the DVD database.
This looks like a very cool little application. I'm curious as to how you create and manage
the database. You mentioned that you store it in an xml format so I'm wondering, does
this mean that you don't use any of REALbasic's built in database support?
I've been looking at doing a similar thing for my DVD and CD collections and I'm not too
sure how to go about storing the data. I don't have the Pro version of RB so I wanted to
avoid using the limited database support available to me with the standard version. Any
tips or hints?
Would you ever consider using your DVD Database program as the subject of one of
your columns? Or possible making the project available for study? That would be very
helpful especially if it provides examples of data storage that don't depend on the
professional version's database features.
Thanks a bunch! I look forward to your future columns.
-Kevin LaCoste
[email protected]
Hi Kevin! Glad you like the DVD project: I will consider your suggestion to use it as the
subject for a future column. It's a little unpolished, and the XML aspect of it is a hack (it
only supports a subset of "real" XML), but it is an interesting example program. I had
thought of interrupting the RBU Pyramid tutorial at some point with a one column
project: my DVD program might be a good example (though it does have some
complicated aspects).
As to your not wanting to use REALbasic's built-in database support, I concur. I've never
used it, or had much of a need for it. I believe it's mostly there for connections to highend database programs (something I have no experience with). For instance, you could
write a REALbasic front-end to an Oracle database.
Like you, I always owned the Standard edition of REALbasic (though I recently
upgraded to the Professional version as I'm wanting to explore compiling for Windows).
Also, a lot of the terminology in the RB help is database jargon I'm not familiar with,
making learning how to use the database features difficult.
As to your own database needs, just write your own! A database is nothing but a
collection of data in a particular format. If you define the format, you can read and write
the data in that format. If you're not planning on sharing the data with other software, the
format of the database is entirely up to you.
For instance, you could use a simple text file for your database. Each line of the database
could be a record. You could divide the fields with a tab character (ASCII 9) or any other
unique character that's not used in the data, so that you can separate the fields. For
example:
movie title[TAB]director name[TAB]year[TAB]description[TAB]length in
minutes[RETURN]
The Abyss[TAB]James Cameron[TAB]1989[TAB]A deep sea oil rig crew
is drafted to rescue a lost nuclear submarine.[TAB]171[RETURN]
Brazil[TAB]Terry Gilliam[TAB][TAB]The best film ever
made.[TAB][RETURN]
The above format is easy to decode with the REALbasic nthField function: just pass it
the current line, the delimiter you're using (in this case, chr(9)), and the field number
you want.
You can, of course, arrange the data in whatever order you want (and include whatever
fields you'd like). The key is to be consistent: if the 5th field is the film's length, you
cannot haphazardly insert in a new field after the director name. All records must have
the same fields in identical order or you have chaos. You must insert blank fields if a
particular movie doesn't have a field's information (with Brazil, in the above, I didn't
include the year or length but the fields are still there)
That's one of the reasons I chose to use XML for my DVD data. XML has the advantage
of being extensible (that's what the X in the name means) because each field is named.
With the above system you only know that the fifth field is the film length because it's the
fifth field and you initially decided that it was the length. That forces you to use a strict
structure. With XML, you can name each field and they can be in any order. Here's an
excerpt from some of my data (note how each film does not necessarily have the same
fields):
<dvd><title>12 Monkeys</title>
<director>Terry Gilliam</director>
<writer>David Webb Peoples</writer>
<year>1995</year>
<genre>Science Fiction</genre>
<marc rating>10</marc rating>
<id>0196</id>
</dvd>
<dvd><title>Brazil</title>
<director>Terry Gilliam</director>
<writer>Terry Gilliam</writer>
<year>1985</year>
<genre>Science Fiction</genre>
<marc rating>11</marc rating>
<id>0026</id>
<edition>Criterion</edition>
</dvd>
<dvd><title>City Lights</title>
<director>Charles Chaplin</director>
<year>1931</year>
<id>0035</id>
</dvd>
XML is more flexible, but it's also more complicated to decode, which takes time, and
the data takes more space on disk (a problem for huge databases). For many databases
having a fixed structure isn't a big problem -- I only did it this way for my DVD
collection because I wasn't sure what fields I would need, and I wanted to play with
XML. In many cases XML wouldn't appropriate for your actual database file, but you
might include XML import and export routines in your program.
There are a number of XML classes for REALbasic out there: while I haven't used it (it's
a commercial product), Theodore H. Smith's XML Engine looks pretty good.
Finally, we hear from Brett Cheng, with feedback from our new CSS format.
Hi!
I just discovered RBU recently and find it to be a very useful resource. I'm still trying to
catch up on the series, but I peeked ahead and notice in #25 you introduce a new format
using CSS.
I have a comment on the change:
- the highlighting of glossary words makes them very difficult to read! Blue highlighted
blue text is how it shows up, using Explorer 5.0 - I'm using default colour settings so I
don't think it's anything particular to my setup. In Netscape 4.7, it doesn't appear much
better.
also, the same blue highlighted words don't seem to show up as highlighted at all when
you print out the document. i.e. the underline is also gone, unlike regular URL links
which do print as underlined - so on the printed page you lose any indication those words
are in the glossary.
Perhaps you could highlight in a lighter colour (yellow?), and also retain the link
underline? Alternatively, how about italicized blue without the highlighting? Just a
suggestion
great series otherwise. Thanks!
My goal was to highlight glossary items differently than standard links, but my original
attempt (white text on blue background) made the entire item appear as white-on-white in
non-CSS browsers (i.e. invisible). I compensated by making the text black, but you are
correct that it's difficult to read.
I've now modified it in a way that should work for everyone: yellow highlight with black
underlined text. In browsers that don't support CSS at all or don't support the background
color option, it appears as a standard underlined link.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 034
RBU Pyramid VII
In our previous lesson, we added the ability for a sound to play when a card is clicked.
But not everyone likes sounds -- some find them annoying. So before we go any further
with sounds, we'll add in a control so the user can turn sounds on or off.
Adding Sound Control
The sound control we'll add is a simple toggle switch: we'll use a basic sound on/off
graphic (included in this week's project file). The two graphics are sound.pct and
nosound.pct -- drag them from the Finder into your project window.
Note: to keep things organized, it works best if you put your imported sounds and
graphics inside a special folder inside the folder your project is in. I call mine "Linked
Resources" but you can name yours whatever you want. REALbasic will look inside that
folder for those resources in the future. Remember, RB doesn't store sounds and graphics
inside your project file: it only maintains a link to the original file on disk.
Drag a BevelButton control onto gameWindow. Give it the following settings (be careful
-- there are a few subtle but critical changes in this list of properties):
Now let's add some code to prevent sounds from being played if the toggle is set to off.
Open the playSound method we added last week and insert this at the top (before any
other code):
if soundBevel.value then
return
end if
Now run the program: when the sound toggle is set to true, that means the sound is off
(no sound). When soundBevel is false, sound is on. Our little check code above simply
returns from the playSound without doing anything if soundBevel.value is true.
To get this to work right, we need to make sure the soundBevel icon is correct. Let's add
a new method called updateSoundIcon to do this. In it, put this code:
if soundBevel.value then
soundBevel.icon = nosound
else
soundBevel.icon = sound
end if
Excellent. Now we just need to put in two calls to that method. For the first, go to the
Open event in gameWindow and type in updateSoundIcon. Then go to soundBevel's
Action event and put in updateSoundIcon. That way when the program is launched it
will update the state of the icon, and whenever the icon is clicked (toggled) it will make
sure it has the correct picture.
Adding a Scoring System
An important part of any game is the ability to track a player's score. With Pyramid, we
need to track two numbers: the player's score (as calculated by the game) and the number
of pyramids the player solves (clears).
Earlier in this process we created a global variable gScore to hold the player's score, so
now let's an a property to count the number of pyramids completed. Open
globalsModule and add a new property (Edit menu, "New Property"): gPyramids as
integer.
Now we need a place to display the score. For this, I could just use a simple staticText
object; after all, we're just displaying some text, not graphics. But a staticText has two
disadvantages. First, it flickers when you update it. Second, it's very plain. If we instead
use a canvas and draw the text manually, we're free to embellish and enhance the
graphical display as much as we'd like.
For RBU Pyramid, we're just going to use a simple display inside a canvas object, but
this could easily be changed to be prettier in the future. For instance, imagine drawing the
score in LCD-like lettering: that'd be cool.
So let's drag a canvas onto our gameWindow. Give it the following settings:
Press Option-tab while selecting scoreCanvas to open the Code Editor. Within
scoreCanvas' Paint event, put this code:
dim y as integer
dim g as graphics
g = scoreCanvas.graphics
g.foreColor = rgb(0, 0, 0) // black
g.textFont = "Geneva"
g.bold = true
g.textSize = 18
y = g.textHeight
g.drawString "Pyramid #: " + str(gPyramids), 0, y
y = y * 2
g.drawString "Score: " + str(gScore), 0, y
As you can tell, this is a mud simple routine. We simply set our font, text size, and color
and draw the text. Note that I don't "hard code" the vertical drawing position of the text -I use the variable y to store that information. Why do I do that? Simply because it's more
flexible: it makes it possible to adjust the size of the text without having to recalculate the
vertical position. (Of course, if you make the text too large, it won't fit within the
confines of our canvas.)
That done, let's add code to actually track the player's score and redraw it. Go to the
updateCards routine. At the very beginning, let's add the following line with some new
properties (variables):
dim count, bonus as integer
Now at the bottom of the same routine, let's add the following (precede it with a few
blank lines):
// See if pyramid has been won
count = 0
for index = 1 to 28
if cardCanvas(index).visible then
count = count + 1
end if
next
if count = 0 then
playSound("win")
gPyramids = gPyramids + 1
// Calculate bonus: two points for each pyramid won
bonus = gPyramids * 2
// Bonus is doubled if first run through
if gFirstTimeThrough then
bonus = bonus * 2
end if
gScore = gScore + bonus
gFirstTimeThrough = true
// Deal a new pyramid
freshDeck
end if
scoreCanvas.refresh
The first part of this (with the count variable) is what checks to see if the user has
completed a pyramid. We do this by counting the number of visible cards. If they're all
invisible, the user has completed the pyramid.
Once we've figured out the user has completed the pyramid, we calculate the player's
score. We play a "win" sound, add one to gPyramids, and figure out the player's bonus.
The bonus calculation is unique to RBU Pyramid (it's not based on any official rules or
anything). I basically wanted the bonus to be higher the more pyramids are solved in a
row, so I add two points for every pyramid solved. Since it's more difficult to solve a
pyramid with only one shuffle of the Deck, I double the bonus if the player managed to
clear the pyramid without reshuffling (gFirstTimeThrough is true). Finally, the bonus
is added to the score, we deal a fresh set of cards, and the score is refreshed (redrawn).
Be patient -- we're almost done. We just need to update our sound playing routine to play
a sound when the player wins (clears a pyramid). Go to the playSound routine and insert
the following into the select case statement:
case "win"
APPLAUSELOOP.Play
If you use a different sound than my applause file, change the name of the sound in the
above to match your sound's name. If you want my sound file, it's located within this
week's download -- just drag "APPLAUSE LOOP" to the project window.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We'll add a preference system to RBU Pyramid so we'll be able to save the state of things
like the sound on/off setting.
Letters
Tripp Mullins writes:
Marc,
Love the column. As a new user it has been a great help.
Currently I am working on an Architectural Calculator, one that processes data in feet,
inches, and fractions. So far I have developed a very basic calculator that can add or
subtract feet and inches, but it uses one editfield for inputting feet and another for inches.
I would like to make it able to input 3' 4 1/2" in one editfield and understand it is three
feet four and one half inches. The fractions are what's getting me. Any suggestions as to
where to look for help?
-Tripp Mullins
Interesting problem, Tripp. I don't know of a resource off hand, though presumably there
are some textbooks and algorithms on the net that would help with this situation. I'd look
for other calculator projects and see if someone else has done something similar. Even if
they aren't done in REALbasic, the actual method they use to solve the problem would be
similar, and it might give you some ideas of how to approach the problem.
The core of the problem to me appears to be converting the string fraction into numbers
that you can work with. Obviously, the problem is simple once the values are converted
to numbers (since fractions are just division).
One possibility would be to break the input into multiple fields. This is perhaps more
work to set up and ugly from the user's perspective, but it's easy to code (just convert the
fraction fields to numbers and divide).
The other approach is to use string manipulation to figure out which portions of the
number are fractions and then convert those. This isn't as hard as it seems. For instance,
without the complexity of mixing feet and inches in the same field, let's look at a separate
inches field that accepts a whole number and a fraction portion.
Like this:
Here's a rough look at the conversion code:
dim i, j, int, numerator, denominator as integer
dim s as string
// Retrieve the whole number portion
s = t
i = inStr(s, " ")
if i > 0 then
// Everything left of the space is the whole number
int = val(left(s, i))
// Trim our input string to just the fraction
s = mid(s, i + 1)
else
// No whole number
int = 0
end if
// Process the fraction portion
i = inStr(s, "/")
if i > 0 then
numerator = val(left(s, i))
denominator = val(right(t, (len(s) - i)))
else
// No fraction, just return entire string as a number
return val(t)
end if
return int + numerator / denominator
I won't get into dreadful detail here, but you can see how we step-by-step break the string
into various portions and convert those to numbers. We use the inStr function to find the
first occurrence of a space -- that means everything to the left of that is a whole number.
Once we've trimmed off the whole number portion, we can concentrate on the fraction,
which is pretty easy as we simply find the slash ("/") and save each number into a
separate variable.
Obviously it gets more complicated if you want to mix feet and inches and parse for " and
' marks, but maybe this code gets you going in the right direction.
The biggest problem with this kind of problem is anticipating all the bad inputs users
might type in. What if the user doesn't put a space before the fraction? What if they forget
the foot or inch mark? What if they include letters? What if they put a slash at the very
beginning or put a space after the slash?
On the one hand you can say "garbage in, garbage out" -- the user gets whatever they put
in. But then a good Mac programmer will try to make these errors difficult to input. For
instance, you could make the editField not accept letters. It all depends on the type of
user you'll have and the amount of time you want to spend coding for rare but possible
circumstances.
BTW, if you want the above fraction project file, I've uploaded it here. I haven't
thoroughly debugged it, so it might have errors, but it should be educational to play with.
Next, we hear from Daniel Price, who writes:
Hi Marc,
I was wondering if you could answer a question regarding resources and RB. I'm
currently working on a game-style program in RB that will draw all of it's main data
(level information, object types and attributes, graphics, sounds etc) from a separate file
with a resource fork called 'DATA' and saved externally in the application's folder. This
is so the program can be easily adjusted and edited by anyone, using an editor program
(which I'm am also writing) or even ResEdit - kinda like Ambrosia's Escape Velocity
The majority of the data is saved in TEXT resources inside the file as short strings. Each
string has a number of fields separated by a '*' character. So, for example, when defining
an enemy ship, the first field contains the name (if applicable), the second the speed,
shield value etc etc. I can create the file using the binaryStream object of the type
specified in the File Types box. However, as soon as I try to write my resources to it, RB
bombs. When I inspect the file with ResEdit, it becomes clear that it does not contain a
resource fork - only a data fork. ResEdit asks me if I would like to give the file a resource
fork (or it can't open it). I say yes and the fork is created. If I then run my project, the data
is inserted and inspection with ResEdit proves that everything is hunky-dory.
This only works, however, if I create a file before hand with a resource fork - very
inconvenient if the user wishes to create a whole new scenario file. RB seems incapable
of creating a file with a resource fork directly. Any ideas?
Also note that I'm using RB 1.1.1 due to it's speed and near-solid stability. I also have a
fully registered copy of RB 3.2 but its very sluggish (even on my 240Mhz G3) and
crashes constantly. Built applications rarely work and it's full of bugs. Looking through
the reference guide, however, it to seems unable to create files with resource forks. Please
keep this in mind.
Please Help!
You're probably just looking in the wrong place, Daniel. The folderItem object includes
a createResourceFork method: you just pass it the file type of the file you want to
create. It will create the file and erase any existing resource fork (if there is one). Once
the fork is created, you can use standard resource fork commands to manipulate the
contents of the fork (create/delete resources, etc).
Here's a screen shot of the online help describing the command:
(I don't have a copy of RB 1.1 but I'm sure that command was there as I used resource
forks way back when I first started with RB.)
That said, you might think about using a binary file instead of a resource fork. While I
love them, Apple seems to be phasing them out in Mac OS X (though they are still
supported). Also, using resource forks mean your file can never be moved to another
platform (such as Windows). Right now you may not be interested in creating a Windows
version of your program, but down the line you might be.
If you go with a binary file, others won't be able to edit it, but since you mentioned you're
writing your own editor anyway, that shouldn't be a problem.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 035
RBU Pyramid VIII
This week we're going to set up RBU Pyramid to save preference settings. As we
continue with the program, we'll add more preferences, but for today we'll just get the
system in place.
Some History
Early in my experimentation with REALbasic, I found that just about every program I
created needed some way to save preferences. It's a common need. Rather than reinvent
the wheel for every project, I devised a simple preference system I use for all my
software.
The capabilities I wanted for my preference system were the following:
•
•
•
•
•
•
Based on a text file, so prefs are editable with any text editor
Expandable, so new prefs can easily be added
Flexible, so different kinds of data can be stored
Easy to add preference settings
Version control, so modifying the preference file format doesn't kill an old
software program
Portable, so I can easily reuse the system in multiple programs
The system I created does the above, and I like it a lot, but it may not appeal to everyone.
For instance, some people don't want the preference files for their programs to be easily
editable with any text editor. I see that as an advantage (especially for debugging), but
some want to keep their preference settings secret. If you like my system, however,
you're free to use it.
How does my preference system work? The basics are simple: every preference is stored
on a single line inside an ordinary text file. I use a unique keyword approach to establish
the identity of each setting. The keyword is preceeded by a number sign and followed by
a space. After the space is the actual setting as established by the user. For example, the
following line could be a preference setting to remember if sound was on or off the last
time the program was used.
#soundon true
If needed, the setting could contain more than one value. The actual format of the setting
varies according to need, but as I long as I'm consistent, it shouldn't cause a problem or
be difficult to use. Let's say I want to remember the size and location of a window. I
could save the following in my preference file:
#windowpos 980,872,385,139
The keyword (or directive) "windowpos" tells me which preference this is. Since I make
up these keywords, I can name them whatever I want. So if I had a floating palette in my
program, I could name my directive "toolpalettepos" just as easily.
The actual setting info, in this case, is delimited (separated) by commas. Why commas?
Why not? I could have used tabs (ASCII character 9) or some other non-typable character
if I wanted, but commas work fine for separating numbers. As to the numbers, it makes
sense that they correspond with REALbasic's normal system of coordinates: the first two
are the horizontal and vertical location, respectively, and the final two are the width and
height, respectively.
Do you see how flexible this system is? Because I use keywords (directives) to identify
each setting, the order of the settings in the file is irrelevant. Some methods of saving
preferences are extremely rigid: the fourth line must always be a boolean (true or false),
the 613th character must always be the number of letters in the user's name, etc. My
system is flexible -- each pref is independent of the others.
My preference file is also easily readable by humans. Look at the following excerpt from
a Z-Write preference file: Can you tell what each line does?
#version 1.0
#rememberpath Misc:Essays: Current:Software Serials.zart
#clock false
#smartquotes false
#spellingenabled true
#buttonbar true
#defaultfont Verdana
#defaultsize 18
#printmargins 36,36,36,36
#useheader true
#usefooter true
#autoexpandglossary true
#stylepalette T,1360,851,193,158
#rtfcreator MSWD
#helpfont Geneva
#infowindowstaysopen true
Remember the version control feature I mentioned earlier? That's important in software
design. For example, let's say you are creating your own file format for storing some
data. If in version 1.0 of your program you make the third line be a number indicating the
number of records saved in the file, but in version 1.1 of your program you move that to
line four, what happens when someone runs version 1.0 of your program and tries to open
a version 1.1 file? Yeah, bad stuff, since data isn't where the program expects it to be.
The later version of your program can be made to support earlier file formats, but your
old software can't know about the new format.
The same problems are evident with a program's preference file. In fact, they are
probably even more of a problem since you may change your data file's format rarely, but
any time you add a preference setting your preference file changes. For instance, since ZWrite's 1.0 release in May 2000, I've changed the preference format dozens of times as I
added new settings. However, because my prefs system is flexible, it causes no
catastrophic problems. An older version of Z-Write just ignores any pref directives it
doesn't understand.
But what happens if I change an existing directive? Let's say I decide to enhance the
"default font" preference setting to let you put in a list of fonts (separated by commas)
instead of a single one (in case the first font isn't installed, it would try the second, etc.).
Wouldn't an older Z-Write would misinterpret the "Verdana,Helvetica,Arial" as a single
font name (since that's all it expects)?
Yes, it would. The solution is to change the keyword. Instead of "defaultfont" I could
name the newer directive "defaultfont2" or "defaultfonts" -- anything that's different so
the older program would ignore the setting.
As you can see, this system is simple yet powerful. There's plenty of room for expansion
for just about any preference we need to save, and we're protected from problems that can
be caused by multiple file format versions.
In Detail
There is one key flaw with my system, as you'll see if you look through the code: because
unknown directives are stripped (ignored) when read, they aren't saved when the
preference file is resaved. That means if you run an older version of the program it will
cause the preference file to lose any more recent directives added by more recent version
of the program.
For example, the current version of Z-Write includes a "textbackgroundcolor" directive
that lets you set the background color of the main text editing window. Older versions of
Z-Write don't, so if you set your background color to yellow, then (either accidentally or
on purpose) run Z-Write 1.1, the updated preference file won't have the yellow
background color setting and the background of your window will be the default white.
Generally, this is only a cause for annoyance, and obviously few people would keep
multiple versions of a program installed, but it is something to remember when using my
system. I am thinking of fixing this oversight in a future version of my prefs system,
making it so it will preserve directives it ignores.
So now that we understand how it works, let's see how we go about implementing this
system in REALbasic!
Adding a Preference System
Normally, when I add my preference system to a program, I drag in a class and module I
already created. Since you don't have that luxury (and I want you to understand the code),
we'll create our manually. But once we've done this, you should be able to easily reuse
these elements in other programs you create to add your own preference system.
The first thing we'll do is create a new class called directiveClass. This is an important
part of my system. After opening your RBU Pyramid project, go to the File menu and
choose "New Class". Name it directiveClass. Open up directiveClass (double-click
on it) and add two properties (Edit menu, "New Property"): name as string and text
as string. Okay, close the class: we're done with it.
Now let's add a new module (File menu, "New Module"). Name it prefModule and open
it up. First we'll add an important property (Edit menu, "New Property"):
gDirectives(0) as directiveClass. That's an array that's going to contain a list of all
the directives (keywords) found in our preference file.
Let's now add a constant to our module:
Next, let's add some methods to this module (Edit menu, "New Method"). For now, leave
the code area empty. First, two simple ones: loadPrefs and savePrefs. And then two
more complicated ones:
Now let's start putting in some code. We'll begin with the simpler routine, bStr. This one
accepts a boolean (true or false) data type and returns either the word "true" or "false".
We'll use this often when we save boolean preference settings.
if b then
return "true"
else
return "false"
end if
That's so simple I don't think I need to explain.
But the next routine, extractDirectives, is the heart of the system and is much more
complicated.
dim theFile as textInputStream
dim theLine as string
dim i as integer
if f <> nil then
theFile = f.openAsTextFile
if theFile <> nil then
while not theFile.EOF
theLine = theFile.readLine
if mid(theLine,1,1) = "#" or mid(theLine,1,1) = "'" then
if mid(theLine,1,1) <> "'" then
// we've got a directive, so store it in global array
i = inStr(theLine," ")
if i > 0 then
// since we Append, gDirectives begin at (1)!
gDirectives.Append new directiveClass
// grabs directive name
gDirectives(ubound(gDirectives)).name =
lowercase(mid(theLine, 2, i - 2))
// grabs balance of line
gDirectives(ubound(gDirectives)).text = mid(theLine, i +
1)
else
// error in directive formatting -- no space, so skip it
end if
else
// It's a comment, so ignore it
end if // mid(theLine,1,1) = "'"
else // mid(theLine,1,1) <> "#"
// we've hit the first non-directive line so skip rest of
the file
theFile.close
return true // sucessfully processed all directives
end if
wend
theFile.close
return true
// either no directives in file
// or only directives in file -- either is ok
end if
end if
return false // unsuccessful for some reason
This looks more messy than it is. We start out by making sure the file passed (f) is valid.
If so, we open it as a text file. We set up a while-wend loop and look through the file
until we hit the end of the file (theFile.EOF equals true when we're at the end of the
file).
Now we get to the good stuff. We read in a single line from the file and examine in.
We're basically looking for one of two things: either the first character of the line is a
number sign (#) or it's an apostrophe ('). If it's neither one of those, we assume the file is
done and we exit the routine.
If the line begins with an apostrophe, it's a comment, so we just skip it. Otherwise we
process the line as a directive. Remember the directive structure? It's #, directive name,
space, and setting. The key there is the space: that divides the name from the settings. So
we find the first space character and store that position in i.
The directives we find we're going to store in the global array we created earlier,
gDirectives. Since gDirectives is an object of type directiveClass, we must create
a new instance of a directiveClass using the new command. (Remember, classes are
objects: you cannot use them until you've created an instance of the object. The class you
see in the project window is only the definition of the object, not an instance of the
object.)
Next we append that new object to gDirectives. The append command allocates new
space in the array (it expands the array). Note that since we're appending, the first real
element in gDirectives begins at index 1 (0 is there but not used as the append adds 1).
Once we've put a new directiveClass item into the array, we can fill it with data. We
store the directive name in the .name property, and the settings text in the .text
property.
To get the directive name, we use mid(theLine, 2, i - 2) which extracts the middle
portion of the line up to but not including the space (i - 2) and starting at the second
character. To see how this formual works, use a sample set of data and test it:
123456789012345
#buttonbar true
123456789
The variable i will be set to 11, since that's the first space character. Subtract two from it
to get 9 (the two comes from the two characters we don't want: the initial # sign and the
space). That 9 is our length. So now our command is, in effect: mid(theLine, 2, 9).
That returns the nine characters starting at position 2. Basically, the word "buttonbar"!
For the setting text, we just grab everything after the space (by omitting a specific length,
the mid command returns the balance of the text).
Once we'll saved those two elements to our gDirectives array, we go to the next line.
When the file's done, we end up with an array of directives. All we need to do then is
process them!
That's done in our loadPrefs routine. It's here we call extractDirectives. Once that's
been done, we use a while loop to step through each directive and see what it is. Each
directive is examined inside a select case statement: if the directive matches a case, we
process it, otherwise the directive is ignored. Simple!
Here's the code for loadPrefs:
dim f as folderItem
dim s, theValue as string
dim i, j, k as integer
//
//
//
//
//
//
//
//
//
//
This routine loads the program's general preferences
from the Preferences Folder. Prefs are in "Directive"
format: each line contains a #directive followed by a
space and the prefs for that item. for instance:
#sound on
We store the parsed directives in the
global gDirectives array and then read through that
to set our global variable flags.
f = preferencesFolder.child(kPrefName)
if extractDirectives(f) then
i = 0
while i < ubound(gDirectives)
i = i + 1
select case gDirectives(i).name
case "nosound"
s = gDirectives(i).text
gameWindow.soundBevel.value = s = "on" or left(s, 1) = "t"
gameWindow.updateSoundIcon
end select
wend
else
// no prefs file, set defaults
gameWindow.soundBevel.value = true // no sound
end if
Note that we used the preferencesFolder function to find that folder. That's a cool
command because it works transparently no matter what operating system RBU Pyramid
is running under. For instance, on Mac OS it returns the Preferences Folder inside the
active System Folder, but under Mac OS X it returns the user's Preferences folder inside
the current user's directory.
The directive we're looking for in this case is one called "nosound". The setting is either
the word "on" or "off" or "true" or "false". If "nosound" is the directive, we look inside
the gDirectives(i).text property to see what's there. Since this is a boolean value, we
can just check for a couple of the situations. If it's "on" or the first letter begins with a "t"
we know it's true. (By checking for just the first letter instead of the entire word, "t" is a
valid setting.)
In the case of RBU Pyramid, we set the results of this boolean test to our soundBevel
control's value setting. Then we update the sound icon with our updateSoundIcon
method.
That's it! We've just read in a preference setting and applied that value to our program.
But of course when you first run the program, there is no preference file. How does RBU
Pyramid react? It does nothing, of course. There is no error: it just uses the default
settings. That's that last bit of code at the bottom. When we add more preference settings,
we'll have to remember to put their default values here.
Now it's time to do the converse of loadPrefs with our saveprefs method. This one
saves all our settings to our preference file.
Put this in your saveprefs method:
dim
dim
dim
dim
f as
s as
t as
i, m
folderItem
string
TextOutputStream
as integer
f = preferencesFolder.child(kPrefName)
if f <> nil then
t = f.CreateTextFile
if t <> nil then
t.writeline "#version 1.0"
// Save sound on/off setting
t.writeline "#nosound " + bStr(gameWindow.soundBevel.value)
t.close
end if // t = nil
end if // f = nil
See how simple that is? We get the folderitem via the preferencesFolder function,
create a text file, and then write our settings to the file. In this case, we first write a
"version" directive (which, as you noticed, we ignored in loadPrefs -- it's just nice to do
this in case we ever need to know what version preference file this is).
Next, we write our "nosound" directive. We use our bStr method to write either "true" or
"false" depending on the state of the soundBevel control.
For now, that's all we're going to do. As we continue with RBU Pyramid, we'll be adding
a bunch of other preference settings and you'll see how easy it is to add them.
There's two other things we need to do before what we've done will actually work,
however. We need to add calls to our loadPrefs and savePrefs methods!
Go to the app class in your project window and open it. In the Open event, put
"loadPrefs" as the first line (we want it to be the first thing the program does). Then, in
the Close event, put "savePrefs".
That's it! When you run the program, it should now remember the state of the sound
on/off control. Try it. Experiment with changing the preference file manually using a text
editor. Try running the compiled beta of RBU Pyramid and see how it changes the stuff
in the file.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
Every good program needs a cool About Box -- next week we'll create one for RBU
Pyramid.
Letters
Here's an unusual item. Jeff Lawrence created an interesting REALbasic program called
"RBU Browser" that lets you easily access the various REALbasic University columns.
You simply choose the column number you want and it's displayed in your default web
browser:
He's given me permission to post the source, so if you're interested, download it here.
Check it out: it's pretty cool!
Rejean has a question about using pictures within a listBox:
Hi Marc,
I would like to thank you a great deal on the work you're doing in sharing your expertise
and knowledge about REALBASIC. RB is a great tool with many "hidden" features and
you help me a lot in discovering them and applying them to my little project. You are the
one who was able to make me understand and figure out what OOP and events driven
apps are all about.
I'm an old timer programmer (BASIC, FORTRAN, APL, COBOL) and I've been looking
for a development package that was easy to learn with the new concept of "events driven
apps" and OOP. I tried several of them but the one I found the easiest to use and learn
was RB.
I just discovered RBU a week ago and I have completed the GenderChanger project.
Once it was running, I started modifying it to improve its robustness (I do silly things
once in a while!).
One thing I would like to do and I cannot figure out how:
In the Edit File Types window, I 've added a fourth column entitled ICON. You see now
where I'm going. I would like to be able to show the icon of the file type and creator. I'm
using FileIcon plug-in package from Doug Holton and it works great in getting the
picture of the icon of the file. Now, the great question is: how do I put that picture in the
listbox. I'm sure it must be feasible.
Again, thank you very much.
Rejean
If you've gotten Doug's FileIcon plug-in to retrieve you a picture object, you're 90% of
the way there. All that's left is to put that picture into the listBox. You simply need to
store the picture into a picture variable and then use the listBox's rowPicture method to
stick the picture into the row.
To get this to work for GenderChanger requires four key changes:
1. You must change the columnCount property of listBox1 from 3 to 4, and change
the columnWidths string to "10%,50%,20%,20%".
2. You must change the Open event of listBox1 to apply the correct headers.
3. You must change the Action event of PushButtonConvert to refer to column 1
of listBox1 instead of 0 to retrieve the file path.
4. You must rewrite listBox1's DropObject routine to insert in the picture in the
row.
Here's the new code for the DropObject routine (note that this will only work if you have
Doug Holton's FileIcon plug-in installed in the plugins folder of your copy of
REALbasic):
dim p as picture
if obj.folderItemAvailable then
do
listBox1.addRow ""
p = fileicon(obj.folderItem, 16, 16, 0)
listBox1.rowPicture(listBox1.lastIndex) = p
listBox1.cell(listBox1.lastIndex, 1) =
obj.folderItem.absolutePath
listBox1.cell(listBox1.lastIndex, 2) = obj.folderItem.macCreator
listBox1.cell(listBox1.lastIndex, 3) = obj.folderItem.macType
loop until not obj.nextItem
end if
You can see that we have to shift all our columns over since we've inserted a new one at
column 0. I'll let you figure out the other changes. If you just want the new source code,
you can download it here.
Here's what it looks like when it works (note that I made the listBox font larger so
there's room vertically for the icon):
If you're adventurous, another change you could make would be to update the icons of
the list of files after they've been converted: the program currently does not do that.
I also don't know if Doug's plugin works for Carbon apps under Mac OS X. I'm sure it
doesn't work for Windows apps (not a concern for GenderChanger, but possibly for other
apps).
Our next letter is from JP Taylor who's have a TabPanel problem:
Hi. I've been reading the REALbasic University column for a while now. It's been a
tremendous help. Here's my problem. My current project uses several TabPanel controls.
How do I place controls on the different individual tabs. Many of the same type of
controls appear multiple times on the different tabs. I tried to copy and past them from
one tab to another. But when I returned to the fist tab, so did the controls. I thought that
maybe the controls had to placed individually. That didn't work either. I went out and got
REALbasic of dummies. It has helped with other problems, but sadly not this one. I have
the feeling that I'm doing some thing wrong that is so basic, so simple that I'm over
looking it. Sadly I have no clue what it is. Can you please help? Thanx for reading.
Attached is a screen capture of my project. The tabs "Primary" and "Secondary" Have
identical controls with some heading changes. This where I'm experiencing the problem.
My Steps: I copied the controls (selecting them with shift+click), then selected the
"Secondary" tab and then paste. I then clicked the "Primary" tab to visually check
alignment. When I did, all the copied controls returned to the first tab.
System INFO:
Computer: Stock G4(450) Cube with 576 MB ram
OS: 10.0.4
REALbasic Version: 3.5.1 trial for OS X (non carbon)
Sadly, while TabPanels are really cool devices, they are problematic. As you've figured
out, they can be difficult to work with, and there are bugs with controls on one tab
"showing through" onto another. Some people are choosing to go with other interfaces
(such as an icon-based pane system similar to what's used by Mac OS X's System
Preferences). With a little effort on your part, however, you can make them work.
For instance, as you've found, copying and pasting control sets pastes them onto the
original layer. The trick I use is that immediately after you paste, while those controls are
still selected, move them off the tab panel. Once they are off the panel you can switch to
a different tab and move them on. Temporarily while you are doing this, make the
window much larger so there's a scratch area off the panel where you can put the group of
controls.
If you let go of the pasted group of controls they will be on the original tab, and getting
them reselected will be a nightmare since it's they are overlapping other controls (use the
undo and start over). One of my top requests for REAL Software is a "select object
below" shortcut, similar to what's found in many drawing programs (like Macromedia
FreeHand's option-click).
My advice is that if TabPanels work for you, use them. If they're confusing and cause
you difficulties, try some other method. The headaches of getting them to work,
especially if they're not working due to a bug in REALbasic, can be overwhelming.
Sometimes the simpler solution is the most practical.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 036
RBU Pyramid IX
A program isn't professional until it has a nice About Box -- a window that displays the
program's version number and author's information. This week we'll add an About Box to
RBU Pyramid.
Adding an About Box
There are as many kinds of About Boxes as there are programs. Some are extremely
fancy, offering up animations or amusing tricks. I've seen some simple programs where I
know the About Box had to take longer to create than the entire program!
How elaborate you want to make your About Box is up to you: for most programs, a
professional looking image is all you need. If you're planning on distributing
applications, you might want to consider standardizing on a common About Box design:
it gives all your programs a similar look and builds consumer confidence that your
programs are of professional quality.
For RBU Pyramid, we're going for a basic window with a pleasant graphic. We're also
going to make double use of the About Box by using it as a Splash Screen: an opening
graphic that's displayed briefly when the program is launched.
The first choice you must make in designing your About Box is deciding if you're better
off creating the entire window in a graphics program or creating the individual elements
within REALbasic. For example, you could simply display your program's name in a
particular font at a large size using a Statictext control. The advantage of this approach
is that it's easy to edit right within REALbasic. The disadvantages are that you can't get
too fancy with the graphics, and you can't be sure the user will have the same font you
do.
As a graphic designer, I'm more comfortable creating graphics in programs like Adobe
Photoshop and Macromedia FreeHand, so I like to use the second approach. That means I
create a large graphic that contains most of the content of my About Boxes. Things that
might change, such as my mailing address or the program's version number, I add in
REALbasic.
I also like my About Boxes to include hot-links to my website(s). In the case of RBU
Pyramid, I want links to both the REALbasic University website as well as the
REALbasic Developer magazine website. Doing this isn't particularly difficult, but it is a
little tricky if you aren't familiar with how RB works. I'll explain more in a minute. For
now, let's get started.
I've already done the graphic creation work for you. Save this graphic to your hard disk
and put it in your project's Linked Resources folder (or whatever you call it). Then drag it
into your project window.
Now for the obvious -- add a new window to your RBU Pyramid project file (File menu,
"New Window"). Give it the following settings:
Notice how we set its backdrop property to the graphic we just imported? That means the
background of our window will always display that picture. It's already starting to look
like a real About Box!
Let's add a statictext item where we'll display the program's version number. Give it the
following settings:
You may be wondering what the "#kVersion" stuff is all about. I'll explain. When you
precede a property setting with a number sign (#), REALbasic assumes the text following
is the name of a constant (the value of a constant never changes while a program is
running). In this case, the constant is named kVersion. Using a constant for the
program's version number is a great idea: you only have to change it in one place and
anywhere your program needs to include the version number, you can just refer to the
constant.
To add the constant (before we forget), open your globalsModule module and choose
"New Constant..." from the Edit menu. Name it kVersion, leave its type string, and put
in "0.5" for the value. (As we go along with RBU Pyramid, we'll increment the kVersion
value appropriately.)
Good. Go ahead and close globalsModule now.
There are several more things we must add to aboutWindow. Drag on two canvas
objects. Give them each the following settings (ignore the super setting for the moment):
Why are we putting canvases on the window? These are going be our hot-link buttons.
They are special because they must take the user to the appropriate website when clicked,
and they must display a pointed finger cursor when the user moves the mouse arrow over
them.
Adding a Custom Hot-Link Class
In fact, these canvases are so special we're going to create a custom class for them! Why
create a custom class? Well, since we have two of these in this program, it makes sense:
we don't have to duplicate the code twice. Also, it will make it easy to reuse this stuff in
another program later.
Go to the File menu and choose "New Class". Name it hotCanvasClass and set its
super to canvas. Now open it up (double-click on it) and add a new property (Edit
menu, "New Property"): url as string.
Within the Events area, go to the Paint event. Here's the code that goes there:
g.foreColor = rgb(0, 0, 200) // Blue
g.textSize = 10
g.drawString url, 0, g.height - 3
As you can probably deduce, this simply draws the url property in blue.
Now, in the mouseDown event, put showURL url. That simply tells the user's default web
browser to display the URL.
In the mouseEnter event, put me.mouseCursor = fingerCursor, and in the mouseExit
event, put me.mouseCursor = arrowCursor. That will cause the canvas to change the
cursor when the user points inside the canvas' rectangular shape or moves it outside of it.
For that to work, of course, you'll need fingerCursor. Here's a link to it: download it
and put in inside your project's Linked Resources folder and then drag it into your
project's window. (If you don't include it in your project, REALbasic will complain that
"fingerCursor" is an unknown object when you attempt to compile your program.)
Guess what? That's it for the custom class. Now there's just one more thing. Go back to
the two canvases you added to aboutWindow and set their super properties to
hotCanvasClass.
Adding a Splash Timer
A "splash screen" is only displayed for a few seconds when a program is launching. The
idea is that it gives the user something to look at and pass the time while the program is
starting up. It shows that something is happening.
Since we want our About Box to close after a few seconds, we'll use a Timer. Drag one
onto aboutWindow. Since timers are invisible (they have no interface for the user), it
doesn't matter where you put it.
Rename the timer splashTimer with these settings:
Now open it for editing (double-click) and type in the following code:
// Automatically close the splash screen
// when time's up.
self.close
Obviously that's going to close the window when the timer activates. But how do we
control when it activates?
Well, since a timer's length of activation starts when its mode value is set to 1, we can set
that when the program is launched. Go to your program's app class and in the Open event,
insert this at the first beginning:
// Opens splash screen
aboutWindow.splashTimer.mode = 1
aboutWindow.showModal
This means when your program is first launched, it will activate the timer and display
the window. Since we set splashTimer to a period of 1000 milliseconds, it will close
itself after one second.
Activating a Menu Command
The About Box is usually displayed when a user chooses the "About" menu command, so
let's add that. Within the REALbasic IDE, you can double-click on the menu object. Click
on the Apple icon and then on the blank space that drops down beneath it. There you can
type in "About RBU Pyramid..." and press Return. Your menu should look similar to this:
Before we continue, change the name of the menu item to "AppleAbout" like this:
Good. Now the interface has been added -- we just have to teach REALbasic what to do
when that menu is chosen. That's a menu "handler" -- it handles the menu call.
Open your app class. From the Edit menu, choose "New Menu Handler..." and in the
dialog that's displayed, choose "AppleAbout". When you click "okay" there'll be a new
AppleAbout menu item in the Menu Handlers section.
Put in this code (which activates aboutWindow):
aboutWindow.showModal
In Detail
We can add menu "handlers" to the entire application via our app class. If we wanted the
menu to only be available when a particular window was open, we could add a handler to
it to just that window. In this case, the About command should be available at all times,
so we add it to the app class.
There's still one more step that critical to getting our menu to work: we must tell
REALbasic that the About menu isn't disabled. (By default, all menus except Quit are
disabled. RB handles the cut/copy/paste menus automatically.) We enable the menu by
going to the app class's EnableMenuItems event and putting in this line:
appleAbout.enabled = true
When we add more menus later, we'll have to enable those as well.
Finishing Up
We're not quite finished, however. Let's open up aboutWindow's code editor (select it and
press option-tab). In the Open event, put this:
rbdCanvas.url = "http://www.rbdeveloper.com/"
rbuCanvas.url = "http://www.applelinks.com/rbu/"
Excellent! That will store the appropriate URL into the url property of each hotCanvas.
That's all that we have to do to initialize those objects: they know how to do the rest
already.
Now in the KeyDown event, type in self.close. That will make sure that if the user hits
any key, it will close the About Box. Do the same for the window's mouseDown event:
type in self.close there. That way if the user clicks the mouse it will also close the
window.
Whew! I know what was a variety of steps, but hopefully you followed along safely.
Your program should have a working About Box now. It should display briefly when you
launch the program, and you should be able to activate it by choosing "About" from the
Apple menu. You should be able to close it either by clicking the mouse or typing any
key. The mouse arrow cursor should change to a finger cursor when you're within the url
text on the window, and if you click, it should connect you to the Internet and take you to
the appropriate website.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We'll add the cool ability to select background textures to RBU Pyramid's window.
Letters
This week we hear from Jeff, who apparently doesn't like games:
Gentleperson,
I would like you to write about something else on your website. I am not really interested
in creating game programs. Could you separate folders for games and non-game
programs?
We, REALbasic users, need to learn how to make interesting programs such as input
data, saving text information from a source file (list of names, addresses, numbers, etc).
An application (homemade) reads the source file into separate boxes - similar to COBAL
program. Or, you could show them how to calculate numbers from the list of box(es).
I hope that you would do that for us.
Thank you.
Jeff
Here is an example:
From (INPUT) Source File:
4930StreetRoadMA403205085482041
0449StreetRoadCT033058605284490
....
to (OUTPUT) REALbasic application:
Name Street:
State:
Zip Code:
4930 Street Road
MA
40320
508
449 Street Road
CT
03305
860
Telephone:
5482041
5284490
....
Thanks for the note, Jeff. First, let me say that while I take your concern seriously, I
obviously can't please everyone with one column. I'm hoping to launch REALbasic
Developer magazine next summer, and in that format we'll have room for a variety of
articles.
Second, the basic principles I'm teaching are valid regardless of the end application. For
instance, last week's lesson covered creating a preference file system that could be used
by any type of program. Just because I'm currently creating a game doesn't mean that you
can't use the same techniques to create a different kind of program. Besides, games are
often more complex from a programming perspective and that's why they're often used
for teaching examples. (They're also less boring than "real" tasks.)
Finally, I see REALbasic University as a long-term project: this is the 36th column, and
we've already covered several types of programs (utilities, graphics, animation, etc.).
RBU Pyramid is our first game. I like to mix things up a bit, so you'll be seeing a variety
of projects as we go along. Feel free to send me ideas for specific projects you'd like me
to do. (Part of the reason we're doing RBU Pyramid is a number of people specifically
requested I do a card game.)
As to your specific question, reading in data and outputting it in a different format is not
too difficult, but the problem is that every person out there probably has a different input
format and wants a different output result. It's difficult for me to create a single solution
that works for everyone: all I can do is demonstrate some basic techniques that you
should be able to adapt to your unique situation.
If we break your problem into several steps we get the following:
1. Read in text file and save in string variable.
2. Parse string variable (extract data).
3. Arrange data in new format and save to new file.
The reading and writing of the text files is a bit beyond what I can do here, but you
should be able to figure that out either from RBU examples or from the REALbasic
online help.
Parsing the data depends greatly on the format of the data. For instance, in your above
example, each piece of data (record) is stored on a separate line. Then each piece of
information within that (each field) is a consistent length: i.e. the house number is always
four characters, the street is ten, state is two, etc. If that's the case, reading in the data isn't
hard:
// theLine is the current line of data
houseNum = mid(theLine, 1, 4)
theStreet = mid(theLine, 5, 10)
theState = mid(theLine, 15, 2)
...
In other cases, the data might be of flexible length but separated by a delimiter (such as a
tab character). That's even easier:
// theLine is the current line of data
houseNum = nthField(theLine, chr(9), 1)
theStreet = nthField(theLine, chr(9), 2)
theState = nthField(theLine, chr(9), 3)
...
Generating the output data can be a little trickier. In the case of your example above, we'd
need a routine that inserts x number of spaces (call it padSpaces). Assuming we have
that (I'll let you write that simple method), we could do something along the lines of the
following:
s = houseNum + " " + theStreet
x = 28 - len(s) // 28 is the rightmost position of the string
newLine = padSpaces(x) + s
x = 7 - len(theState) // 7 is the rightmost letter
newLine = newLine + padSpaces(x) + theState
x = 17 - len(theZip) // 17 is the rightmost letter
newLine = newLine + padSpaces(x) + theZip
x = 21 - len(thePhone) // 21 is the rightmost letter
newLine = newLine + padSpaces(x) + thePhone
What we are doing here is right justifying the text. Since we know the desired end
character position, we subtract that length from the length of text we're inserting and that
difference is the number of spaces we need to insert. We do this with all the data fields
and end up with a long newLine variable that contains the entire line. Output that line to a
disk file and you're all done!
Hopefully, Jeff, this will get you started in the right direction.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 037
RBU Pyramid X
I hope everyone had an enjoyable Thanksgiving holiday. Now it's back to REALbasic
programming!
This week we're going to add a fun feature to RBU Pyramid: the ability to specify a
background pattern for the game's main window. This will involve drawing graphics,
adding an option to our preference file, and using the contextual menu control.
Adding New Objects
We'll begin with the creation of a background canvas. Drag a new canvas object to your
project window. Give it the following settings (properties):
If bgCanvas is covering the other objects in your window, with it selected, go to the
Format menu and choose the "Move to Back" command. That should fix things.
Next, let's add a contextual menu: it's the tool that looks like this:
Drag it to your window and stick it anywhere. (Since it's not visible to the end-user, it
doesn't matter where you put it.) Rename it to bgContextualMenu.
Now, before we get to doing some stuff with these controls, let's add some global
properties we're going to need. First, we need a global picture property that will hold the
background picture. We'll also need a string variable to remember the picture's name, and
we'll create a constant that will be the name of the folder where the patterns will be
stored.
So open globalsModule and add the following properties (Edit menu, "New Property"):
gBackgroundPicture as picture
gBackgroundPictureName as string
To add the constant, go to the Edit menu and choose "New Constant" and put in
kBGPicts for the name and Backgrounds for the value.
Good. Now we're all set for some coding!
Drawing the Pattern
Close globalsModule and open the bgCanvas code editor. Go to the Paint event and put
in this code:
dim w, h, x, y, nX, nY as integer
//
// This routine repeatedly draws a pattern as background picture
//
if gBackgroundPicture <> nil then
w = gBackgroundPicture.width
h = gBackgroundPicture.height
nX = (me.width / w)
nY = (me.height / h)
for x = 0 to nX
for y = 0 to nY
g.drawPicture gBackgroundPicture, (x * w), (y * h)
next // y
next // x
end if
Looks simple, doesn't it? All this does is draw the pattern a bunch of times -- enough to
fill the entire bgCanvas. First, we make sure gBackgroundPicture isn't nil. If it is, we
don't draw anything. (Remember, nil means it doesn't exist. If you try to draw a nil
graphic, your program would crash.)
The actual drawing is simple. We figure out how many graphics it will take to fill the
window in each direction (horizontal and vertical) and we store that calculation in nX and
nY. Then we set up two for-next loops that repeatedly draws the graphic. Each time
through the loop, the graphic is drawn one graphic further over (or down). The result, if
the graphic is a proper pattern, is a seamless background.
Moving on, let's go bgCanvas' MouseDown event. Here is where we check to see if the
user's asking for a contextual menu (on the Mac this is a Control-click; on the PC, this is
a right-button click).
if IsCMMClick then
bgContextualMenu.open
return true
end if
Pretty simple, isn't it? We call the IsCMMClick function and open bgContextualMenu if
the result is true. If the user has clicked the mouse button on bgCanvas without holding
down the Control key (i.e. it's not a contextual menu click), we do nothing.
Handling the Contextual Menu
In the past, we've gotten our graphics into our project by dragging them into our
REALbasic project window. That's great for permanent graphics that we don't want the
user to change. But in the case of background patterns, we want the user to have the
ability to add their own graphics. So instead of embedding our graphics, we'll keep them
in a separate folder next to our application. Whatever graphics are in that folder will be
available for the user to select within RBU Pyramid.
To give the user an interface to those files, we'll use a simple contextual menu: it's a
menu that pops up when the user clicks the mouse button while holding down the Control
key (on a Mac, at least). The contextual menu is told to display itself by calling it's open
method.
What happens when we tell bgContextualMenu to open? Well, what we want to happen
is have it display a list of graphic files within the "Backgrounds" folder. Since that list is
dynamic (always changing), we can't prebuild the list: we must build it when the user
clicks.
Here's how we do that:
dim i, n as integer
dim f as folderItem
dim p as picture
// Populates the contextual menu with options
me.addRow "None"
me.addSeparator
f = GetFolderItem("").child(kBGPicts)
if f <> nil then
for i = 1 to f.Count
p = f.item(i).openAsPicture
if p <> nil then
me.addRow f.item(i).name
end if
next // i
else
beep
end if
It's not as complicated as it seems. The first few lines are simple: we are adding rows
(menu items) to our popup contextual menu. We first add a "None" option and then a
separator. After that, our goal is to add the pictures in the "Backgrounds" folder.
In Detail
This is where we used to run into a REALbasic, uh, bug. Apparently, this is fixed in
REALbasic 3.2 or better.
The GetFolderItem("") command is supposed to return a folderItem that points to the
folder your application is within. That's important, since otherwise we'd have no way of
knowing where to find the "Backgrounds" folder. Since different end users might put the
RBU Pyramid game folder to different places on their hard drive (i.e. one puts it in a
"Games" folder, another on his "Apps" drive), we need this function.
The problem came from the fact that we don't have a finished application yet. When you
ran your program within REALbasic's IDE, the GetFolderItem("") function returned a
folderItem pointing to the folder where REALbasic was located! This caused an odd
problem: when you ran within the IDE, REALbasic can't find the "Backgrounds" folder,
but the identical application, compiled to a stand-alone application, worked fine.
The simple solution was use the if debugBuild conditional to tell if we are within the
IDE or a stand-alone application. If debugBuild is true, we must use a manual path
instead of the GetFolderItem("") function. I used to have to do something like this:
if debugBuild then
f = GetFolderItem("RBU Pyramid:RBU Pyramid Ÿ 1.0").child(kBGPicts)
else
f = GetFolderItem("").child(kBGPicts)
end if
The above code worked for my particular setup, but anyone else would have to change
the hard-coded path above to match their machine. Fortunately, since REAL Software
fixed this bug, this isn't an issue any more. But knowledge of old workarounds can be
important when you need to think of a workaround for a different issue, and the
debugBuild function can be helpful.
Once we've got a valid folderItem pointing to the folder containing our "Backgrounds"
folder, we then look for its "child" (an item inside it) named "Backgrounds" (we use the
constant kBGPicts instead of hard-coding the real name.
Next, we check to make sure our folder pointer is valid (not nil). If it is, we loop through
every item in that folder and check to see if it's a picture file. (Who knows? The user
could have thrown some text files in there!)
How do we know if it's a valid picture file or not? Simple: we try to open it as a picture.
If there's an error, our picture property, p, will be set to nil. If the file is a valid picture,
we add the name to our contextual menu.
All this sounds like a lot, I know, but remember, it happens in a split second. The user
shouldn't even notice a delay. (I suppose, with hundreds of files in the "Backgrounds"
folder and a slow computer, it might take a few seconds for the contextual menu to
display, but that's an unlikely scenario. For our purposes, this method works fine.)
Once we've got the menu built, it's displayed to the user and the user can select an item
from that menu. What happens then is whatever we tell the Action event to do. Go there
and put in this:
// Loads the picture chosen
loadBGpicture(item)
Yes, all we do is pass this event to a method: nothing really happens here. Why use a
separate method? It's often cleaner and it helps if you need to call the same routine from
several places. For instance, we're calling this now from within a contextual menu, but
what if there was a regular menubar menu item that called the same routine? Separating
the code into a method makes it easier to reuse that routine.
So let's add a new method (Edit menu, "New Method"). Give it these settings:
Here's the code for the loadBGpicture method:
dim i, n as integer
dim f as folderItem
dim p as picture
//
// Performs the contextual menu command chosen
//
if item = "None" or item = "" then
gBackgroundPicture = nil
gBackgroundPictureName = ""
bgCanvas.refresh
return
end if
f = GetFolderItem("").child(kBGPicts).child(item)
if f <> nil then
p = f.openAsPicture
if p <> nil then
gBackgroundPicture = p
gBackgroundPictureName = f.name
bgCanvas.refresh
end if
end if
First we see if the user chose the "None" option. If they did, we set
gBackgroundPicture to nil and redraw bgCanvas (if there was already a picture there,
it now goes away). Then we do a return command, which stops executing this method.
If the user selected a file name, we attempt to open it as a picture. If the picture's good
(not nil), we then assign it to gBackgroundPicture. We put the picture's name into
gBackgroundPictureName. Then we redraw bgCanvas (by telling it to refresh itself).
That's it! Your program should compile and run now, though if you don't have a
"Backgrounds" folder in the same folder as your project, it won't let you choose a
background graphic. (If you downloaded the compiled RBU Pyramid beta, you can copy
the "Backgrounds" folder from it, or get it from today's project file.)
With a valid "Backgrounds" folder, your version of RBU Pyramid should let you choose
various background patterns. There's only one problem: when you quit Pyramid, that
setting isn't saved. Let's fix that!
Adding a Preference Setting
Remember when I explained my preference file system I said that it was easy to
add/remove preference options? Well, now's our chance for you to see how easy it really
is.
Open prefModule and go to the loadPrefs routine. Somewhere within the select case
routine, add the following case:
case "bgpicture"
gBackgroundPictureName = gDirectives(i).text
gameWindow.loadBGpicture(gBackgroundPictureName)
It doesn't matter where you put this (first or last), since the code will only be executed
when the pref setting matches the name "bgpicture". The code simply stores the picture
name into our gBackgroundPictureName variable (were you wondering what it was
for?) and then tells loadBGpicture to load the background picture passed. (See? Making
loadBGpicture a separate routine was important!)
Now go to savePrefs and do the reverse. This time, we save the picture name (nothing if
no background picture is being used).
// Save bg picture setting
t.writeline "#bgpicture " + gBackgroundPictureName
That's it! That wasn't too hard, was it?
Run the program, select a background graphic, then quit it. When you relaunch it, the
same background should be visible. Cool, eh? (Extra credit: open the RBU Pyramid
Preference file in a text editor and see what's in it!)
It should look something like this:
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We've been focusing on jazzy user-interface features for a bit, so in our next lesson we'll
get back to actual game logic and fix a few issues to make it play better.
Letters
Rachel Msetfi writes with a simple but common problem:
Hi Marc,
I'm trying to learn to write simple programmes in RB and have found your column very
useful. I wonder if you could help me with this little query that may seem obvious, but
has been bugging me for ages!
I want to ask my user for a response and will present an editfield. How do I write the
contents of the editfield to a text file and can I write the contents of multiple editfields to
the same text file?
Hopefully waiting for an answer!
Rachel
Easily done, Rachel! The best way is to store the contents of an editField in a string
variable (a string variable can contain virtually unlimited text). That allows you to
"concatenate" -- join -- multiple strings (the contents of several editFields) together.
For example, let's assume you've got a save routine (a method named saveStuff). Inside
this routine, you'll need to join the editFields together and save the results into a file.
The first question: where is the file? Working with files in REALbasic can be a little
tricky until you understand the logic behind them. Remember, RB has essentially two
types of file objects. The main one is a folderItem, which is a pointer to a specific file
(either one that exists or one that may exist). Once you have a folderItem object, you
can use its methods to create either a text or binary file: that's the second file object you'll
need. It will either be of type textOutputStream or binaryStream (depending on the
kind of file you want to create). In your case, we'll use a textOutputStream object.
For your situation, we'll assume the reader can choose a filename and location to save the
file (thus we'll use RB's getSaveFolderItem function).
The second part of your task is to decide how you will divide the different fields of data.
For example, if you had two editFields, one with a person's name and another with a
person's phone number, and you wrote that information to a text file as a single piece
data, you'd end up with a file containing:
Steve Jobs408-555-1212
From that data, how would you figure out which is the name and which is the phone
number? It's easy for a human to tell, but much more difficult for a computer. But if you
separated the two pieces of data with a unique character (such as an asterix [*] or an
unprintable carriage return or tab), it would be easy for you to know where one field
stops and the next starts.
With those two questions answered, we can write our routine. Assuming we have the
following editFields:
Our saveButton action routine will contain the following code:
dim f as folderItem
dim out as textOutputStream
dim s, tab as string
// Tab character
tab = chr(9)
// Prompts user for a file name and location
f = getSaveFolderItem("txt", "Sample SaveStuff Data")
if f <> nil then
// User didn't cancel, so save the data
out = f.createTextFile
if out <> nil then
// File was created okay, so save data
// Combine editFields into s variable
// (the fields are separated by tab character).
s = nameField.text + tab + ageField.text + tab
s = s + phoneField.text + tab + commentField.text
// Write s to file
out.writeline s
out.close // close the file
end if // out = nil
end if // f = nil
The comments within the above code should explain things fairly well, and hopefully this
example should get you started. You can download the full sample project here.
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 038
RBU Pyramid XI
Lately we've been concentrating on adding some "whiz-bang" features to RBU Pyramid,
such as last week's ability to change the game window's background graphic. For the next
few lessons, we're going to concentrate on finishing up the core of the game, to make it
more playable. Some of this will be ugly but important stuff, like adding an undo system,
and some will be more graphical, such as the help system and a high score mechanism.
Before we begin, however, some of you may have noticed that the original RBU Pyramid
beta application posted in column I expired at the end of November. My original
intention was to have the final version released before that date, but several things
conspired against that, including a bout of the flu I'm currently enduring (if part of this
column sounds irrational, it's probably fever talking ;-). But I'll have a new final version
posted as soon as it's finished (in the next week or so).
Adding Available Moves Detection
This week we're going to add an incredibly useful feature to RBU Pyramid: move
detection. Move detection just means our program can tell what valid moves are
available. That sounds simple, but just getting a computer program to understand the
rules of a game is tough.
From a programming perspective, move detection must analyze every possible card
match and see if the two cards are a valid match (the card values add up to 13).
From the player's perspective, move detection works like this: holding down the Shift key
at any time during the game will highlight any available card matches.
There's another key benefit to the move detection feature: it will tell us when there are no
available moves, meaning the game is over. That's helpful: is there anything more
annoying than playing a game where you've reached a dead end and yet the computer
doesn't tell you that?
So the first thing we're going to do today is to add a global "game over" property. Open
up globalsModule and add this new property (Edit menu, "New Property"): gGameOver
as boolean.
Now find your updateCards method inside gameWindow. Scroll through it until you see
the comment reading "// See if pyramid has been won". In the blank space above that
line, we're going to insert a few new lines of code. (Make sure you add this before the
comment but after the "next // row" which should be the previous line.)
Here's the new block of code to insert:
// See if there are any moves left
if not gFirstTimeThrough and uBound(gTheDeck) = 0 then
findAvailableMoves
if gGameOver then
endOfGame
end if
end if
What this does is check to see if the deck is empty (no more cards to draw) and that we've
already gone through the deck once. If so, we scan for available moves. If the game's
over (no available moves), we call an end of game routine. The effect of this is that the
game will stop when there are no more moves left.
Since we're referring to the endOfGame method, let's add it. Go to the Edit menu and
choose the "New Method" command. Name it endOfGame. For today, we're just going to
leave it blank. (But we need to add the method, otherwise REALbasic wouldn't let your
program compile since it wouldn't know what endOfGame meant.)
Do that same thing again, and this time name the method findAvailableMoves. While
we're at it, go ahead and add a showAvailableMoves method as well.
Since the showAvailableMoves is simpler, we'll add its code first:
dim i as integer
findAvailableMoves
if showingMoves then
for i = 1 to 31
if cardCanvas(i).highlight then
cardCanvas(i).refresh
end if
next
else
for i = 1 to 31
if cardCanvas(i).highlight then
cardCanvas(i).highlight = false
cardCanvas(i).refresh
end if
next
end if
This method will be called when the user holds down the Shift key: it will redraw the
pyramid with playable cards highlighted. Since the findAvailableMoves method sets
the highlight property of each card, all we have to do is check that property and force
the card to redraw if it's true.
If showingMoves is false (meaning the Shift key is not down), we turn off the highlight
setting of each card and force the card to redraw non-highlighted.
That's pretty straightforward. But showingMoves is a new property: we must add it to
gameWindow. Go to the Edit menu and choose "New Property" and type in showingMoves
as boolean.
Finding Valid Moves
Now we come to the complicated part: the findAvailableMoves method is where all the
key work is done to calculate if a move is valid or not. Here's the code -- I'll explain it in
a minute:
dim i, j, n as integer
for i = 1 to 31
if cardCanvas(i).highlight then
cardCanvas(i).highlight = false
end if
next
for i = 1 to 28
if cardCanvas(i).topCard and cardCanvas(i).visible then
// is a King?
if gCards(cardCanvas(i).card).number = 13 then
cardCanvas(i).highlight = true
end if
// temp discard
if cardCanvas(30).clickable then
if gCards(cardCanvas(i).card).number +
gCards(cardCanvas(30).card).number = 13 then
cardCanvas(i).highlight = true
cardCanvas(30).highlight = true
elseif gCards(cardCanvas(30).card).number = 13 then
cardCanvas(30).highlight = true
end if
end if // cardCanvas(30).clickable
// main discard
if cardCanvas(31).clickable then
if gCards(cardCanvas(i).card).number +
gCards(cardCanvas(31).card).number = 13 then
cardCanvas(i).highlight = true
cardCanvas(31).highlight = true
elseif gCards(cardCanvas(31).card).number = 13 then
cardCanvas(31).highlight = true
end if
end if // cardCanvas(31).clickable
for j = 1 to 28
if j <> i and (cardCanvas(j).topCard and
cardCanvas(j).visible) then
if gCards(cardCanvas(i).card).number +
gCards(cardCanvas(j).card).number = 13 then
cardCanvas(i).highlight = true
cardCanvas(j).highlight = true
end if // total = 13
end if // j = i
next // j
end if // cardCanvas(i).topCard
// Check for overlapping match
if cardCanvas(i).row < 7 and (not cardCanvas(i).topCard) and
cardCanvas(i).clickable and cardCanvas(i).visible then
n = i + cardCanvas(i).row
if cardCanvas(n).clickable then
cardCanvas(n).highlight = true
cardCanvas(i).highlight = true
end if
n = i + cardCanvas(i).row + 1
if cardCanvas(n).clickable then
cardCanvas(n).highlight = true
cardCanvas(i).highlight = true
end if
end if // is clickable but not on top
next // i
// Check if discard cards match
if cardCanvas(30).card > 0 and cardCanvas(31).card > 0 then
if gCards(cardCanvas(30).card).number +
gCards(cardCanvas(31).card).number = 13 then
cardCanvas(30).highlight = true
cardCanvas(31).highlight = true
end if
end if
// Check for end of game
gGameOver = false
if not gFirstTimeThrough and uBound(gTheDeck) = 0 then
gGameOver = true // assume no moves left
for i = 1 to 31
// if we find a highlight, that means there's a move left
if cardCanvas(i).highlight then
gGameOver = false
end if
next
end if
Yes, that's a lot of code for something that seems like it should be simple! But it's not as
bad as it seems once you understand what's going on. Let's step through it and see.
The first loop simply sets the highlight property to false for every card. That's simple
enough.
Then we start a for-next loop through the first 28 cards (the pyramid, not counting the
discard piles). We check for the simplest thing first: a king (removable by itself). If the
card is a topCard (not covered by another card) and it's a king (number = 13) then we set
its highlight to true (meaning it's a valid play).
Next, we want to see if a playable card in the pyramid can combine with a discard card to
be a match. So first we only examine playable pyramid cards by restricting our
examination to topCards that are visible (therefore playable).
We add the value of card i of the pyramid with the card on top of the temporary discard
pile. If the two added together equal 13, we've got a match! If they don't, we do a quick
check to see if the temp discard card is a king, and if it is, we set its highlight to true.
Note that there may not be a card on temporary discard pile: that's why we first check to
see if that card is clickable, meaning that it's a playable card.
We do the same thing again with the main discard pile: simple.
But we still haven't checked for all possibilities yet: we haven't compared cards in the
pyramid to other cards in the pyramid. That gets a little more complicated.
To do that, we start a new for-next loop with a variable j, and loop through every card
in the pyramid. For each card, we first make sure that i and j aren't the same (otherwise
we'd be comparing the same card to itself), that it's a topCard, and that it is visible. That
ensures that we're only dealing with playable cards.
Since we already know that card i is playable, now that we know j is playable, we just
have to see if the two are a match (they add up to 13). If so, we set the highlight of both
cards to true.
We're getting closer, but we're still not finished. Remember, if two cards overlap each
other and add up to 13, they're removable, so we must check for overlapping cards. This
is more difficult since the underneath card is not a topCard and yet it must be directly
underneath the overlapping card for the match to be valid.
Fortunately, we've already done most of the ugly math for this calculation in our
updateCards routine. Remember how it calculates if a card underneath another is a
match and sets its clickable property to true? (See RBU Column 031 for more on
that.)
So we can cheat a little and use that previous calculation. All we do is find a playable
card that is not a topCard -- if it's clickable, then it must be part of a match! Then we
just find which of the two cards on top of it is also clickable, and we've got the two
cards we need to highlight.
The formulas n = i + cardCanvas(i).row and n = i + cardCanvas(i).row + 1 set
n to the number of the card on top of card i, so we can simply see if
cardCanvas(n).clickable is true or not to tell if we've got a valid match. Simple!
But we're still not quite finished. There's one more possibility of a match: the two discard
piles could match each other! So we add a few more lines of code to see if the two added
together total 13, and we set their highlight property to true if so.
Finally, we could be at the end of the game: what if there are no more cards to draw and
no moves available? We check for this by making sure there are no cards left to draw and
that we've already gone through the deck once. If that's the case, we simply look for the
any highlighted cards: if there is even one, there's a valid move so the game's not over. If
there are none, however, gGameOver = true and the game will end.
Whew! That was a lot of technical stuff, so I hope you followed it well (and I hope I
explained it clearly). This does get complicated, but one of the things I've noticed is that
REALbasic's object-oriented design greatly reduces the complicity. For instance, with the
above, we're referring to cards as though they are real objects, completely ignoring the
fact that they are nothing more than collections of data in a computer's memory!
That's what happens as you work with data objects more and more: instead of rationally
thinking "Ah, x a property of object y," you begin to think of your object's properties as
settings and characteristics of your objects.
Checking for the Shift Key
Your program should run now, but the key new feature -- move detection -- won't
actually work until we do one more thing. We must add a way to detect when the user
presses down the Shift key.
That gets a little tricky, because pressing the Shift key, while it is a key on the keyboard,
does not activate gameWindow's keyDown event. We can manually check to see if the
Shift key is down using the keyboard.asyncShiftKey function, but that only is checked
when we call it. We'd have to call it constantly for it to do what we want. So how can we
do that?
The only alternative is to set up a timer object: each time the timer activates, it checks
to see the state of the Shift key. If it's down, it highlights the cards. If it's up, it draws the
cards normally. We can set the timer to be called as frequently as we need: ten times a
second is good. (It's fast enough the user won't notice a delay and yet not so frequent as to
slow down the game.)
Drag a timer object to gameWindow and give it the following settings:
Good. Now in moveTimer's Action event, put the following:
dim original as boolean
original = showingMoves
if keyboard.asyncShiftKey then
showingMoves = true
else
showingMoves = false
end if
if original <> showingMoves then
showAvailableMoves
end if
Since moveTimer's code is executed ten times every second (remember, we set its period
property to 100 milliseconds), it will be executed multiple times while the Shift key is
being held down. That means we don't want just a simple toggle: that would mean the
slow and complicated showAvailableMoves and findAvailableMoves routines would
get executed ten times a second!
So what we do is create a new variable, original, and store in it the state of
showingMoves before we check the keyboard. Then if keyboard state changes
showingMoves, the two won't match, meaning we've had a change in state. If that's the
case, we redraw the cards by calling showAvailableMoves. That way
showAvailableMoves is only called once per Shift key down (instead of dozens or
hundreds of times). Slick, eh?
Well, that should do it for this week. The game should run and show you available moves
when you hold down the shift key. If you would like the complete REALbasic project file
for this week's tutorial (including resources), you may download it here.
Organizing Your Project Window
Oh, one more thing. I've noticed that our project window is getting rather crowded. Let's
use a cool feature of REALbasic to organize our project's elements into folder. Go to the
File menu and choose "New Folder" three times. Rename the folders like in the picture
below, and put the appropriate elements into each folder:
Isn't that much better?
Next Week
We'll continue by adding in a system to remember players' high scores.
Letters
This week we hear from another Marc, Marc Gyarmaty, who writes with a question
regarding arrays:
Hi Marc!
I´ve got a little problem with arrays in RealBasic.
example dim a(10) as string a(0)="this is a string standing in position 0"
but now the complete string stands in the index 0. If I display it (e.g editfield1.text=a(0)),
the whole string is displayed. I just want the first character (or if a(10), the tenth char).
In C/C++ just 1 character of the string would stand in it. And thats exactly what i want - i
just don't know how. I could not find any docs or samples. Do you know how thats
works?
thanks in advance,
marc
PS: there is a type 'array', but the compile won´t accept it e.g. dim a(10) as array
I can see how this would confuse you, Marc, if you're familiar with other programming
languages such as Pascal or C which don't have a string datatype. In those languages, a
"string" of letters is always stored in an array, meaning that the first character in the
string is at array index 1, the second at array index 2, etc. While that can be handy for
accessing the individual letters of a string, it's a pain because the size of your string is
usually fixed (i.e. you can set the maximum length of the array/number of letters in the
string, but you can't change that while the programming is running).
The solution to your problem is simple, however. Since a(0) in your example is a
string, you just use the mid() function to grab a single letter inside that string. The
mid() function is flexible and powerful: you can use it to retrieve a single letter or a
series. For instance, the following are valid uses of the mid() function (stick this code in
a pushButton if you want to try it).
dim s as string
s = "The quick brown fox ran fast."
msgBox mid(s, 1, 1) // first letter
msgBox mid(s, 1, 3) // first three letters
msgBox mid(s, len(s), 1) // last letter
msgBox mid(s, 10, 1) // tenth letter
msgBox mid(s, 10) // everything after the 10th letter
The mid() function takes three parameters: the first is the name of the string you are
examining, the second is the position you want to examine, and the third is the number of
letters to extract. The third parameter is optional: if you don't include it, it will return all
the letters in the string past (and including) the position point. (So in the above, the final
line returns the phrase " brown fox ran fast.")
Just remember, REALbasic doesn't use arrays for strings, and thus strings can be any
length, up to the available memory (to a maximum of two gigabytes, I believe).
Keep those letters coming! I may not answer your question immediately, but I'll
hopefully get around to it in a future column. (If you don't want your correspondence
published, just be sure to indicate that when you write. Otherwise I'll assume it's fair
game.)
.
REALbasic University: Column 039
RBU Pyramid XII
This week we're going to do something that sounds simple, but isn't: add a high score list
to our game. This isn't brain surgery, but as you will see, it effects many areas of our
game, making the implementation complicated. In fact, we won't even finish this today -this will be a two-part lesson. Be prepared as we jump around the code a lot!
The techniques used for adding this high score list are simple and should be reusable in
other games.
Adding a High Scores Window
The first thing we'll need is a special "high scores" window where we'll display the list of
top scores. This could be a simple dialog box, but that's not as convenient for the player
since the scores can only be seen when the game's over. Instead we'll go the extra mile
and make our window a floating window -- that's a special kind of window that floats
above document windows and is typically used for tool palettes. That way the scores can
always be visible (or not, as the player prefers).
With your RBU Pyramid project, add a new window (File menu, "New Window"). Give
it the following settings:
(Note that the full title property reads, "RBU Pyramid All-Time High Scores".)
Now open HighScoreDialog (double-click on it) and drag on a listBox control. Give it
these settings:
The contents of the initialValue field aren't displayed, so here's what you put there:
"Rank Name Score Date". Please note that those aren't spaces separating each item -those are tab characters.
In Detail
If your HighScoreDialog comes up with incorrect column names, you didn't put in the
tabs correctly. There must exactly one between each name. REALbasic will assign each
name to a column based on them being divided by tabs.
You could do this manually, within the listbox's Open event, with code like this:
scoreList.heading(0)
scoreList.heading(1)
scoreList.heading(2)
scoreList.heading(3)
=
=
=
=
"Rank"
"Name"
"Score"
"Date"
If you don't do it via code, however, REALbasic will assume the first line of the
initialValue property to contain the headings.
If you don't tell RB how to name the headings anywhere, it will just use the default
"Column 0," "Column 1," "Column 2," etc.
Now that you've done that, let's initialize our listBox. Open scoreList in the Code
Editor (double-click on it). Go to the Open event and put this:
// Center rank, score, and date columns
me.columnAlignment(0) = 2
me.columnAlignment(2) = 2
me.columnAlignment(3) = 2
All this does is set the appropriate columns to center alignment. We don't want column 1
to be centered, since that's the player's name and it looks best flushed left (the default).
Next, go to sortColumn of scoreList and put in these two lines:
me.headingIndex = -1
return true
That just prevents the user from sorting the listBox. By default, any multi-column
listBox in REALbasic will automatically sort itself when the user clicks on the various
headings. In our case, we don't want that -- high scores should always be displayed bestto-worst. So we intercept the sort event and tell RB that the headingIndex is really -1,
meaning that the user hasn't clicked on a specific column (and therefore there's no need
to sort anything).
Remembering a Window's Location
As an extremely particular and organized Mac user, one of my pet peeves is programs
that don't remember where I put a window. If I resize a window and put it someplace, it
should be in that same place the next time I run that program. This applies especially to
floating palettes such as the high scores list we're working on.
There are countless ways you could use to save a window's location info, but we'll take a
simple direct route. It's not necessarily any better or worse than any other, though it does
integrate well with my preference system.
What we'll do is save our window positioning info in a global property called gScorePos.
It's of string type, so open globalsModule and add a new property (Edit menu, "New
Property"). Type in this gScorePos as string in the dialog that pops up.
Let's also add another global property: gScoresShowing as boolean.
Good. Now go back to HighScoreDialog and let's add a new method (Edit menu, "New
Method"). Name it saveWindowInfo and put this code into it:
gScorePos
gScorePos
gScorePos
gScorePos
=
=
=
=
str(self.left) + comma
gScorePos + str(self.top) + comma
gScorePos + str(self.width) + comma
gScorePos + str(self.height)
All this does is save the window size and location, separated by commas. Simple!
(Remember, the special self function refers to the window owning the routine. In this
case, that's HighScoreDialog [saveWindowInfo is inside it], so self.left is exactly the
same as HighScoreDialog.left.)
But wait a second -- what's a comma? Does REALbasic know what that is? No, it does
not: go back to globalsModule and let's add a constant (Edit menu, "New Constant").
Name it comma and give it a value of "," (no quotes).
I warned you we'd be jumping around today! (And we've just gotten started.)
Now we run into a tricky little situation. Basically, what we want to have happen is that
when the user resizes or moves the HighScoreDialog, we save that info. So it seems like
all we need to do is put in a call to saveWindowInfo within each of the Resized and
Moved events, right?
Not so fast. What happens is that when the window is initially opened (opened for the
first time), it is resized and moved to the correct position -- and doing that causes
REALbasic to fire the Resized and Moved events! If those events save the window
position, they overwrite (erase) the original info that we haven't used yet. (If that doesn't
make your brain spin, I don't know what will!)
This bug took me a while to find when I wrote the first draft of RBU Pyramid: everything
looked fine, but the program wasn't saving the window position info. I finally had to step
through the program step-by-step to figure out what was going wrong.
The solution, however, is simple: we just need a flag to tell us when the window is
initially opening. So let's add a property to HighScoreDialog (Edit menu, "New
Property"): opening as boolean.
Now go to Resized and Moved events of HighScoreDialog. Put in this same code for
both:
if not opening then
saveWindowInfo
end if
See how that works? If the window's initially opening, it won't call saveWindowInfo. In
other words, it will only save the new window details if the user's changed the window's
position or size.
But for this to work, we must do some initialization stuff when the window is opened. So
go to the window's Open event and put in all this stuff:
gScoresShowing = true
opening = true
if countFields(gScorePos, comma) = 4 then
highScoreDialog.left = val(nthField(gScorePos, comma, 1))
highScoreDialog.top = val(nthField(gScorePos, comma, 2))
highScoreDialog.width = val(nthField(gScorePos, comma, 3))
highScoreDialog.height = val(nthField(gScorePos, comma, 4))
end if // countFields = 4
opening = false
See how we've framed the window resizing stuff with opening = true and opening =
false statements? That way when we're programmatically adjusting the window it won't
save that location info.
As you can see, it's easy to extract the window info from gScorePos. Since each field of
info is separated by a comma, we use that as our divider and tell nthField() to grab each
field one by one. Since nthField() returns a string and we need a number, we use val()
to convert the string type to a number type, and then we assign that number to the
appropriate window property. About the only way to screw this up is to do things in a
different order: make sure that if your window data is saved in left, top, width, height
order that you retrieve it in the same order!
When HighScoreDialog is closed, we'll need to save the current window position and
set the gScoresShowing property. This goes in the Close event:
gScoresShowing = false
saveWindowInfo
Guess what? We're done with HighScoreDialog. You can run the program and it will
work, but of course we haven't done anything with HighScoreDialog yet, so it won't
seem any different.
Adding a Menu
The player can't see the high scores window because we haven't displayed it, or given the
player any way to display it!
So let's add a menu command to display the window.
Within your RBU Pyramid project, double-click on the Menu object. You should see a
menubar within a little window. To the right of the Edit menu within this window, there's
a gray rectangle. Click on it to highlight it. Like this:
Now type in "Options" (no quotes) and press return. That should have added an Options
menu to your program. On the blank line underneath that, select the blank area and type
in "Show High Scores..." (no quotes).
You can give the menu item a Command-key shortcut by typing "H" (no quotes) on the
CommandKey property field.
Now all we've done is tell REALbasic we need this menu item -- we haven't told it do
anything with it yet. Since the OptionsShowHighScores option is a command that is
application-wide (not restricted to gameWindow), we want to add a handler to this
command within our app class.
So open App class and add a Menu Handler (Edit menu, "New Menu Handler") -OptionsShowHighScores should be available on the popup list. Select it and click OK.
REALbasic should have added the menu handler underneath the existing AppleAbout
handler. If you click on it, we should be able to add code to it on the right side:
if gScoresShowing then
highScoreDialog.close
else
gameWindow.showScores
end if
All this does is if the window is showing, it closes it, or if it's closed, it displays it.
Simple!
But if you run the program now, you'll find that our new menu command is grayed out!
What's up with that?
By default, REALbasic keeps menu items inactive. You must specifically tell RB that the
command is enabled for it to be available for the user.
Go to the EnableMenuItems event of app. There should already be a line of code there.
Underneath that, add this (separated by a blank line or two, if you want):
if gScoresShowing then
OptionsShowHighScores.text = "Hide High Scores"
else
OptionsShowHighScores.text = "Show High Scores"
end if
OptionsShowHighScores.enabled = true
See how clever we're getting? We not only enable the menu item, but we change the text
of it so when the window is visible it says, "Hide" and when it's hidden it says "Show."
Very slick!
But when you try running the program now, it gives you an error, doesn't it? It says
"unknown identifier: gameWindow.showScores". That's because we never created a
showScores method within gameWindow!
Open gameWindow and add a new method (Edit menu, "New Method"). Name it
showScores. For now, put it highScoreDialog.show for the code -- next week we'll fill
in the rest.
That should give you a program that's runable. No actual high scores are displayed (yet),
but you can move and resize the window and show/hide it at will. (Of course, the details
of the resize are not saved in the preference file yet -- we'll add that capability next
week.)
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We add high score loading and saving routines.
Letters
This week we hear from Paul Thompson, who writes with a common REALbasic
situation:
Dear Marc
Here's a puzzle.
I know how to select the contents of an editField as you might do on opening a window:
namefield.setfocus
namefield.SelStart=0
namefield.SelLength=Len(namefield.Text)
The problem is that you have to put the name of the editField in the method code. What I
am trying to do is create a method which could apply to any field. You would call the
method with the name of the editField as a parameter, eg:
selectText(editField1)
where the method would be:
sub selectText (fieldName as ???)
fieldName.setfocus
fieldName.SelStart=0
fieldName.SelLength=Len(fieldName.Text)
end sub
As you can see from my ???, what data type do I send to the method? If I use a string, it
comes up with the error "Not an Object".
I suspect there is no way of calling a method with a control as parameter but I hope you
know better.
You're so close, Paul. The answer's simple... an editField is a class, a class of
"editField". (If you select an editField and look at the "super" property on the Property
list it will say "editField".)
So the solution is simple. "EditField" is name of the class! This works:
sub selectText (fieldName as editField)
fieldName.setfocus
fieldName.SelStart=0
fieldName.SelLength=Len(fieldName.Text)
end sub
I do this all the time -- as you've figured out, it's a valuable technique!
A variation of this is that you can store an editField's reference inside a variable. For
example, you could create a property "cf as editField" and then within an editField's
GotFocus handle put in "cf = me".
What good is that? Well, let's say you have a window with multiple editFields. Let's say
you have a "Select All" command (like you do above). You want the command to work
in all fields, but to do that, it must know which is the current field. By storing the current
field in the cf variable, your Select All routine can just be:
if cf <> nil then
cf.selStart = 0
cf.selLength = len(cf.text)
end if
You'd probably also want to put a "cf = nil" in each editField's LostFocus event. That
way when the user moves out of an editField, cf is set to nil, and when they move
into a new editField, cf is set to that editField. And if you set up your editFields as a
control array, you only have to put in this code in once no matter how many editFields
you have!
Hope this helps.
Next, we hear from David J. Gauthier, who has a question about the REALbasic
documentation:
Hi Marc: I did not know where else to ask you this, I hope you don't mind. I was hoping
you could tell me if the printed documentation for Real Basic is worth getting.
Thanks,
Dave
That's an interesting question, Dave. Unfortunately I don't know the answer as I never
bought it. I think it might be the same as the downloadable PDFs, but simply printed, but
I'm not 100% sure on that one.
Either way, I suspect the docs are decent, but not outstanding -- you'd be better off with
one of the REAlbasic books out there (either "RB for Dummies" or Matt Neuburg's more
advanced book -- see links at the top of this page). You might want to join one of the
REALbasic mailing lists and post this one to the crowd there -- I'm sure a few have
bought the printed docs and could help you.
If any RBU readers have a comment on this, feel free to let me know your thoughts on
the subject.
Our final letter this week, comes from Michael Krzyzek, who has a real doozy of a
question:
I've been following the Pyramid game and decided to port a Risk type game that I had
helped write for the Newton and later implimented in a primitive way on the Mac.
I found I could draw images into a canvas and apply a mask. This allows me to display
irregular areas like countries and continents. What I can't figure out is how to limit the
clickable areas of an irregular shape. I thought that it might happen automatically since it
seems that your cards are not clickable on the corners, but in my canvas the whole
rectangle is clickable.
In my other implimentations I could check whether a specific set of coordinates was in a
region or even over a black pixel in a bitmap, but have not found similar functionality in
RB.
A corollary question is there an equivelant of a region in RB. A region is basically an
area but does not have to be contiguous. Multiple shapes, regular or irregular, can be in
the same region.
Thank you in advance for any help,
Michael Krzyzek
Tactile Systems, Inc.
Excellent question, Michael!
Actually, the corners of my cards in RBU Pyramid are active: it's just the corner area is
so small it's rare you're able to click within the few pixels between the rectangular canvas
edge and the curved corner.
First, REALbasic does not support a region-type graphic. Yes, that's a bummer, since the
Mac OS has support for regions and cool routines like "is this x,y point within region y?"
making what you want to do easy. It's harder in REALbasic.
Now you could figure out the mathematics and do all the calculations for this kind of
thing manually within REALbasic, but it would complicated and (probably) slow.
A simpler method, which is what I would use, has some disadvantages, but should work
in most cases.
You mention you have a mask that's the shape of a particular continent. A mask (in
REALbasic) has white areas transparent and black areas solid. Since the mask is just
another picture, you can examine the clicked point inside the mask and see if the point is
black or not. If it's black, we're inside the mask area!
I've written a tiny program demonstrating this: click on North America and it beeps:
But if you click within the clear area of Gulf of Mexico or the Great Lakes, it doesn't
beep.
Hopefully this will get you started. For anyone who'd like this code, here's a project file
to download.
.
REALbasic University: Column 040
RBU Pyramid XIII
Last week we started the process of adding a high scores system to RBU Pyramid. We
created a high scores window and added a menu command to show and hide the window.
Like many things that seem simple, this is one that effects many areas of the program.
Today we'll finish this part of the program, creating a data structure for the high score
info, adding routines to display the data, and methods that will save and retrieve the
information.
The High Scores Data Structure
If you've learned anything about the way we do things here at RBU, you'll know that
creating an appropriate data structure is way up there on the Importance list. The most
basic data structure is an array, so we'll use that, but for the data organized within that
array, we'll create a custom class.
Add a new class to your project window (File menu, "New Class"). Name it
highScoreClass. Within it, add the following properties (Edit menu, "New Property"):
Now open globalsModule and insert this property: gHighScoreList(10) as
highScoreClass. That will give us a ten-position array of our class (yes, technically it's
eleven as the zeroth position is a valid array element, but we'll only use elements 1
through 10).
Cool! Our data structure is established, but we need to initialize that structure as well as
write routines that will load and save the data.
Load and Save Data Methods
It's pretty obvious that data loading routines are exactly the opposite of data saving
routines: the question is, which do you write first? Does it make a difference?
The answer is yes, it does make a difference. Always write your data saving routine first,
then use that as the basis for your file loading routine. The reason is that it's easy to forget
a step during the writing of the first routine. After all, it's the first. A mistake in the save
routine usually isn't fatal (your program will still work, it just won't save all the data), but
an error in the loading routine and your program might crash.
Open your prefModule module and let's add a couple methods (Edit menu, "New
Method"). The first is saveHighScores. Here's the code:
dim out as binaryStream
dim f as folderItem
dim i, n as integer
f = preferencesFolder.child(kHighScoreName)
if f <> nil then
out = f.CreateBinaryFile("myBinary")
if out <> nil then
for i = 1 to 10
out.writeByte len(gHighScoreList(i).name)
out.write gHighScoreList(i).name
out.writeLong gHighScoreList(i).score
out.writeDouble gHighScoreList(i).theDate.totalSeconds
next // i
out.close
return
end if // out = nil
end if // f = nil
As you can see, the routine is fairly basic: we obtain a folderitem pointing to our high
score file (the name is the in the constant kHighScoreName which we'll add in a
moment). Then we create a binary file.
What's a binary file? Nothing but a series of binary data (i.e. numbers). That means if you
write the number 73 as a byte to a binaryStream, it will not be stored as the twocharacter string "73" but as character "I" (which is ASCII 73).
If you look in REALbasic's online help for the binaryStream object, you'll see that
unlike the textOutputStream object, which only has methods for writing lines of text or
text, binaryStream has options for writing bytes, doubles, longs, etc. That can be helpful
if you need to store exact data (such as a date or a very long number) since converting a
number to a string (text) tends to introduce rounding errors. The problem (or difficulty)
with a binaryStream is that you must know the exact order of the data in the file in order
to retrieve it. For instance, to read back a string of text, you must know exactly how many
letters are in the string.
That's easily solved, however, by simply storing the length of the string first, then the
actual string data. As you can see in the above, that's what we do: we first write a byte
representing the length of the data, then we write the data as a string. For the score and
the date, we don't need to do this, since we're writing those as fixed-length pieces of data:
a long and a double are both 4 bytes (32 bits) long.
Since there are ten scores to save, we do this ten times (using a for-next loop). Then we
close the file and we're done. Simple!
To load the data is simply the reverse. Add loadHighScores and paste this in:
dim in as binaryStream
dim f as folderItem
dim i, n as integer
f = preferencesFolder.child(kHighScoreName)
if f <> nil then
in = f.openAsBinaryFile(false)
if in <> nil then
for i = 1 to 10
gHighScoreList(i) = new highScoreClass
n = in.readByte
gHighScoreList(i).name = in.read(n)
gHighScoreList(i).score = in.readLong
gHighScoreList(i).theDate = new date
gHighScoreList(i).theDate.totalSeconds = in.readDouble
next // i
in.close
return
end if // in = nil
end if // f = nil
// if no file is read, set high scores to defaults
for i = 1 to 10
gHighScoreList(i) = new highScoreClass
gHighScoreList(i).name = kDefaultName
gHighScoreList(i).score = 0
gHighScoreList(i).theDate = new date
next // i
This is only a little more complicated. The outer parts of the routine are nearly identical,
but once we get ready to read in the data, we must create new instances of each array
element (of type custom class highScoreClass) using the new command.
Once we've got an instance initialized, we're ready to input the data. This part is exactly
the reverse of the save routine. First we read in a single byte -- that tells us the length of
scorer's name. Then we read in that name and put it into the name property of
gHighScoreList(i). Then we read in a long and put it into score. For theDate, we
must first instantiate it with the new command (it's a date object, after all). When we do
that, it defaults to today's date. We can set the date by storing the saved date into the
date's totalSeconds property.
We do this ten times, once for each element in the array, and we're done.
The final part of the code is used in case there is no high score file found: if that's the
case, we have no data to read, so we instantiate the array elements and insert default
values. You may notice one of these values is the default name, kDefaultName. While
we're thinking about it, let's add this constant.
Open globalsModule and add a new constant (Edit menu, "New Constant"). Give it the
name kDefaultName and the value "No Name" (no quotes).
Do this again, adding a constant named kHighScoreName with the value "RBU Pyramid
High Scores" (no quotes). That should take care of establishing the name of the high
score file.
There's one more thing we need to do: we must define the "myBinary" file type so that
RBU Pyramid knows what kind of binary file to create. (This just helps associate our
high score file with RBU Pyramid.)
Go to the Edit menu and choose "File Types...". Click the "Add" button and fill out the
dialog like this (save it when you're done):
As long as we're here, let's set the Creator type for our application. From the Edit menu,
choose "Project Settings" and make it look like this:
Excellent! That will help when we compile our finished application to a stand-alone
application.
Okay, our high score saving and retrieving routines are written, but we haven't called
them yet, so they won't do anything. We need to tell our program to load the high scores
when the game is launched, and save the high scores when the game is finished.
In your app class, find the Open event and put loadHighScores just before the
loadPrefs line. In the Close event, add saveHighScores after savePrefs. That's it!
Saving the Window Settings
Last week we made it so HighScoreDialog remembers its position and size, but we
didn't save those to our preference file. We'll do that now. Using our preference system,
it's easy to add this with just a few lines.
Within prefModule, open savePrefs. After the t.writeline "#version 1.0" line,
add the following:
// Save High Score Window status (open/closed and position/size)
t.writeline "#scorewindow " + bStr(gScoresShowing) + comma +
gScorePos
This simply saves a string of data beginning with "#scorewindow " and continuing with
either a "true" or a "false" (depending on whether gScoresShowing is true or false).
Then we add a comma, followed by gScorePos (which, if you will remember, is a string
of numbers separated by commas, representing the left, top, width, and height values.
It might look like this:
#scorewindow true,629,45,325,220
We could have saved this info using two separate preferences, of course, but there's
nothing wrong with doing it this way (though it is a little more complicated). To retrieve
the data, we just do the reverse.
Open loadPrefs and within the select case statement, add the following case:
case "scorewindow"
// finds first comma
j = inStr(gDirectives(i).text, comma)
// puts the rest of the line into gScorePos
gScorePos = mid(gDirectives(i).text, j + 1)
gScoresShowing = nthField(gDirectives(i).text, comma, 1) =
"true"
Remember, our preference system automatically strips off the leading number sign ("#")
and trailing space, so all we search for here is the keyword "scorewindow". The rest of
the preference is stored in the gDirectives(i).text variable, so we search for the first
comma: everything to the left of that comma is our true/false window open/closed setting,
and everything to the right of that comma is our gScorePos string.
The mid() function returns the middle portion of a string, starting at the position where
you tell it, and retrieving the number of letters you tell it. But there's a cool variation: if
you don't specify the number of letters you want back, it just returns everything to the end
of the string! That's how we fill gScorePos above: everything to the right of the comma
goes into gScorePos.
The final line sets gScoresShowing to true or false. The condition is if the first field
(separated by a comma) matches the word "true" -- that condition is either true or false,
and thus gScoresShowing is either true or false.
There's one more thing we need to do: if gScoresShowing is true, we must display the
high scores window. We do this within the Open event of gameWindow. Add in this code
there:
if gScoresShowing then
showScores
end if
Displaying the High Scores
We've now done just about everything for preserving the high scores, but we haven't set
up a system to display them. HighScoreDialog just displays an empty window!
Open gameWindow and go to the showScores method we added last time. This time we're
going to fill it with some code. Put in the following (the final line is the same as before):
dim i, n as integer
highScoreDialog.scoreList.deleteAllRows
for i = 1 to 10
highScoreDialog.scoreList.addRow str(i)
n = highScoreDialog.scoreList.lastIndex
highScoreDialog.scoreList.cell(n, 1) = gHighScoreList(i).name
highScoreDialog.scoreList.cell(n, 2) =
format(gHighScoreList(i).score, "#,###")
highScoreDialog.scoreList.cell(n, 3) =
gHighScoreList(i).theDate.shortDate
highScoreDialog.scoreList.cellBold(n, 2) = true
next // i
highScoreDialog.show
This mess just fills the listBox in HighScoreDialog with the current high score info.
We go through a loop of ten scores. We first add a new row, putting in the high score
number (which goes into the first column by default). Then we put in the actual score
info from our data structure array. Each element in the structure is put into a separate
column: name, score, and date. Score is formatted with a comma in the thousands'
position (if necessary). Then we bold the actual score value with the cellBold method.
The final line displays the score window, if it's not already visible.
Now you might be wondering why we put this in a method instead of putting this inside
HighScoreDialog's Open event. The reason is simple: the scores change. If the player has
the score list always visible and they generate a new high score, the high score list must
update immediately. So we need a routine like this to refresh the score listing. That's also
why the first line of this method is highScoreDialog.scoreList.deleteAllRows -- if
we didn't have that and the scores were already visible, you'd just get more and more
scores displayed (added to the existing list) instead of erasing the current list (if there)
and starting fresh.
Whew! We've done a lot so far in the last two lessons, but believe it or not, we're not
finished!
If you run the program now, it will display the high score list, you can view/hide the high
score window, you can move it and it will remember its size and location, even between
program launches. However, we never added in any code to save a high score to the high
score list, so if you finish a game, it won't be added to the list!
Saving a High Score
Again, we've got a situation that seems simple, but is more complicated than meets the
eye. What happens when a user generates a high score? First, we've got to compare the
current score to all the others. If this is higher than any of those, we've got a new high
score. If that happens, we need to ask the user for his or her name. Yes, that means we
must add another dialog box!
Adding a dialog means we need a way to pass info back and forth between it. So go to
globalsModule and add a new property (Edit menu, "New Property"): gDialogReturn
as string.
Now, I don't know about you, but I hate it when games continually ask me for the same
name over and over -- I like it when they remember who I am. So let's also add a
gLastName as string property. We can use that to remember the player's default name.
With that done, let's write the routine that will add a new high score to the list. For now,
we'll just pretend the askNameDialog has already been created.
Go to the endOfGame method we added last time. Put this code in it:
dim i, n as integer
dim hs as highScoreClass
//
// This means the game is over, so we see if the player has
// a high score.
//
n = 0
for i = 1 to 10
if gScore >= gHighScoreList(i).score then
n = i
exit
end if
next // i
// do we have a new high score?
if n > 0 then
hs = new highScoreClass
gDialogReturn = gLastName
askNameDialog.ShowModal
hs.name = gDialogReturn
hs.score = gScore
hs.theDate = new date // today's date
gHighScoreList.insert i, hs
gHighScoreList.remove 11 // delete last element
gLastName = gDialogReturn
showScores
end if
The first part is simple: we step through each score in the high score list and compare it to
the current game's score. If the current score is higher, we've got a new high score. Since
n represents the array element number of the beaten score, if n is greater than zero, we've
got a new high score.
Once we've gotten a new high score, we create a new variable of type highScoreClass.
We pass the old name (gLastName) to the askNameDialog, and what we get back we
store into our variable. Then we insert that into the gHighScoreList array and remove
the last element.
Finally, we set gLastName to whatever name was returned in gDialogReturn, and we
display the high score window.
Adding an Ask Name Dialog
For the last routine to work, we must create a new window (File menu, "New Window").
Name it askNameDialog and give it the following settings:
The dialog box itself should look like this:
The gray box in the upper left corner is a 32-by-32 pixel canvas object (called canvas1).
The editField is called nameEdit, and the pushButton is named okButton.
The code for askNameDialog is simple. Within nameEdit's Open event, put this me.text
= gLastName. For Canvas1's Paint event, put g.drawNoteIcon 0, 0 -- that draws the
standard "note" icon.
For okButton, this goes in the Action event:
gDialogReturn = nameEdit.text
if gDialogReturn = "" then
gDialogReturn = kDefaultName
end if
self.close
This simply saves the contents of the editField. If it's empty, it puts in the default name
(using the kDefaultName constant we created earlier). Then it closes the dialog box.
Now that we've added another item to remember, let's save it via our preference routine.
In prefModule, open savePrefs and add this line in the middle (the exact positioning is
irrelevant, as long as it's before t.close):
// Save last name entered
t.writeline "#lastname " + gLastName
And within loadPrefs, add the following case:
case "lastname"
if gDirectives(i).text <> "" then
gLastName = gDirectives(i).text
else
gLastName = kDefaultName
end if
This just saves and restores the gLastName variable so we can remember the player's
name even between game launches -- how thoughtful is that!
Well, that's enough for this week. We've finished adding the high score system to our
program: it wasn't difficult in terms of understanding, but it was a lot of work scattered
throughout the game. Unfortunately, some features are like that: there's no way to avoid
complexity.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
Actually, next week's column won't be published next week -- I'm going to take a couple
of weeks off and enjoy the holidays. I suggest you do the same. Relax and spend time
with your families. After the break, we'll add a helpful instructions window to our game,
and then in the column after that we'll tackle the biggie: adding an undo system to RBU
Pyramid. See you next year!
Letters
Our first letter is from Dustin, who writes:
I've been using realbasic for about 3 days now and find it a pretty kewl language. I have
quite a bit of VB experience but I'm encountering an error that I have never seen before.
When I try to run debug I get an unexpected token error. See for yourself. I'm just trying
to create a simple program based on loose statistics that plays a whole bunch of hands of
cards at given probabilities.
An "unexpected token" error, Dustin, means that REALbasic encountered a character it
didn't expect while attempting to decipher your code. This is usually just a simple typo,
like when you insert an incorrect character in the middle of some normal code.
For instance, the msgBox command is looking for whatever follows to be a string (either
quoted text or a variable name). If you follow it with a strange character like an equals
sign, REALbasic will generate an "unexpected token" error:
// These all generate "unexpected token" errors
msgBox =
msgBox \
msgBox ]
In your case, you've got the code:
if WinOrLoss > then bank=bank+Wager
That's the problem: REALbasic is expecting an end-of-line character after then. Since
you've added bank=bank+Wager, that confuses the parser. The solution, of course, is to
move that code to its own line:
if WinOrLoss > then
bank=bank+Wager
end if
Unlike some BASICs, which might allow you to combine the two statements on one line,
REALbasic is particular about its structure: an if-then statement cannot have anything
more than "if condition then" on a single line. I hope that helps you get going!
Next, we've got a letter from Paul, who's got a slew of questions!
Hi Marc,
First, thanks for your tutorials. They're very helpful. I have both Real Basic books and
agree with you, one's too simple and the other expects a level of knowledge that I just
don't have yet. I've got a few questions. I'm trying to create an app to go on a CD-ROM
for the windows platform and have run into my inability to do certain things. The project
is for a family history and has lots of text and scanned photos that I need to display in
windows (read only, the user can only look at them and select another window to view).
[1] What's the best way to display formatted text given to me in MS Word format? The
CD has to run as an app that you put the CD in your drive, double-click and go. Should I
put the files in a folder on the CD and use getFolderItem? Or static text?(seems limited)
Or edit text? What is the native file format of text for Real Basic?
[2] One of the graphics is the family tree chart. It's way wide(like 400 by 6000) with little
boxes on it with people's names. Is there a way to put it in a scrolling window and put
buttons on top of the boxes? Or invisible buttons above the boxes to go to that person's
bio page/window?
If I put it in as a graphic on a canvas I can't see it to line up the buttons. If I put it in as a
backdrop it doesn't seem to scroll. Do I need to recreate this graphic inside a window?
There is about 200 of these boxes with lines connecting them, so it's real problem if I
need to do it that way.
There's also an index of the names on the chart that I'd like to be able to have the user be
able to double-click on to go that name's bio page/window. However it's not a one to one.
Some names (wife, child) would all go to one bio (the Husband,father's bio).
[3] List box(how do I tell it to open the window?)? or static text with buttons? Again
there's about 200 of these.
[4] Now, I'm wondering if I've bitten off more than I can chew and wondered if you knew
of someone in the NYC area who consults that I could hire to help me do this? Or
perhaps someone willing to do phone or email help with the code?
Thanks again,
Paul Goodrich
To make sure I didn't miss answering any of these, Paul, I've numbered your questions
and I'll respond to them by number. Hopefully this will help: if I've misunderstood or
haven't explained something well, please let me know.
Answer 1: Word is a proprietary format owned by Microsoft. They don't go telling the
world how the format works. You could reverse engineer it, but that's a lot of work. A
better way is to convert Word files to RTF (Rich Text Format) which is designed for
portability. But you still have to get RTF into a format REALbasic can deal with.
You asked what REALbasic's built-in file format for text is, and the answer is styled text.
Styled text is a basic standard for text with certain attributes, such as fonts, various text
sizes, bold, italic, etc. It's used by the Mac OS as a way to let you copy and paste
formatted text between applications.
I have created a free program, called RTF-to-Styled Text, which converts RTF
documents to styled text files REALbasic can open (using a folderItem's
openStyledEditField method). Once your files are in styled text format, you should be
able to put those into an editField with all the styling intact (at least the styling
REALbasic supports).
Answer 2: You are correct that a backdrop graphic does not scroll. If you want to scroll
the graphic, you must draw it manually yourself within a canvas. You'll need to work
with separate scrollBar controls and adjust the drawing of the picture accordingly (i.e. if
the user scrolls, you've got to redraw the picture with the new position). For a good
example of how to do this, look at RBU 021.
As for the controls on top of the picture, you can do this as well, though it gets more
complicated. You'd have to use a canvas' scroll method, being sure to pass true for the
scrollControls parameter.
The bigger problem I would foresee would be that with so many of these controls, it
would complicated to put controls in all those locations. You'd have hundreds of them to
keep track of, not a fun job.
A better way would be to use the mouseDown event of the canvas and use math to figure
out which part of the picture the user is clicking on. You'd have to take into consideration
that part of the image may be cut off (scrolled).
For example, in the above diagram, the distance for xDif and yDif would have to be
added to the x and y coordinates where the user clicked to give you the true coordinates
of which part of the picture was being clicked. So if the user clicked at position 10, 5 and
the picture was scrolled 5 left and 5 down, the actual picture coordinates clicked on
would be 15, 10.
Of course for this to work, you'd have to know the coordinates of all the "clickable"
places on your picture. If this is a fixed picture and not likely to change, you could
manually input in all the coordinates (in rectangles). If you stored them in an array
structure, each time the user clicked, you could look through the array to see if the user
clicked inside a valid "hot spot" and the act accordingly.
The last thing you talk about, having various points bring up the same reference data,
makes me think you'd be best off creating some kind of database system. Within the
database you could allow links between records. For each "hot spot" you'd need to store
(invisibly) information (such as an id number) that would uniquely describe the record
you are linking to. Obviously this would be more difficult to set up than a single-minded
system designed for your one graphic, but it would be more flexible in case the graphic
changes and/or you want to expand the program beyond the original purpose. Either way,
you are talking about a complicated project -- you might want to start with a simpler
program.
Answer 3: How to tell a listBox to open a window? Well, with any object, you can
include code in the format of windowname.show to display the window named
windowname. Where you put this code depends on how you want your program to work.
For instance, putting it within a canvas' mouseDown event will open the window when the
user clicks there. Putting it within a listBox's doubleClick event would do it when then
user double-clicks on a line, etc.
Answer 4: REAL Software maintains an excellent list of REALbasic consultants on their
website. I'm sure one of them would be perfect for your project.
.
REALbasic University: Column 041
RBU Pyramid XIV
Welcome back, everyone! I trust everyone had a restful holiday and are geared up for an
exciting new year. REALbasic University is back in session, and we're continuing with
our RBU Pyramid project.
Adding a Help System
We're nearing the end and almost finished with our game. But a good game needs
instructions: what if the user doesn't know the rules of Pyramid? So today we'll add an
instruction window.
For RBU Pyramid I wanted a simple help system. Nothing fancy, but competent, and
above all, I wanted it easy to create. I could have used the standardize help system I use
for my software programs, except that it's complex (it translates an HTML help file to
styled text) and it didn't work under Windows (I've since completely rewritten that help
system).
The traditional help system I use is a two-paned window, similar to my Z-Write word
processor. On the left is a listBox of topics, and on the right is the help text. When you
click on a topic, it's displayed on the right. Elegant, but difficult to program, especially
when you add in features like the ability to adjust the width of the listBox.
But I did like the idea of multiple topics: a user doesn't like having to wade through a 10page help file just to find out how to play the game. So one of the first thing I decided
was instead of using a listBox-and-EditField combo, I'd use a popupmenu instead. The
popupmenu would contain the topics and they'd be displayed in an editField underneath
it. I quickly created a window that looked like this:
You should be able to recreate this easily: that's a staticText in the upper left, a
popupMenu to its right, and beneath them is an editField. That's it!
The window has the following properties (settings):
The editField should have these properties:
The text property of the editField should be filled with the actual help text. I've already
created this, so you can just put in this text file.
For a help system like this, you have several choices with how to get the text into the
program. The simplest is just to include the text in the program itself (by putting the text
into an editField), which is exactly what we've done. Other choices include putting the
text into the resource fork of the program (which wouldn't work for a Windows version
of your program) or in an external file (which could be a program if that external file
can't be located, i.e. the user moved or deleted it). In our case, the simplest solution is
also the best for our needs.
Our next problem is that we must divide our help text into separate topics. Once again, I
went for the simplest solution: I decided that each section of text would begin with a
headline in a particular format. I chose the following format:
*** topic goes here ***
That's three asterisks followed by a space, the topic name, another space, and three more
asterisks. By searching the help text for text in that format, I can break the text into
various sections and extract the topic names. (In computer programming lingo, this is
called parsing.) As long as that particular heading text is unique (there aren't a series of
asterisks in the body of the text), we're fine.
The traditional method I'd use for this kind of system would be to set up an array data
structure, with the text for each topic as separate string elements in the array. That,
however, is more complicated than we need: why not just have program search the help
text for the topic name and then scroll to it? That way there's only one piece of text and
the topic menu just lets a user easily jump to an appropriate subject.
Coding Popupmenu1
Let's try it! Open the code editor for InstructionsWindow (option-tab while the
window is selected in your project list or double-click popupMenu1) and find the code for
popupMenu1. Go to the Open event and put in this code:
dim s as string
dim i, n as integer
n = 0
s = editField1.text
i = inStr(s, "*** ")
while i > 0
n = inStr(i, s, chr(13)) // rest of line
me.addRow mid(s, i, n - i)
i = inStr(i + 3, s, "*** ")
wend
// select first topic
me.listIndex = 0
What does this do? Well, in simple terms, it scans the help text for help topics and adds
them to popupMenu1's menu. There's not much code here, so how does it accomplish this
magic? Let's analyze it.
We start out by initializing some variables: n is set to zero, s becomes our help text, and i
is an integer representing the first occurrence of "*** " in the text (note the space after the
three asterisks).
Then we set up a while loop: while i is greater than zero we keep repeating the loop.
Since i is set by the inStr function, which returns the character position of a search-for
item within some text, it is set to zero when the search fails. So our while loop will
therefore continue until there are no more occurrence of "*** " in the text!
Next, we find the end of the line of the topic. We do that by searching for a return
character (chr(13)) within the help text, but starting the search at position i. We save
this information into the variable n.
Then we use the mid function to grab the entire topic line. Since n is the end of the line
and i is the start, the length (in characters) of the line is n - i. We pass that to mid,
starting the selection at i, our start point, and grabbing n - 1 characters. The result is the
topic: we pass that to popupMenu1's addRow method, which simply adds a menu item
with our topic as the text.
Finally, we reset i with a new search: it's still looking for "*** " but we start the search at
i + 3. Note that that's past were we last stopped. If we started right at i, the search
would immediately be successful as it would find the same topic again! (Our while loop
would also never end -- the program would be stuck forever in an endless loop.)
Finally, we set popupmenu1 so the first menu item is selected.
Since all this happens when the window is first opened, from the user's perspective, the
popupmenu simply has a list of help topics in it.
But what happens when the user selects a menu topic? That's pretty simple. Go to
popupMenu1's Change event and put in this:
dim i, j as integer
if me.listIndex > -1 then
i = inStr(editField1.text, me.text)
j = inStr(i, editField1.text, chr(13))
editField1.selStart = len(editField1.text)
editField1.selStart = i - 1
editField1.selLength = j - i
end if
The first thing we do is check to make sure that popupMenu1.listIndex does not equal 1. That's because -1 means that no menu item is selected. Any other value means the user
has chosen an item on the menu, so we then simply search the text for that item.
The variable i is set to starting position of the search-for text (me.text, which is the
topic name on the menu), and j is the end of that text's line. Then we just highlight
(select) that text by setting the selStart and selLength properties of editField1.
Selecting the text effectively scrolls the EditField.
In Detail
Note that before we select the topic headline, we first set editField1.selStart to
len(editField1.text): why do we do that?
Look at what that instruction does: it moves the cursor to the end of the text. The next
lines then move the cursor to topic headline. So why add add that extra move to the end?
The answer has to do with how REALbasic editField's scroll. When you move the cursor
to some text that's scrolled off-screen, REALbasic automatically scrolls the text so the
text at the cursor point visible. But that just means it can be anywhere within the
viewable area, not necessarily centered or at the top of the field. By first moving the
cursor to the end of the text, REALbasic has to scroll backward, stopping when it reaches
the cursor position. That effectively puts the topic right at the top of the editField
where it's easily seen!
Note that newer versions of REALbasic include a scrollposition property for
editFields so you can scroll without moving the cursor, but I wrote RBU Pyramid
before those features were available to me, so this is the old-fashioned work around. For
extra credit, try rewriting this to use the newer methods.
Remembering the Window Position
We've got our basic help window, but we're not quite done. As usual, we want an elegant
Macintosh program. That means a program that will do nice things like remember the
location and size of the help window. That's extra work for us, but that's the Macintosh
Way.
As you'll see, this is very similar to what we did for HighScoreDialog. First, let's add
two new global properties. Open globalsModule and add two properties (Edit menu,
"New Property"): gHelpOpen as boolean and gHelpPos as string. The first will tell
us if the help window is open or not, the second will contain details about the window's
size and position.
Like we did for HighScoreDialog, we need to prevent our window from saving window
size changes when we resize it programmatically (we only want it to save when the user
changes the window), so let's add a new boolean property to InstructionsWindow:
opening as boolean.
Now add the following to the window's Open event:
gHelpOpen = true
opening = true
if countFields(gHelpPos, comma) = 4 then
self.left = val(nthField(gHelpPos, comma, 1))
self.top = val(nthField(gHelpPos, comma, 2))
self.width = val(nthField(gHelpPos, comma, 3))
self.height = val(nthField(gHelpPos, comma, 4))
end if // countFields = 4
opening = false
Here we're just setting our global gHelpOpen property to true, meaning that the help
window is open. We set opening to true so we can adjust the window position
ourselves, and then we change the window's size and position based on the info in
gHelpPos. (The if countFields line is just an error check to make sure that gHelpPos
contains good data.)
Next, go to the Close event and put in this:
gHelpOpen = false
saveWindowInfo
The first line simply says the window is closed, and the second calls a saveWindowInfo
method (which we'll write in a moment).
Now go to both the Resized and Moved events and put in this code in each:
if not opening then
saveWindowInfo
end if
This just says if we're not in the process of opening, save the new window size and
position information.
Now let's write that saveWindowInfo method. Add a new method (Edit menu, "New
Method") and name it saveWindowInfo. Here's the code for it:
gHelpPos
gHelpPos
gHelpPos
gHelpPos
=
=
=
=
str(self.left) + comma
gHelpPos + str(self.top) + comma
gHelpPos + str(self.width) + comma
gHelpPos + str(self.height)
As you can see, this just saves the current window details into the string, separated by
commas.
Now that we've got this info saved, we need to remember for the next launch of the
program. That means adding the info to our preference file. That's easy, of course. Let's
open PrefModule and find the loadPrefs routine. Insert in this case statement (where
doesn't matter, as long as it's before the end select line):
case "helpwindow"
gHelpPos = gDirectives(i).text
This will simply load gHelpPos with whatever help window info has been saved. The
reverse, saving gHelpPos, is just as easy. Open savePrefs and put in this line before
t.close:
// Save Instructions Window position/size
t.writeline "#helpwindow " + gHelpPos
Excellent! Note that unlike the score window, we don't bother saving the open/closed
state of the help window.
We're almost finished: we just need a way to make the help window appear.
Adding An Instructions Menu
Open the menu item within your project window. Go to the Options menu and click on
the blank item at the bottom of the menu list. Type a hyphen ("-") and press Return. That
inserts a separator line. Do that again, but type in "Instructions..." and press Return.
While selecting the Instructions menu item, go to the Properties Palette and find the
CommandKey property and type in an I. Your menu should look like this:
Now open up the App class (within your project window): we need to do something with
that menu command.
Within the Events section, go to EnableMenuItems and insert this code (it doesn't matter
where):
if gHelpOpen then
OptionsInstructions.text = "Hide Instructions"
else
OptionsInstructions.text = "Show Instructions"
end if
OptionsInstructions.enabled = true
This will enable (ungray) the Instructions menu command. We change the text of the
menu item depending on whether the help window is open or closed.
But what about the command itself? Easy: go to the Edit menu and choose "New Menu
Handler" and in the dialog that pops up, select "OptionsInstructions" from the popup
menu and click okay. That should insert an OptionsInstructions handler within the
Menu Handlers section. Go there and put in this code:
if gHelpOpen then
instructionsWindow.close
else
instructionsWindow.show
end if
Simple: if the help window is open, it is closed, and vice versa.
Guess what? We're done! At least for this week. RBU Pyramid is starting to look very
polished, but there's still a few little things to go. We'll finish them up in a few more
lessons. For now, give the program and whirl and check out the new help window!
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We tackle the big bear: adding an undo system after the fact.
REALbasic News
REAL Software announced this week the release of REALbasic 4.0. This new version
adds some new features including significantly revamped ListBoxes and EditFields and
an improved IDE. You can find about all about the latest release at realbasic.com. I'll
have a column about these new features in a few weeks.
REALbasic is available for US$149.95 for the Standard Edition and US$349.95 for the
Professional Edition packages, direct from REAL Software. REAL Software offers
academic and volume discounts as well as license-only options. Upgrades to REALbasic
4 range in price from $29.95 to $89.95 for owners of REALbasic 3.5.
Letters
Our letter this week is from Remek, who has a -- gasp -- math question!
hi
i am new to REAlbasic programming but i have learned enough about it as to solve the
mathematical problem that i had
i won't bore you with the reasons for this but this is the problem
i'm try into figure out the product of all the numbers from 1 to 1000
this is a relativly simple task by using
i=1
for x = 1 to 1000
x= x * i
next
this i know
however the problem that i have encountered is that if i go higher then 32, as in for x =
1 to 32, i get an answer of 0
could you help?
Okay, my palms are breaking out in a sweat here. Just the word "math" terrifies me and
gives me nightmare flashbacks of high school algebra, but I'll do my best to figure this
out and explain it in a way that even I can understand.
First, your code above seems to have some troubles. If I'm understanding what you're
wanting -- the product of all numbers between 1 and 1000 -- that's a problem that can be
represented like this (with the asterix standing for multiply):
result = 1 * 2 * 3 * 4 * 5 * 6...
For that to work, you don't want to use x for both the for-next counter and as the result
of your calculation as the calculation will change the value of x. You want something
more like this:
dim i as integer
dim x as integer
listBox1.deleteAllRows
x = 1
for i = 1 to 100
x = x * i
listBox1.addRow str(i)
listBox1.cell(listBox1.lastIndex, 1) = format(x, "###,###")
next // x
Note that I've taken the liberty of rewriting your routine so it displays the results of each
pass into a listBox. When you run this, you get this:
As you can see, the calculation does start to go awry after the 33rd iteration. Why is that?
To answer that, we need to learn a little about how computers handle numbers. You've
probably heard of the binary system: ones and zeroes. Computers are always binary:
everything is either a one or a zero. So how does a computer work with an 8 if there is no
eight?
Simple: it uses several ones and zeroes to represent an 8. So the number 8 in binary is
"1000". Think about it, with a single digit, the highest binary possible is 1 (zero or one
are the only two combinations). But with two digits (or bits, since bit means binary digit),
you've got four combinations (00, 01, 10, and 11). That's the numbers 0-3!
If you keep expanding this concept, more digits equals bigger numbers. Three digits eight
combinations, four sixteen, five thirty-two. Wait a second, there's a pattern developing...
Digits Combinations
1
2
2
4
3
8
4
16
5
32
6
64
7
128
8
256
See how it works? The number of combinations is the number two raised to the power of
the number of digits. So two to the power of 4 is sixteen, two to the power of eight is 256,
and two to the power of 16 is 65536, and so on!
Now there's a very simple relationship between the number of digits required to represent
a number in binary and the maximum value that can be expressed by those digits.
Remember how one bit (1 or 0) can only represent two values? Since zero is one of those
values, the maximum number than can be expressed with a one bit number is one. Thus
the max number that can be expressed is always one less than the number of
combinations!
So with three binary digits (bits), there are eight combinations and the maximum number
that can be expressed is 7 (8 minus 1). For eight bits, the max number is 255. For sixteen,
it's 65,535.
What this means is that numbers, on a computer, are not infinite: there's always a
maximum value that can be stored!
You've heard terms like 8-bit, 16-bit, 32-bit, etc.? Well, that just represents the largest
number that particular computer can work with!
Since Macs are 32-bit computers, and REALbasic's number datatype is a 32-bit number,
that's all we've got to work with. So how big a number can a 32-bit number represent?
Let's see... two to the 32nd power is 4,294,967,296, so the biggest number is
4,294,967,295!
However, there's another issue. Since numbers can also be negative, REALbasic limits
integers to a range of plus/minus 2,147,483,648. That means the smallest number you can
use is -2,147,483,648 and the largest is +2,147,483,648.
Interestingly, 2,147,483,648 just happens to be exactly half of 4,294,967,296. Do you
think that's a coincidence?
Now take a quick look back at the screenshot of our program. See how it ran out of steam
(numbers) around the 33 point? See how that number is very near our maximum number?
Yup, once the number got to big, it became a zero!
But what if you want to use bigger numbers? Is there nothing you can do?
Well, REALbasic also has the double data type. I know we're used to using them to
represent real numbers (also known as fractions or numbers with a decimal point). But
doubles are 64-bit numbers. That's quite a bit bigger. According to REALbasic's
documentation, a double can be any value between 2.2250738585072012 e-308 and
1.7976931348623157 e+308. Whew! Those are some big numbers!
So if we make the simple change of making x a double instead of an integer:
dim i as integer
dim x as double
listBox1.deleteAllRows
x = 1
for i = 1 to 100
x = x * i
listBox1.addRow str(i)
listBox1.cell(listBox1.lastIndex, 1) = format(x, "###,###")
next // x
And we run the program again, we get the following:
Wow, that's much bigger!
However, you can see we still run out of numbers before we even get to sixty iterations!
Our little math problem just generates too huge numbers!
Oh well. That's the best we can do, so we'll have to live with that. (There are some clever
ways around that, but they're beyond the scope of my extremely limited mathematical
skills.)
I hope this little lesson on numbers has helped people: it certainly took me a few years to
understand it all! I remember in high school, when I started programming, having to
actually go to ask a teacher why a game I was programming wouldn't store the large
sums of money I required. It turned out that since my BASIC used 16-bit numbers,
65,535 was the largest value I could use. I didn't really understand the teacher's
explanation until years later. But since my game dealt with dollar values in the millions
(it was a financial strategy game like Monopoly but with more money), my workaround
was to simply make $1,000 the smallest amount of money and add three zeros to
whatever number was stored (so 65,535 was really $65.5 million). That worked!
BTW, if you'd like the source for the program above, you can get it here.
.
REALbasic University: Column 042
RBU Pyramid XV: Undo Part 1
Admittedly, last week's lesson wasn't too challenging. But this week we've got a
mountain to climb. We're going to add an undo system to RBU Pyramid.
To be honest, we're going about this the wrong way. An undo system is so fundamental
to the operation of a program that it should be one of the first things you design. When
you're planning your program, especially the data structure, think about how you'll
implement undo. This is even more critical when you want to support multiple undos,
undos of various kinds of actions, and have a redo system as well.
It's certainly not impossible to add an undo system after the fact, but it's not easy. You'll
find tracking down bugs to be a horrible pain. In fact, I've known there are bugs in RBU
Pyramid's undo system for months -- and I've been putting off fixing them!
But for our lesson today we're going to be creating the buggy version -- extra points to
whomever catches the bugs. We'll fix the problems in a future column, but meanwhile
you'll get chance to see how difficult this is to add after the fact, as well as how
complicated it is to debug.
Undo Basics
As always, we start by planning what our undo system is going to do. Is it going to be a
single undo/redo or multiple undo/redo?
The nature of the Pyramid card game is such that a player follows various paths by
making irreversible choices: Do you match the 8 and 5 in the pyramid or the 8 in the
pyramid and the 5 on the permanent discard pile? Because of those multiple paths, it can
sometimes be valuable to back up several moves and try a different trail.
Also, since I was writing RBU Pyramid specifically with the idea of using it in a tutorial,
I felt a more complex, full featured undo system would be most valuable for RBU
readers. Thus I made the decision that I wanted to support multiple undos. In fact,
unlimited undos. The player should be able to undo all the way back to the beginning of
the game!
However, since a pyramid can be cleared (won) and redealt as many times as the player
keeps winning, I also decided that the undo system would be reset with each card shuffle.
There's no point in a player going back to a game they won. That does make our job a
little easier, but it's still going to be rough.
Let's look at what happens in a typical Pyramid card game.
The user clicks on two visible, uncovered cards, attempting to find a match. If the values
of the cards add up to 13, the cards are removed. Kings are removed with a single click.
There are two discard piles: a temporary and a permanent. When the user clicks on the
Deck, a fresh card is placed on the temp discard. If there's already a card there, it is
moved to the permanent discard. Cards already in place on the permanent discard pile
just stack up on top of each other (face up). Worst of all, when the Deck runs out of cards
the first time, the permanent discard cards are flipped over and become the new Deck!
So already we can see several challenges. To support undo, we must be able to keep track
of what the user has done. If a pair of cards have been removed, they must be put back.
We must also watch what happens with the Deck and two discard piles, reversing any
changes therein. And -- and this is the killer one -- we must be able to put the Deck back
even after the user redistributed the permanent discard pile!
In principle, an undo system is simple: we just keep track of what the user has done, step
by step, and reverse it step by step, if the user requests it. Simple, right?
In practice, however, it can be much tricker. For example, let's say the user deletes
something. If your program actually deletes the data, it's gone. There is no undo. You
can't regenerate that data from nothing. So the only way undo works is because the
program doesn't actually delete it: it just pretends to do so while keeping a backup copy
around in case the user changes their mind.
You can probably see how unlimited undo eats up memory. Just a few years ago, this
meant that few programs offered more than one undo or maybe a handful, such as 10.
Today, most computers have hundreds of megabytes of RAM and huge hard drives, so
with simpler programs, multiple undos are feasible (though still complicated). Even
Adobe Photoshop supports multiple undos via its History palette (though telling
Photoshop to remember more than a few undos can dramatically slow down the program,
especially if you're editing 100 megabyte photos).
In the case of RBU Pyramid, program resources are not a significant problem: with a meg
of memory we could probably remember thousands of user actions. But it's something to
remember if your program creates a lot of data.
Creating an Undo Data Structure
The first thing we start with when designing our undo system, of course, is with a data
structure. We've got to have some way to remember everything the user does. In our case,
any action by the user could result in multiple things happening: the Deck could advance
or reset, there could be movement between the two discards, and cards could be removed.
That's a lot of action to remember!
So the first thing I did was to create a custom undo class, containing properties telling me
the state of things when the user does something. Within your project, create a new class
(File menu, "New Class"). Name it undoClass and add the following properties to it
(Edit menu, "New Property"):
As you can see, we've got a number of flags (flags are usually booleans that you use to
tell you if a state exists or a condition has changed) and several properties which will tell
you which cards were on the discard piles or selected by the user.
That gives us a structure for our undo data, but we still have no place to put it. Let's
create a global array for the undo data. Open globalsModule and add this property:
gUndoList(0) as undoClass.
Now since we want the undo data to be erased whenever a new shuffle of cards is dealt,
let's make sure it gets deleted whenever there's a new shuffle. Go to the freshDeck
method (within globalsModule) and scroll to the bottom. Somewhere after the // Erase
the discard line, put in this:
// Erase undos
redim gUndoList(0)
That should finish our work with globalsModule for now. Close it and let's get into
gameWindow's code editor (Option-tab while the item is selected).
Adding Undo Routines
The first thing we'll do is add a few methods (Edit menu, "New Method"). Name the first
one doUndo, the second eraseUndo, and the third saveUndo. For all of them, leave the
"parameters" line blank.
Now let's add property to gameWindow. Go to the Edit menu, choose "New Property" and
type in theUndo as undoClass and click okay.
Now let's write the saveUndo method. This is what you'll call whenever you want the
current state of the program saved (i.e. right after the user has done something undoable).
Here's the code for saveUndo:
dim n as integer
// This routine saves the last action into the undo array
// Expand the undo array by one
n = uBound(gUndoList) + 1
redim gUndoList(n)
gUndoList(n) = new undoClass
// Copy the passed value to the array element
gUndoList(n).deckReset = theUndo.deckReset
gUndoList(n).deckAdvance = theUndo.deckAdvance
gUndoList(n).mainDiscard = theUndo.mainDiscard
gUndoList(n).pyramidCard1 = theUndo.pyramidCard1
gUndoList(n).pyramidCard2 = theUndo.pyramidCard2
gUndoList(n).tempDiscard = theUndo.tempDiscard
enableMenuItems // make sure edit menu is active
The first thing we do is enlarge our global undo array by one element: we need to make
room for the new data. We do this by finding the current size of the array with the
uBound function, add one that value, and redim the array with the new size.
Next, we initialize that new array element with a new instance of the undoClass object.
(Remember, classes are objects, and thus must be instantiated before they can be used.)
Once we've done that, the rest of the code is simple: we just assign the values in theUndo
to the matching values in the array element.
In Detail
Note that we cannot just simply put gUndoList(n) = theUndo. While RB won't
complain about that code, it doesn't actually transfer the values of the individual fields.
Don't ask me why -- it sort of makes an alias of theUndo in gUndoList(n) and changes
to either affect both. It creates a very messy situation with unpredictable results. Trust me
-- it's not something you want to debug!
The eraseUndo method is very simple: it just resets everything in the undo object to their
default values. We need that, because as you'll see, most routines that call saveUndo only
modify the field or fields of theUndo that need to be changed. If we didn't erase theUndo
first, those fields would contain old, incorrect data that would be saved in the undo array.
Here's eraseUndo:
theUndo.deckReset = false
theUndo.deckAdvance = false
theUndo.mainDiscard = 0
theUndo.pyramidCard1 = 0
theUndo.pyramidCard2 = 0
theUndo.tempDiscard = 0
As I said, very simple. However, doUndo is not:
dim i, n as integer
// This routine undoes the last action saved in the undo array
// First, we try to figure out what kind of undo situation we have.
// There are multiple possibilities:
//
- A match was made and cards removed
//
- The deck was advanced
//
- The deck was reset
n = uBound(gUndoList)
// Is it a deck reset?
if gUndoList(n).deckReset or gUndoList(n).deckAdvance then
if gUndoList(n).deckReset then
// Flip deck back over
redim gTheDiscard(0)
for i = 1 to uBound(gTheDeck)
gTheDiscard.append gTheDeck(i)
next
// Erase the deck
redim gTheDeck(0)
// Was there a temp discard card?
if gUndoList(n).tempDiscard > 0 then
// There was, so put it back
gTempDiscard.card = gUndoList(n).tempDiscard
gTempDiscard.selected = false
gTempDiscard.clickable = true
gTempDiscard.refresh
end if // gUndoList(n).tempDiscard > 0
// Was there a main discard card?
if gUndoList(n).mainDiscard > 0 then
gDiscard.card = gUndoList(n).mainDiscard
gDiscard.clickable = true
else
gDiscard.card = 0
gDiscard.clickable = false
end if // gUndoList(n).mainDiscard > 0
gDiscard.selected = false
gDiscard.refresh
// Reset deck counter
if not gFirstTimeThrough then
gFirstTimeThrough = true
end if
else
// Undo advancing the deck
// Move temp discard back to deck
gTheDeck.append gTempDiscard.card
// Was there a card on the temp deck before?
// if so, move it
if gUndoList(n).tempDiscard > 0 then
gTempDiscard.card = gUndoList(n).tempDiscard
gTempDiscard.selected = false
gTempDiscard.clickable = true
gTempDiscard.refresh
else
gTempDiscard.card = 0
gTempDiscard.selected = false
gTempDiscard.clickable = false
gTempDiscard.refresh
end if // gUndoList(n).tempDiscard > 0
if uBound(gTheDiscard) > 0 then
// No card on temp discard, so move card on main discard to
temp discard
gTempDiscard.card = gTheDiscard(uBound(gTheDiscard))
// Remove it from the main discard pile
gTheDiscard.remove uBound(gTheDiscard)
// Set the top card to last item of gTheDiscard
if uBound(gTheDiscard) > 0 then
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.clickable = true
else
gDiscard.card = 0
gDiscard.clickable = false
end if
gDiscard.selected = false
gDiscard.refresh
end if // uBound(gTheDiscard) > 0
end if // gUndoList(n).deckReset
// Undoing deck action, so refresh the deck
gDeck.refresh
else
// not a deck action, therefore must be a match of some kind.
// Figure out which cards and undo them.
if gUndoList(n).pyramidCard1 > 0 then
// Reset the card
cardCanvas(gUndoList(n).pyramidCard1).visible = true
cardCanvas(gUndoList(n).pyramidCard1).selected = false
cardCanvas(gUndoList(n).pyramidCard1).refresh
gScore = gScore - 1
end if // gUndoList(n).pyramidCard1 > 0
if gUndoList(n).pyramidCard2 > 0 then
// Reset the card
cardCanvas(gUndoList(n).pyramidCard2).visible = true
cardCanvas(gUndoList(n).pyramidCard2).selected = false
cardCanvas(gUndoList(n).pyramidCard2).refresh
gScore = gScore - 1
end if // gUndoList(n).pyramidCard2 > 0
if gUndoList(n).tempDiscard > 0 then
gTempDiscard.card = gUndoList(n).tempDiscard
gTempDiscard.selected = false
gTempDiscard.clickable = true
gTempDiscard.refresh
gScore = gScore - 1
end if // gUndoList(n).tempDiscard > 0
if gUndoList(n).mainDiscard > 0 then
// Add undone card back to discard pile
gTheDiscard.append gUndoList(n).mainDiscard
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.selected = false
gDiscard.clickable = true
gDiscard.refresh
gScore = gScore - 1
end if // gUndoList(n).mainDiscard > 0
end if // gUndoList(n).deckReset or gUndoList(n).deckAdvance
// Remove this undo (it's been used)
gUndoList.remove n
enableMenuItems
updateCards
Woah, what the heck does all that do? Well, I'm not going to tell. I'll just let you figure it
out for yourself.
Just kidding! I wouldn't be that cruel. Figuring out someone else's code is bad enough,
and wading through undo code is the worst. The first thing to do is to take this gently,
step by step. Then it's not so overwhelming.
Note that the code is heavily commented: I didn't do that so much for the tutorial as I did
for my own sanity while writing this. Trust me: when you get into hairy stuff like this, it's
best to go slow and comment like mad!
Remember the purpose of this routine: this method is called when the user chooses the
undo command from the Edit menu. So our code here must figure out exactly what
actions we need to undo and do that.
The first thing we do is figure out the size of the undo array. That's because the last
element is the one containing the undo info we need. So, using that array element, we
look to see if there was a Deck advance or Deck reset.
The Deck reset is the most complicated. If that's the state, we have to do a bunch of stuff:
•
•
•
•
•
We erase the permanent discard pile and refill it with the cards in the Deck.
Then we erase the Deck.
We check to see if there was a temp discard card: if there was, we put it back.
We check to see if there was a main discard card (logically there should be, but
we check anyway) and if there was, make sure it's clickable.
Finally, we reset the Deck counter (that's the flag that tells us if the user has reset
the Deck yet: since the user just did that, but is undoing it, we must reset it back to
show that the user has not reset the Deck).
If it's not a Deck reset, then it's a Deck advance (the user drew a card from the Deck).
This is simpler, but still involved:
•
•
•
First, we move the card that's currently on the temp discard pile back into the
Deck.
Then we check to see if there used to be a card on the temp discard before the
Deck advance: if so, we put it back from the main discard.
Then we make sure if there's a card on the main discard, that it's clickable.
After either of these actions, we must refresh the Deck (gDeck) so it is redrawn with the
appropriate appearance.
Finally, if neither of the above conditions were true (Deck reset or Deck advance), the
user must have made a match. The only thing tricky about this is figuring out what kind
of a match: two pyramid cards, two discard cards, or a combination of pyramid and one
of the discard piles?
That sounds intimidating, but it's not that bad. If there's a value greater than zero within
gUndoList(n).pyramidCard1 or gUndoList(n).pyramidCard2, we know those are
pyramid cards. All we do is restore those cards to their visible, clickable state.
(Remember, cards involved in matches in the pyramid are never actually deleted, just
hidden!)
If there's a value within either of the discards, it's slightly more complicated as we have to
restore the original value and put a card back onto the stack of the main discard pile
(from a programming perspective, we add a card element to the gTheDiscard array).
Note that for any of these occurrences -- a card match -- we decrement the player's score.
We wouldn't want them racking up a super high score by cheating!
Finally, after all this stuff has been done, we remove the current undo object from the
undo array: it's been used so we don't need it any more. Then we call enableMenuItems
to make sure our menus are up-to-date and updateCards to make sure all the cards are in
the correct state (selectable, etc.).
That's it! We're done!
Just kidding, unfortunately. We've only scratched the surface. All we've done is write the
code to support the undo data structure -- we have never actually called any of these
routines anywhere in our program yet, so running RBU Pyramid right now would do
nothing differently.
However, this is all we've got time for this week, so we'll finish the rest of the undo
system next week.
Meanwhile, if you would like the complete REALbasic project file for this week's tutorial
(including resources), you may download it here.
Next Week
In Undo Part 2, we'll have less theory and get that darn undo system working!
REALbasic University Quick Tip
It's easy to forget that the REALbasic IDE supports contextual menus: control-clicking
within the Code Editor will bring up a quick list of areas of your program that have code,
such as events, methods, and properties. Selecting one of these will jump you to that
element.
Letters
This week a couple readers respond with suggestions for last week's letter regarding large
numbers in REALbasic.
George Bate writes:
Dear Marc
Remek can easily calculate 1000! by using the wonderful free MPCalc plugin written by
Bob Delaney. It emulates an HP RPN Calculator and can be downloaded from:
http://homepage.mac.com/delaneyrm/MPCalcPlugin.html
The answer to 16 decimal digits (ie approx.) is:
0.4023872600770938 x 10 to the power 2568
You can, if you wish and have the time, perform the calculation to a precision of up to
30,000 decimal digits (really). The range of numbers that can be handled is
10 to the power -167100000 to 10 to the power +167100000
10,000,000! is 0.1202423400515903 x 10 to the power 65657060 (check if you don't
believe it)
Sadly, 100,000,000! is too large to calculate by this method.
As a matter of interest I have used this plugin to implement the original HP Calculator in
REALbasic.
That's great, George! Thanks for the link... though RPN (Reverse Polish Notation for
those of us without math degrees) is something I've never mastered. I'm glad Bob
includes a tutorial.
Next, John Johnson (you can tell his parents had a sense of humor ;-) chips in with:
I just recently read your response on large numbers in REALBasic. If your reader is a C
whiz, he might try grabbing a copy of Numerical Recipes in C and simply translating
over to REALBasic. A copy online can be found at
http://www.nr.com/nronline_switcher.html. They have a section that deals with arbitrary
precision arithmetic, including (if I remember correctly) arbitrary-size integer arithmetic.
Actually, the translation shouldn't be too hard. The only thing: finding the product of
every number from 1 to 1000 is a huge problem, and it will take an extraordinary amount
of memory to even hold the behemoth of a number, not to mention the large amount of
time required to perform the simple multiplications.
My advice: find a better problem to use the computing resources.
Incidentally, finding the number of digits of the product from 1 to 1000 is easy using
logarithms. Throw this code into the open event of a 3-column listbox:
dim i as integer
for i = 1 to 1000
me.addrow Cstr(i)
me.cell(me.LastIndex,1) = CStr(log(i)/log(10))
if i=1 then
me.cell(me.LastIndex,2) = "0"
else
me.cell(me.LastIndex,2) = CStr(CDbl(me.cell(me.LastIndex-1,2)) +
log(i)/log(10))
end if
next
Now, round up the number in the last column. That is the number of digits in the product
of all numbers from 1 to the number in the first column. At 1000, you get 2568 digits!
Hope this helps. Cheers!
Groan. I knew I shouldn't have opened to door to a math question! I failed algebra twice.
(Okay, technically I got a D the first time and dropped the class halfway through the
retake, but it's pretty much the same thing. Math and Marc just don't mix well.)
I guess if I knew what a logarithm was I'd be impressed, but I do note that your 2568
digits just happens to match the power amount George used in his letter... I guess that's
not a coincidence, but I'm too math ignorant to understand the significance. Oh well. I'm
sure this info will be helpful for someone out there.
REALbasic University: Column 043
RBU Pyramid XVI: Undo Part 2
Last we started on a complex part of RBU Pyramid: adding an undo system after the fact.
I mentioned this is much harder to do after the program's written than if you plan for it at
the design stage. If you didn't believe me before, you'll see why today. We're going to be
jumping around a lot between routines, so pay close attention.
Saving the Undo State
The main thing that needs to happen to get our undo system is working is we must save
the correct undo state every time the user does something. So that means we'll be adding
code into a dozen different places -- no getting around that.
Let's start at the top: we'll just go through our list of methods and make the appropriate
changes to each routine. Open gameWindow's Code Editor and reveal the list of methods.
The top one is called cardKing (it gets called when a King is removed). Click on it to
bring up the code on the right.
At the very top, before anything else, put in this line:
eraseUndo
Gee, that wasn't hard, right? Well, that's the easy one. Next, we need to go to the first ifthen-else statement. Just before the else, put in a couple blank lines and type in this:
theUndo.pyramidCard1 = index
saveUndo
Beyond the else, at the next if, we'll insert this after the // Temp discard pile
comment.
theUndo.tempDiscard = gTempDiscard.card
saveUndo
After the next else and the // It's on the discard pile! comment, put this:
theUndo.mainDiscard = gDiscard.card
saveUndo
Wow, that was fun, wasn't it? Inserting code in the correct places after the fact is yucky.
Just in case you made a mistake, here's the complete cardKing routine with the undo
code in place:
eraseUndo
// Is the King on a discard pile?
if index <> 30 and index <> 31 then
// not a on discard pile, so just remove it
cardCanvas(index).visible = false
cardCanvas(index).refresh
theUndo.pyramidCard1 = index
saveUndo
else
// It's on one of the discard piles
// Figure out which
if index = 30 then
// Temp discard pile
theUndo.tempDiscard = gTempDiscard.card
saveUndo
gTempDiscard.card = 0 // erase card
gTempDiscard.refresh
gTempDiscard.clickable = false
else
// It's on the discard pile!
theUndo.mainDiscard = gDiscard.card
saveUndo
// Delete top card
gTheDiscard.remove uBound(gTheDiscard)
if uBound(gTheDiscard) <> 0 then
// Still some left, display top card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.clickable = true
else
// None left, empty discard pile
gDiscard.card = 0
gDiscard.clickable = false
end if // uBound(gTheDiscard) <> 0
gDiscard.selected = false
gDiscard.refresh
end if // index = 30
end if
// Increase score
gSelection = -1
gScore = gScore + 1
updateCards
So what did we do with our additions? Basically, for each condition (depending on where
the King was located), the appropriate card information was saved into the theUndo
object and then we called saveUndo to add that undo happening to our undo array. The
result is that now the King being picked and removed has been saved.
Theoretically, the program now supports undoing a King removal. However, if you try to
run it, you'll find a couple problems. The first is a strange "nilObjectException" error
message when you click on a King. You'll notice the error takes you to the eraseUndo
method: that's where the error happened. But why?
Our error message tells use we're attempting to use an object that doesn't exist. But where
are we doing that in eraseUndo? It's such a simple routine there doesn't seem to be much
of anything there. The only object is theUndo and it...
Think back: remember when I said classes must always be instantiated as objects? We
have an object, theUndo, but we never instantiated it!
Quick, go to the gameWindow's Open event and put this in at very top:
theUndo = new undoClass
eraseUndo
Ah, that fixes it! No more error.
But then you realize something really dumb: the undo menu command is grayed out! Of
course that's easily fixable, but it does take a couple steps.
First, go to the enableMenuItems event and put in this code:
if uBound(gUndoList) > 0 then
editUndo.enabled = true
end if
That basically says, if there are undos available, make the undo menu command available
(enabled). This works because we have an array of undos -- if the number of elements in
that array are less than one, there are no undos saved in it.
Next, go to the Edit menu and choose "New Menu Handler." In the dialog box that comes
up, select EditUndo from the popup menu and click okay.
Then, within the EditUndo menu handler, type in doUndo.
Now if you run RBU Pyramid and you find a clickable King, you can remove it and then
undo it! Very cool, eh?
But of course that only works for just the King, and only for that move. We must add the
saveUndo code to every action the player can perform. Here we go!
Fortunately, the cardMatch method is simpler. Just make sure your code looks like this:
eraseUndo
// "Delete" the matched cards
cardCanvas(index).visible = false
cardCanvas(gSelection).visible = false
theUndo.pyramidCard1 = index
theUndo.pyramidCard2 = gSelection
saveUndo
// Increase the score
gScore = gScore + 2
gSelection = -1
updateCards
Since this is just made up of two cards from the pyramid, this is easy. We just save the
two cards into theUndo and call saveUndo to preserve it.
Then we've got the deckAdvance method. This one's a little more complicated. Rather
than detail every change (there are only three) I'll just give you the full routine with the
new code:
playSound("deck")
eraseUndo
// Is there an old selection?
if gSelection <> -1 then
// Deselect it!
cardCanvas(gSelection).selected = false
cardCanvas(gSelection).refresh
gSelection = -1
end if
// Are there are cards left?
if uBound(gTheDeck) >
theUndo.deckAdvance
theUndo.tempDiscard
theUndo.mainDiscard
saveUndo
0
=
=
=
then
true
gTempDiscard.card
gDiscard.card
gSelection = -1
// Is the temp discard empty?
if gTempDiscard.card > 0 then
// Move the temp discard card to main discard pile
gTheDiscard.append gTempDiscard.card
gTempDiscard.card = 0
end if
// Add one to temp discard
gTempDiscard.card = gTheDeck(uBound(gTheDeck))
// Remove last item from deck
gTheDeck.remove uBound(gTheDeck)
gTempDiscard.clickable = true
gTempDiscard.selected = false
gTempDiscard.refresh
// Redraw discard card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.selected = false
gDiscard.clickable = true
gDiscard.refresh
drawCard(29, true)
if uBound(gTheDeck) > 0 then
wait(6)
drawCard(29, false)
end if
else
// No cards left, reset the deck
theUndo.deckReset = true
if gTempDiscard.card > 0 then
theUndo.tempDiscard = gTempDiscard.card
end if
if gDiscard.card > 0 then
theUndo.mainDiscard = gDiscard.card
end if
saveUndo
// No cards left -- reset the deck if this is first time through
if gFirstTimeThrough then
resetDeck
gFirstTimeThrough = false
end if
end if // uBound(gTheDeck) > 0 (there are cards left in the deck)
updateCards
As you probably figured out, except for the initial eraseUndo command there are really
just two situations here: either the Deck advanced or the Deck was reset. We simply save
the appropriate settings into theUndo and save it.
Next we've got our discardMatch routine. Again, it's more complicated since there are
multiple possibilities of what happened (for instance, we have to figure out which discard
was involved). Here's the complete new code (with several additions):
// It's on one of the discard piles!
//
// Figure out which one is the discard and "delete" the other
// (We don't want to "delete" the discard canvases since they are
reused.)
eraseUndo
if index <> 30 and index <> 31 then
cardCanvas(index).visible = false
theUndo.pyramidCard1 = index
end if
if gSelection <> 30 and gSelection <> 31 then
cardCanvas(gSelection).visible = false
theUndo.pyramidCard1 = gSelection
end if
// if it's the temp Discard, we set its contents to zero
if index = 30 or gSelection = 30 then
theUndo.tempDiscard = gTempDiscard.card
gTempDiscard.card = 0 // erase card
gTempDiscard.selected = false
gTempDiscard.clickable = false
gTempDiscard.refresh
end if
if index = 31 or gSelection = 31 then
// It's the discard pile
theUndo.mainDiscard = gDiscard.card
// Delete top card of discard pile
gTheDiscard.remove uBound(gTheDiscard)
if uBound(gTheDiscard) <> 0 then
// Still some left, display top card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.clickable = true
else
// None left, empty discard pile
gDiscard.card = 0
gDiscard.clickable = false
end if
gDiscard.selected = false
gDiscard.refresh
end if // index = 30
saveUndo
// Increase the score
gScore = gScore + 2
gSelection = -1
updateCards
For this routine, two cards have been picked: index is one of them, and gSelection is
the other. We need to save both values, as appropriate, into theUndo.
But we don't want to saveUndo until both values have been saved. This was a bug in my
original code: I called saveUndo after each setting (just like in the other routines),
resulting in multiple calls to saveUndo within the same method! Remember, the undo
array grows each time you call saveUndo, so that caused phantom undos to be saved. The
revision above, has a single call to saveUndo at the end.
In Detail
It's quite possible that this is also a bug in the other routines, but the undo system seems
to work pretty well now. I'm planning to have a "bug followup" column after we're
finished with RBU Pyramid, where I'll track down and reveal fixes to the bugs I've found,
so I'll cover that in the future.
Meanwhile, test the program in a variety of situations and see if you can find problems
with undo. Even for such a simple program as a card game, there are a number of tricky
situations that might not undo correctly (and it's tough to find them without playing
Pyramid a lot). Feel free to send me bug reports and/or fixes.
Well, that's it for another column. I hope you learned something, and I truly hope you
plan your undo system better than I did. Think about how we could have done this better:
for instance, what if our original data structure included undo information as part of the
structure?
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We're almost but not quite finished with RBU Pyramid -- we're going to have a
"miscellaneous" column next time, throwing in a number of little fixes and enhancements
to many areas of the program that should just about get us done.
REALbasic University Quick Tip
I picked this up from REAL Software head Geoff Perlman, at MacWorld, when I
complained about how difficult it is to select underlying objects within the REALbasic
IDE. He revealed that REALbasic 4 has a new contextual menu: Control-clicking on a
window brings up a menu with a Select submenu which lists every object and control in
that window! Awesome. Worth the price of an RB 4 upgrade to me.
Letters
Here's a note from Charles, with a question about canvases.
Great columns, Marc!
Here's my question.
I have a listbox with about 200 items in it. In the Change event, I have a static text field
display text associated with that item when it is selected. I also have a Canvas control that
I would like to display an image associated with that item.
In the example below, there is an invisible column that contains an index number
associated with the item in the list. That is how I associate the text to be displayed, and
how I hope to associate the image to be displayed.
It usually runs like:
dim n, c as integer
dim imagename as string
c= me.columnCount -1
if me.selCount > 0 then
//this is the index value for the field
n = val(me.cell(me.listindex, c))
//sets the text
StaticTextDescription.text = gTheSetMembers(n).DisplayText
//creates a string, like image1
imagename = "image" + str(gTheSetMembers(n).Index)
Canvas1.backdrop = imagename //shoot!
else
StaticTextDescription.text =""
end if
The line right before the "else" is where it fails.
I would like to embed the image files as picts or pict resources in the application, but I
can't figure out a way to get the canvas to take a variable name as a picture.
Any insight would be very much appreciated.
Charles
You've got a great start, Charles, and you've done the listBox portion correctly (using an
invisible column). However I see your problem. You've defined imagename as a string,
which is bad. That's because a canvas' backDrop property will only accept an item of
type picture. If you'd said dim imagename as picture the canvas1.backdrop =
imagename line would work (but of course the program would stop at the line above,
where you assign a string to imagename).
That's the part where I get a little confused: I don't know enough about your data
structure to understand what you are doing with imagename. For example, it appears
gTheSetMembers(n).Index is a number, and it looks to me like you're attempting to
automatically generate a picture's name by combining the word "image" with the number
(i.e. "image7").
If that's the case, you're almost there: you just need to load the actual picture data into a
picture type variable. There are several ways to do this, depending on where the
pictures are stored.
If your pictures are in separate files in a particular folder, you could pass the string name
you generated above to a routine that loads the picture from the named file and returns it.
If you had this routine:
sub loadPicture(name as string) as picture
dim f as folderitem
f = getFolderItem("").child("Pictures").child(name)
if f <> nil then
return f.openAsPicture
else
return nil
end if
end sub
This would be valid:
Canvas1.backdrop = loadPicture(imagename)
Your loadPicture routine is just grabbing a picture from disk and passing it to
Canvas1.backdrop. Of course this assumes you've got a "Pictures" folder (with pictures
in it) in the same directory as your program.
If, on the other hand, you want your pictures stored inside your program, you'd have to
take a different tack.
Let's say you dragged images into your RB project window and they were named
image1, image2, image3, etc. You can't refer to them via your imagename variable, since
it's only going to contain a string. What you are wanting to do is store a variable inside
a variable, and that can't be done. If you store the name "image1" inside imagename, it's
just text, not the actual variable named image1. Remember, pictures dragged into your
project are the names of objects -- while you can dynamically generate the name of the
object, you can't store the actual object inside your variable. How to get around this?
You'd have to write a routine like this:
sub loadPicture(name as string) as picture
select case
case "image1"
return image1
case "image2"
return image2
case "image3"
return image3
// etc.
end select
end sub
See how this works? It takes the name (as a string) of the picture you want, and then,
hard-coded within your program, returns the appropriate picture object. You'd have to
have a case for every picture in your program. Not a problem for 20 or 30, but if you've
got hundreds, this could become a pain.
Here's a better solution: store the pictures as picture resources inside either a separate
resource document or inside your program!
The approach is very similar for either one. Using Apple's free ResEdit program, create a
new document. One by one, paste in the pictures you want in the document. They should
be of type PICT. Then, name each one according to your scheme. (In ResEdit, selecting
"Get Info" on a resource will allow you to name it.)
Your PICT resource list might look something like this:
If you name your resource file "resources" (no quotes) you can drag it into your RB
project's window: that will embed the contents of that file within your program when you
compile it. (The file can contain any resources you want, not just PICT resources.) Note
that the file is not copied into your project, just linked: you can edit the external file and
when you compile your program, it will use the file's current contents.
Then all you have to do is pass imagename to this routine to retrieve the picture:
sub loadPicture(name as string) as picture
dim rf as resourceFork
rf = app.resourceFork
if rf <> nil then
return rf.getNamedPicture(name)
end if
return nil
end sub
The above routine simply gets the named picture from your program's resource fork. If
you wanted to use a separate resource file instead, you could just modify this routine to
get the data from that file (instead of rf = app.resourceFork you'd write something
like rf = getFolderItem("").child("picturedata").openResourceFork).
With any of these methods, the loadPicture method returns a picture object, so you'd
just assign that to Canvas1 like this:
Canvas1.backdrop = loadPicture(imagename)
Of course one disadvantage of using resources is that your program won't work under
Windows: resources and resource fork objects are Mac only (they work fine under Mac
OS X, by the way). That may or may not be an issue for you.
REALbasic University: Column 044
RBU Pyramid XVII: Polishing Part I
My original intention when creating this series of tutorials, building a complete game
from start to finish, was to have everything carefully planned (and even written) in
advance. That way I'd leave nothing out and the steps would follow in a logical order.
Unfortunately, reality is what happens while you're making plans, and of course things
didn't work out quite the way I planned.
One consequence of this is that there are a slew of small fixes, enhancements, and
changes I made to RBU Pyramid during its development that we haven't covered, yet
none of these are significant enough to warrant a column of their own. So today I present
a hodge-podge of code snippets and polishes. There are a half-million of these to add, so
this will be a two-part lesson. We'll also jump around our project quite a bit, so stay
focused and follow along carefully.
Adding the RBU Logo
While developing RBU Pyramid, I realized that it would be a good idea to release the
final version of the program as a freeware product. After all, the source code is fully
available, so it's not like I can sell the game, but I could promote the game as freeware
and use it to publicize REALbasic University (and in turn, REAlbasic Developer
magazine).
It was this thinking that led me to name the program RBU Pyramid, add appropriate RBU
and RBD links in the About Box, and add the RBU logo to the main game screen. That's
what we're going to do now. It won't be difficult.
Open your project file and double-click on gameWindow. Drag on a new Canvas object.
Name it logoCanvas and set its Left property to 13, Top to 14, Width to 83, and Height
to 48.
Now add this picture to your project (download it, place it in your Linked Graphics
folder, and then drag it into your project window):
Once that's imported, select logoCanvas and set its backdrop property to rbusmall
(that's the name of the above graphic).
Perfect! Looks cool, and it's in the right place, away from our pyramid of cards. Next we
just need to add some basic functionality.
First, it would look better with a border, so let's go to the control's Paint event (press
option-tab while selecting logoCanvas to open the Code Editor). Put in this line of text:
g.drawRect(0, 0, g.width - 1, g.height - 1)
Next, since we want it to open the About Box when it is clicked, put this in the
MouseDown event:
aboutWindow.showModal
That's it! Try running the program and testing it just to make sure it works.
Adding a New Game Menu
Currently RBU Pyramid starts a new game when you launch it, but there's no way to start
a new game when the program is running. Obviously, that's not good. Also, under Mac
OS X the File menu's Quit item is automatically moved to the application menu, and
since that's the only menu item, the File menu is completely blank (under Mac OS X)!
Double-click on the Menu object within your project. Within the menu window, go to the
File menu and click on the blank space underneath the Quit menu. Type in "New Game"
(no quotes) and set the CommandKey property to N. Press Return to lock in your settings.
Drag the menu item up to the top position. Since the program under Mac OS Classic will
have a Quit item on the File menu, it's a good idea to add a separator between the two
commands. Go back to the blank line and type a hyphen (-) and press Return. Drag the
created separator up and put it between the other commands. Under Mac OS X, the
separator won't show up (since it would be the last item on the menu and that would be
silly).
We now have our menu command, but we need to activate it. Now here's an important
thing to remember about how REALbasic works with menus. The system is so slick and
transparent it's easy to forget how powerful it is. But it can be confusing at times, so let
me explain how the system works.
Beginning RB users quickly understand the concept of menu handlers: the handler is like
a method with the name of a menu command. Whatever code's inside that handler gets
executed when the user chooses that menu item. Easy enough.
But after that intial bit of understanding, they are often puzzled at why you can insert
menu handlers into so many places. After all, each window has it's own menu handlers,
as does the application class, and custom classes as well! Why so much duplication? Or is
it duplication?
Here's the trick: think about how menus work within an application. There are some
obvious things. For instance, if something is selected (text or an object), the Edit menu
should be available with Cut and Copy and maybe Paste (if there's something on the
clipboard). However, if no object or data is selected, there's no reason for the Edit menu
to be active.
And what about a program like a word processor, that lets a user open multiple
documents? When all the documents are closed, you surely want the Quit, New
Document, and Open document commands to be available!
And that's the key: menu commands that work on data are always inside a window. After
all, no data can be selected or pasted if there's no window to view the data!
Menu commands that don't require data, such as Quit, Open, or New, don't belong in a
window. That's because if that window is closed, those commands wouldn't be available!
Once you understand that fundamental aspect of menus, it's easy to see where menu
handlers go. New and Open menu items go within your program's Application Class
(often named App). Since the App class is there when your program is launched and is the
last thing closed when your app is quit, those commands will always be available
regardless of the state of the windows.
REALbasic always first checks with the Application Class for establishing the state of
menus, then the frontmost window, and last of all individual controls. The benefit of this
priority method is that each more specific object has the ability to override the more
general object's setting. Let me give you an example.
Let's say you had a situation where you had a ListBox of data and you wanted the user to
be able to copy and paste these items. (The kind of data they represent is irrelevant -- they
could be pictures or folderItems.) REALbasic normally automatically activates the Copy
and Paste commands as needed for EditField text editing, but since it doesn't know
what kind of data the ListBox represents, it can't automatically support that. But you
could easily create a custom class of ListBox type and add in menu enablers and
handlers within that class. The effect to the user would be that the Copy and Paste
commands are available when data is selected within the ListBox.
You see how powerful this is? Your object -- the Window or ListBox or EditField or
Canvas -- knows how to turn on menus and what to do when the user chooses the
command! When the user clicks on the EditField, the Copy command copies text.
When they click on the ListBox, it copies folderitems (files, or whatever data you want).
When they click on a canvas, it copies a picture.
If an object isn't available or isn't visible, the menus for that object aren't available either.
This makes menu handling much easier for you, the programmer, since you just add
menus to the objects needing them.
This also gives you a way to override menu functions: the same menu command can have
different effects depending on the context. For instance, choosing a Preferences
command while a document is open could offer settings for that specific document, but
selecting Preferences when no documents are open would offer global program settings.
In Detail
You'd do this simply by adding a menu handler into both your App class and your
window object. In the app class you'd bring up the global preferences dialog, and within
the window you'd bring up the document specific preferences dialog. Simple! The only
thing to remember is that within the window menu handler, you must return true at the
end -- otherwise the menu handler of the app class would run as soon as the window one
was finished.
You can easily test this by adding fileNewGame menu handlers to both the app class and
gameWindow and putting in msgBox commands in each stating which is which. Then when
you run the program and choose "New Game" from the File menu, the appropriate
message will be displayed depending on whether or not gameWindow is visible.
In the case of RBU Pyramid, we only want one game to take place at a time, so the
approach we'll take is to put our fileNewGame menu handler within our App object. (You
can add the menu handler by opening App, selecting "New Menu Handler" from the Edit
menu, and choosing fileNewGame in the popup menu that's displayed.)
Of course before the menu is active, we must enable it. Add this line anywhere inside
app's EnableMenuItems event.
fileNewGame.enabled = true
Now we just need to make the command do something. It seems like that should be as
simple as inserting a call to our newGame method within fileNewGame.
However, adding this command brings up another problem: what happens if the user's
already in the middle of a game? It would be horrible if the player was in the middle of a
record-breaking game and accidentally hit Command-N and a new game was started
without any warning, wouldn't it?
So yes, we must add a confirmation dialog. First, let's put in our code for the menu
handler:
// Confirm restart
gDialogReturn = ""
confirmDialog.showModal
if gDialogReturn <> "" then
gameWindow.endOfGame
newGame
end if
We'll reuse the same gDialogReturn string we used for other dialogs to tell us what the
user did in the dialog (clicked okay or not). Since all we need is a yes or no, we'll just
assume if there's anything at all in the string it's a positive response -- the user wants to
abandon the current game.
If so, we run our endOfGame routine (so the user can get added to the high score list if
appropriate) and then call newGame.
Now let's create confirmDialog. Add a new window to your project (File menu, "New
Window"). Give it the following settings (via the Properties palette):
Onto the dialog we're going to want four items: two pushButtons, a staticText, and a
canvas. Make them look something like this:
Name the left button cancelButton and the right button okButton (and set their
respective cancel and default properties as well).
The size of the canvas should be 32 wide by 32 tall. That's because it's going to draw a
standard "caution" icon. If you select it and press Option-Tab, you'll be taken right to
canvas' Paint event, were we want the following line of code inserted:
g.drawCautionIcon 0, 0
Good. Now go to cancelButton's Action event and put in this code.
gDialogReturn = ""
self.close
That just makes sure our global string is empty and closes the dialog.
In okButton's Action event, put this:
gDialogReturn = "Yes"
self.close
This also closes the dialog (important), but puts a "Yes" into our global string.
Go ahead and run the program. It should run fine, and the "New Game" menu command
should work. However, you might note an ugly detail: even when you don't start playing
a game by clicking on any of the cards, choosing "New Game" always brings up the
confirmation dialog. Now I don't know if you play pyramid the way I do, but I often will
do several redeals before starting a game, waiting for a decent shuffle. But if every time I
press "New Game" to redeal the cards I have to answer a silly dialog, I'm going to be
annoyed. So let's fix this.
Let's add a global variable, gGameStarted, to our globalsModule. (Open it and choose
"New Property" from the Edit menu.) Type in gGameStarted as boolean as the
property value.
Now we just need to set gameWindow to make gGameStarted true when appropriate. We
don't want it to be true unless the user is obviously playing the game, so we'll add
gGameStarted = true at the top of the following routines:
•
•
•
•
cardKing
cardMatch
deckAdvance
discardMatch
So basically, if the user advances the Deck or makes any kind of match at all, we assume
the game is started.
But once the user's finished with a game, we must reset gGameStarted to false, so add
this line to the very bottom of endOfGame:
gGameStarted = false
Excellent work. Now we just need to check the state of gGameStarted before we call
confirmDialog. So go back to app, open the fileNewGame handler, and make it look
like this:
if gGameStarted then
// Confirm restart
gDialogReturn = ""
confirmDialog.showModal
if gDialogReturn <> "" then
gameWindow.endOfGame
newGame
end if
else
newGame
end if
This just makes it so we only throw up the confirmation dialog whenever the game has
actually started. If the game hasn't started, we just start a new game immediately
(effectively redealing the cards).
So run the program and test it. When you don't make any matches, you won't get a
confirmation dialog when you hit Command-N. But when the game's started, it will ask
before resetting. Hit Cancel in the confirmation dialog and nothing happens. Click Okay
and you get a new shuffle. Awesome!
Well, we accomplished a few things today, but we'll finish up the polishing next week.
Have courage: we're almost done! If you want the complete REALbasic project file for
this week's tutorial (including resources), you may download it here.
Next Week
We continue with our wrap up lesson, polishing the program to make it shine.
Letters
This week we have a question on background pictures from Cap:
Dear Marc,
you got a very interesting way to load background pictures in your app.
I want to port this method of loading skins into another app (a new project of mine).
Let's say, this app consists of 10 different windows. By now, my app is able to upload
pictures (getopenfolderitem) and to put a copy of them into a special folder. Now I used
your method "LoadBGPictures" in every of these 10 windows.* in the open-event of
these windows I put "window1.loadBGPicturex(gBackgroundpicturename1)" (once this
was in the load prefs folder, but this took too much time at the project launch)
* the loadprefs-method looks like this:
.
.
.
select case gDirectives(i).name
case "bgpicture"
gBackgroundPictureName = gDirectives(i).text
case "bgpicture1"
gBackgroundPictureName1 = gDirectives(i).text
case "bgpicture2"
.
.
.
So what do these windows load, when they are opened? They are able to load pictures out
of the folder, where I put the copies of the getopenfolderitem-pictures.
Unfortunately, this way of loading skins is not very fast.
Question 1: How do I make a copy of a picture (let's say it's a GIF) in PICT without icon
resources (you know, the preview stuff and all those things that make pictures fat;
compare this to the feature of GraphicConverter to make very small-kB pictures)
Question 2: Is this way of loading skins the best one, if I load skins for more than one
window? Is there a "trick" to load pictures faster?
Thank you, Marc!
Greetings,
Cap
It sounds to me like speed is your main problem: you don't want a delay when the user
changes the skin. With programming, most things are compromises. One way is faster but
more complicated, or perhaps it uses more memory.
In your situation more memory might be the key: you could pre-load the pictures into the
program so when the user chooses a different skin, it's already loaded and ready. Of
course if you need different pictures for each window, and if you offer a number of skin
styles, that could be a lot of pictures to remember!
The key when attempting to optimize (speed up) a routine is to know exactly what area is
causing the slow down. Is it loading the pictures from the disk file? Or is it displaying the
pictures in the window?
Once you know where the speed drag is, you can set about solving it. You mentioned
once putting the picture loading routine in your prefs but it made the program take too
long to launch. A solution to that might be to use a thread: we haven't talked about those
in RBU yet, but threads allow code to execute without halting the other aspects of your
program. They can be simple and very tricky, depending on your usage.
A simple thread works like this: you add a class to your project and give it a super of
thread and name it threadClass. Drag it to your project window to instantiate it (RB
will rename it threadClass1). Open it in the Code Editor and in the Run event, put your
picture loading code.
For a test, put in this:
dim i as integer
for i = 1 to 1000000
i = i + 1
next
beep
Then, in the Open event of your program, you could do something like this:
threadClass1.run
What this will do is get the thread starting to execute. Any code in the thread will be run,
while the rest of your program will continue. The user will still be able to do things. If
you try this, you should hear a beep 3 or 4 seconds after you run the program.
The only thing to remember is you must be careful to never assume, elsewhere in your
code, that the picture loading (or whatever the thread was to do) is finished. Always
check variables for nil, or even create a global "gThreadDone" boolean so you can
know that the thread finished and was successful.
It might also help to remember that speed is relative: if you can trick the user into
thinking that something didn't take as long as it did, they'll think your program is fast. For
instance, if you wait until a window is opened before loading the picture, the user might
see a delay in the window's opening. But you can set the window's backdrop property
before the window's displayed, only showing the window once everything is ready. (You
might need to temporarily hide the window with window.visible = false until you're
ready to window.show.)
I know this doesn't answer your question exactly, but I don't know enough about your
specific project to advise you in anything more than general terms. (If I misunderstood
something, feel free to send me a follow-up question.)
As to your first question, about saving a PICT without extra fluff, many graphics
programs (like Adobe Photoshop) can do this. Photoshop can even save PICTs as JPEG
compressed PICTs, which are considerably smaller than normal pictures (but they require
QuickTime installed on the machine the program is running on to decompress). I'm not
sure why you specifically require PICT graphics: REALbasic will accept JPEGs and
GIFs dragged directly into a project window (or you can load those off disk just like
PICT format files). If QuickTime is installed (and few Macs don't have at least some
version of it), you can read in almost any format picture file.
Next, we have some words of gratitude from Frank:
Mr. Z.
thanks for the lessons. I have been reading "The Realbasic book" back to front a number
of times and was very pleased to find your website with the projects.
Although I consider myself a very experienced IT professional (ex Cobol, Pascal, Basic
etc programmer), working with an OOP language like RealBasic is a bit different. The
projects help me to construct someting and I have so far been able to figure what actually
happens. Although there are still some mysteries, I start to get a good feel for it.
Bottomline is I would have given up on Realbasic if I hadn't found your lessons.
My billion dollar question is: How on earth do you plan/design (the technical design so to
speak) an OOP software package, and how do you actually document it?
Regards
Frank
Thanks for the kind words, Frank. I can only say that my approach to teaching is based
on my own experience, knowing how I struggled to learn all this stuff for years. I still
feel like I know every little, which is probably good, lest I get a swelled head!
And your billion dollar question is indeed intriguing: I'm certainly no OOP expert and I'm
often baffled by things or realize in retrospect that I could have used more OOP in an app
and it would have been considerably easier, but I keep trying and learning, and that's
what really counts. Documenting OOP is certainly tricky: objects, like real living beings,
can do things on their own. That's what gives them power, but it also can make working
with them challenging and intimidating. Sometimes they escape and you feel like you're
trying to find a bunch of lost cats!
.
REALbasic University: Column 045
RBU Pyramid XVIII: Polishing Part II
Have you ever heard the comment that "ten percent of a program takes 90 percent of the
work?" I've found that to be the case. Getting a program 90% of the way done is fairly
easy: it's that last ten percent that's the killer.
Polishing a program to final form is a large part of that final ten percent. Last week we
began the process, and this week we continue. I'd hoped to accomplish this in just two
lessons, but it seems there's more to do than I remembered. We'll finish this up in next
week's column.
Adding Keyboard Deck Advance
One feature I've been missing in our version of Pyramid is a way to quickly draw cards
from the Deck. What's easier than pressing the space bar?
Open gameWindow's Code Editor and go to the KeyDown event. Put in this code:
if key = " " then
if (uBound(gTheDeck) > 0 or gFirstTimeThrough) then
deckAdvance
end if
end if
Here we check to see if the user pressed the space bar: if so, we check to see if the Deck
has cards left (there are items within our gTheDeck array), or it's the user's first time
through (in which case a Deck advance is still valid). If either of those conditions is true,
we call our deckAdvance routine. Simple!
Adding Safe Shuffle
One of the original reasons for my writing RBU Pyramid was to stop the annoying
tendency of most Pyramid games to deal "unwinnable" hands.
My solution was to write a "safe shuffle" routine, whereby RBU Pyramid would check a
shuffle to make sure it was winnable before showing the shuffle to the player. Somehow
in the course of this tutorial, we never wrote that routine. So let's add it now.
Open globalsModule and add a new method (Edit menu, "New Method"). Name it
safeGame and give it a return type of Boolean:
Now we must figure out the algorithm we're going to use to solve our problem. We could
have our routine actually virtually "play" a game of Pyramid, attempting to solve the
puzzle with the current deal of cards. If the game turns out to be unbeatable, we know
we've got a bad deal, so we reshuffle the cards and try again.
There are two problems with that, however. One, programming a routine to actually play
Pyramid is complicated: we're getting into a lot of sophisticated decision-making.
Second, playing a whole game, even virtually, takes time. Even if it only takes half a
second on a fast computer, keep in mind that "unwinnable" hands are fairly common: our
safeGame routine might be called several times in succession before a winnable hand is
dealt. The player might have to wait several seconds between deals. We don't want that.
Our solution is a common compromise in programming. We don't need to guarantee that
an unwinnable hand is never dealt: we just want to greatly lesson the chances of such a
thing happening. If we can stop 99% of the bad hands from being seen, it will eliminate
most of the frustration a player is likely to encounter.
So what can we do to check for unwinnable hands without actually checking every
possible move to make sure the game is winnable?
The answer to this took me a little time: I first analyzed a number of unwinnable deals I
received (yes, playing Pyramid was research) to figure out what happens in an
unwinnable hand.
I discovered an unwinnable pyramid is when you get blocked: the card you need to win is
either behind the front cards of the pyramid (covered and not choosable) or past you in
the discard pile and you're on the last redeal of the Deck.
The latter can happen when you don't play the game well: it's quite possible to turn a
winnable game into an unwinnable game by not using your resources well. For instance,
let's assume you've got a pyramid with three Twos and one Jack in it. That tells you there
are three Jacks in the Deck. But if you advance through the Deck and don't use the Jacks,
on your second and final time through the Deck, you must use those Jacks to clear at least
two of those Twos or you're toast. Once the Jacks you need are in the Discard pile, you
can't get back to them (at least not without a huge amount of luck).
At least that type of unwinnable hand can be solved by planning ahead: that's part of the
game of Pyramid. But sometimes you'll be dealt pyramids like this one (from RBU
Pyramid I):
This is an example of an impossible shuffle: the two 2's near the bottom of the pyramid
each require a Jack to be removed, yet there are three Jacks near the top of the pyramid,
meaning that a maximum of one exists within the Deck. Even worse, the top card is a 2,
requiring a Jack to match. What a mess! It's a Catch-22: the game cannot be solved.
That's impossible right out of the box! You might as well quit now, because there's no
way to solve this shuffle.
What causes this kind of impossible situation? The key problem with unwinnable hands
comes from having too many of the same cards in the pyramid.
Think about it: the three Jacks in the pyramid makes this tough to solve in the first place.
Having a couple Twos in the front makes it impossible.
So now we've got something: we know that too many of the same card within the
pyramid makes for a possibly unplayable game.
But there's another conclusion we can draw from this analysis. Having the three Jacks in
the pyramid by themselves isn't the end of the world, but it's the three Jacks in
combination with several Twos that make it impossible. That tells us that too many valid
pairs in the pyramid could cause a problem.
So now we've got our algorithm! All we need to do is count the frequency of each card in
the pyramid, see how many pairs we've got, and if the numbers are too high, don't use
that shuffle! That should be very fast as well, since we don't have to calculate a huge
number of possibilities, just count a few items.
However, there is a flaw in this idea. In an actual pyramid game, the order of the cards is
critical: it is entirely possible to have three Jacks and three Twos in the pyramid and have
a winnable game. So our algorithm, to be accurate in predicting bad hands, should check
the order of the cards in pyramid.
But just because it's possible doesn't mean it's likely. The chances are much greater than
the game is unwinnable if there are a lot of the same card in the pyramid.
Remember the compromise I spoke about earlier? This is where it comes into play. Since
we're not concerned with absolutes, our algorithm should work for us. Yes, we might
eliminate a few winnable deals, and yes, we might miss a few unwinnable hands, but this
algorithm should work to catch 99% of the bad shuffles.
So, how do we implement our algorithm? Let's look at the code I came up with:
dim freq(13) as integer
dim i, j as integer
const max = 5
// Count how many of each card in pyramid
for i = 1 to 28
j = gCards(gTheDeck(i)).number
freq(j) = freq(j) + 1
next
// if there are 3+ of any one kind, reshuffle
for i = 1 to 12 // (We can ignore 13, Kings)
if freq(i) > 3 then
return false
end if
next
// Now check for excessive pairs
if freq(1) + freq(12) > max then
return false
end if
if freq(2) + freq(11) > max then
return false
end if
if freq(3) + freq(10) > max then
return false
end if
if freq(4) + freq(9) > max then
return false
end if
if freq(5) + freq(8) > max then
return false
end if
if freq(6) + freq(7) > max then
return false
end if
return true
What does this do? First, it defines an integer array, freq, which has 13 elements (not
counting the zeroth, which we don't use). Then we count through the 28 cards in the
pyramid. For each card we get the number value (1 to 13, Ace through King) and use that
as the index for freq, and we increment that element by one.
The result is that when we're done examining the pyramid cards, we have an array in
which each element contains the number of cards that appear in the pyramid. The type of
card corresponds to the array's index. So if there are three Aces and two Tens in the
pyramid, freq(1) would contain 3 and freq(10) a 2.
The next thing we do is check to see if there are four of any single card (we ignore Kings,
since they don't require a match to be removed). We simply count through our freq array
and if any element has a number higher than three, we return false for our method
(meaning the shuffle is bad).
Then what we do is see how many matching pairs of cards there are in the pyramid.
We're not just looking for the actual number of matches, but for the total of two kinds of
cards. For instance, Aces and Queens are a match, so we add up the number of Aces and
the number of Queens. If that's greater than max, our constant, we return false for a bad
deal.
We do this for every type of possible match: Jacks and Twos, Tens and Threes, etc.
You'll notice I have used a value of 5 for max. That means if there are more than five
matching cards (i.e. at least three of each), we mark the shuffle as bad. So five would be
okay: three Aces and two Queens would be an okay deal.
How did I come up with this value for max? Through calculation and a little trial and
error. You are free to do your own testing and modify max if you think a different number
is better. For example, a value of four would reject the three Ace/two Queen deal above,
which you might think is too difficult a shuffle. Basically, the lower the value the easier
the game.
Which brings us to a very important point: what we are doing in our safeGame method is
essentially cheating. We're taking some of the random chance out of the game. When I
first started playing pyramid, I found that games were easier if there was a King at the top
of the pyramid. So I used to hit the "Deal" button until I got a "good" shuffle. Obviously,
that only helped me on the first pyramid, and it was no guarantee I'd clear the pyramid,
but it did make the game slightly easier.
In the case of RBU Pyramid's safeGame method, we're not cheating to the point of
making sure an easy shuffle is dealt, but because our algorithm isn't 100% accurate -- it
does eliminate winnable games -- the result is we never see some of the more challenging
shuffles. Remember, the toughest Pyramid games are when there are several of the same
card in the pyramid and the player has to use their Deck resources wisely. With safeGame
active, a player may never encounter one of those tough but winnable games.
Adding Safe Shuffle Menu Option
The solution to our "safeGame equals cheating" problem is to let the player decide. Some
players may want it on, others may keep it turned off. So let's add an option to turn this
feature on/off via a menu.
Open the menu item within your project window and go to the Options menu. In the
blank area at the bottom, type "Safe Shuffling" and press Return. Go back to the blank
area and put in a hyphen (that's a minus sign) and press Return. REALbasic will add a
separator to the menu. Drag both items to the top so the menu looks like this:
Excellent. Since we're going to need a way to remember the current state of safe
shuffling, let's add a global property to reflect this. Open globalsModule and add a new
property (Edit menu, "New Property") and name it thusly: gSafeShuffle as boolean.
You can close globalsModule now.
Let's enable the menu. Open app and go to the EnableMenuItems event. Put in this code
anywhere you like:
OptionsSafeShuffling.enabled = true
OptionsSafeShuffling.checked = gSafeShuffle
This will enable the menu and make sure that its checked state (whether there is a
checkmark by the menu item) reflects the current value of gSafeShuffle.
Now that the menu's enabled, we must do something when the user chooses it. Add a
menu handler (Edit menu, "New Menu Handler") and choose "OptionsSafeShuffling" in
the popup menu that's presented.
Use this for the handler's code:
gSafeShuffle = not gSafeShuffle
This just toggles (inverts) the safe shuffle mode. If it's on, it's turned off. If it's off, it's
turned on.
Calling SafeGame
It's cool we've got a menu and all, but you may have noticed we have yet to actually call
safeGame at any point in our program! If you run RBU Pyramid right now, it won't act
any differently than before. The "Safe Shuffling" menu option will be available and will
check and uncheck properly, but the game won't actually use any safe shuffling.
Go to our freshDeck routine in globalsModule. Right at the beginning you'll find code
that looks like this:
// Build a full deck and shuffle it
shuffleDeck
Replace that with this:
// Build a full deck and shuffle it
if gSafeShuffle then
do
shuffleDeck
loop until safeGame
else
shuffleDeck
end if
See the difference? Before we just called shuffleDeck once. Now we check to see if safe
shuffling is turn on. If it isn't, we just call shuffleDeck as before. But if it is, we run
shuffleDeck multiple times until safeGame reports that we've got a good shuffle.
(So you can see by this that if our safeGame routine was too strict or too slow, dealing a
new game's cards could take a while.)
That's all we've got time for this week. Your homework assignment is to play RBU
Pyramid and test out the differences between shuffles with Safe Shuffle on and off.
Enjoy!
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
In Polishing, Part III, we'll get the game gleamingly clean.
Letters
This week's letter is from David, who sounds like he's having a real problem with
REALbasic.
REALBasic community,
I have some questions concerning RB. I bought RB a year ago, plus 3 books, for a total of
$200. The book has instructions for creating a demo, an application that claims to be an
editor. Along the way, the instructions indicate that you should run this demo in debug,
open a file, and attempt to edit it. This never worked!
Initially when you run the demo, a code listing appears with a line highlighter, indicating
that it doesn’t recognize the keyword in that piece of code. So you either have to declare
all variables in every piece of code, as there is no place to declare global variables.
If you are running RB, building this demo editor, and attempt to open a text file and edit
it, you crash the system! Something is missing here. If you are running the compiler or
assembler or whatever this is, and attempt to run an editor (like WORD or WORKS or
AppleWorks) at the same time, no doubt there will be a conflict.
I have seen a listing containing names of applications presumably created with RB, and
the email address of the programmer who invented them. I have sent emails to a few of
these people, and to the company. Apparently no one, not any of the people in the
company nor any of the programmers found in the listing have ever built this demo file.
I am use to creating a program where code is entered in a text file using an editor, which
is then interpreted or compiled. The entire contents of the file can be listed on a terminal
or lineprinter. Not so here, as the code is hidden in hundreds of places. How does anyone
know what little piece of code is related to what? You can't print it out. Is it possible to
locate all data entry, then save a copy of that to a text file, and attempt to create a listing
from that? Again there is a problem because we are running this RB assembler/compiler,
and a text editor at the same time.
Is it necessary to do screen dumps of every code listing, then print the pictures, and
retype in the text from the picture into a text file? Maybe the pictures could be printed
and scanned in as a text document, etc. I was hoping that there was something more sane.
Got any clues? Have you attempted to do this demo editor application that is listed it the
book from RB? Thanks in advance.
David Blish
Sorry to hear you're having such a problem getting going with REALbasic, David. Might
I suggest you begin with the first few lessons of REALbasic University, which are
designed for the very beginner?
You don't specify which book or demo you were attempting to build, so I can't help you
with that exact situation (if you'd like my help with that demo, let me know which one it
is). I can, however, attempt to explain a bit about REALbasic's philosophy of
programming, which might help settle some of your frustration.
You are correct that code in REALbasic is scattered throughout the program. From some
perspectives, that's a disadvantage. For instance, it's almost impossible to have a
traditional "code listing" of a REALbasic program. You can "Export Source" via the File
menu, but the code is missing many of the property settings and all of the user interface
and imported elements, and thus is almost useless for recreating a full project. Newer
versions of REALbasic include an option to save your project as XML, which can be a
way to edit the code outside of the REALbasic IDE. (But with RB's terrific
"autocomplete" feature, why would you want to edit code elsewhere?)
It would certainly be easier for me to teach REALbasic if code was all in one place, but
in truth today's high-level languages are all becoming similar to REALbasic in that there's
code in multiple places (C has dozens of header files, for instance, almost every modern
programmer works in some sort of application framework, such as Codewarrior's
PowerPlant).
Instead of fighting REALbasic's "scattered" nature, embrace it. The advantages far
outweigh the occasional disadvantage. It's part of Object-Oriented Programming.
REALbasic makes that metaphor even easier to absorb by embedding code within actual
interface objects. A button, for instance, has the code inside it that gets executed when it
is clicked: that makes intuitive sense.
Yes, there are plenty of other places code can hide that aren't as obvious, but the
metaphor is still the same, the problem is that it's difficult to create a physical
representation for an abstract concept, like the "Open" event, for instance.
I don't pretend that understanding REALbasic's system of programming is child's play or
that you'll get it overnight, but it does make sense and once you're used to it, it's a very
natural way of working. (It took me less than a week of regular use to become used to it,
but then I'd had some experience with HyperCard, which used a similar metaphor.
REALbasic is actually much better at displaying your code than HyperCard ever was,
however.)
Best of all, once you become competent at programming in REALbasic, you can create
your own "reusable objects" -- modules and classes you write once and use in multiple
programs. That saves you eons of time and is exactly why object-oriented programming
has taken over the software development world.
Finally, you mention REALbasic's "inability" to create global variables. Nonsense: any
property added to a module is inherently global. You can add a property (variable) to a
window, and that variable's available to all routines in that window, but what if the user
closes the window? But modules can't be closed by the user so they're always global.
.
REALbasic University: Column 046
RBU Pyramid XIX: Polishing Part III
Last week we added our safeShuffle routine, which hopefully ensures RBU Pyramid
won't deal impossible shuffles. But we aren't quite finished polishing the program.
Adding Another Preference
The first thing we need to do is add a way to save the gSafeShuffle setting. That way
the user only has to set it one time and the game will always use that setting. Fortunately,
because of the preferences system we're using, this isn't difficult.
Open prefModule and go to the savePrefs routine. Within the main if-then statement,
put in these lines:
// Save SafeShuffle setting
t.writeline "#safeshuffle " + bStr(gSafeShuffle)
This simple writes another line to the preference file. It uses the word "safeshuffle" as the
directive name, and then adds either a "true" or "false" depending on whether
gSafeShuffle is true or false. (Remember, the bStr function we wrote a while back
accepts a boolean and returns true or false in text form.)
The modification to loadPrefs is just as easy: anywhere within the select statement
just add this:
case "safeshuffle"
gSafeShuffle = gDirectives(i).text = "on" or
left(gDirectives(i).text, 1) = "t"
This looks for a directive named "safeshuffle" and if it finds it, checks to see if the setting
is "on" or true. The "on" part isn't necessary, but I like to support it for more flexibility
in case the preference file is manually edited. This way the setting could be "on" / "off"
or "true" / "false" -- and since we only check the leftmost character of the setting, just "t"
also works!
Adding a Mac OS X Icon
Our program runs well under Mac OS, but it's got some problems under Mac OS X. We'll
deal with some of those next week, in our "bug fix" round. But today, we want to polish
the program by adding one of those big, really nice Mac OS X icons to RBU Pyramid.
How do we do that?
Working with icons under regular Mac OS isn't too hard, since Apple's free ResEdit
includes a basic icon editor. You can also copy icon resources or a picture from a
painting program like Adobe Photoshop and paste them right on the application icon
within the Build Settings dialog (or Build Application, in versions of REALbasic prior to
4.0). Under Mac OS, this generally works well enough.
You're technically missing a few icon resources (like the older black-and-white kind),
and extremely picky people will tell you you ought to edit every single icon variation by
hand, but since hardly anybody has a black-and-white Mac these days, it works fine in
most cases.
To add an application icon for Mac OS programs, you can paste an icon over this
default icon within the "Build Settings" dialog ("Build Application" in older
versions of RB).
Mac OS X is a different animal, however. Mac OS X adds two new icon sizes, a "huge"
size of 48 x 48, and a giant "thumbnail" size that's a whopping 128 x 128. That's how
come Mac OS X programs are able to use those photorealistic icons of hard drives and
stuff. Regular Mac OS icons max out at 32 x 32.
To support these new icon sizes, Mac OS X uses a completely new icon format that is
incompatible with the old way. If you build a Mac OS X application using the paste
method, your Mac OS X program will use REALbasic's default "cube" app icon (Classic
apps will still use the old icon like before).
Even worse, because ResEdit hasn't been updated in years, it can't work with the new
Mac OS X style icons. So what do you do?
The answer, unfortunately, is that you must spend some money. Fortunately, it's not
much money. There are a number of excellent icon editing programs out there that will
solve this dilemma, but the best value is undoubtedly Iconographer from Mscape
Software. It sells for a mere $15, comes in both Classic and Mac OS X versions, and has
all sorts of sweet features, such as the ability to understand Photoshop's transparencies
when you copy and paste from Photoshop.
[Click for full view -- 315KB file]
You could create your entire icon within Iconographer, but I prefer to work in traditional
graphics programs first and finish them off in Iconographer. That's just because I'm more
comfortable in FreeHand and Photoshop.
Before we continue, let me say that I'm not an illustrator and I'll admit up front that I'm a
terrible icon artist. My designs vary between "competent" and "hideous," depending on
who's doing the judging. People who can create icons truly impress me: it's a talent to be
able to draw something stylish and recognizable with just a handful of pixels.
Proper icons should always be created by hand, pixel-by-pixel, for each icon size. I, of
course, being such a bad icon artist, ignore that rule. I figure I can create a giant icon and
resize it downward, which does not work very well. So ignore my bad habits and do your
icons the right way! (Or hire an icon artist.)
In the case of RBU Pyramid, I'd originally created a Mac OS-style icon that wasn't that
bad. It's certainly not fancy, but it gets the idea of a pyramid solitaire game across:
Under Mac OS X, however, that 32 x 32 pixel icon looks rather pathetic, so in the spirit
of polishing, I decided to create a 128 x 128 thumbnail version with a great deal more
detail.
The creation of icons probably merits a few columns of its own, but it's really beyond the
scope of today's tutorial for me to explain every detail of how I created this, so in short I
drew the card shapes in FreeHand, took those as layers into Photoshop and added
shadows, and assembled the final icon in Iconographer. The mask is "fuzzy" -- it has
transparency -- and I created that in Photoshop. That's what allows the soft shadows of
the cards to blend in with whatever background the icon is on.
Next, I used ResEdit to create a blank file which I named Resources and I put it in the
"Linked Resources" folder of RBU Pyramid. This is very important, because we need a
way to get the new icon into our project.
Then I used Iconographer's "Save Into File..." command and chose the file Resources.
Iconographer automatically saved the icon as resource number 128, which is the proper
number for an application icon. (If RBU Pyramid created documents that required a
custom icon, its resource number would be 129.)
I made sure that I saved the icon as "Mac OS Universal (Resources)" format (that's vital).
Finally, I dragged the file named Resources into my project window. That's it! Now
when I build the application, the new icons will automatically be included!
To test this, I went to REALbasic's "Build Settings" dialog via the command on the File
menu. (With versions of REALbasic earlier than 4.0, this is called "Build Application"
and it won't look the same as this screenshot.)
I set everything up like you see here and then built the applications (in RB 4, using the
"Build Application" command on the File menu). The result is a Mac OS X application
that uses the large 128 x 128 icon when appropriate, and the less detailed red-and-gray
version when small. You can see both in this view in the Finder:
What's really sweet is the way the larger icon automagically resizes when it's in the Dock
and you've got magnification turned on:
Doesn't that look better than the red-and-gray version? Granted, the card symbols are
rather oddly shaped when you study them closely, but that's because I allowed Photoshop
to generate them from the original vector artwork instead of hand-drawing perfect hearts
and spades and stuff, pixel-by-pixel.
If you would like the complete REALbasic project file for this week's tutorial (including
the new icon resource file), you may download it here.
Next Week
We fix bugs!
Letters
Our letter this week is a nice tip from Richard Sinclair. He writes in response to JP
Taylor's TabPanel problem (mentioned in an earlier column).
Although I'm a rank amateur at RB (working on my first app, actually); I may have a
more elegant solution to JP's problem.
Rather than paste the controls, because the Paste remembers layers (as in many graphics
apps) ... why not use the duplicate function, as I have with much success. Simply select
the controls in their current tab, switch to the tab you would like them duplicated to, and
hit command D. Immediately move them up five and left five to counteract the duplicate
commands default move, and you're there. Obviously you can omit that last part if you
don't need them in the exact same position. Bonus: Say you want to have the same
control, in the same place, in all five tabs of a five tab panel. Create the first, and position
and customize it. Switch to the next tab (with the control still selected) and duplicate.
Move the control up and over five (as described above). Immediately, before you do
anything else (including de-select), switch to the next tab and duplicate (command d)
again ... without the move. Repeat for each tab. Not only will they duplicate, they will all
be in the right place as the duplicate function will remember the move and duplicate it as
well.
Just thought I'd pass that along. (Of course, since my version of RB is not quite as new as
JP's, it may not work as I've described any more).
Thanks for the tip, Richard!
Next, we've got a note from Paul Thompson, who wonders:
Hello again Marc
Do you know of any plugs-ins which would add long-lamented BASIC commands to
RealBasic? I'm talking about examples such as:
1) a simple if...then statement such as
if a then b else c
rather than
if a then
b
else
c
end if
2) the return of the Input and Data commands
3) a modified Select Case where you can use Booleans such as
select case x
case (x<5)
etc
I know you can get around these with other commands but they have a conciseness and
simplicity that I miss.
Paul Thompson Reading, UK
I doubt the structural commands you talk about in 1 and 3 could be done via a plug-in.
Plug-ins usually add new commands; they don't revise the way existing code works. I
would suggest you tell REAL Software what you're wanting so they can expand
REALbasic's syntax.
The "input" command is harder to simulate in a GUI environment, though you could
write your own dialog box class that prompts the user for a line of text (similar to the
msgBox command, except that it returns a value). There were actually suggestions that
REAL Software include this at the REALbasic User's Group meeting at Macworld in San
Francisco this past January: my suggestion was to make it work similar to the "display
dialog" command in AppleScript. We'll see if they add that for REALbasic 4.5.
The "data" command, however, can easily be duplicated within REALbasic. I do this
kind of thing all the time. For instance, let's say you wanted to assign each month name to
a different array element. You could do something like this:
dim monthArray(12) as string
monthArray(1) = "Jan"
monthArray(1) = "Feb"
monthArray(1) = "March"
...
But that's a lot of ugly typing. It's not bad for something that only has twelve elements.
But if you had lots of data to load, it would be exceedingly yucky.
BASIC's "data" command would be ideal here: include a list of items and load them oneby-one into the array. Hey, guess what? We can do that! Just use the nthField function
like this:
dim months as string
dim monthArray(12) as string
dim i as integer
months ="Jan Feb March April May June July August "
months = months + "Sept Oct Nov Dec"
for i = 1 to 12
monthArray(i) = nthField(months, " ", i)
next
The months string can be as large as you like: as long as your data has a consistent
delimiter, you can grab individual pieces of data from it.
.
REALbasic University: Column 047
RBU Pyramid XX: Bug Fixing
Sorry about the erratic updates lately, folks. Applelinks' move to new servers last week
had some DNS issues that made the site inaccessible to many, so I decided to postpone
the column. I've also been very busy with something you'll all be excited about -- but I'll
have announcement about that in an upcoming column.
For now, it's back to RBU Pyramid!
Kill Those Nasty Bugs
No program is completely polished until all the known bugs are caught. It's difficult to
eradicate every bug, but your goal as a programmer should be to fix every bug you know
about. For example, it's quite possible there's a rare bug that is only noticeable in certain
situations -- you might have never encountered the situation and therefore never seen the
bug.
That happens to be the case in RBU Pyramid. Fortunately, one of our sharp-eyed readers,
George Bate, happened to discover a bad end-of-game bug and alerted me to it. Here's his
letter and explanation.
Dear Marc,
Quite by chance, as a result of playing the game when I should have been of studying the
code, I have happened on a small error in Pyramid. I noticed that the program goes wrong
when a pyramid is cleared after the last card has been dealt from the deck. Actually you
only see it if the interim score is also a "top ten". After the last match, the end of game
procedure is entered and the top score dialogue is displayed. When this is dismissed, the
applause is heard and the game continues normally.
The problem starts in updateCards. This calls findAvailableMoves if there are no
cards left in the deck and it is the second time through. In findAvailableMoves the same
condition leads to gGameOver being set true. That is only overruled if there is a
highlighted card; which in this case there isn't. updateCards then calls endOfGame which
calls showScores if the current score is in the top ten. Finally, updateCards detects the
cleared pyramid, adjusts the score accordingly and continues the game.
The error can presumably be corrected by modifying the condition:
not gFirstTimeThrough and ubound(gTheDeck) = 0
A further check is needed to see if the pyramid has been cleared before assuming the
game is over.
George Bate
Good catch, George. Let's see about fixing that, shall we?
The difficult part of this fix isn't the fix itself -- it's testing to make sure that your fix
actually worked! It's tricky to get a pyramid that you'll win with an empty Deck.
George gave me his solution, which was to check to see if the top card is visible. That's
an excellent method. The logic goes like this: if there are no cards left in the Deck and
there are no more valid moves, there are only two possibilities: either the game is over or
the pyramid has just been cleared. If there's still a card at the top of the pyramid,
however, we know that the pyramid wasn't cleared and that the game is actually over.
So go to the updateCards routine and find the block of code that looks like this and
replace with this new code:
// See if there are any moves left
if not gFirstTimeThrough and uBound(gTheDeck) = 0 then
findAvailableMoves
if gGameOver and cardCanvas(1).visible then
endOfGame
end if
end if
The only change is the middle if-then statement, where we check to see if gGameOver is
true. This time we also check to see if the first cardCanvas is visible: if it is, we know
the game is truly over, so we pass control on to the endOfGame routine.
I finally did manage to hit this condition during a game and it passed, so I think the bug's
fixed.
Initializing the Scores
George also reminded me that we needed to initialize the scores to zero in our newGame
method: I had this in my original version of the program but forgot to transfer it to over to
the tutorial version.
In Detail
If you're wondering how I've been writing this tutorial, I had the complete game finished,
and I basically rewrote it, piece by piece, like you're doing each week, by copying code
from the original version and pasting it into the tutorial version.
Unfortunately, REALbasic does not let you open two projects at the same time, making
this method of working difficult. My solution?
I run two copies of REALbasic simultaneously -- right now it's version 4 and 3.5.2 -with a copy of the game project in each version. Then I can quickly switch between the
finished game and the in-progress game and add the code for that week's lesson. It works
great!
This is a useful tip for everyone, if you've got pieces of code you want from one project
into another one -- you don't have to keep opening and closing projects, just open each in
their own copy of RB. (I suspect they must be different versions, but I haven't really
tested that. I just did it this way because I happened to have both installed and it was
easier that way.)
Open globalsModule and find the newGame routine. Insert this at the beginning:
gPyramids = 0
gScore = 0
gFirstTimeThrough = true
gGameStarted = false
gGameOver = false
This just makes sure to reset the score to zero at the start of a game -- otherwise a
person's score would just increase from the last game!
Adding a Deck Label
During the process of fixing the end-of-game bug, I noticed a small detail that got
overlooked in our polishing lessons. I forgot to add a Deck label feature! (This was
something I had in my original version of the program.) A Deck label is just a small
StaticText label beneath the Deck that tells the user how many cards are left in the
Deck.
So drag a StaticText onto gameWindow and give it the following settings:
Now we just tell to display the number of cards left. Go to the drawCard routine. At the
very bottom of the routine you should fine three end if statements, like this:
end if
end if
end if
Before the last one we want to insert a bit of code. It should look like this:
end if
end if
if index = 29 then
DeckLabel.text = str(uBound(gTheDeck))
end if
end if
This just simply checks to see if we're drawing the Deck card (number 29), and if we are,
to write the number of cards left in the Deck -- the uBound of gTheDeck -- into
DeckLabel. Simple!
Mac OS X Symbol Font
One of the most obvious bugs in RBU Pyramid occurs in the Mac OS X version. When I
originally wrote RBU Pyramid last spring and tested in Mac OS X 10.0, it worked fine.
But in the fall Apple released 10.1 and something changed: the Symbol font no longer
draws properly!
Here's what RBU Pyramid looks like under Mac OS X (10.1 or better):
Yikes! That is ugly!
I don't know if the change Apple made is a bug or intentional, but it sure is strange.
There's nothing wrong with our code -- it's just that Apple made it so the card symbols
won't display.
To test this, I copied the card symbols to BBEdit with the font set to Monaco. Here's what
that looks like:
When I change the font to Symbol, we have our card shapes!
But here's the strange thing: if I copy that same text and paste it into TextEdit, it looks
like this:
Isn't that bizarre? It's the same letter, the same font, but what is displayed is different in
the two programs. My theory is that it has something to do with Carbon versus Cocoa, as
BBEdit 6.5 is a Carbon app and TextEdit is a Cocoa app. But then REALbasic programs
are Carbon -- so shouldn't they display correctly?
(If you want to learn more about the difference between Carbon and Cocoa, Geoff
Perlman, CEO of REAL Software, has written an excellent white paper on the subject.)
But the bottom line is that regardless of why this happening, we must come up with a
workaround: our use of the Symbol font in OS X isn't going to work.
So how do we fix this? It's not too difficult. Instead of using a font, we'll just draw
pictures. Pictures are more difficult to work with than fonts, as you'll see in a moment.
The first thing we need are a bunch of card graphics -- I've created an archive of them
here (they're also available as part of this week's project archive). We need four graphic
files, one for each suit: a spade, diamond, club, and heart.
However, we soon will run into another problem (part of the reason I went with fonts in
the first place). When a card is selected, it is highlighted by reversing the colors. In the
case of a red card, the red text shows up on black selected card, so we don't need to do
anything special there. But for the black text cards, we change the text to gray so it's
visible. But how do we do that with a graphic?
The answer is to create more graphics: in this case, a special version of each of the black
cards, a club and a spade reversed. So in total we'll have six graphic files. After putting
them inside our "Linked Graphics" folder, we drag them all into our project file. You'll
see they each import with the name of heart, spade, club, etc.
Now find your drawCard routine in gameWindow. This is the part we must modify.
Midway through the routine you should find code that looks like this:
g.textFont = "Symbol"
g.textSize = 30
g.bold = false
g.drawString theSymbol, 28, g.textHeight - 4
Replace that text with this:
g.textSize = 30
g.bold = false
g.textFont = "Symbol"
#if targetCarbon then
select case theSymbol
case chr(169) // Hearts
g.drawPicture heart, 23, 5
case chr(170) // Spades
if reverse then
g.drawPicture spadereverse, 23, 5
else
g.drawPicture spade, 23, 5
end if
case chr(168) // Diamonds
g.drawPicture diamond, 23, 5
case chr(167) // Clubs
if reverse then
g.drawPicture clubreverse, 23, 5
else
g.drawPicture club, 23, 5
end if
end select
#else
g.drawString theSymbol, 28, g.textHeight - 4
#endif
Wow, that looks a lot more complicated, right? Actually, it's not. We keep our standard
Symbol font drawing method for most versions of RBU Pyramid, so the majority of our
routine stays the same. All we're really doing here is intercepting the drawString
command -- before we use it, we check to see what platform we're compiling for. If our
target is is Carbon (Mac OS X), then we substitute pictures instead of the font.
In Detail
This is known as conditional compiling. It's a very common and powerful technique.
Obviously there are important differences between Mac OS and Windows, between Mac
OS and Mac OS X, and even PPC versus 68K (although starting with REALbasic 4, RB
doesn't support compiling for 68K Macs any more). Rather than have to write completely
separate projects for each platform, REALbasic lets you -- on the fly -- substitute the
correct code for the platform.
You could do something like this:
#if targetCarbon then
msgBox "This is a Carbon app."
#else
msgBox "This is not a Carbon app."
#endif
If you compiled Mac OS and Mac OS X versions of your program, they each would
display a different message when you ran them. Note: this won't work in the IDE, only in
the compiled application, because the IDE will always reflect the state of the environment
where it is running. i.e. if you're running the IDE under Mac OS X, targetCarbon will
always be true in the IDE.
There are a bunch of different environmental variables you can use to test what kind of
application is being created: Target68K, TargetWin32, etc. Read more about them in
RB's online documentation.
So how does our little picture-substitution thing work? Well, we already have a variable,
theSymbol, which contains the kind of card we are drawing. So we can just check that to
see what it is: for instance, if it's a spade, we draw the spade graphic instead of the font.
Note that for the two black cards, we also check to see if reverse is true, and if it is, we
draw the reversed version of the graphic.
So does it work? Well, run the program and see.
Oh no! Disaster! What in the world happened? Our Mac OS X version now looks like
this:
Sure, the graphics are being substituted properly, but they aren't looking right at all. For
instance, that heart is a white box not a heart shape. What can we do?
The answer is simple. We need a portion of our graphics to be transparent. That will get
rid of the white box effect. Fortunately, REALbasic makes this easy to do. Within your
project window, select one of the graphics. On the Properties palette, change the
transparency setting from "none" to "white." Do that for the four suit graphics (not the
reverse versions, or selected black cards won't display correctly) and save the project.
When you run it now, it should look like this:
Yeah! That's much better.
RB4 Instructions Bug
There still are a few more things we need to fix. For instance, REALbasic changed the
way editFields work: it used to be that if you changed the selection of text, RB would
automatically scroll to that line to make the text visible. Starting with REALbasic 4,
however, that is no longer the case. (Whether this is a bug or a feature, I don't know.)
The problem is that this change kills our Instructions Window: when the user uses the
popup menu to select a topic, the editField does not scroll to display that topic.
In Detail
Keep this in mind with future upgrades to REALbasic. Any time you compile your
program with a new version of RB, you should thoroughly check your program for new
bugs. Test every feature and make sure it still works. Sometimes RB has been changed to
work differently, and other times you depended on REALbasic acting certain way and
that behavior has changed in the new version.
This is easily fixed, however. EditFields now include cool new scrolling commands
(commands many in the RB community have been asking for for years). All we need to
do is to manually scroll the editField to the appropriate topic. It just takes one line of
code!
Open InstructionsWindow and go to the Change event of popupMenu1. Before the final
end if, put in this line:
editField1.scrollPosition = editField1.lineNumAtCharPos(i - 1)
What does this do? First, we know from our earlier code that i - 1 is the starting
character of the selected topic. (Remember, every character in an editField is numbered
from first to last.) But to scroll an editField we need to know the line number the
character is on, not the character number. The line number is tricky to calculate because it
changes depending on the font size and width of the editField (since paragraphs will
wrap to different heights depending on their width).
The answer is the new lineNumAtCharPos method: we pass this a character number and
it returns the line number that character is on. Then we send that line number to the new
scrollPosition method, which scrolls the editField so that line is the top line. Simple!
By the way: while this fix isn't needed in REALbasic 3.5.2, it works fine in that version
(it doesn't hurt anything).
As long as we're here within InstructionsWindow, lets update our help text. I've created
a new version of the rules which can grab here. Copy and paste that text into
editField1.
Final Polishes
We're almost done. We need to change the version number of our program. Open
globalsModule, go to the constants and find kVersion. Double-click on it and change
the value to 1.0. Now when you run the program, the correct version will display in the
About Box.
We'll also need to compile the various versions of the program. If you're using
REALbasic 4 Professional, you can compile a version for Windows as well as Mac OS
and Mac OS X. Here are the settings you'll want to use in the "Build Settings..." dialog
(found on the File menu).
Here's what the program looks like running under Windows 95 (within Virtual PC, of
course -- I don't believe in owning a real PC ;-).
There are a number of extra polishes we could, and probably even should do, to make our
program fit in better with the various operating systems. For instance, my brother, who's
a programmer for the Dark Side, once told me RBU Pyramid for Windows didn't quite
follow Windows' "Interface Guidelines." My initial reaction was to laugh out loud: there
are interface standards under Windows?
But he is correct: if I was serious about marketing this program to Windows users, I
should make sure it conforms with the standards on their platform, to the best of my
ability. That means fonts, shortcut keys, etc. would all need to be adjusted to be more
appropriate for Windows. (There are a host of these kinds of issues, and I'm just
sophisticated enough to know the problems exist, but I haven't had enough cross-platform
experience to know how to fix them.)
I also didn't test this with different versions of Windows: I only have Windows 95 and I
don't relished giving Bill Gates more of my money just to test my solitaire card game on
his various operating systems. (Isn't that what Windows is for -- playing solitaire?) I
figure if it works under 95, it should work on the others, but I can't confirm that.
Some of the same issues exist with the Mac OS X version. For instance, Mac OS X
programs have a Preferences menu option on the Application menu by default. Within a
REALbasic program, the menu is there but grayed out. To be a true Mac OS X
application, we should fix this, but it's a kludgy fix, requiring either a plug-in or an OS
call, and I won't bother with it for RBU Pyramid.
Overall, I'm pretty pleased with the program. It's a fun game, and I learned a lot in the
creating of it (and teaching about it). I hope you enjoyed it as well!
Final RBU Pyramid Application and Source Files
If you would like the complete REALbasic project file for the final RBU Pyramid 1.0
release (including all sounds, graphics, and other resources), you may download it here:
RBU Pyramid REALbasic Project File (335K)
If you're just interested in playing RBU Pyramid, here are the compiled versions of the
final 1.0 release:
RBU Pyramid 68K (858K)
RBU Pyramid Classic (1,000K)
RBU Pyramid Mac OS X (1,200K)
RBU Pyramid for Windows (638K)
(Since there was interest, I decided to compile a 68K version of RBU Pyramid with
REALbasic 3.5.2 which supports 68K. No warranties, expressed or implied.)
Finally, if you want to let your non-programming friends try RBU Pyramid (it is a fun
game), give them this permanent URL:
http://www.applelinks.com/rbu/047/index.shtml#download
That will take them right to this section for easy downloading!
REALbasic University Quick Tip
You'll notice in the above I named the files generically. For instance, for the Classic
version the filename is "rbupyramidclassic.hqx" -- there's no indication of a version
number.
When I first started releasing software, I would name my files with the version number
inside it, like "rbupyramidclassic1.0.hqx". The problem I discovered is that many
software download sites will link directly to your download file, and if you keep
changing the name, those links break.
You are therefore better off keeping the filenames generic. For instance, on my STS
website, "z-write.bin" is always the name of the current release of Z-Write. Older
versions, which I make available, are named with the version number. You can always
change the hypertext label to refer to the appropriate version number, but by keeping the
actual filename generic, others can easily link to it even when you update the program.
For REALbasic University, for instance, I'll update RBU Pyramid as needed (perhaps a
1.0.1 update), but I won't need to change the filenames at all: new downloaders will
automatically receive the latest version.
Next Week
We'll have a look back at RBU Pyramid and analyze what we accomplished.
REALbasic University: Column 048
RBU Pyramid XXI: Wrapping Up
RBU Pyramid is finished! Yeah! I'm sure we're all sick to death of card games, and it will
be fun to go explore other topics, but before we do, I'd like to take this column to
examine what we accomplished with RBU Pyramid, and analyze the success/failure of
RBU during this project.
Did you enjoy the creation of RBU Pyramid? Were the lessons too drawn out? Was the
pace too slow, too fast, or just right? I'm interested in your opinion, of course, but I'll
offer my own analysis as well.
My Original Goals
Wow, we've spent a whopping twenty weeks working on RBU Pyramid. That's more
columns than I originally anticipated (I was thinking of about a dozen), but then it's
difficult to judge a project like this until you actually do it.
(Guilt trip: Now that you see how complicated even a simple card game is to create, I
hope you'll all remember to pay your shareware fees next time you're asked!)
I know there are a few people who didn't like me spending so much time on a mere game,
but I felt this was a valuable project for a number of reasons.
First, I wanted to show the development of a complete, full-featured program, with
support for multiple undos, a help system, saved preferences, graphics, a high score chart,
etc. All the tutorials I've ever completed create something basic and ordinary. Many times
it's an example program, not even finished! When I first started programming, I found I
could easily finish a tutorial, but the real challenge was going beyond that: I'd decide I
wanted to add a tiny extra feature or enhancement (like a fancier About Box), and then
bang my head against the wall in frustration. If only there was source code showing me
how someone else solved those same problems!
I knew from the beginning RBU Pyramid was going to be an extensive lesson, but since
it's the kind of thing I always wanted when I was learning to program, I wanted to write
it.
My second goal with this series was to demonstrate the actual process of writing a
complete program. Beginning programmers often stumble through a project, learning as
they go, wondering the entire time if they're even vaguely on the right track. I wanted to
show that even seasoned programmers have to make complex decisions, compromises,
and even mistakes. That's encouraging for beginners, and the process of second-guessing,
revising, and correcting mistakes is valuable for everyone.
The third goal I had in mind was to continue the RBU tradition of actually explaining
why things are done the way they are. I hate tutorials that just say, Step 1, do this, Step 2,
do that, etc. If that approach worked, everyone who's done a painting-by-numbers kit
would be a famous artist!
If there are multiple ways to do something, I like to justify the approach I'm taking. If
we're learning about solving a particular problem, I like to explain the concepts behind
that situation, and detail alternative solutions.
So, how did I do? Did I meet my goals? Was the pace of learning too slow or too fast?
Only you, my readers, can answer that question, and the answer might be slightly
different for each of you. Overall, I'm pleased with the results of the project.
The aspect of following a complete project from concept to completion worked very
well: there were many small lessons that grew naturally out of the project context. Those
small lessons -- explanations about data structures, bug fixes, exchanging data with a
dialog, etc. -- are too small to have an article written about them, yet they crop up all the
time in real-life projects and are often the thing that frustrates a beginning programmer.
While the project dragged on a little longer than I'd anticipated, for the most part it was
because certain aspects of the project just needed more explanation, and I don't regret
taking the time to explain things. I do, however, wish I'd planned ahead just a tad bit
more: I had my version of Pyramid almost but not 100% finished before I started this
series, and that caused some problems in creating the tutorial. More significantly, I
should have planned in advance exactly which parts of the program I was going to
explain in which columns -- written a complete breakdown of the program in outline
form. Since I didn't, the columns wandered a bit, and I ended up needing a series of
"miscellaneous" columns to wrap things up.
There was one thing I had expected to do in this series that I didn't. When I conceived of
this type of tutorial, my original thinking was that it would follow, step-by-step, the
moves I'd originally taken as I created the program. It would be like looking over my
shoulder as I programmed. You'd see me make mistakes, revise my strategies, amd insert
debugging code in an effort to track down odd glitches.
I would still like to do that kind of tutorial someday, but it couldn't be on a project as
complex as RBU Pyramid. There were several reasons I didn't do RBU Pyramid that
way.
First, for that kind of tutorial to follow my every move, I must document everything I do
down to the smallest detail. In the case of RBU Pyramid, I'd already started writing the
game before I'd decided to use it as part of a REALbasic University project. By that time
I'd forgotten most of my early steps and it would have been artificial and inaccurate to go
back and retrace my steps.
Second, that level of detail would take too much time. It would be interesting to read and
there'd be value in it, but for a weekly column it would take over a year to finish the
game!
For example, look at the undo system I created for RBU Pyramid. For the column, I
presented that across a couple lessons and that was that. In real life, of course, I added the
undo system after the game was almost done (not a good strategy) and it took me some
degree of hassle to figure out how to wedge that into my existing design. There were
some major bugs and I spent several long Saturdays playing RBU Pyramid and
attempting to undo in an effort to figure out which actions didn't undo properly. If I'd
documented my entire process, it would have taken four or five columns. And that's just
for one feature!
Finally, I questioned how much value there'd be for you to wallow in my mistakes. Yes,
it might ease your ego a little to know that even a (hem) pro like me makes mistakes, and
yes, you might find my debugging techniques helpful in your own bug hunting
expeditions, but in many ways reading about that might be like reading two or three
rough drafts of Moby Dick!
So in the end, I decided to present a fairly straightforward, finished solution (minus a few
bugs), and in retrospect, I still feel that was the correct decision.
In Detail
By the way, if you're wondering why I'm performing this self-critical analysis, it's
because I desire to learn from what I've done. This is a valuable habit I learned from my
high school journalism teacher.
After every issue had been printed and delivered, the journalism class would gather to
critique the newspaper. We'd each grab a copy, a big red marker, and spend an hour
pouring over it for the slightest error. We'd mark obvious mistakes like spelling errors
and crooked headlines, but we'd also circle less noticeable problems like poor layouts,
awkward writing, or mediocre photography.
Good points were also marked, and we'd note if an area that was weak improved. For
instance, we had a period where our headlines were always done at the last minute and
they were plain and boring (like "Speech Team Wins Tournament"), so we made a
concious effort to improve our headline writing and the whole class was pleased when
headlines became more interesting and inviting (like "There's No Debate About It: Our
Speech Team Rules").
Because the whole class was involved, the critique became a sort of competition: who
could find the most errors? When everyone was finished, we'd all compare our findings,
and similar to the scoring of a Boggle game, only errors that no one else found were
considered valuable.
We didn't actually keep score, of course, and there was never any blame addressed for
any of the mistakes. It was all in the interest of education and improving ourselves, and I
found it wonderfully therapeutic and valuable. I do it today in many parts of my life: you
can apply the same type of analysis to anything.
In every project, there is always pressure and compromise, and often you'll feel like you
did well in the cirmcumstances, but in real life, when paper meets ink, circumstances
don't mean squat: it's all in the results. Taking time to look back and study what you did
can be humbling, but it's also rewarding, for you see your successes as well as your
mistakes.
In the case of RBU Pyramid, studying what was good and what was weak will help me
write better tutorials in the future, and we'll all be the beneficiaries. And it's good for me
to write about in public like this, because you'll read it and tell me if I'm right or wrong!
(Please do, by the way: feedback is what makes this process work.)
What We Learned
So what did we learn in developing RBU Pyramid? It's surprising, when you go back and
through the twenty lessons, how much we covered. Here are a few of the key techniques
and skills we studied:
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
Planning an interface
Developing a data structure
Drawing playing cards programmatically
Creating a card object (a custom canvas) that knows how to draw itself, be
selected, etc.
Using a control array
How to shuffle cards
Adding sound effects
Handling Preferences
Creating an About Box and Splash screen
Creating a "hot canvas" object (links to an Internet URL)
Using Contextual menus
Drawing a background pattern
Figuring out to detect valid moves
Adding a high score dialog
Remembering a window's size and position
Adding a help system
Adding an undo system
Adding a confirmation dialog box
•
•
•
•
•
Passing information between a dialog and your application
Working with menus
Safe shuffling
Adding a Mac OS X icon
Tracking and fixing bugs
While the tutorial was specific to RBU Pyramid, the basic techniques we covered are not.
You can reuse many portions in other programs: the Preference system, the About
Box/Splash screen system, the code that saves a palette's size and location, the help
system, the background pattern drawing, the undo system, the hotCanvas custom canvas,
etc.
Remember, REALbasic makes it easy to reuse code: you can drag a custom class,
module, or window from your project to your hard drive to create a copy of that on disk.
Then you can easily drag that into another project to reuse it.
REALbasic University Quick Tip
Starting with REALbasic 4, you can allow multiple programs to link to the same module
or class! This is a terrific feature: if all your programs rely on the same class, for instance,
you can make a single change to that class that will update all your programs (obviously,
you do have to recompile them).
To do this, drag a module or class into your project while holding down the Command
and Option keys. It's just like making an alias in the Finder: in fact, like the Finder, RB
will change the arrow cursor to show a tiny alias return arrow to remind you that you're
creating an alias.
Just remember never to delete that module or class from your hard drive: that's your
master copy and there's no longer a copy inside your project!
That's a Wrap
Well, that's enough of a wrap-up for RBU Pyramid. I hope you enjoyed the project and
learned something, and I hope you find time in your busy schedule to actually play the
game occasionally.
Next week we'll have a little change of pace, and then we'll get back into a series of
smaller, simpler, "one-shot" lessons for a while.
Next Week
A special surprise...
Letters
Proving that you can learn something new every day, I received a couple letters from
people explaining what's going on with the Symbol font weirdness I mentioned in last
week's column.
First, from Avi Drissman:
I'm not a usual reader, as I don't use REALbasic, but I followed a link from MacFixIt,
and I have an answer for you about the Symbol font problems you were having.
You were speculating that the differences between the behavior of BBEdit and TextEdit
were due to Carbon/Cocoa. In fact, it's due to the fact that TextEdit is doing Unicode
correctly.
In BBEdit, you typed ©ß ® (I hope these symbols come through the mail), changed the
font to Symbol, and you got the card suit symbols. That happened because BBEdit does
not (apparently) use the new Unicode-savvy font routines. The old font routines see an 8bit character, and show symbols the way they always did.
But TextEdit uses much smarter font routines. When you type ©ß ® into TextEdit, it
sees them as Unicode--U+00A9 (©), U+00DF (ß), U+2122 ( ) and U+00AE (®). The
card symbols you're looking for are actually somewhere else in Unicode: U+2660
(spade), U+2663 (club), U+2665 (heart), and U+2666 (diamond). So even though you
changed the font to Symbol, the font routines make sure that the actual symbols it's
displaying don't change. It figures that if you wanted the card symbols, you would have
given it the correct Unicode characters for them.
So how do you actually type those things? You need to turn on some keyboard layouts.
Go to the International preference pane, and click the Keyboard Menu tab. There are two
different keyboard layouts that would work. If you enable the Symbol layout, and use it
to type in TextEdit, you'll get all the symbols you expect, because the Symbol layout is
built so that all the keystrokes generate the Unicode needed to produce the same symbol
that you would have gotten with that keystroke in the Symbol font back in Mac OS 9
days.
A different keyboard layout that would also work is the "Unicode Hex Input" keyboard
layout. That lets you type the actual Unicode code points. For example, to get the spade
(U+2660), you pick that keyboard layout, hold the option key, and type "2660".
I imagine that REALbasic uses the new font routines, and that's why it stopped working.
Maybe one of the font routines that it used got smarter about Unicode from 10.0 to 10.1. I
can't really say.
Avi
I also received this nice explanation from the manager of Apple Computer's Font &
Language department (sorry about the special characters not being correct, but this is the
way they came through in the email and I'm not sure what they're supposed to be -- you
still get the message, however):
Dear Mr. Zeedar,
I read your recent RealBasic University column on problems displaying the Symbol font
on OS X, after seeing it mentioned on MacFixIt. I thought I would write to you to try to
clear up a little of the mystery.
This problem is caused by the interaction between an old technology and a new
technology. The old technology is WorldScript, which was Apple's approach to
supporting different languages, and the new technology is Unicode.
In WorldScript, each writing system has its own character set, and a given character code
(say, 0xBD) means different things in different "script systems" (e.g., Roman, Japanese,
etc.) and sometimes even in different fonts in the same script system.
In Unicode, an international standard (www.unicode.org), every character has a uniquely
assigned value; Unicode is a single character set that covers most of the world's writing
systems. The character code for a character does not change in Unicode, no matter what
language or font is being used.
So how does Symbol fit into this? Symbol is a font that uses its own, unique character
set, but declares itself to be a font that uses the MacRoman character set. You can see this
in your article when you change the font in BBEdit; the same character codes given
different glyphs.
The issue with Symbol and Unicode is that in Unicode, the characters in Symbol (like the
card suits, ’ôÝ’ô£’ô ’ô¶) have their own, unique character codes which are distinct from
the character codes used for characters like ©. You cannot change © to ՙ by changing
the font in Unicode. © is Unicode value 0x00A9; ՙ is Unicode value 0x2665.
So the issue is not Carbon vs. Cocoa; the issue is WorldScript vs. Unicode. All Cocoa
applications are based on Unicode; Carbon applications are a mix, though most still use
WorldScript at the moment. SimpleText and BBEdit are WorldScript-based applications.
Normally this is all handled transparently; when text on the clipboard is converted to
Unicode, Cocoa notices that the font is Symbol and assigns the right Unicode character
values. However, BBEdit does not put font information on the clipboard, and so Cocoa
had no way of knowing that the font in question was Symbol. If you try the same copy
and paste experiment from SimpleText to TextEdit rather than from BBEdit to TextEdit,
you'll see that the data comes across fine.
Once the characters were in TextEdit with the wrong character values, there was no way
to change them by changing the font. Cocoa does font substitution, so when you had a
© in your document and tried changing the font to Symbol, Cocoa said "Dear me! This
font doesn't have © in it. I'd better find another one to use." That's why you continued to
get © even though you had Symbol selected. In Unicode, to paraphrase Gertrude Stein,
a © is a © is a ©.
As to why your RealBasic application stopped working, that's hard to say; I think the
developers of RealBasic would have to look into it. It's possible either that the font
information was lost, or that RealBasic is using Unicode to draw and is converting the
information from the Symbol character set incorrectly. I can assure you that nothing
changed in Mac OS X 10.1 that would prevent Symbol from being drawn correctly; I've
verified that the characters display correctly in both Unicode and non-Unicode
applications.
Thanks for both of these explanations -- Mac OS X is quite a different beast than regular
old Mac OS.
Finally, we've got an interesting question from a reader in France:
Dear Marc,
I have a question about initializing scores to zero in newGame methode. I tried to fix this
little bug myself before your last column. I added at the end of the newGame code the two
lines:
gPyramide=0
gScore=0
It did not work. So I added a third line:
scoreCanvas.refresh
and it worked fine.
But I see that you put at the top of the methode only the two first lines, and it works. Can
you explain why the "scoreCanvas.refresh" was needed in the first case and not in the
second?
Thanks for an answer, and thanks a lot for the columns. (and sorry for my English!)
Jacqueline Gouguenheim-Desloy
Your English is no doubt better than my long-neglected French, Jacqueline! (Mon
français est terrible! ;-)
Your question is a good one: I've often seen situations where an item needed a "refresh"
to work properly, and it can be a mystery as to when it's needed and when it's not. All the
refresh command does is force the item in question to redraw. In your case, your code
did work -- the actual score was reset to zero -- it just didn't update the visible score the
viewer saw. Forcing the redraw made the new score display.
Normally REALbasic "knows" when an item needs refreshing and handles it
automatically. I don't know the specific order that RB follows when it decides to refresh
something, and I haven't analysed exactly what's happening in this case, but my suspicion
(and this is my unqualified and non-scientific answer) is that the change is happening too
fast.
In your case, you put the initialization stuff at the end of the method, while I put it at the
top. In my case, several other things happen before the end of the method (a fresh deck of
cards is dealt, for instance). That gives the initialization change time to "sink in."
My theory is that in your case, the value of the score is changed at the very end, but the
program has already moved past the "drawing the score" portion of the program and so it
draws the old value.
Scientifically, this is bunk: it doesn't make any sense at all. I mean, when you change the
value of a variable, it should be changed immediately. And a drawing routine that updates
the score ought to reflect that immediately. But I've seen too many times where that
wasn't the case and that's how I developed my "sink in" theory.
I certainly don't take my theory seriously, but it does make a good rule of thumb. Even
you must have instinctively sensed it because you added a refresh command when you
saw your scores weren't updating: why else would you think of adding that as a way
around the problem?
So the short answer is I don't know why this happens sometimes. But forcing a field to
refresh, unless you're in the middle of a long loop, is generally harmless. (In the middle
of a loop it will make your loop 100 times slower.) So refresh away and don't worry
about it: it's generally a minor problem (though it can be disconcerting to debug).
And maybe some RB expert out there has a better answer: I'd be happy to publish it in a
future column.
.
REALbasic University: Column 049
An Exciting Announcement
Faithful readers of this column may have noticed that postings have been a little erratic of
late: that's because I've been distracted. (And I'm not going to apologize. You'll see why
in a minute. ;-)
Eighteen months ago, when I proposed a REALbasic programming column to the good
folks at Applelinks, I decided to link the column with REALbasic Developer, an ezine I'd
conceived of back in 1999. I had a vague notion that someday, maybe, my dream of a
professional publication devoted to REALbasic would come true.
I've now been writing REALbasic University for just over a year -- this column is the
forty-ninth (!) -- and I'm very pleased to announce that my dream is becoming reality.
Starting in July, I'll be publishing REALbasic Developer, a new magazine devoted to
all things REALbasic!
REALbasic Developer isn't going to be an ezine, but a real, printed magazine. I feel
strongly that there's a market out there hungry for tutorials, tips, news, reviews and good
old fashioned basic programming information.
There's risk in going to print immediately, before a market has been established, but I feel
it's worth it. Despite all the hype about the Internet, most people still prefer their reading
material printed. An ezine just doesn't have the respect and magnitude of a printed
publication.
So REALbasic Developer will be printed. It will also be available in digital format
(Adobe PDF) for those who prefer their magazines virtual.
For the past six months I've been laying the groundwork for a top-quality magazine. I
convinced busy people like REAL Software's Joe Strout and authors Matt Neuburg and
Erick Tejkowski to serve on the magazine's Editorial Board (they'll also write articles, of
course). Between us we'll make decisions about the magazine's content, making sure we
cover a wide variety of interesting subjects, as well as articles that target beginners,
intermediate, and advanced users.
I've also signed up editors and writers, and the team is working hard to produce columns
and features which will fascinate and educate. I must insert that I'm impressed with the
depth and quality of these individuals: they're talented people, with a passion for
REALbasic, and their suggestions for articles and columns are top quality. In fact, it
seems our chief problem may end up being too much content instead of not enough!
(That's a great problem to have.)
For years I've been dreaming about this, making vague, distant plans, and now it's almost
reality. In case you can't tell, I'm very excited -- this is a huge adventure, an adventure I
hope you'll share with me.
What Will REALbasic Developer Be Like?
I'm going to give you, the readers of RBU, a sneak peak!
We'll have REALbasic news and announcements; reviews of products programmers can
use, such as books and software tools; interviews with prominent people in the
REALbasic community; a "postmortem" feature, which analyzes a project after
completion, revealing what worked and didn't work in the development process; in-depth
tutorials, explaining how to do various tasks in REALbasic. For example, for the first
issue, Joe Strout has written a great article on various methods of animating graphics in
REALbasic.
We've also got some terrific columnists who will write on a specific topic in every issue.
For instance, Matt Neuburg is writing an Algorithms column. That's going to be great for
folks like me, who aren't computer scientists. We'll finally get standard computer science
algorithms presented to us in REALbasic syntax!
We'll also have a column each for Beginners and Advanced users; columns on working
with 3D graphics, compiling for Intel, User Interface issues, and more. We'll have an
"Ask the Expert" section, where you can send in your toughest REALbasic questions and
we'll answer them.
We're even going to have the RB Challenge, a bimonthly programming problem you'll
have to solve. Those who come up with the best solutions can even win prizes! (And
don't worry if you can't solve it: the answer will be presented in the next issue.)
Need to promote your website? There'll also be a Classified Ads section, where readers
can advertise products, skills, or seek an expert REALbasic consultant. (We'll also be
accepting display ads for companies interested in reaching the vast audience of
REALbasic users.)
There's more, of course, but that gets you a taste. As we get closer to publication, I'll be
posting more details about the contents of REALbasic Developer on the RBD website.
Speaking of the RBD website, we have big plans for it as well. There will be some free
content for everyone, and subscribers will have special access to additional resources
(articles, source code, project files, discussion forums, etc.).
How can you help?
First, a publication like this needs subscribers. Lots of them. If you're interested in
REALbasic programming: learning it, using it, creating software for any use, you need to
subscribe to REALbasic Developer. It's going to be awesome.
The normal print subscription price for one year (six issues) of REALbasic Developer is
$32 (this is for U.S. addresses). But I've got a special one-time offer. I'm making
available a limited number of Charter Membership subscriptions at 50% off the normal
subscription price!
However, this offer expires April 15, 2002, and it's limited to the first 500 subscribers. So
subscribe now!
If you miss the Charter deadline, don't be too devastated: I'll be offering Early Bird
subscription rates at 25% off (that's $24 for U.S. addresses).
Some of you may prefer to wait until REALbasic Developer begins publishing this
summer: that's fine. Either way, it's a great deal. A subscription to RBD is about the cost
of a typical computer book, which isn't bad at all.
Some of you may prefer to receive your subscription in PDF format instead of print.
That's also an option we're offering, and the cost is half of the print subscription
($16/year). We aren't offering any special discounts for early digital subscriptions,
however.
Besides subscribing, there are other things you can do. Purchasing advertising (classified
or display) in REALbasic Developer will certainly help. We aren't planning on having too
many ads, but we would like to fill our quota.
You can also buy overpriced RBD clothing and merchandise from CafePress: we don't
make much money off it, but it looks cool, and it's great publicity.
Telling others about REALbasic Developer is a huge help. Link your websites to the
magazine site, and let everyone know you're a REALbasic fan. If you like, you can use
this small version of the RBD logo as a link graphic:
There's also always a need for contributors: if you have ideas for an article or review
you'd like to write, our door is always open. Even if you're not a writer but have a
suggestion for something you'd like to see in REALbasic Developer, tell us about it.
How will this affect RBU?
As of right now, the changes will be minimal. I plan to continue REALbasic University.
It's good publicity for the magazine, and I enjoy it. I have no plans to incorporate RBU
into the magazine, but I might mirror RBU on the RBD website as free content (new
columns will be posted on AppleLinks first).
In the short-term, there might be several consequences. For one, I'm very busy launching
the magazine, so it's possible over the next few months I'll miss a column or two (I'm
going to try my best not to, but there's no sense killing myself either ;-). Column postings
might be a little irregular as well (I shoot for Wednesdays, but don't always make it).
I will also shorten the columns. Some of the RBU Pyramid columns were over 4,000
words! (That's a lot for a weekly column.) I'll write tutorials that are more focused and
teach a single technique.
Long-term, the magazine will make REALbasic University even better. I'll be focusing a
great deal of my attention on REALbasic issues and I'll have more time to write.
I might make other changes, however: for instance, the magazine will feature a questionand-answer column, so I'm wondering if I need my Letters section. I'd keep it for
feedback issues, of course, but perhaps technical questions would be better served in the
magazine?
Conclusion
I hope you're as excited as I am about the magazine. It's a dream I've had for a long time,
and I love being able to help people. Sharing great REALbasic solutions to problems is
just totally cool.
I also hope you didn't mind me using this column as a plug for the magazine, but it's such
a huge event, I felt it was warranted. (Don't worry: RBU won't become a shill for the
magazine. In the future I'll mention what's happening at RBD, perhaps as a news item,
but I don't plan to devote entire columns the publication.)
Next Week
We get back to programming with the SpamBlocker utility.
Letters
For our letters this week, we have a follow-up on the "refresh" issue addressed last time,
and some comments on my critique of the RBU Pyramid tutorial.
First, Jacqueline from France writes back:
Dear Marc,
Thank you very much for your long and interesting answer. It made me thinking a lot. I
read again the two methods "newGame" and "freshDeck" and I noticed the following
facts: the last line of "newGame" is "freshDeck" and the last line of "freshDeck" is:
gameWindow.refresh
In the Language Reference, Rb says in Window Class methods
"refresh : repaints the entire contents of the window".
So, when I put the two lines
g.Scores = 0 g.Pyramids = 0
at the end of newGame, the "gameWindow.refresh" was performed with the old scores
and scoreCanvas remained unchanged. But you put the two lines at the top of newGame
and when gameWindow "repaints its entire contents" the scores were already initialized
to zero. So it worked perfectly, scoreCanvas was refreshed by the window with the good
scores. I tried to put the two lines just before the last line of newGame (i.e. before
"refresh") and it works good. It seems very clear now, and your "sink in" theory is the
good one!
I enjoy to can exchange and discuss with someone who knows much more than me. The
Pyramid was very instructive, but it is necessary to read and read again and think about
all the lines of code! I found the "Undo" method specially difficult. Please go on with
columns like these ones. Thanks again
Thanks, Jacqueline! I'm glad you're learning.
Next, we've got some comments from Harrie, who signs himself "A Mac addict in
Tennessee."
Marc,
I think that you did a fantastic job with RBU Pyramid. Sure it took 20 lessons to get it
done but considering all that was covered along with the fact that I know this isn't your
only task in life, I think that the number is very reasonable.
I recently retired from a major electrical utility company in Michigan after 33 years of
developing computer programs and systems on IBM mainframe computers. So while I
am a novice at writing code for the Mac OS, I have a long experience at writing code.
The last 15 years I was the main support person for the company's in-house time sharing
system which was IBM's VM operating system which ran on its own machine. A lot of
the code that I wrote was in the form of programs that the user could use to make it easier
for them to interface with major systems offered on the computer. As well as coding
systems I designed classes and wrote manuals that were compilations from a number of
IBM manuals aimed at better explaining what the typical user would need to get their
tasks accomplished. So I am very familiar with what it takes to aid people in learning
something new and therefore am very familiar with the effort that you have put in to
RBU University. Knowing that I commend you for what you have done. I also say that I
am very appreciative for what you have done. It is never an easy task to jump in to a
totally new language, and this is my very first OOPs language, believe it or not. Yes, RB
does a lot for you but there is still a lot of coding the user has to do and a lot of little
tricks to learn. Thanks to RBU pyramid I now feel much better about creating the
preferences file and an undo system in my programs along with a number of other little
tricks that you have brought out to date.
Thanks for your effort and thanks for all the knowledge that you are so willing to share.
Just one more thing that I would like to mention. In all of my years writing mainframe
code I was often teased about the method I used in writing the code. Believe it or not, I
always wrote every line of code on paper before I ever sat down at the screen and entered
it in to the editor. I originally did this because when I started things were done on
punched cards and you were lucky if you could get two runs in one day. By writing all of
the code on paper I would find logic errors before I even entered the code in to the
computer. I could see where some original idea was weak and improve on it before I
went to the computer. It would also inform me that I did not have all of the information I
needed in some instances and could go back to the user(s) and get the answers needed.
When machines got faster this way of doing things was ingrained in me and I never
stopped. The funny thing was that in my career I rarely had bugs in my code when it went
in to production. I would have customers sometimes ask for additional features that
neither they or I thought of in the initial design phase. I will admit that I am not as
faithful at doing that with REALbasic but then I am only writing code for my own
personal use so I don't have to worry about someone else suffering if a bug crops up.
Keep up the good work and may our upcoming project that you have mentioned be a
huge success.
Sounds like you've had an interesting career! Your point on paper coding is excellent.
While I rarely write out actual code on paper, I often make sketches of my data structures
or interface. It's faster that doing it on computer, and changes are easy. Almost every time
I do this I catch mistakes I would have made if I'd just dived into the project without a
few seconds of thought first.
Dear Marc,
I have found your recent RBU Pyramid columns invaluable, as I try my hand at a project
which, sadly, is not feasible in Hypercard. It takes a lot of code to make a program that
actually does something novel and useful as opposed to a glorified "Hello World" and it's
not often that you get someone willing to make a tutorial on this scale.
I also learned a lot from the Beginning Mac Programming book and the MyPaint
program, for the same reason.
Best wishes
Peter G, Quebec City
Peter was also kind enough to give me a brief French tutorial. :-)
Finally, an enthusiast letter from Drew, who obviously has a problem with his Shift key
(though he surprisingly gets REALbasic correct ;-).
I absolutely love your site man!! I desperately wanted to learn a programming language
for the longest time. I tried C, Java, and UNIX but I got bored after awhile, i felt like i
was getting nowhere so i stop. Then I heard about REALbasic, downloaded it and tried it
out... i love it. everyday i'm at my computer trying to learn more. Now that i found your
site i will be in tune. You are doing a wonderful job.
drew
Thanks to everyone who reads this column, and I hope you'll all become part of
REALbasic Developer as well. Here's to an exciting future!
.
REALbasic University: Column 050
SpamBlocker: Part I
This week we're going to start on a simple but extremely useful utility. Those of you who
own or create web pages are probably aware that if you include an email link on your
website, sooner or later some slimy person will grab that email and use it to send you
unsolicited email. In short, you'll get spammed.
Spammers don't actually visit your site personally, of course. They need to harvest
millions of email addresses. So they send out software robots that search the web for
foolish people who publish emails on their websites.
Of course, if you don't publish an email address, it's difficult for people who you want or
need to get a hold of you (like customers) to do so. So what's the solution?
Some people solve this by putting extra stuff in their email addresses like
[email protected] The email address won't work until you remove the
"REMOVEME" text. Others write out the email in words: marcatrbudotcom.
Those are poor methods, however: I doubt my mom wouldn't bother with the effort of
figuring out how to send you an email. You're practically guaranteed to lose customers if
you try those systems.
There are some complicated solutions involving Javascript, but we're going to look at a
much simpler method that helps obscure your email from spambots.
Some clever HTML experts figured out that web browsers are designed to convert special
characters first, before they process the HTML. Therefore, if you encode your email links
as HTML special characters, the spambots, which are looking for "mailto:" references,
won't find any on your pages.
HTML 101
For those of you who use WYSIWYG HTML editors and aren't comfortable with raw
HTML codes, a bit of review might be in order.
Briefly, to make an email link on a website, you enclose the text you want to be the link
with anchor tags with an href (hyperlink reference) pointing to your email address. It
looks like this:
<a href="mailto:[email protected]">Click here to send me mail.</a>
What we want to do is obscure this by encoding the portion in quotations as HTML
character entities. An HTML character entity is either the ASCII number of a character or
the name of a character, enclosed within an odd group of letters.
For example, the copyright symbol can be written as either of these:
&#169; or &copy;
An HTML character entity is always preceded by an ampersand and ended by a
semicolon. If you're specifying a character by ASCII number, precede the number with a
number sign.
So if we replace all our "mailto:" text with & and ; gibberish, the spambots won't be able
to tell we've hidden an email link on our web page! But web browsers will still be able to
read the link and from the reader's perspective, it will work normally.
SpamBlocker Design
There are a number of web pages and freeware programs that will use the system
specified above to convert an email address for you. The way these usually work is you
type in an email address and it will convert it to HTML gibberish which you can copy
and paste into your HTML file.
That's pretty cool, but if you need to convert more than a handful of emails or a few
pages, it's a lot of copying and pasting.
This was my situation as I was setting up the REALbasic Developer website. There were
a number of different email addresses (corresponding to different departments and people
at the magazine) and multiple pages. The thought of copying and pasting all that HTML
was depressing.
Immediately, I thought of REALbasic. How hard could it be to create an RB program to
do all that work for me? Even better, I decided my program would work more efficiently:
with it, you could drop a whole bunch of HTML files and it would convert all the
"mailtos" in all the files in seconds!
I fired up REALbasic and in less than ten minutes I had SpamBlocker working. A few
extra minutes of polishing and bug fixing, and I was using my little program for real
work!
Is it any wonder I love REALbasic?
The Interface
This is SpamBlocker's wonderful, intuitive interface. Obviously, it could be made
considerably prettier, but remember I did this in ten minutes. ;-)
To create this, I started a new project and dragged a staticText onto Window1. I put
checked the "multiline" option on the Properties palette and added some instructional
text. (This is surprisingly important for a simple "interfaceless" application like this.
When you come back in six months, you'll wonder how to use it.)
This is really all the interface SpamBlock needs, since it's only going to accept file drags.
The Code
The first thing we need to do is tell Window1 to accept file drags, so we go to Window1's
Open event and put in this:
me.acceptFileDrop("text")
Since we haven't defined the file type "text" we must do that before we forget. Go to the
Edit menu and choose "File Types..." and in the dialog, click "Add..." and fill it in like
the graphic below.
What does this do? It just tells REALbasic what kind of file "text" is (it's a text file).
Since we've told Window1 to accept file drops of that type, a user won't be able to drop a
graphic or application or Word document on the window.
(If we just accepted every kind of file, you'd have to manually check the types and put up
an error dialog for invalid files. This way the incorrect file will just "bounces back" when
the user attempts to drag it in.)
So, what happens when one or more files are dropped on the window? Well, those files
are sent to the DropObject event. Let's go there and add some code on what to do with
the files.
while obj.FolderItemAvailable
parseFile(obj.folderItem)
if not obj.nextItem then
exit
end if
wend
beep
msgBox "All done!"
Simple, isn't it? The obj object is a DragItem. A DragItem can contain more than one
thing (like several folderItems), but you can't access them willy nilly. Basically, obj
initially points to the first item. When you call obj.nextItem, it returns true or false,
depending on if there are more items. If there are more items, obj is set to the contents of
the next item.
So to grab several items, you must either memorize each item as you look at it, or process
it immediately. In our case, we're doing the latter.
First, we check to see if there is a folderitem available (by checking
obj.FolderItemAvailable). If there is, we're in a while-wend loop. The loop will
repeat while obj.FolderItemAvailable is true, so that means until there are no more
folderItems inside obj.
Next, since we know we've got a folderItem inside the DragItem object, we can do
something with it. Rather than include our processing code here, we'll create a separate
method to do the real work. For now, we just go ahead and put in the method's name and
send it a file (a folderItem). Our method is called parseFile so
parseFile(obj.folderItem) will call it.
After we've processed the file, we check obj.nextItem to see if there's another file. If
there's not, we call exit, which kicks us out of the while-wend loop. (If there is another
file, the loop just repeats and processes the next file.)
When we're out of the loop, we're done, so we beep and put up a message to that effect.
As I said, simple.
No source file this week, as there's not much code yet. We'll finish this next week and I'll
give you the complete REALbasic project file.
Next Week
We finish SpamBlocker.
News: REAL Software's Cubie Awards
Like they did last year, REAL Software is again sponsoring the Cubie Awards for great
achievements with REALbasic. Winners receive free REALbasic upgrades and other
prizes.
The contest ends April 26, 2002, so everyone needs to nominate their favorite
REALbasic software in each category before that date. To nominate someone, or some
product, send email to [email protected] Don't be shy, either. Nominate
yourself! (Keep in mind this isn't a vote; stuffing the ballot box isn't necessary.)
Products must be created in REALbasic and be Premier partners in the Made with
REALbasic program. The entries will be judged on quality, fitness to the task (fun game?
useful utility?), polish, verve, and brio.
Each winner in the nine categories (Advocate of the Year, Cross-Platform, Developer
Tool, Game, Internet, Mac OS X, Utility, Overall, and Business) will receive prizes.
The Categories:
Mac OS X: The best application for Mac OS X. It need not fall into any particular
category, and can also support the classic Mac OS (and heck, Windows too). Business:
The best tool for conducting some sort of commercial activity, whether it is a traditional
business application or a tool for creating stuff that makes money for the user.
Game: The best game. We will know it when we see it, can be pure entertainment,
action, strategy, educational, you name it, it's all good.
Internet: Best tool for the Internet. This could be anything useful in that rather large
arena, from a server tool, to a decoder, player, testing tool. If it uses IP (or its cousins) or
some known protocol or format, it's in!
Cross-platform: The best example of an application that embraces both the Mac OS and
Windows.
Utility: The best application that does something useful. This too is intentionally broad!
All it has to do is crank out the utiles, and we will be all over it.
REALbasic development aid: This includes plug-ins, classes, modules, frameworks,
pre- and post-processors, anything that helps some REALbasic developer get the job
done.
Overall: The very best REALbasic application we receive. It can be a double winner, or
it can be one of those None of the Above fliers that just blows us away.
Advocate of the Year: Finally, there's a special category for the REALbasic advocate of
the year, that person who has done the most for the community, and helped make
REALbasic known everywhere as the truly great tool that it is.
Letters
This week, Steen writes with a follow-up about RBU 023.
Hello
I have read some of your fantastic column about Realbasic, and especially column
number 023.
I was glad to see how we use LittleArrows for numbers.
But how about letters?
After several hours I had to give up, and ask for help. But before I printed this mail I did
a search for it on the net. But I did not find much of interest.
My question is simple: How do we replace the number with letters in LittleArrows? (only
1 letter at time)
I'm looking forward for your answer.
Best regards
Steen Horjen
While I'm not sure exactly what you're wanting to do with this, the principal of advancing
letters is exactly the same as for numbers. In fact, letters are numbers, from the
computer's point of view.
The major difference between making a LittleArrows control that advances/decreases a
letter instead of a number, is that there are a finite number of letters, so we must add a
check to make sure we don't outside the appropriate range.
The second difference is that we no longer use val() to convert the text to a number:
instead we use asc() which returns the ASCII number of the letter. (Note that this only
works for a single letter, not a string.)
Here's the revised code for the Down event of a LittleArrows control (it assumes you've
got a staticText with a single letter A to Z as the text):
dim n as integer
n = asc(staticText1.text)
if keyboard.optionKey then
n = n + 10
else
n = n + 1
end if
if n < asc("A") then
n = asc("Z")
end if
if n > asc("Z") then
n = asc("A")
end if
staticText1.text = chr(n)
staticText1.refresh
Once we've got the current letter's ASCII number, we can increment or decrement that as
appropriate. Then we just check to make sure it's within range (I've arbitrarily decided to
limit it to capital letters in the range A to Z for this example, but you could limit it any
way you wanted). Note that I've made it "wrap around" -- if the person goes above A the
next letter is Z, and below Z is A. As a user, I always prefer that as a shortcut versus
having to click the down arrow 26 times to get to Z.
The Up event is just like this, except it subtracts instead of adds.
I hope that helps you, Steen!
.
REALbasic University: Column 051
SpamBlocker: Part II
Last week we started SpamBlocker, a little program to obscure HTML email links within
web pages. We started off building our interface and setting the program up to handle text
files dropped onto the main window. Now we'll finish up with the code that searches the
text and converts the emails to gibberish.
The ParseFile Method
This is the core of the program. This is where we search through the file and convert any
"mailto" URLs we fine.
The steps we follow are these:
1. Load in the text file
2. Find and replace all the "mailto" references
3. Save out the new file
Create a new method (Edit menu, "New Method") and name it parseFile. For
parameters, put in f as folderItem. Click okay.
Here's the code:
dim
dim
dim
dim
in as textInputStream
out as textOutputStream
s, searchString, replaceString as string
i, j as integer
searchString = "<a href=" + chr(34) + "mailto:"
in = f.openAsTextFile
if in <> nil then
s = in.readAll
in.close
i = inStr(s, searchString)
while i > 0
replaceString = ""
i = i + len(searchString)
j = inStr(i, s, chr(34))
if j > 0 then
// Found one, so convert it
replaceString = "mailto:" + mid(s, i, j - i)
replaceString = convertString(replaceString)
// Insert it
s = mid(s, 1, i - len("mailto:") - 1) + replaceString + mid(s,
j)
end if
i = inStr(i, s, searchString)
wend
out = f.createTextFile
if out <> nil then
out.writeLine s
out.close
end if
end if // in = nil
Woah. It looks like a lot of stuff. But it's not so bad when we break it down.
Initially we define the variables we'll be working with. We then set searchString to the
actual text we'll be searching for: <a href="mailto:. Note that chr(34) is a doublequote mark, like ".
Next, we attempt to open the file as a text file. The variable in is a textInputStream
object. If for some reason the file fails to open, in will be set to nil, so skip the file. If
it's not nil, we process it.
First we read in the entire contents of the file and put the text into s. Then we close the in
object (we're done with it). (Technically, the file will eventually be closed automatically
by REALbasic, but it's still a good habit to do it yourself.)
Now we start another loop. This one searches until it can't find searchString inside s
(the file). We tell it this by assigning i to the results to the inStr() function. inStr()
returns 0 if it can't find what you're searching for; otherwise it returns the character
number where the found string starts.
In Detail
If you're not that familiar with inStr(), put this code in a button and try it:
// Returns 2
msgBox str(inStr("Mischief is my cat.", "is"))
// Returns 9
msgBox str(inStr("Mischief is my cat.", " is"))
// Returns 1
msgBox str(inStr("Mischief is my cat.", "Mischief"))
// Returns 9
msgBox str(inStr("Mischief is my cat.", " "))
// Returns 0
msgBox str(inStr("Mischief is my cat.", "q"))
Assuming our search string was found, i is the starting number. We next want to find the
end of the "mailto" URL. Since the HREF element is enclosed in double-quotes, we
search for a double-quote (chr(34)). But we don't want to start the search at i -- since it
occurs first, the search would stop at the " within searchString!
So we add the length of searchString to i, so we start searching at the end of
searchString. We put the result of this search into j. The range i to j now represents
the email address. The difference between the two variables is the length of the email.
We can use that info to extract the email with the formula mid(s, i, j - i). This
grabs the string starting at i and it grabs j - i characters.
In Detail
This may not be clear, so here's a diagram.
Assume the text is our file. At first i would be equal to 19, then 35 when we add the
length of searchString to it. Since the end quote mark is at 51, and 51 - 35 = 16, if we
grab the 16 characters starting at 35, we've got the text highlighted in yellow!
Once we've got the text we need to convert, we pass that to convertString, a method
we'll write in a minute that actually encodes the email.
The new string is inserted into the file:
s = mid(s, 1, i - len("mailto:") - 1) + replaceString + mid(s, j)
The first mid() function grabs the first part of the file (everything up to the find point, i).
By subtracting the length of "mailto:" we get rid of the "mailto:" in the original file and
use the one we encoded with convertString. We add in our replaceString (which has
been encoded) and then tack on the remaining text in the file. (Remember, if you don't
tell mid() to return a certain number of characters, it returns everything to the end of the
string.)
The result of all this is a new s that contains everything it did before, except the email
address itself has been encoded.
We then start the search over again, looking for another "mailto" line.
Our final step is to write out the new file. We set out to a textOutputStream object, and
we only work with it if it doesn't equal nil. If it's a valid textOutputStream, we write
all of the new s to the file, erasing the old file in the process.
Important Note: for this particular program, I chose to overwrite the original file. This
is not smart. When you're testing SpamBlocker, make sure you try it on test or backup
files, not irreplaceable files, until you're positive it's working correctly. Once you've
overwritten a file, there's no way to get back the original!
The ConvertString Method
We're almost done: we just need to write the method that actually encodes the email
address. This is fairly simple.
First, create a new method (Edit menu, "New Method") and set it up like this:
Now add this code:
dim i, n as integer
dim s as string
s = ""
n = len(theString)
for i = 1 to n
s = s + "&#" + str(asc(mid(theString, i, 1))) + ";"
next
return s
Ooh, complicated, isn't it! ;-)
As you can tell, we pass in the string we're wanting to convert as theString. We obtain
the length of it and set up a for-next loop from 1 to that length. Then we examine each
letter in the string.
For each letter, we put "&#" in front of it plus the ASCII number of the letter and end it
with a semi-colon. The mid() returns the individual letter. Asc() returns the ASCII
number of that letter. Str() converts that number to a string so we can add it to s. We
finish by adding a semi-colon.
Once we've processed all the letters in theString, we return s, which contains the same
letters encoded as HTML entities. Simple!
That should pretty much do it. Go ahead and save and run the program. I've created a
testfile.html which you can use to test SpamBlocker. Here's what it looks like after
conversion:
It looks weird, but the email links still work fine! How effective this is at stopping Spam
is another question, but it can't hurt.
Extending SpamBlocker
I've just been running SpamBlocker from within REALbasic, but if you wish, you could
compile it to disk as a stand-alone application. If you did that, you'd probably want to
make it support dropping files directly onto the SpamBlock icon (instead of just the
SpamBlocker window).
I leave that as a task for you, but I will give you a few hints:
•
•
•
You'll need to add an application class object to SpamBlocker.
You'll put the code that handles dropped files within the app's OpenDocument
event.
You'll need to check the "icon" checkbox within the file type dialog (if you don't,
your compiled app won't accept dropped files).
Another improvement you could make would be to have SpamBlocker support handling
dropped folders (right now it only accepts text files). To get that to work you'd have to
add a new folder file type (use the popup menu and choose the "special/folder" item), tell
the window to accept folder drops, and then recursively parse the contents of the folder(s)
dropped.
So those enhancements are your homework assignments, if you're so inclined.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
Something cool and exciting, of course!
Letters
Our first letter this week, concerns last week's column. REALbasic guru Thomas
Tempelmann alerted me to a technicality regarding my definition of ASCII.
The ASCII code only defines the "simple" U.S. characters, with codes from 0 to 127,
nothing more.
So, you can not call the #169 code an ASCII code, because there is no such ASCII table
defining that code. I believe the best term would be to call it the ANSI 8 bit char set, even
though both ANSI and ISO are standards committees that have numbers for the
individual standards. So, the correct ref would be something like ANSI 8997 or so. Just
that I don't remember the number for ANSI and ISO.
Thomas
Thanks for the clarification, Thomas. My explanation of ASCII in the RBU glossary is
more accurate, but I tend to lump all character numbers under ASCII, and that's
technically inaccurate.
Next, we've got a question from a medical doctor:
Hi-
I am an old-time programmer (FORTRAN, Pascal, BASIC era...) who is learning a few
new tricks. I'm delving in to REALbasic to implement a few projects to make my medical
office work more efficiently. Naturally, if I can make these work well enough and make
them usable enough, I hope to market them to other physicians too.
My problem in a nutshell is this. I want to implement a database of drug names in a
hierarchical fashion to ease selection of a particular drug for prescription writing, recordkeeping, etc. The hierarchy looks like this:
DrugClass0
DrugSubClass0
DrugName0
DrugName1
...
DrugNameN
DrugSubClass1
DrugName0
DrugName1
...
DrugClass1
DrugSubClass0
I'm looking for a database structure and RB code to implement something like the
"column view" in OS X for selection of a drug. Do you know if there is anything out in
the RB universe that does this?
In the same vein (that's a medical joke, BTW), I'd like to allow a user to start typing a
medication name and to have the software automatically complete (or suggest in dimmed
characters a completion) of the name with a popup list of possibilities based on the first
letters typed. You obviously know what I'm aiming at. I know that this is a different
question than my first one. I can visualize a possible solution using a database
implementation and a SQL select statement using wild cards for completion of the entry.
Do you know if there is code existing that I might find and use so that I don't have to
reinvent the wheel?
Thank you very much for your kind assistance!!
Harlan R. Ribnik, M.D.
Unfortunately, I haven't seen anything like the "column view" structure you're looking
for. It sounds like a great thing, and it seems like someone should have created a class
that does this, but I'm not aware of it. That doesn't mean it doesn't exist: if anyone out
there knows of a REALbasic class that implements a Mac OS X column view, let me
know and I'll publish something about it here.
Meantime, you could simply use a dynamic listbox that changes with what the user types
(similar to Claris Emailer's address book display of names that match the letters you
type).
I can, however, help you with the second part of your question. My own Grayedit class
gives you way to add autocompletion to your own projects. You feed it a list names and
when the user starts to type in a name, the completed name is displayed in gray, like this:
Pressing the tab key "fills in" the full name in black.
Grayedit is a free class, so download the sample project and try it out. It does require an
array of names, so you'd have to create a method to dynamically generate that list from
your SQL database (which doesn't sound difficult).
.
REALbasic University: Column 052
Zoomer: Part I
Many times I've been asked, "How did you learn all this REALbasic stuff?" My answer is
simple: I taught myself.
How do you teach yourself programming? You experiment, that's how. You play. You
try things. It takes patience, perhaps some research, maybe even some assistance from
someone else. But eventually you figure things out and get your program to work.
Sometimes these experiments are real programs you want or need. Sometimes they're just
learning projects, like my little Shooting Gallery game, which I wrote simply to learn
about sprites. Sometimes these projects are rather useless on their own: they don't do
anything but help you learn a particular technique.
I've got literally hundreds of REALbasic projects like that. That's how I learned. I'd try to
get RB to do something, or test a particular feature. So that's your word of warning: what
we're going to do today is rather useless. But hopefully you'll learn something.
DrawInto
REALbasic includes an unusual command, drawInto. In earlier versions of REALbasic,
it didn't work very well, but most of the bugs seem to be fixed in RB 4.
The online help describes the command like this:
When I first noticed this, it intrigued me. Apparently this would draw the contents of a
window into a graphics object. What good is that? Well, since a picture object
includes a graphics object, you could draw a window into a picture!
But what good is that? The first thing I thought of was that once you've got the window
as a picture, you could draw it larger: basically zooming into the window! This could be
cool, I thought. I could have a floating window that acts as a magnifier to a text field.
That way you could keep your text at a reasonable size (10 or 12 points) but be able to
read it in the zoomed window.
To test this, I wrote Zoomer, a simple experiment. It works pretty good, though I don't
know how practical it would be in a real program. But it was good for learning.
Creating Zoomer
Start a new blank project. On the default window, drag on an editField, a Slider
control, and several staticText objects. Arrange them to look something like this:
Set the properties of editField1 like this:
If you'd like, put some default text into the editField. That will make testing easier,
since it's rather pointless to zoom in on an empty editField!
zoomSlider should have these characteristics:
Now here's a little tip. StaticTexts are rather useless as controls. After all, they just
display some information. But if you add each staticText as a separate control, they
clutter up your Code Editor window:
But if we give all the StaticTexts the same name, making them a control array, only a
single object is displayed in the Controls list, which is easier on the brain.
So I've named all the statics "Label" and let them be numbered in a control array. (Since
we're not doing anything with these staticText objects, the actual numbers are
irrelevant, except for the bottom one [which has a default text of "4"] since that one we'll
change dynamically as the user drags zoomSlider. That staticText needs these
properties (especially note that the array index needs to be 3):
Next, we need to create a window for our zoomed view. Go to the File menu and choose
"New Window." Give it the following settings:
Guess what? That's all we need to do for zoomWindow! It doesn't even contain any code!
How is that possible? It's possible because everything happens in window1. From there
we'll control what's displayed by zoomWindow. The negative side effect of this is that
zoomWindow won't redraw properly since it doesn't know how to draw itself. (In a real
program you'd want to do this differently, but today we're just experimenting, so this
behavior is okay.)
Adding Code
Let's start by adding a couple methods to window1. First, let's create a method called
zoomAmount. This method will simply return the current zoom level (set via the
zoomSlider control).
Go to the Edit menu and choose "New Method" and give it the name zoomAmount and a
return type of Double. Then put in this code:
label(3).text = str(zoomSlider.value / 2) + "x"
return zoomSlider.value / 2
What we are doing here is adjusting the value of zoomSlider. We do this because a
slider control cannot move in fractions, only whole integers. By setting zoomSlider's
minimum and maximum values to double what we want and then dividing the current
value by two, we end up with the range we want but with the ability to zoom to odd
levels like 1.5x, 3.5x, etc.
The first line sets the staticText display to show to current zoom amount. The second
line returns the current zoom amount after calculating it. Simple.
But now we need to make the core of the program. This method is the one that draws the
zoomed area. For now, let's just put in an empty method. Choose "New Method" from the
Edit menu and call it drawZoom, with x as integer, y as integer as the parameters.
Leave the method blank for now: we'll finish it next lesson. For now, let's go to the
Events list within the window1 Code Editor. Find the event called mouseMove and put in
this line:
drawZoom(x, y)
We've just told the program that every time the mouse is moved within window1, to
redraw the zoomed window!
But if the user's typing in the editField, they wouldn't be moving the mouse, right? So
let's go to the Controls area, expand the editField1 tab, and go to the selChange
event. There we're going to put in this:
drawZoom(system.mouseX - window1.left, system.mouseY - window1.top)
Eek -- what is all that?
Okay, first a quick lesson in coordinates. Coordinates are horizontal (x) and vertical (y)
values that represent individual pixels on the screen. But there are different kinds of
coordinate systems. There's what's known as the global coordinate system: that's the
entire screen, including multiple monitors. Then there are various local coordinate
systems: those are the coordinates that represent a single window or object.
For example, in the following diagram, the yellow area represents the screen (obviously,
a very small screen ;-).
All the x and y values are in global coordinates. But inside the window, the white box,
which represents a canvas object, the lx and ly values are local coordinates. In this case,
they are local to the window. (Note that this is just the drawing area of the window: the
window's title bar is not part of the coordinate area.)
However, within the canvas, if you wanted to draw that red circle, you'd draw at local
coordinates relative to the canvas: g.drawOval(10,10,30,30) would draw a circle 30
pixels square at ten pixels in and ten pixels over from the upper left edge of the canvas.
Note that the canvas itself is on the window and thus canvas position info, like
canvas1.left, would be window coordinates.
All this just means you have to be careful where you get your coordinate information, and
be aware of what type of coordinates you are wanting. You may have to translate from
one coordinate system to another.
That's what's happening on our situation. Our drawZoom routine, which asks for two
coordinates, wants window coordinates. With mouseMove that wasn't a problem: the x
and y values passed to the mouseMove event were in window coordinates, so we just
passed them right on.
But now, within the selChange event, we don't have window coordinates to work with.
Instead, we use the system.mouseX and system.mouseY functions, which return global
coordinates. So we must translate those to window coordinates.
The translation is easy: we just subtract the window's left and top properties from the
global x and y coordinates. Look back at that diagram. See how the upper left of the
window is 42, 77? Subtract that from a global coordinate, like the 240 that's the width of
the window. 240 - 42 is 198. So 198 would be the rightmost pixel of the window (in
window coordinates)!
In Detail
The numbers in my diagram are approximate: REALbasic's calculations are exact, while
mine aren't taking into account things like the window's border. But the general idea of
what I'm conveying is accurate.
If you'd like to explore more about coordinate systems, play with this simple test program
I wrote: testcoords.rb. It displays local and global coordinates as you move the mouse:
So all we're doing in the selChange event is translating global coordinates to window
coordinates and passing those to our drawZoom routine.
Whew! That's enough of a lesson today. Next week we'll get into the actual drawing of
the zoomed area (which is somewhat complicated). To give you a taste of what to expect,
here's what our program will look like in action:
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We finish Zoomer.
REALbasic Developer Magazine Update
A few weeks ago I officially announced REALbasic Developer, the new printed
magazine I'm launching this summer. Quite a few of you chose to become Charter
subscribers, and I'm immensely grateful and pleased by your support. Charter
subscriptions were higher than I projected, which is wonderful.
While the Charter membership offer has expired, we are now offering "Early Bird"
subscriptions at a 25% discount. If you're interested in suscribing, you can save money
and support the magazine by buying a subscription now. Once we begin publishing in
July, subscriptions will be full price.
In other news, we're making great strides forward in this mammoth venture. Authors are
turning in some great articles, and we're working hard to make this not just the best
REALbasic publication, but a great magazine period. I'll post further updates in this
column as we progress.
Letters
Last week, in response to Dr. Ribnik's quest for an RB class that would display info
similar to Mac OS X's Column View, I mentioned I wasn't aware of one. As I suspected,
one does exist, as Owen revealed to me:
When I read your response to the letter asking about an RB implementation of OS X's
column view, I remembered seeing just such a thing. I thought it had been called
OpenBrowser or something along those lines, and it was pretty advanced seeming.
Unfortunately, I was unable to find it with google. However, I did find ColumnBrowser
by Amar Sagoo. It's a very basic version of this view, without any more advanced
features (ie, no finder icons of files & folders, no information about a file in the last
column when the file is selected, etc.) It is a good basic implementation though, so I
thought I'd pass along the info. Here's a link to it (it's the first thing on the page):
http://www.realbasic.com/learn/programmers/Class/ListBox/Premier.html
-- Owen
"In the beginning the Universe was created. This has made a lot of people very angry and
has been widely regarded as a bad move." - The Restaurant at the End of the Universe, by
Douglas Adams (1952-2001)
Thanks, Owen. Anyone who quotes Douglas Adams in his sig can't be all bad! ;-)
Next, we've got some puzzles from Sven:
How's it going? I just began learning REALbasic recently, I have 4.02 and I am running
it on Mac OS X (10.1.3). I am finding RBU very useful so far, even though I have run
into some complications, such as my Sprite Animation lesson not functioning, and neither
did yours, but in different ways. However my question isn't about that lesson.
I am a Game Art & Design student at the Art Institute of Phoenix, so I constantly am
coming up with game concepts galore. I have this project I want to work on that I
somewhat developed quite a while ago, and have now completely expanded upon. I have
some roots in BASIC, but am having some trouble making the switch to OOP, and it has
been some time since I coded anything in BASIC as well.
I have need for some help in doing such: I need to make a prebuffer for my sprites that
has the ability to scan a folder within the REALbasic project file, count how many items
there are, and find their names, and load them all, prior to their usage. i have tried to
implement it in a progress bar, but my code isn't getting what i want, i can only seem to
figure out how to grab contents from an outside folder, and then i can't seem to buffer
them, but i have the ability to find how many and what their names are.
Also i need some more help in the sprite animation itself, I have use for multiframe
objects, say a walking-left set of 3-6 frames or jumping set of 3 frames for example. I
need some more in running this. Also can masks be applied to sprites/spriteframes.
Another question, i need to know how one can change the users monitor settings, such as
in a games options window. Also I need to know how I can fade screens, and not the
quicktime effect of crossfading between two pictures, i need to be able to fade the
desktop to black to transition into the program opening, and then use this again for game
transitions. Is it possible to tap into the other monitor settings to do this as well, such as
dropping screen brightness? Or maybe there is a better means of doing so.
I have a lot of things drawn already, although hundreds more graphics are necessary. I
have worked out much for gameplay, and statistic settings and character creation, but i
have a long way to go, and i am learning as i go, trying to apply pieces of knowledge
from tutorials as i move ahead, but it is frustrating especially when what I've learned isn't
enough and I keep trying to jump ahead, and then going backwards...
Thanks for all your help. This game will be a task of all tasks, but i'm trying to work from
a small point and work outward.
(in the future i may be asking you to help with the interaction with worldmap and
collision detections, because a lot of it cannot be based on what pixels touch what. and
also the inventory system which involves the ability to select an icon by cursor and drag
it (becoming the cursor/or at least attached to it) and dropping it into an equipment slot
(i.e. hand, torso, head, etc.) and have it bring along all of its statistical information and
character modification data.)
Thank you for all your help, it will be greatly appreciated.
-Sev Gerk
About my sprite lesson not working: I've heard there are problems with sprites in Mac OS
X, so that could be it. There may also have been changes to sprites in more recent
versions of RB that cause problems with my older code. I'll revisit sprites again one of
these days and get those projects working again.
To your questions: you first ask about pre-buffering and bringing in pictures. I had a
detailed answer to a question along similar lines regarding pictures from an external
source recently. It was not for sprites, but the principals are the same (since sprites just
need a picture object to define what they look like). Read my answer to Charles, in
Lesson 043 and see if that helps get you going.
Next, can masks be applied to sprites? You can either set whites to be transparent, or you
can apply a mask to a picture and the assign the picture to the sprite. (See the
picture.mask in online help for more on masking.)
Finally, your question about controlling the screen settings of a user. You can do this via
a Declare statement, but you're probably better off using a plug-in. I don't know where
one of these is off the top of my head, but I know I've seen them, so they're available. I'd
do a Google search and see what you can come up with. (If another reader knows of a
plug-in that does this, let me know and I'll post it here next week.)
As to your future questions on collision detection and such, you're getting mighty
sophisticated, and beyond the scope of REALbasic University. I suggest you subscribe to
REALbasic Developer magazine, which will have room to cover more advanced topics
than I can do here. RBD has a question-and-answer column which will be perfect for
those kinds of issues.
.
REALbasic University: Column 053
Zoomer: Part II
Last week we started Zoomer, a little experiment to test out REALbasic's DrawInto
command. Our goal is to create a virtual magnifying glass that will display one window
enlarged inside another.
We set up the basics of our project and learned all about the difference between global
and local coordinate systems, which are significant for this project. Today we'll finish
Zoomer by creating the actual code that will display the zoomed window.
The drawZoom Method
We created the drawZoom method last time, but left the contents empty. Let's put some
code in there now!
The first thing we'll do is adjust the size of zoomWindow to make sure it's not bigger than
our zoomed in area. Why do this? Well, if we didn't, zoomWindow would attempt to
display more of a picture then there actually was. For example, say our zoomed picture is
200 pixels wide but the window is 300. Our drawPicture command tries to fill the entire
zoomWindow with the picture. Since the picture isn't big enough, random stuff from
memory ends up being drawn as a picture. It doesn't really hurt anything, but it sure looks
strange:
While there are other ways to prevent this from happening, I chose to keep things simple
and not let the user make zoomWindow bigger than the zoomed area. Since the zoomed
area is magnified in zoomWindow, the maximum size of zoomWindow changes based on
the zoom percentage set by zoomSlider. (We set the maximum size via zoomWindow's
maxWidth and maxHeight properties.)
Here's the code for drawZoom:
dim p as picture
dim x1, y1, zW, zH as integer
// Adjust zoomWindow size
zoomWindow.maxWidth = editField1.width * zoomAmount
zoomWindow.maxHeight = editField1.height * zoomAmount
if zoomWindow.width > zoomWindow.maxWidth then
zoomWindow.width = zoomWindow.maxWidth
end if
if zoomWindow.height > zoomWindow.maxHeight then
zoomWindow.height = zoomWindow.maxHeight
end if
zW = zoomWindow.width * zoomAmount
zH = zoomWindow.height * zoomAmount
// Allocate space for our zoomed picture
p = newPicture(zW, zH, 16)
// We should really check here to see if p
// is nil or not, but I'm too lazy ;-)
// We'll just assume there was enough memory to allocate p
// This draws the window into our picture object.
window1.drawInto p.graphics, 0, 0
// This part calculates what portion of the editField
// we're going to be drawing, based on the cursor
// position.
x1 = x - editField1.left
if x1 < 1 then
x1 = editField1.left
elseif x1 + zoomWindow.width / zoomAmount > editField1.width then
x1 = editField1.width - zoomWindow.width / zoomAmount +
editField1.left
else
x1 = x
end if
y1 = y - editField1.top
if y1 < 1 then
y1 = editField1.top
elseif y1 + zoomWindow.height / zoomAmount > editField1.height then
y1 = editField1.height - zoomWindow.height / zoomAmount +
editField1.top
else
y1 = y
end if
// This actual draws the zoomed picture.
// Note that zW and zH represent the magnified
// picture size.
zoomWindow.graphics.drawPicture p, 0, 0, zW, zH, x1, y1,
zoomWindow.width, zoomWindow.height
After adjusting zoomWindow's size (if necessary), we then allocate space for p, our
picture object. The size of p is the size of zoomWindow multiplied by zoomAmount. Since
16-bit and 32-bit pictures look almost exactly alike, we go with a 16-bit picture to save
memory (that's the 16 in the newPicture line).
Once we've got our picture object, we use the drawInto command to draw window1 into
the graphics object of p. The simplest way to understand the drawInto command is to
think of it like taking a screenshot of the window. In our case, we're storing that
screenshot into p -- and later we're drawing p in zoomWindow, except drawing it at a
larger size. Note that when we use the drawInto command, we set it to draw at
coordinates 0, 0 -- that means the upper left corner of window1 will be drawn at the upper
left corner of p.
In the case of Zoomer, our goal was to only draw editField1 larger, not window1 in its
entirety. That means we must constrain the "view" of zoomWindow so it only displays the
enlarged editField.
We do this in two ways. First, we've made the size of p relative to the size of editField1
(certainly no larger). Second, we control what portion of p is drawn into zoomWindow by
controlling where we begin drawing.
To understand this, we need to understand how the drawPicture method works. Here's
the method's description in REALbasic's online help:
The first three parameters are simple: the picture object and the coordinates of where to
draw that object. The parameters after that are optional. (Remember, stuff between square
brackets is optional.) The simplest use of drawPicture would be drawPicture p, 0,
0. But in our case, since we want to manipulate the size of the picture, we need to use
those extra parameters, which can be confusing. So let's explore exactly what they mean.
The first two extra parameters are the width and height of the destination: in other words,
the picture will be resized to fit this width and height. If the destWidth and/or
destHeight is smaller than the actual width and height of the picture you are drawing,
the picture will be shrunk as it is drawn. If destWidth and/or destHeight are larger than
the actual width and height of the picture, the picture will be enlarged. If you pass the
actual size of the picture, then it's drawn at actual size (no enlargement or reduction).
Just think of these two parameters as percentages in the form: width/height *
percent. So a command like this:
drawPicture p, 0, 0, p.width * 50%, p.height * 50%
would draw p at 50% size. Likewise,
drawPicture p, 0, 0, p.width * 150%, p.height * 150%
would draw p at 150%.
Of course, in reality, REALbasic doesn't understand percentages directly -- you must
decimals (like .5 for 50% and 1.5 for 150%). And of course the drawPicture command
won't work without the rest of the parameters on the line, so it's important you know how
to use those as well.
The next parameters are a little hard to visualize at first. These are the sourceX and
sourceY parameters. They represent the starting coordinates of where you'll begin
drawing from. If these are zero, you'd begin drawing from the upper left corner of p. But
if these are greater than zero, you'll start drawing further down and over into the picture,
effectively clipping off some of the picture's left and top by not drawing it.
It's hard to explain sourceX and sourceY without also explaining sourceWidth and
sourceHeight: those indicate how wide and high a chunk of p we'll grab to draw.
Between the four parameters, you can grab all of p or any rectangular sub-portion of p
and draw it, at any size you want (actual size, larger, or smaller).
To grab all of p, just pass 0, 0 as the sourceX and sourceY parameters, and p.width and
p.height as the sourceWidth and sourceHeight parameters.
To grab a sub-portion of p, pass the left and top coordinates of the rectangular area you
are wanting to grab, and the width and height of that rectangle. It's easiest to visualize
this with a diagram:
This shows you exactly what we're doing with Zoomer. The faint background picture is p
-- all of p. But since zoomWindow is smaller than p, we're only drawing a sub-portion of p.
The coordinates of the upper left corner of what you see in zoomWindow is what we pass
as the sourceX and sourceY parameters. The width and height of that sub-portion is the
width and height of zoomWindow.
Calculating those upper left coordinates requires a little math. Remember, we're grabbing
a rectangular sub-portion based on where the user's arrow cursor is located. The new
upper left coordinates are represented by x1 and y1. The actual location of user's mouse
are x and y.
As you'll remember from our last lesson, these are in local window coordinates. Because
of that, we want to translate these to editField1 coordinates. We do that by subtracting
the left/top of editField1 from x/y.
x1 = x - editField1.left
y1 = y - editField1.top
That gives us adjusted coordinates relative to editField1. If we just stopped there,
Zoomer would work. In fact, try it: temporarily comment out the if-then code as shown
below and try running the program:
// This part calculates what portion of the editField
// we're going to be drawing, based on the cursor
// position.
x1 = x - editField1.left
'if x1 < 1 then
'x1 = editField1.left
'elseif x1 + zoomWindow.width / zoomAmount > editField1.width then
'x1 = editField1.width - zoomWindow.width / zoomAmount +
editField1.left
'else
'x1 = x
'end if
y1 = y - editField1.top
'if y1 < 1 then
'y1 = editField1.top
'elseif y1 + zoomWindow.height / zoomAmount > editField1.height then
'y1 = editField1.height - zoomWindow.height / zoomAmount +
editField1.top
'else
'y1 = y
'end if
What happens is that the sub-portion we draw is not limited to just editField1. Zoomer
now zooms the entire window, and even beyond -- showing us garbage stuff from
random memory.
This certainly isn't fatal, and it is interesting, but it's not what we're wanting. Since we
only want to zoom editField1, we need to adjust x1 and y1 to make sure they're within
editField1's range. So all that complex-looking if-then stuff does is make sure x1 isn't
less than editField1.left or editField1.width!
The elseif portion of the code:
elseif x1 + zoomWindow.width / zoomAmount > editField1.width then
simply gives us a value that's back to the actual (non-magnified) width. Remember,
zoomWindow's width represents a zoomed width. If we compared it to
editField1.width we'd be comparing apples and oranges. By adjusting it back to actual
size (via division of the zoom amount), we compare the actual width amounts of the two.
This ensures that we don't let zoomWindow display stuff past the right side of
editField1.
(I'm just explaining the width portion here, but it's exactly the same code for the height,
except we use the height values.)
The result of all this is x1 and y1 values that are within range of editField1's position on
window1. With this formula, x1 will never be too far left or too far to the right, and the
same with y1 never being too high or too low.
The final bit of code is the "simple" drawPicture command. Now that we've done all our
calculations, we just draw the sub-portion of the picture. We start drawing p at 0, 0, the
upper left corner of zoomWindow. We pass zW and zH, our zoomed (enlarged) width and
height values, for the width and height parameters. All that work we did with x1 and y1 is
now used since we tell drawPicture to start drawing from those coordinates. And
finally, we tell it the width and height of the sub-portion of p we're drawing is the width
and height of zoomWindow (meaning that zoomWindow will be entirely filled with a
zoomed graphic).
zoomWindow.graphics.drawPicture p, 0, 0, zW, zH, x1, y1,
zoomWindow.width, zoomWindow.height
That's not so bad, now that you understand it, right? It's a little confusing keep track of all
those similar-looking parameters, but REALbasic 4 does help. When you click on the
drawPicture text in the Code Editor, it will display a tip window which gives you a brief
summary of the command and its parameters:
Once you understand how the command works, the shorthand is usually enough to
refresh your memory without having to scroll through the online help for the complete
description.
Whew! That was a lot of stuff, but we made it through. While this little demo doesn't do
anything particularly useful, you hopefully learned some valuable techniques from the
lessons. Here's an animation of Zoomer in use:
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We make our own painting program. So to speak.
Letters
Our first letter is from William, who asks a very common question about REALbasic.
First, thank you for your recommendations on my last question about fractions, which
you posted in a previous column. Your suggestions helped in the direction of my project.
However, I have a new problem, as my projects keep growing in size.
Is there such a thing as a global variable? I would like to set d as an integer and have
various windows or buttons adjust the numerical value of d. While I can set d as an
integer globally, it's value is not global. I have found a work around by using static text
with visible false and setting it as d in all the windows and buttons, but it seems there
should be an easier way. Am I overlooking something?
Just add a property (variable) to any module. A module is a collection of properties,
constants, and methods. You get it via the File menu (choose "New Module"). Anything
you add to a module is global to your entire program.
Think about it: if you add a property to a window, it's within that window, so it's not
global. But a module isn't within anything but your project, so it's global. (It makes sense,
but REAL Software needs to explain this better.)
BTW, you can refer to a window's properties via the object chain. For instance, say
you've got Window1 with a boolean property called isDone. Another window or routine
could refer to isDone (even though it's not global) by saying Window1.isDone.
Sometimes that's helpful and better than using globals. (Remember, globals use memory
the entire time your program is running.)
Next, we hear from a guy named Squid. At least, that's the name he gave in his email. (I
swear I don't make this stuff up!)
Hello!
Good job on making a great column, it helped me more than a book I got that was
supposedly for "beginners"! Anyway, I'm writing a program, and I can't figure out how to
make a button pressed in one window, open up another window. I know it sounds really
basic (no pun intended), and it was probably answered in an earlier column, but I just
recently found RBU and haven't even gotten to lesson eleven.
Hi Squid!
What you want to do is easy: in the Action event of a the button, just type the name of the
window you want to open, add a period, and the Show method. Like this:
window2.show
You can also do:
window2.showModal
That will make the second window open as a modal dialog (stopping all other action in
your program), if it's a dialog and not a document window type.
For more on this, look through some of the RBU programs where we handle dialog
boxes.
.
REALbasic University: Column 054
SimplePaint: Part I
Occasionally, my boss at the printshop where I work brings one of his children in for a
few hours. The youngest now is age 5, but a few years ago the oldest was that age. Kids
that young have short attention spans, and it was always a challenge finding something
entertaining to keep them out of trouble.
Drawing always amused them, and we discovered they were fascinated by drawing on the
computer. So I'd load up a paint program on an extra Mac we have and let them play
around. Initially I tried Adobe Photoshop, since that's the bare minimum of a paint
program in my book. Of course that was far too complicated for the girls, so I switched to
ClarisWorks. That was sort of satisfactory, but I discovered I had to keep a close eye on
them: without supervision they'd click outside the window, bringing up the Finder, and
then they'd create all sorts of havoc, moving folders around and typing gibberish for file
names.
For three or four seconds I considered searching for a free "KidPaint" type program on
the Internet. But I was too lazy to find and test all those programs. Instead, the idea
quickly hit me: I'd write my own.
I loaded up REALbasic and in just a few minutes I had a very basic paint program. It was
so much fun I quickly added some cool sophisticated features. The end result is a very
simple paint program specifically designed for kids. Sure, there's probably cooler or
better stuff out there, but this did exactly what I wanted and the price was right.
Program Specifications
Since this program was targeted for children, I had a narrow range for my specifications.
I came up with the following mental list of criteria:
•
•
•
•
•
•
No menu bar
No floating palettes or windows
All commands should be one keyboard letter or mouse click
Saving shouldn't require navigating a save dialog
No dialog boxes at all, in fact
No way to switch applications or accidently quit
I decided that since the girls found the whole "tool palette" thing confusing (it's
fundamentally modal), I wouldn't offer the traditional drawing program options of letting
you draw circles, lines, or rectangles. Instead, there'd only be a pencil tool. In this
program there's only one mode: painting with pixels and that's it.
Since the girls rarely wanted to go back and edit their creations, I decided not to bother
creating a way to reopen previous drawings. The girls would draw something, save it,
erase the screen, and start again.
There was also no point in creating a way to print the pictures: if they wanted, I'd print
the saved images for them out of Photoshop.
A Blank Screen
In the old days of REALbasic, hiding the menubar was a difficult trick requiring system
calls or a plugin. With REALbasic 4, however, it's easy. Just uncheck the
MenuBarVisible property of a window!
Create a new project. Rename window1 as paintWindow and give it these settings:
Note that checking the HasBackColor property gives the window a white background,
which is what we want since that will be our drawing area.
Go ahead and save your project in a "Simple Paint" folder with "SimplePaint.rb" as the
name. (Note that ".rb" is the extension REALbasic uses for project files under Mac OS X,
so it's good to use that even under earlier operating systems.)
Remembering the Drawing
Since we'll be doing various kinds of manipulations of our drawing (saving, editing, etc.),
it's a good idea to store it inside a picture variable.
Open paintWindow and add a new property (Edit menu, "New Property") p as
picture. As long as we're here, lets add some other properties we'll need.
To get SimplePaint working as a bare bones paint program, we don't have to define these
right now, but we're thinking ahead. The c property will hold the color we're currently
painting with. The xWidth and yWidth properties represent the size of the paintbrush
we're using. Eventually we'll add a way to modify these properties so the child can pick
different colors to draw with and change the size of the paintbrush.
Initializing
Before we can begin drawing into our picture variable, we must initialize it (in technical
terms, allocate [set aside] memory [storage] for the variable). We do that with the
newPicture method. The picture must be as large as our window (which is as large as
the main screen, since we checked the window's FullScreen property). We also want the
picture to be full color, so we use 32 as newPicture's parameter (meaning the picture will
be 32-bit color picture). (On machines without much memory, you could get by with 16
or 8, though 8-bit color is only 256 colors.)
Since this must happen early in the program's launch, let's put it in our drawing window's
Open event. Put this code in paintWindow's Open event:
p = newPicture(screen(0).width, screen(0).height, 32)
if p = nil then
beep
msgBox "Sorry, there's not enough memory to run SimplePaint."
msgBox "I must quit now."
quit
end if
self.backdrop = p
xWidth = 9
yWidth = 9
Note that if there isn't enough memory for p, we quit the program. Not exactly the most
polite way to quit, but better than a crash. (If you wanted, you could try to allocate the
picture at 32 bits, and if p = nil then try again at 16 bits. Just make sure you again
check that p isn't nil after that.)
Once we've got a valid p, we assign that to paintWindow's backdrop property. That
means whatever the picture p looks like will be the background of paintWindow. This
way we don't have to do any drawing or redrawing of p -- that happens automatically
whenever paintWindow needs it.
Finally, we set the default sizes of our xWidth and yWidth values.
Watching the Mouse
All painting programs work on the same basic principle: when the user clicks the mouse
button, a pixel is drawn at that click location. Ours is no different.
Go to the MouseDown event of paintWindow and add this:
return true
This just tells REALbasic that we'll be handling the MouseDown event. The default
response is to return false, meaning that we're ignoring the MouseDown. By returning
true, REALbasic will now allow the MouseDrag event to happen. And that's where we'll
do most of our work.
In Detail
Wondering why we use MouseDrag instead of MouseDown?
That's because MouseDrag is an event that happens repetitively, while MouseDown only
happens once, when the mouse button is initially clicked. Once the mouse button is
down, MouseDrag reports the current coordinates of the mouse cursor as the user moves
the mouse around. That's the data we need.
In the MouseDrag event, we'll add this code:
dim x2, y2 as integer
x2 = x
y2 = y
drawPixel(p.graphics, x2, y2)
You might wonder why we bother declaring the x2 and y2 variables -- wouldn't it be
easier to just pass x and y on to drawPixel?
Yes, it would, but we're again thinking ahead: in the future we may want to manipulate
those x and y properties.
The final line passes a graphics object (from p, our picture), and the drawing
coordinates, to our drawPixel routine. What drawPixel routine you ask? The one we're
about to create!
Drawing a Pixel
To actually get anything to draw, we need to create that drawPixel routine we just
called. Add a new method (Edit menu, "New Method") and name it drawPixel. Put g as
graphics, x as integer, y as integer on the parameters line. The code is fairly
simple:
dim n1, n2 as integer
n1 = xWidth \ 2
n2 = yWidth \ 2
g.foreColor = c
g.fillOval(x - n1, y - n2, xWidth, yWidth)
self.refreshRect(x - n1, y - n2, xWidth, yWidth)
What are n1 and n2? They are the center point of the "pixel" that's going to be drawn.
Remember, our "pixel" isn't actually one pixel in size: it's the size of xWidth and yWidth
(which we set to 9 by default). We'll draw that extra-large pixel using REALbasic's
fillOval method.
When REALbasic draws a circle, the coordinates where you start drawing that circle are
the upper left of the box the circle is drawn in. For example, the red circle below is drawn
by giving it the dimensions of the outer box (upper left corner and width and height).
In a sense, a circle is just a box with extremely rounded corners!
But for our purposes, we want the center of the circle to be the x and y coordinates where
the user clicked. That means we must do a little math to adjust those drawing coordinates
so they're half the width and height of the circle off from the original coordinates. So by
starting to draw the circle to the left and up, the center of that circle is at the original
coordinates.
Next, we set our drawing color to c and we draw the circle (oval, since the width and
height could be different). Finally, we tell paintWindow to refresh (update) the area we
just drew.
Simple, eh? Try it. The program works and will let you doodle:
But there's no way to change the drawing color, the size of the brush, or save the picture.
We'll work on that next time.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We enhance SimplePaint with a color palette and other features.
News
The development of REALbasic Developer magazine is proceeding well. Articles have
been written and edited, and we're working on the layout of the first issue. Here's a sneak
peek of what the cover of the premiere issue looks like:
Subscription pre-orders are now available at 25% off the normal price.
Letters
Several people wrote to tell me that our last project, Zoomer, does not work under Mac
OS X. Well, it half works: the editField is magnified, but the text inside it is not.
Apparently some of the bugs that used to exist with the drawInto method still exist under
Mac OS X (in the past, drawInto had a bug where it would only draw the first few lines
of the editField and not draw text that had been scrolled).
First, I want to apologize for not testing Zoomer under Mac OS X. That was an oversight.
More and more people are using OS X for development and I'll try to make sure
everything we do for RBU works for both platforms. At minimum, if something won't
work, I'll let you know.
Second, if you'd like to get Zoomer working under Mac OS X, you can try this
workaround. It won't fix the editField's text not being shown bug, but it will give you
something to magnify.
Open the drawZoom method. Do a find/replace with these settings:
Click the "Replace All" button.
What this does is let Zoomer magnify the entire window instead of just editField1.
That gives you some stuff to look at -- if you want, drag some more controls onto
window1 to give you more things to zoom.
Next, Julian writes with a problem with popupMenus.
Hi,
Firstly keep up the good work - your column is excellent and very informative. I have
been able to use many of your techniques for my own stuff - thank you.
I am trying to use PopUpMenus to navigate up and down a hierarchy of lists and data and
have met a small problem.
When a PopUpMenu is populated dynamically from within a program it does not show
any text until the listIndex is set, like:
andSomeAfterPopup.listIndex = 0
The trouble is that I do not want this update to trigger a 'change method.' I only want to
trigger the change method code say:
linkField.text = me.text
when the user clicks on the PopUp and chooses a new value.
I have coded around it by setting a global 'gMouseWithinAfterPopUp' so the change code
reads:
if gMouseWithinAfterPopUp = true then
linkField.text = me.text
else
do nothing
end if
And of course the MouseEnter and exit events flip the global.
This solves the problem but is not what I might call elegant.
Have I missed something here, or do you know of a better way?
Also I agree that much of a RB programme is self documenting but it might be an idea
for a comment/description box at the bottom of each new Method and Property dialog
box - with the facility to print/ not print the comment/descriptions. I tend to set up a
'dummy' method if I want to write a saga about a program.
Yours at sea
Jules.
The problem you are describing is very common. While your solution is one method, it's
complex. A simpler solution, which is what I frequently use, is to create a "myChange"
variable that tells my code it's a programmatic change, not a user action.
So your initialization event would look like this:
myChange = true
andSomeAfterPopup.listIndex = 0
myChange = false
Then, within the Change event, you have code like this:
if not myChange then
// Put whatever you want to happen
// when the user changes the popup here.
.
.
.
end if
I use this technique frequently when I implement an undo/redo system. That's because an
undo system generally saves a user action in some kind of buffer (in order to possibly
undo it later). The problem is when you need to redo a user's old action or restore data to
its previous state via undo, that action will be regarded as a user's action: your program
can't tell the difference between something you do programmatically (via code) or
something the user is doing. But creating a boolean variable that tells the routine that this
is a programmatic action means you can avoid doing what normally might happen when
the user does that.
Still, this solution requires extra work on your part. What would be nice -- we ought to
suggest it to REAL Software -- would be a built-in global boolean that would return true
if a call to a routine was made programatically and false if it was made by the user.
You'd still have to enclose your code with a check to this value, but you wouldn't have to
worry about setting the boolean yourself.
As to your other comment, about having a description area within methods, it's not a bad
idea. Until REAL Software adds that, most people add a dummy method to describe a
class, something like this:
.
REALbasic University: Column 055
SimplePaint: Part II
In our previous lesson, we got the basics of a paint program going. This week we must
add some features to make it more complete.
Adding Color Capability
Because SimplePaint is for kids, we don't want confusing interface elements like floating
palettes and toolbars. So how do we implement a way to change colors? Simple: we make
the color palette part of the drawing!
Little kids can't tell the different between a floating palette and something that's part of
the drawing area anyway, so we'll just make a color palette that's part of the drawing area.
That way they can't unintentionally move or close it.
Save this image to your SimplePaint project folder:
Now import it into your project by dragging it from the Finder to the project window.
That should add an italicized item called "palette" to the list of objects in your project.
(The italics tells you the object is imported but not embedded -- that is, it is linked to the
original object on your hard drive. That means if you delete the original, your project
won't compile.)
Next, we need a canvas object within which to display the picture of our color palette.
Drag a canvas -- that's the rectangle with the sky picture (not the sunset) -- from
REALbasic's tool palette to paintWindow. Give the canvas these property settings:
Good. Now we just need to add some code to make the color palette work -- actually
change the color of our drawing tool.
It's really very simple. Double-click on paletteCanvas to open its code editor. The
Paint event is probably selected, but we want the mouseDown event. Switch to it and put
in this line of code:
changeColor(me.graphics.pixel(x, y))
The part within parenthesis simply returns the color of the pixel that the mouse cursor is
pointing at. Remember, me means the object that contains the code -- in this case,
paletteCanvas. The graphics object of paletteCanvas contains whatever is drawn
there (i.e. our palette graphic), and the pixel method returns the color of the pixel at the
x, y coordinates passed.
So all this line does is send the color clicked on to a method called changeColor. Now
we just have to write that method!
Go to the Edit menu and choose "New Method." Fill in the dialog like this:
Here's the code for the method:
dim tempC as color
tempC = newColor
if tempC <> c then
oldC = c
c = tempC
end if
This routine is very simple. We store the new color in a temporary variable called tempC.
We compare it to c (which is the current drawing color). If they don't match, there's been
a color change. So we save the current color into oldC and make the current color match
tempC (the new color).
But sharp readers will realize this isn't going to compile: we have never defined oldC!
Even sharper readers are wondering: what is oldC -- why are we saving the old color?
The reason behind oldC is that in a moment we're going to add a cool (but ridiculously
simple) feature: a way to toggle between the previously used color and the current color!
For that feature to work, we must remember both the current color and the previous
color: hence the need for oldC. So let's add the property: go to the Edit menu, choose
"New Property," and give it this declaration: oldC as color.
Now run the program. It should work great: if you click on a color in the color palette, it
will begin drawing in that color. Since white is one of the colors on the palette, you can
even erase now!
But enough playing around. Let's add that feature I was just talking about. Go to
paintWindow's keyDown event and put in this code:
// Swap colors
if key = " " then
changeColor(oldC)
end if
As you can see, this swaps the current and previous colors if the user presses the space
bar. Try it -- it's pretty cool!
But here we notice a little problem: there's no way to know what the current color is
without actually drawing a dot. Wouldn't it be better if there was some indicator of the
current color?
Adding a Color Indicator
Let's add another canvas right under paletteCanvas. Give it these settings:
So we know what this is, give it a label (drag on a staticText -- the tool palette item
with the 'Aa'):
Your paintWindow should look something like this:
Now we just need to put in some code. Double-click on colorCanvas and put this code
into the Paint event:
g.foreColor = c
g.fillRect(0, 0, g.width, g.height)
// Draw border
g.foreColor = rgb(0, 0, 0) // black
g.drawRect(0, 0, g.width - 1, g.height - 1)
As you can probably figure out, this just fills the square with the current color. Then it
draws a black border around the box. Why draw a border? (Hint: what would this canvas
look like without a border if our current drawing color was white?)
But if you run the program now, you'll see that the current drawing color never changes!
What's up with that? Surely we've done everything correctly.
The answer is that yes, we've done everything correctly, but we've forgotten one small
detail. ColorCanvas is only drawn when the Paint event happens. Since the Paint event
normally happens only after a dialog box or a window from another program covered up
the control, our program, which is designed to run by itself, taking over the entire screen,
will never automatically redraw itself!
So the solution is we must force the control to redraw. We can easily do that with a
colorCanvas.refresh command. So simply go back to our changeColor method and
make it look like this:
dim tempC as color
tempC = newColor
if tempC <> c then
oldC = c
c = tempC
colorCanvas.refresh
end if
Cool! Whenever our drawing color changes, colorCanvas is forced to repaint itself. Now
when you run SimplePaint, the current color is reflected in the color indicator box.
Another Problem
You may notice, however, that our program lets the user draw right over the controls
we've added. That looks rather lame:
I've got a solution, but I'd like to challenge you to come up with yours before next week.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We fix the control-drawing problem of SimplePaint, and add more features.
News
REALbasic Developer magazine is pleased to announce the immediate availability of
Classified Advertising. Classifieds are text-only ads in various categories offering or
seeking products and services.
Classified Ads in REALbasic Developer are a fantastic value. Reach thousands of
REALbasic users for as little as $12! Ads get even cheaper if you run the same ad in
multiple issues. They're a great way to promote your business, website, product, plugin,
"Made with REALbasic" software, recruit employees, etc.
To order a Classified Ad, simply fill out the interactive online form which will tell you
the exact cost of your ad, and pay via credit card.
The deadline for advertising in the premiere issue of REALbasic Developer is May 22,
2002, so place your ad today!
Letters
Our first letter this week comes from Charles, who ran into a problem with the Windows
version of RBU Pyramid.
Marc,
I added the scroll code to use with RB 4 in my Instructions help menu. And it works. But
here is my question, when I compiled my project for a Windows build, and use the
popupmenu that has this code, the text is highlighted in black. The black disappears when
you click the edit field. My question is: Is there a way to control for the highlighting so
that is not unsightly black.
By the way, on the Mac it is a light yellow.
Charles
Strange bug, Charles. I know very little about Windows compiling, so I'm not sure of the
"correct" solution to this. On the Mac, the color of highlighting is controlled by settings
in the Appearance control panel and REALbasic uses that setting -- perhaps there's an
equivalent setting on the PC (but either RB's not using it or it's set to black).
But in truth, RBU Pyramid was originally written with REALbasic 3 before RB had
programmatic scroll support of editfields. The way to scroll then was to move the cursor
by selecting some text and the editfield would automatically scroll so the selection was
visible. Now that RB supports scrolling, there's no need for the help topics to be
highlighted.
You can either turn off highlighting with an editField1.selLength = 0 command, or
use the editField1.scrollPosition method to scroll to the appropriate line. (The
latter method is probably best, since you can scroll the help topic headline to the top of
the editField. In fact, we do that in the bug fixing session of RBU 047.)
Next, Ersin writes:
I want to make a program that the user can give 2 numbers and the program calculate the
sum and write the sum of the numbers.
I don't know how I can make this.
Thank you
Hi Ersin!
We've briefly covered things like this before, but to summarize, the problem you are
dealing with is that editFields contain strings (text) and number variables (integers or
doubles) contain only numbers. You cannot do math operations on text and you cannot
do string operations on numbers.
Fortunately, REALbasic provides methods for you to convert from one type to the other.
The val() function turns a string into a number. The str() function does the reverse
(number to string).
So to add two editFields together and put the result in a third editField you'd do
something like this:
editField3.text = str(val(editField1.text) + val(editField2.text))
The two val() functions turn the contents of editField1 and editField2 into numbers
so you can add them. The addition is enclosed by an str() function which turns the
result, which is a number, into a string, which is put into editField3.
These first two methods, as you've probably figured out, do not work:
// Adding strings doesn't add but
// concatenates (joins) them, so
// "1" + "3" = "13"
editField3.text = editField1.text + editField2.text
// Now we're adding numbers, but you
// can't put a number into a string without
// first converting it to string with the
// str() function
editField3.text = val(editField1.text) + val(editField2.text)
// This works!
editField3.text = str(val(editField1.text) + val(editField2.text))
.
REALbasic University: Column 056
SimplePaint: Part III
At the end of last week's lesson, we had SimplePaint working with different colors, but I
showed how we had created a problem, allowing the user to draw into the area of our
color indicator:
Obviously, we need fix this.
Preventing Drawing
As they say, in the gruesome words of days past, there's more than one way to skin a cat.
I'm sure there are dozens of ways to stop the user from drawing in a certain area, and no
doubt creative readers came up with some interesting ideas, but I, of course, being
infinitely lazy, prefer the simple approach.
Let's add a new canvas to paintWindow. Give it these settings.
Now your nodrawCanvas might be covering up your paletteCanvas -- if so, simply
select nodrawCanvas and choose "Move to Back" from the Format menu.
To prevent drawing within nodrawCanvas, we must add some code to our mouseDrag
event. Find that event (it's within paintWindow's Events list) and right after the initial
dim x2, y2 as integer line insert this:
dim w, h as integer
w = nodrawCanvas.left + nodrawCanvas.width
h = nodrawCanvas.top + nodrawCanvas.height
if (x < w) and (y < h) then
return
end if
Guess what? We're done! This simply checks to see if the cursor is within
nodrawCanvas's area, and if so, it returns without doing anything (no drawing). Simple!
If later we decide to add more "controls" to our program, we can just make
nodrawCanvas bigger and our program will work without any changes.
Visualizing the Brush
One of the features we want to add to SimplePaint is a way to change the size/shape of
the brush. Because of the way we set up the program, this is remarkably simple to do.
There are two things we must do to add this feature. One, we need a visual way to see the
current shape of the brush. Second, we need a simple interface the user can use to change
the shape/size of the brush.
Because it's more complicated, let's start with the visual display. This will be similar to
the color indicator we added last lesson. First, add a staticText label with these
settings:
Next, add a canvas with these properties:
Your paintWindow should look something like this:
Double-click on shapeCanvas to open its code editor. For the Paint event, put in this
code:
dim x, y as integer
// Calc center coordinates
x = me.width \ 2
y = me.height \ 2
if c = rgb(255,
g.foreColor =
g.fillRect(0,
end if
drawPixel(g, x,
255, 255) then
rgb(0, 0, 0) // black
0, g.width - 1, g.height - 1)
y)
// Draw border
g.foreColor = rgb(0, 0, 0) // black
g.drawRect(0, 0, g.width - 1, g.height - 1)
As you can see, this first figures out the center of the square -- that's where we'll draw the
brush shape.
Next, we check to see if the current color is white -- if it is, we fill the entire box with a
black background (otherwise the white brush would be invisible).
Then we call our drawPixel routine, telling it draw a "pixel" at the center of
shapeCanvas.
Finally, we draw a black border around shapeCanvas, representing the maximum size of
a brush.
Quick Quiz: why do we draw the border last? (The answer is that if we did the fill last, it
would cover up the border!)
There's one more step we have to do get this to work. Just like with colorCanvas, we
must force shapeCanvas to redraw. So go to our changeColor routine and insert
shapeCanvas.refresh after colorCanvas.refresh.
Now run the program -- you'll see that the sample brush shape changes color whenever
you click on a different color.
Changing the Brush Size
We still need to create a way to change the brush size. The simplest way to do this is with
keyboard commands. How about the plus and minus keys to enlarge and shrink the
brush?
Kids probably won't mess with the shape of the brush much, but we'll make that a feature
since it's easy to do. We'll use the numbers on the numeric keypad to adjust the shape.
Numbers 4 and 6 will shorten and lengthen the horizontal dimensions (width) of the
brush, and 8 and 2 with stretch or shrink the vertical dimension. The + and - keys will
enlarge and shrink both dimensions.
Go to the KeyDown event and add this code to what's already there.
if key = "4" then
xWidth = xWidth - 3
if xWidth < 3 then
xWidth = 3
end if
shapeCanvas.refresh
end if
if key = "6" then
xWidth = xWidth + 3
if xWidth > 30 then
xWidth = 30
end if
shapeCanvas.refresh
end if
if key = "2" then
yWidth = yWidth - 3
if yWidth < 3 then
yWidth = 3
end if
shapeCanvas.refresh
end if
if key = "8" then
yWidth = yWidth + 3
if yWidth > 30 then
yWidth = 30
end if
shapeCanvas.refresh
end if
if key = "-" then
xWidth = xWidth - 3
yWidth = yWidth - 3
if xWidth < 3 then
xWidth = 3
end if
if yWidth < 3 then
yWidth = 3
end if
shapeCanvas.refresh
end if
if key = "+" then
xWidth = xWidth + 3
yWidth = yWidth + 3
if xWidth > 30 then
xWidth = 30
end if
if yWidth > 30 then
yWidth = 30
end if
shapeCanvas.refresh
end if
You'll notice that all we do here is modify the xWidth and yWidth values. Very simple,
but effective. Most of the code is checks to ensure the values are within range (at least 3
pixels but not more than 30). We also refresh shapeCanvas so it will reflect the new
shape.
Go ahead and run the program -- it should let you change the size of the brush by using
the number and plus/minus keys.
That's it for now -- next week we'll add a way to save pictures.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We continue with SimplePaint, adding the ability to save drawings.
Letters
Jacqueline has a suggestion for SimplePaint's "draw over the color palette" problem.
Hello Marc,
Thanks for the pretty app!
About the problem in last week column, I think that we can draw a line ( a red one,
because kids are fond of red!) on the right of paletteCanvas. In the Paint event of
paintWindow we put these lines of code:
g.foreColor = rgb(255,0,0)
g.penWidth = 7
g.drawLine 195, 0, 195, me.height
and in MouseDrag:
if x2 > 195 + xWidth then
drawPixel(p.graphics, x2, y2)
end
But the blank space on the left of the screen is not very nice -- I should like a better
solution.
And now, a little question; in the Paint event of colorCanvas you write:
g.fillRect(0, 0, g.width, g.height)
Why do you say g.width and g.height instead of me.width and me.height?
Thank you very much for all!
In answer to your question, it doesn't matter if you use g.width or me.width: the result is
the same (since g, in this case, is part of me, g.width will always equal me.width).
As to why I did it the way I did, I don't have a clear answer. I suppose I did it simply
because I was focused on g, since we were concentrating on filling it, but I don't know. I
also can't think of any negatives to either method, except that occasionally you might
redefine g to be different then the graphics property of the current object (me) and in that
case the two widths wouldn't necessarily be the same.
Next, we hear from Carol, who's a new reader:
Hi!
As a newcomer to REALBasic, I just discovered RBU. Perfect! Keep up the great work!
I've begun skimming over the RBU archives, and the "in-house programs" article caught
my attention. I thought I'd add my "two cents" regarding what I'd like REALBasic to do.
As a collector (antique and contemporary items), I've been searching for a number of
years for a program to keep track of my assorted collections. While there are numerous
choices for PC users, I have yet to find any for us Mac fans, other than a dealer/inventory
control app priced at $575 !?!
After much researching and comparing of the available options (Virtual PC, a PC card, or
REALBasic), I've finally decided on REALBasic. After all, they say if you want
something done right, do it yourself! Although I have absolutely no background in
programming, and am only semi-computer-literate, my initial experimenting with RB's
demo has encouraged me to attempt an actual application. THANK YOU for providing
the much-needed lessons for beginners like me.
I had also considered purchasing at least one of the related books, so I was also happy to
see your comparison between the Dummies book and the Definitive Guide. Apparently
there is now a third book, hot off the press, entitled "REALBasic for Mac" (Michael
Swaine, Peach Pit Press, January 2002). Have you had a chance to review this one? If so,
it would be great to read your views in a future RBU.
One more idea: How about a lesson explaining how to add an import feature for graphics
such as scanned photos (my collectibles), and possibly even text from another program
such as Multi-Ad Creator or Quark Xpress? Is this possible with REALBasic? If you've
already covered this, I apologize. I haven't yet gotten through all of the archived articles - but I'm working on it!! : )
Thank you for your time.
Carol Mehler
Thanks, Carol!
Your project is an excellent one and I wish you luck. Let us know how it goes. At some
point I plan to cover a database project in RBU -- but first I need to learn more about
databases.
Michael Swaine's REALbasic book has been delayed, but will be coming out this
summer and I do intend to review it.
Finally, your questions about importing text and graphics is an interesting one. Graphics
aren't too difficult -- REALbasic can open any graphic format that QuickTime supports.
To "import" a picture into your program, you just use the openAsPicture method of a
folderItem. The result is a standard RB picture object -- you can then store that in a
database or manipulate it or whatever you want.
Text is another matter, however. While plain text is easily worked with, text in
proprietary formats such as Microsoft Word, Multi-Ad Creator, or Quark XPress is not.
That's because those companies generally don't tell their competitors how their file
format works. Some companies reverse engineer the format (figure it out via trial and
error), but that's a huge effort. More common is to require the user to export the text from
a proprietary program into a standard format, like plain text or RTF (Rich Text Format).
Then your program could translate that into a format REALbasic understands. (Plain text
is the easiest, of course, but you lose formatting such as coloring, fonts, type sizes,
boldface, and italics.)
My own free RTF-to-Styled Text is an example of a REALbasic program working with
RTF. It converts RTF to standard Mac styled text format (such as what SimpleText and
REALbasic use). The program is open source -- you can download the REALbasic code
here. It uses the open source RTFParser library from Belle Nuit Montage.
Finally, Harri has a suggestion for RBU:
Hi Marc!
I'd like to learn to do some 3D animation with REALbasic, so could you please sometime
write a column of it.
Thanks for the suggestion, Harri!
Unfortunately, my knowledge of 3D is nil, so I doubt I'll be covering it any time soon
(though I'm curious and if I find some time to investigate it, I might do a tutorial of my
own).
Not to push my magazine, but you might want to look at a subscription to REALbasic
Developer -- we've got a column on 3D in every issue, plus we'll be doing features on 3D
as well (Joe Strout, who wrote REALbasic's 3D feature, is one of our authors). The
magazine won't be all 3D, of course, but we'll definitely be covering the topic.
.
REALbasic University: Column 057
SimplePaint: Part IV
Our children's painting program is progressing nicely, but there's one very important
feature we need to implement to make it worth using: the ability to save pictures.
Saving Pictures
Since the design philosophy behind SimplePaint is that there should be no confusing "file
save" dialogs, we've got to come up with a simple way to save pictures. The method I
chose is for each picture to be saved with a default name to a default folder. The tradeoff
is the user doesn't get to choose the name of the picture or where it is saved, but the
benefit is simplicity and no "file save" dialog.
Saving pictures isn't difficult, but it will require several pieces of code. First, open your
SimplePaint project and open the Code Editor of paintWindow (double-click on the open
paintWindow). In the KeyDown event, add this code (it doesn't especially matter where as
long as it's not within another if-then segment):
if key = "S" then
savePict
end if
All this does is call the method savePict if the user presses the "S" key. (Note that
though we're using an uppercase "S" for key detection, because REALbasic isn't case
sensitive by default, this will work for either "s" or "S.")
Now we need to create that method. On the Edit menu, choose "New Method" and call it
savePict (leave the parameters and return fields blank).
The way our save routine will work is this: we'll store saved pictures in same folder as the
SimplePaint application. We'll just number the pictures so each will have a unique name.
But sharp readers will notice a problem with that system. It works fine during a single
session. However, once a few pictures have been saved and the program is relaunched,
the counting begins at one, and there's already a "Picture 1" on the disk! Our new picture
would overwrite the old one, which would be incredibly rude.
So somehow we must make sure we don't save the new picture on top of an old one. The
trick is to find the highest numbered picture on the disk. Adding one to that number will
give us our new picture number. Here's how that routine works.
Create a new method called getNumber and have it return an integer. Here's what the
code looks like:
dim i as integer
dim f as folderItem
i = 0
do
i = i + 1
f = getFolderItem("").child("Picture " + str(i))
loop until not f.exists
return i
As you can see, this is remarkably simple. All we do is increment a value, i, by one, and
keep checking to see if a file named "Picture " + i exists or not. If it does, we keep
adding one to i. If "Picture i" doesn't exist, we know it's okay to save a new picture with
that name, so we return the value of i!
Now go back to savePict and put in this code:
dim f as folderItem
f = getFolderItem("").child("Picture " + str(getNumber))
if f <> nil then
f.saveAsPicture(p)
msgBox "'" + f.name + "' Saved!"
else
beep
end if // f = nil
This creates a new folderItem within our default folder (getFolderItem("") returns the
folder of your application). The folderItem's name is "Picture " plus a number -- the one
calculated by our getNumber method.
To save the picture, we use our folderItem's saveAsPicture method, passing it p, our
drawing. That's it!
Getting Rid of a Dialog Box
You may notice that our save routine displays a dialog box. It's just a message box,
confirming the save, but for young users, that can be confusing. They won't notice the
modal dialog and won't be able to figure out why they can't draw any more.
So how do we tell the user the picture's saved without a dialog? A mere beep could
indicate an error, not success.
There's a simple solution right within REALbasic's own online Help!
Bring up REALbasic's online help and type "speech" as the search term. Here's what
comes up (you'll probably have to scroll a bit to see this code as it's not at the top of the
help window):
Now create a new method like this:
To do speech, I just dragged in the code from RB's help window (code with a dotted
border can be dragged into the Code Editor to add it) and modified it.
This code uses a declare statement to access the Mac OS's built-in speech routines
(which are not accessible via a standard REALbasic command). Once we've declared a
routine, we can call it like any other REALbasic routine. REALbasic just passes our
command on to the operating system.
Because system commands are not available for all platforms, we therefore must check
the platform before making the OS call. We do that with the conditional compilation
commands, #if-#endif, and REALbasic's special "target boolean" values, targetMacOS
and targetCarbon, which return true if we're running Mac OS or Carbon (Mac OS X).
(Note that because Carbon apps can run under Mac OS, there's no way to tell if your app
is really running Mac OS X -- all you can tell is if you're running under Carbon.)
Unfortunately, the online help code doesn't work under Mac OS X (it causes the
application to quit). At first I wondered if speech was supported under Carbon, but a
quick check on Apple's developer's website showed that the SpeakString command is
supported under Carbon. To make it work, I therefore set up a targetCarbon version of
the function replacing "SpeechLib" with "CarbonLib" -- and it worked!
(Note that not all Carbon commands are as easily translated. Sometimes the Carbon
commands have new names or different parameters from their Mac OS equivalents,
which can get complicated to translate. If you don't know what you're doing with
declares, it's best not to mess with them unless you enjoy watching your computer crash.)
Here's the finished routine:
dim i as integer
#if targetMacOS then
declare function SpeakString lib
pstring) as integer
#endif
"SpeechLib" (SpeakString as
#if targetCarbon then
declare function SpeakString lib
pstring) as integer
#endif
"CarbonLib" (SpeakString as
#if targetMacOS then
i = SpeakString(theString)
return true
#endif
return false
If the speech is successful (available), we return true, otherwise false (which would be
the case if the user didn't have an operating system capable of speech).
In Detail
You'll notice I first check for targetMacOS then targetCarbon. Why?
Well, since Carbon is only available on Macs, an app running under Carbon will always
return true to the targetMacOS question. That means the function will initially be
defined via "SpeechLib" -- which will crash a Carbon app. But the second statement
checks to see if we're running Carbon. If we are, the function is redefined as calling
"CarbonLib" -- and that works.
The final statement only bothers to check for targetMacOS since whatever Mac OS we're
running will work with the SpeakString command. (The exception is 68K Mac OS, which
requires a different declare, but since REALbasic no longer supports 68K compiling, I
didn't bother with that, though you could support it if you're using an older version
REALbasic.)
Now we just need to make some minor changes to our savePict method so instead of
displaying a dialog we speak a message to the user:
dim f as folderItem
f = getFolderItem("").child("Picture " + str(getNumber))
if f <> nil then
f.saveAsPicture(p)
if speakTheString(f.name + " Saved!") then
else
msgBox "'" + f.name + "' Saved!"
end if
else
beep
end if // f = nil
Note that if speech isn't installed, the routine will display the confirmation dialog as
before.
Erasing the Screen
We've now given SimplePaint the ability to save pictures, but what happens after that?
Usually the child will want to start a new picture -- we need a way to let the child erase
the screen.
Go to the KeyDown event of paintWindow and insert in this:
if key = chr(8) then
// Erase all
p.graphics.clearRect(0, 0, self.width, self.height)
self.refresh
end if
This just checks to see if the user pressed the Delete key (ASCII 8) and if so, clears the
picture with the clearRect method and refreshes the screen. Easy!
Adding an Eraser
It's usually easier to tell children to simply use white to erase, but we'll go ahead and add
an erase keyboard command. All it does it change the current color to white.
In paintWindow's KeyDown event add this:
// Eraser
if key = "E" then
changeColor(rgb(255, 255, 255)) // white
end if
Now pressing "E" switches the color to white so you can erase.
That's it for today. Next time we'll add some more features like changing the mouse
cursor and some sounds.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
More SimplePaint polishing.
Letters
Our letter this week is from David in Arizona. He writes:
Hello Marc,
Thank you for making your REALBasic tutorials available, I have enjoyed reading them.
I have a question that I hope that you have the time to answer.
I have set up a RB program that has three windows. Window1 is an introductory page,
followed by Window2 where data is entered, and Window3 where some calculations
using the data from Window2 are performed and the results shown.
I would like to have any formulas or data manipulation in Window3 recognize & use the
data that was entered while Window2 was showing.
If in window2 a numeric value is entered into a textfield, and then the value of the
textfield is assigned to a variable, how can I use this variable in Window3?
When I try to do this, the variable's value is always reset to zero in Window3.
I appreciate your time, help and information.
Thanks!
Peace,
David
Hi David! The simplest solution for your situation is to just to refer to the properties of
the other windows as properties of other windows. That sounds way more complicated
than it is. Just put the window name in front of the property name, separated by a period.
For example, to delete all the rows of a listBox on window2, put:
window2.listBox.deleteAllRows
You can refer to any object or property of another window simply by putting the
window's name in front of it (followed by a period). These are all valid:
// editFieldB is on window1
editFieldB.text = window2.editField1.text
// This is the same as the previous line
window1.editFieldB.text = window2.editField1.text
// total is a number property
total = val(window2.editField1.text) * val(window2.editField3.text)
The other approach is to use global variables -- add properties to a module and they're
accessible by all windows and methods. But that could be more complicated since you'd
have to store the stuff from the one window into global properties, then access the globals
from the other window. Just accessing another window's data directly is simpler.
.
REALbasic University: Column 058
SimplePaint: Part V
Last week we made it so SimplePaint can save drawings. This week we'll concentrate on
adding some user interface niceties.
Adding a Painting Cursor
One annoying thing about SimplePaint which you probably noticed is that it always has
an Arrow cursor. The Arrow is fine for selecting menus, but wrong for drawing. Let's
change it.
I created a simple dot-shaped cursor in ResEdit and saved it as a cursor resource. You can
grab it here. Install it by dragging it into your project file.
To use the cursor, we've got to insert a line of code in the appropriate places. First, since
we want paintCursor to be the default cursor, let's activate it when paintWindow is
opened. Add this line of code at the bottom of paintWindow's Open event:
self.mouseCursor = paintCursor
Now go up to the Controls area and find nodrawCanvas. In its mouseExit event, put in
the same line. In its mouseEnter event, put in this:
self.mouseCursor = arrowCursor
That should activate paintCursor when the program is launched, and switch between it
and the arrow when the user moves into the color palette area. Simple!
Adding Sounds
One thing you'll notice about SimplePaint is that it is quiet. Kids, however, are not quiet.
They like sounds. So let's add some sounds to make the program more interesting.
First, let's make a list of the sounds we want. One simple idea is just to have a key click
sound -- a click that plays whenever the child presses a key on the keyboard. Another
idea is an explosion type sound -- we'll use that when the child presses the Delete key to
erase the current picture. Here are some ideas for sounds in SimplePaint (feel free to add
your own):
Key Click
When the user presses a keyboard letter command
Explosion
When Delete key is used to erase picture
Save
When picture is to be saved
Colorswitch
When the user switches to a new color
Brush Enlarge
When painting brush is made bigger
Brush Shrink
When painting brush is made smaller
Now that we know what we need sounds for, we must find the appropriate sounds. Where
you do get sounds? If you have the right equipment and skills, you can record your own
sounds. (You need a microphone, recording and sound editing software, and depending
on your Mac, a sound input device.)
If it's just for a personal project (i.e. something you're not planning on sharing or selling),
you are free to "steal" sounds from other applications. Use Apple's free ResEdit to open
the resource forks of programs and look for SND resources.
Games often include lots of cool beeps, explosions, and laser zaps. But almost every Mac
app has some sounds in it. For example, Adobe GoLive 4.0 includes a gunshot sound
(don't ask me why):
But since most digitized sounds are copyrighted, you can't just borrow a sound from
someone else and use it in your app without permission.
However, there are websites with free sounds and sound clip libraries you can buy which
allow you to use those sounds in your own projects royalty free (you don't have to pay a
royalty for each copy of your program you distribute). I happen to have one of these, so
the sounds I include here are from an old library (no longer being sold).
In my case, since I have a collection of sounds, I have to search through it to find sounds
that might match my needs. The key click and explosion ones are easy -- but what kind of
a sound is a "save" sound?
That's where you need to use some imagination, trying to come up with appropriate
sounds for abstract events. You also have to work with whatever sounds you've got
available (in my case, limited to whatever's in my library). Don't restrict your sound
usage to what the sound is for -- often sounds for one thing will work for another kind of
event. For instance, a mild gunshot might work as a warning or even a keyclick sound. In
SimplePaint, I chose to use a sound originally called "Key turn" -- the sound of a key
turning in a lock -- as my "colorswitch" sound. For the "save" sound I chose to use a
simple orchestra chord, which sounds appropriately triumphant. For the enlarge/shrink
commands, I went with up/down sounds.
The entire collection of sounds are available here (they're also included in this week's
project file).
Once you've got the sounds decompressed and copied to your project's folder, just drag
them into your REALbasic project file's window. Your window should look something
like this:
You'll notice that the sounds are in italic -- that's a reminder that they're not embedded
within your project (do not delete the originals).
To play a sound, simply type in the name of the sound, add a period, and the word play.
Like this:
keyclick.play
Quick Tip 1: if you drag a sound icon from the project window to the Code Editor, RB
will insert in the appropriate play code for you.
Quick Tip 2: you can double-click on sounds within the project window to play them.
That's helpful if you can't remember what a sound sounds like.
Let's put our sounds to work. Open the Code Editor of paintWindow and find the
KeyDown event. Here we'll simply insert in lines to play the appropriate sounds for each
kind of event. This is what the finished routine looks like:
if key = "S" then
save.play
savePict
end if
if key = chr(8) then
// Erase all
blast.play
p.graphics.clearRect(0, 0, self.width, self.height)
self.refresh
end if
// Eraser
if key = "E" then
changeColor(rgb(255, 255, 255)) // white
end if
// Swap colors
if key = " " then
changeColor(oldC)
end if
if key = "4" then
down.play
xWidth = xWidth - 3
if xWidth < 3 then
xWidth = 3
end if
shapeCanvas.refresh
end if
if key = "6" then
up.play
xWidth = xWidth + 3
if xWidth > 30 then
xWidth = 30
end if
shapeCanvas.refresh
end if
if key = "2" then
down.play
yWidth = yWidth - 3
if yWidth < 3 then
yWidth = 3
end if
shapeCanvas.refresh
end if
if key = "8" then
up.play
yWidth = yWidth + 3
if yWidth > 30 then
yWidth = 30
end if
shapeCanvas.refresh
end if
if key = "-" then
down.play
xWidth = xWidth - 3
yWidth = yWidth - 3
if xWidth < 3 then
xWidth = 3
end if
if yWidth < 3 then
yWidth = 3
end if
shapeCanvas.refresh
end if
if key = "+" then
up.play
xWidth = xWidth + 3
yWidth = yWidth + 3
if xWidth > 30 then
xWidth = 30
end if
if yWidth > 30 then
yWidth = 30
end if
shapeCanvas.refresh
end if
That was pretty simple, but you might have noticed that we didn't add sound to the
"eraser" and "color swap" commands. Why? Because it's easier to insert a line in the
changeColor method (that way we only have to insert one line of code in one place):
dim tempC as color
tempC = newColor
if tempC <> c then
colorswitch.Play
oldC = c
c = tempC
colorCanvas.refresh
shapeCanvas.refresh
end if
Go ahead and run the program now -- you'll see it's very noisy! Kids will love it.
As it turns out, we didn't even use the key click sound. Maybe you can come up with a
use for it, or just delete it.
That's it for this week's lesson. If you would like the complete REALbasic project file for
this week's tutorial (including resources), you may download it here.
Next Week
We finish up SimplePaint with a few more enhancements and an unusual variation.
Letters
No letters this week as I'm still recovering from an all-nighter watching the USA beat
Portual 3-2 in the World Cup! Wow, what a game. Absolutely eerie watching unranked
U.S. players like Tony Sanneh stonewall the World Player of the Year Luis Figo (who
also happens to be the world's most expensive soccer star). Incredible.
I know most Americans pay as much attention to soccer as to tiddlywink competitions
(I'm delighted to be the exception), but I doubt most in the U.S. realize the magnitude of
this upset. A San Francisco radio station guy compared it to Portugal's baseball team
beating the New York Mets in the (mis-named) World Series. That's probably close,
except what if it wasn't the Mets, but an All-Star team made of all the best U.S. players?
That's what the World Cup is: the best of each country battling it out in a month-long
competition. There's nothing bigger than the World Cup, which dwarfs the Olympics
(more people watch one World Cup than a decade of Olympics, winter and summer
combined), and this year's competition is really fantastic. There's already been two huge
upsets and only one nil-nil draw with an average of nearly three goals per game. I realize
the hours of the live telecasts are insane, but that's what Tivo and VCRs are for. I'd
encourage everyone to support the U.S. team by tuning in (or recording) the next two
USA games and cheering on the underdogs!
DATE
CHANNEL
EVENT
TIMES
June 10 Korea Republic vs. USA
p.m. PT
ESPN2
June 10 Korea Republic vs. USA (replay)
a.m. PT
ESPN2
June 11 Korea Republic. vs. USA (replay)
a.m. PT
Classic
June 14 Poland vs. USA
PT
ESPN
June 15 Poland vs. USA (replay)
a.m. PT
ABC
2:25 a.m. ET / 11:25
2:00 p.m. ET / 11:00
1:00 p.m. ET / 10:00
7:25 a.m. ET / 4:25 a.m.
1:00 p.m. ET / 10:00
By the way, those of you who are students of sociology and are fascinated by the bizarre
dichotomy between U.S.-centric sports and the rest of the planet should check out Andrei
Markvovits' excellent book, Offside: Soccer and American Exceptionalism (the link is to
my review). It's an intriguing history of sport in general, with the focus on why soccer
was marginalized in the U.S. while it became the number one sport in just about every
other country. It's a good read even if you're not a soccer fan.
.
REALbasic University: Column 059
About the Delay
Let me apologize for the delay in publishing this column. I'd love to blame it exclusively
on the astonishing, unprecedented success of the U.S. Men's Soccer team leading to late
night viewing parties, but in truth several issues converged to make the delay
unavoidable.
First, last week I was putting the first issue of REALbasic Developer magazine to bed.
(In layman's terms, that means finalizing everything and sending it off to the printer.) The
first release of anything is always nerve-wracking, but I'm happy to say that everything's
going extremely well and the early reviews of the premiere issue are resoundingly
positive. (See the News section at the end of this column for more details.)
Second, I'm in a transition phase at my day job, training my replacement and getting
ready to be a full-time magazine publisher and software developer.
And finally, there were SimplePaint incompatibilities with Mac OS X I had to fix before I
could release this column, and since I only discovered these last week, I decided to delay
publishing.
But enough of that: on with REALbasic University!
SimplePaint: Part VI
In our previous lesson, we added a painting cursor to replace the standard Arrow cursor,
and added lots of neat sound effects. Let's continue along that vein today.
Finding the Cursor
Young children often find using the mouse a little strange, especially if they're not
familiar with computers. Our new paintCursor, while better than the Arrow cursor, is
small, and sometimes can be difficult to find. So we're going to add a cool cursor locator
feature!
This is a simple feature, but it requires a few steps. We'll need a simple method that will
flash some colored circles at the cursor position. To do that, we'll want a slight delay
between each circle drawing (otherwise the circles would draw so fast no one would see
them). So let's add a delay method (Edit menu, "Add Method") to paintWindow:
dim t as integer
t = ticks
while ticks - t < 2
wend
That's pretty simple code, isn't it? It just waits at least two ticks (a tick is a 60th of a
second) before finishing. The function ticks returns the number of ticks since your Mac
was booted, so it's a number that's always growing. We save the original tick value, then
subtract that from the current value, to see the difference. When that difference exceeds
two ticks, the routine is over.
In Detail
A variation of this same code is useful for timing how long an operation takes. For
instance, I once had a program where I was trying two different methods of doing
something -- which was faster? Since we're only talking about fractions of a second in
time it was impossible to tell visually, but by using the ticks function to record the
starting time, and then recording the difference at the end of the call, I was able to see
which method was superior. This is a common technique to use during debugging and
optimizing (making a program more efficient).
Sometimes ticks aren't fast enough for your purpose. You can use the microseconds
function instead: it returns time in one millionth of a second intervals!
With our delay method written, we can create a new method, flashCursor. That has this
code:
dim i, x, y as integer
dim g as graphics
cursorflash.play
x = system.MouseX
y = system.MouseY
g = self.graphics
g.penWidth = 8
g.penHeight = 8
g.foreColor = rgb(255, 255 * .75, 0) // yellow-orange
for i = 1 to 5
g.drawOval(x - (i * 5), y - (i * 5), (i * 5) * 2, (i * 5) * 2)
#if TargetCarbon
Declare function GetWindowPort Lib "CarbonLib" (window as
WindowPtr) as integer
Declare sub QDFlushPortBuffer Lib "CarbonLib" (port as Integer,
region as Integer)
QDFlushPortBuffer GetWindowPort(self), 0
#endif
delay
next // i
delay
delay
self.refreshRect(x - 25, y - 25, 50, 50)
You notice that this routine plays a new sound called cursorflash. Drag it into your
project window to install it.
The next part of this routine uses a REALbasic function called system and retrieves the
current x/y coordinates of the mouse cursor in global coordinates. Why do it that way?
Well, the only other way to get the cursor's location is via an event like mouseMove.
We're not in that event -- we're in a custom method. We could add code to the mouseMove
event to record the cursor's location every time the user moves it, but this way is more
efficient since we only get the location when we need it.
Once we've got the mouse cursor's current location, we draw a series of ever-larger
circles. You'll notice we have some weird code next: a #if followed by some declare
stuff. What's all that?
Well, it turns out a cool feature of Mac OS X messes us up. Mac OS X features automatic
double buffering -- that's where whatever you draw on the screen is saved as a picture
ready to be popped on the screen instantly instead of drawing it bit by bit. The advantage
is flicker-free animation. However, in the case of our flashCursor routine, it means that
Mac OS X won't refresh the screen until after the final draw command: you won't see the
cursor flash at all!
What to do about this? Well, REALbasic itself has no built-in solution, but we can easily
make a couple system calls to tell the OS to immediately flush the buffer (draw whatever
we've drawn so far). That's what those declare statements do. By protecting those
commands with the #if targetCarbon - #endif lines, we ensure that only Carbon
(Mac OS X) apps will try to use those commands. The regular refresh commands work
for the Mac OS version of SimplePaint.
In Detail
This useful animation tip solved a problem for me, but I can't take credit for the solution.
That came from Joe Strout, who works for REAL Software. In his terrific article, "Three
Ways to Animate," which appears in the premiere issue of the new REALbasic
Developer magazine (see page 18), he explains this problem and offers a solution.
Joe's article details three different animation techniques: using a canvas, a
SpriteSurface, and an Rb3DSpace. Each have their own advantages and disadvantages,
but rather than trying to figure all that out for yourself, Joe's done all the work. Well
worth a read if you're interested in animation.
Note: the first issue of REALbasic Developer will be published in July 2002.
Subscriptions are available now.
Once our cursorFlash routine is written, we need a way to activate it. I've chosen to do
so via the Tab key. So within your keyDown event, add this bit of code:
if key = chr(9) then
flashCursor
end if
Now any time the user presses the Tab key the cursor will flash, showing them the
location of the cursor. Cool, eh? Kids love the effect and the cool sound.
Fixing a Mac OS X Compatibility
This will teach me to have different versions of Mac OS X on different machines. For the
last column, I tested the Speech routines on an older version of Mac OS X. Then I
received reports from readers that it didn't work -- they received a "can't find speechLib"
error message.
The solution turned out to be simple. I have no idea why the old code worked on the
earlier Mac OS X, but this one works on Mac OS X 10.1.5:
dim i as integer
#if targetMacOS then
#if targetCarbon then
declare function SpeakString lib
pstring) as integer
#else
declare function SpeakString lib
pstring) as integer
#endif
#endif
"CarbonLib" (SpeakString as
"SpeechLib" (SpeakString as
#if targetMacOS then
i = SpeakString(theString)
return true
#endif
return false
As you can see, I first check to make sure we're running on Mac OS, then under Carbon.
If we're not running under Carbon, we call "SpeechLib" instead of "CarbonLib." The rest
of the code is the same as before.
Sorry about the problem, but we all learn!
That's it for this week. Last time I promised an "alternative" version of SimplePaint, but
I'll release that next week.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Next Week
We explore a new project and Marc releases his "Etch-a-Sketch" version of SimplePaint.
News
There are only a few days left to get your REALbasic Developer subscription at the Early
Bird discount price. Starting July 1, 2002, the regular price for a one-year (six-issue)
subscription will be $32.
The first issue of REALbasic Developer is going to press now and will be delivered in
July. To give you a hint at what you'll be missing if you don't subscribe, here's a look at
the Table of Contents for the premiere issue:
We're talking over 50 pages of articles, tutorials, columns, reviews, news, and other
goodies. Miles and miles of source code (subscribers will be able to download complete
RB project files from the RBD website). Subscriptions are a must for all REALbasic fans!
Letters
Dear sir,
By far this is the best and most comprehensive guide to RB that I've found to date,
granted I've only been playing with RB for a week now. Which will also tell you
something about where I am in terms of understanding everything.
I have just completed most of GenderChanger Tutorial 11 and I have a vague
understanding of what's going on. The explanations have been great and I too started out
with BASIC years ago. Since then however I have had little if no experience with
programming.
Anyhoo, I think the apporach you are taking is a good one and many things ARE going
over my newbie head but an equal amount of things are sticking too. So overall it's great
to start getting my mind wrapped around the way RB works and a hanful of the
Properties/Commands etc. that you've dealt with in the GC tutorial. Not to mention that
GC will be a great program for me to use once I am finished! =)
That being said, I am having issues with debugging (who isn't?) but RB gives me some
clues as to what's wrong with my code and I've been able to rectify some of my isues
with a bit of logic and pouring back over tthe previous tutorials. But I am getting a
"Parameters Expected" error on a certain line of code, I have double checked this with
your tutorial and it is exactly the same. Is there some other place where i might have to
change something other than the idicated line that RB highlights for me? This aspect of
debugging is especially frustrating and i fear I haven't the know-how to get through it on
my own. Any suggestions?
Sincerely - Keith Bahrenburg
Thanks for the note, Keith!
Debugging is a hugely complex issue. It's something that's been in the back of my mind
to cover, and your letter has reminded me of that. I plan to do a series of columns
specifically about debugging and we'll cover your situation (and others). I'll discuss
standard debugging techniques, reveal some debugging tips of my own, and offer tips
from readers.
To get that process started, readers who have debugging tips should send them to me and
I'll post them as part of that series.
.
REALbasic University: Column 060
Monotab: Part I
My favorite thing about REALbasic is the way it incorporates into my thinking.
Whenever I have a problem to solve, even a trivial one, I turn to REALbasic. It's like a
best friend that's always around to help.
But I must be alert to opportunties for REALbasic to help me. Any time I do anything
repetative I ask myself, "Can I write a program that will make this task easier?"
One task that has cropped up from time to time is that I'll have some tabular data in a tabdeliminated format and I need to convert it to a format suitable for emailing. (This often
happens when I copy data from a table on a web page.) Since most email programs are
text-only, a tab-deliminated table will appear something like this (data courtesy Major
League Soccer):
Team
GP
W
L
San Jose Earthquakes
17
Dallas Burn
16
7
Colorado Rapids 17
8
New York/New Jersey MetroStars
Los Angeles Galaxy
16
Kansas City Wizards
16
Chicago Fire
15
6
D.C. United
15
6
New England Revolution 16
Columbus Crew 16
5
T
10
3
8
16
7
5
7
8
6
8
Pts
5
6
1
7
7
5
2
1
9
3
GF
2
27
25
7
2
6
20
19
1
18
GA
32
24
24
2
23
21
24
18
19
21
27
19
28
23
20
21
22
22
27
27
16
24
22
22
31
Eek! That looks awful and is nearly impossible to read. (You can't even tell my Quakes
are leading the league! ;-) But if we convert those tabs to spaces and displayed the table
in a monospaced font, we have a text-only table that's easy to read.
Unfortunately, that's a tediously mind-numbing task do by hand, and most of us wouldn't
bother. Fortunately, I'm the kind of person that's picky about how emails are formatted,
and I have REALbasic to assist me.
Planning Monotab
For today's project, we're going to create a program that will convert a tab-deliminated
file into a text file where each piece of text is separated by the appropriate number of
spaces. That last condition is what makes a simple procedure complicated, as the number
of spaces are potentially different for each bit of text.
21
Before we begin programming, let's do a little thinking about exactly how Monotab will
work.
First, we need a way to input a text file into our program: I suggest a simple drag-anddrop interface rather than mess with an awkward "Open Document" dialog.
Second, let's think about how we'll actually handle the conversion. What algorithm will
we use?
We obviously cannot simply replace tab characters will a set number of spaces since the
number of spaces change depending on the length of the text. For example, in the sample
text displayed earlier, we have a list of Major League Soccer teams and their current
standings in the league. But since the team names are of different lengths, the number of
spaces after the team name changes if we want the next column to start at the same place.
Here's what that table looks like when we replace each tab with three spaces:
Team
GP
W
L
T
Pts
GF
GA
San Jose Earthquakes
17
10
5
2
32
27
16
Dallas Burn
16
7
3
6
27
24
19
Colorado Rapids
17
8
8
1
25
24
28
New York/New Jersey MetroStars
16
7
7
2
23
24
Los Angeles Galaxy
16
7
7
2
23
20
22
Kansas City Wizards
16
5
5
6
21
21
22
Chicago Fire
15
6
7
2
20
24
22
D.C. United
15
6
8
1
19
18
22
New England Revolution
16
6
9
1
19
27
31
Columbus Crew
16
5
8
3
18
21
27
21
Yuck! So in order to display our table correctly, we must do some calulations and figure
out the appropriate number of spaces for each field.
There are two calculations that must be done. First, we must figure out where each
column must start. That's more difficult than it sounds as we'll see in a minute. Second,
we must calculate the number of spaces for each line of each field. Let's look at the first
few lines in the example to see how this works.
Team
GP
W
L
T
Pts
GF
GA
San Jose Earthquakes
17
10
5
2
32
27
16
Dallas Burn
16
7
3
6
27
24
19
Colorado Rapids
17
8
8
1
25
24
28
New York/New Jersey MetroStars
16
7
7
2
23
24
21
To align "GP" so it's further right than the end of "San Jose Earthquakes" we must put at
least 17 spaces after "Team." However, it's easy to see that since "New York/New Jersey
MetroStars" is even longer than "San Jose Earthquakes" even 17 spaces aren't enough!
That should tell us that in order to calculate the minimum number of spaces between
fields, we must first discover the widest data within the current field (in this case, the
"New York/New Jersey MetroStars" phrase).
However, you'll see that even when we do that, it isn't always enough. Sure, the first two
columns are aligned, but for the wide column, there isn't much breathing room between
the first two columns:
Team
San Jose Earthquakes
Dallas Burn
Colorado Rapids
New York/New Jersey MetroStars
GP
17
16
17
16
W
10
7
8
7
L
5
3
8
7
T
2
6
1
2
Pts
32
27
25
23
GF
27
24
24
24
GA
16
19
28
21
Wouldn't it be nice to have a minimum amount (like three spaces) between each field?
Even better, why not allow the user to dynamically specify this amount?
That leads us to our third question: what kind of interface does Monotab need?
Obviously, this is a program for internal use: it isn't going to be sold or distributed, so it
doesn't need much beyond the bare essentials. It could even not have any interface at all
and be run as a command-line program (i.e. drop a file onto the Monotab's icon and it
converts it and quits)! However, since this is REALbasic and even bare bones interfaces
are simple, we'll give our program an interface.
My first thought for an interface is that since this is a program working with tabular data,
it makes sense to organize the data inside a listbox, with one row of data for each line in
the text, and a column for each field. That way the user can check the data to make sure it
was parsed correctly before converting it. To set our "minimum spaces" amount, we'll use
a slider control.
Before we create Monotab, we need a test file we can use to make sure our program is
working correctly. Prior to the competition, I used Monotab to generate a text-only World
Cup soccer TV schedule which I emailed to friends and family. Here's the original tabdeliminated file you can use for testing Monotab (you should Option-click on the link to
download the file, otherwise your web browser will display the file).
Setting up the Interface
Create a new blank project file in REALbasic (launch RB and choose "New" from the
File menu). Double-click on the Window1 window created by default and attempt to make
it look something like this:
That's a listbox at the top. Its properties don't matter too much, since we'll change most
dynamically as the program runs, but it would be a good idea to set all the "lock"
properties (lockLeft, lockRight, etc.) so that the listbox's size will change if the user
enlarges the window.
The slider control is named padSlider and has maximum set to 25 and value set to 3.
The pushButton is called exportButton. Other than those properties, the defaults
should work for everything else.
StaticText1 is a textual indicator of padSlider's current value: so we just need to put the
following code in its Open and ValueChanged events:
staticText1.text = str(me.value) + " spaces"
If you've set padSlider to update dynamically (liveScroll is true), the staticText
will be redrawn as the user slides the control.
Next, we need to add the ability to accept a dropped file. As usual, this is a two-step
process. First, we must tell REALbasic about the kind of file we'll accept. In this case,
that's a text file. So go to the Edit menu and choose "File Types." There you'll add a new
type (click the "Add..." button) and give it settings like this:
Second, we've got to tell listBox1 to accept files of this type. Within the Open event of
listBox1 put this code:
me.acceptFileDrop("text")
Perfect! Now any text files dropped on listBox1 will be sent to the DropObject event.
Next week we'll handle parsing those files and write the code that does the actual
conversion.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
SimpleEtch
I previously promised a special version of our SimplePaint project, so I'll briefly present
that here.
The idea is to modify SimplePaint into SimpleEtch, a virtual Etch-a-Sketch drawing
program. If you remember using an Etch-a-Sketch as a child, you'll remember that once
you put the "pen tip" down, you cannot lift it up and move the cursor without drawing.
Thus all Etch-a-Sketch drawings are made of a single continuous line. That makes it
awkward (or shall we say challenging), but that's also what makes it unique and
interesting.
Here's a drawing I made with SimpleEtch:
To change SimplePaint into SimpleEtch isn't especially difficult and I won't explain it in
detail here. The basic differences are:
•
•
•
•
•
Since an Etch-a-Sketch has no color, we eliminate SimplePaint's color palette and
other interface items.
When the program is initially ran, or after deleting an existing drawing ("shaking"
the Etch-a-Sketch), we allow the user to use the mouse to position the cursor at
the starting position. After that, the mouse is no longer used.
Once the mouse has been clicked to set the starting position, the user moves the
cursor (and draws a point) with the arrow keys.
Because the cursor can be moved over previous drawing, making the black cursor
impossible to see on a black drawing, we make the current drawing point red.
(See the red cursor point at the bottom right of the above drawing.)
I also removed the feature allowing the user to change the drawing point size,
hard-coding it instead.
The complete REALbasic project file for SimpleEtch is available here: download it and
study the changes I made to see how the variation works.
Next Week
We finish Monotab.
Letters
This week we've got a question from Spain! Manuel writes:
Dear Marc:
What would be the REALBasic equivalence of the following CodeWarrior's Pascal
instruction:
getValue := ord(firstValue >= secondValue)
where getValue, firstValue and secondValue are of extended Pascal Types
Yours sincerely,
Manuel Urrutia Avisrror
Professor of Urology
University of Salamanca
SPAIN
Interesting question. I'm not sure of the answer, since I'm not sure what you are trying to
do here. Pascal used to be my favorite language (I never learned C), but it's been a while
since I've used it. Unless I'm completely missing something, the statement above is an
odd one.
The ord() function returns the ASCII value of the character passed. For instance, pass it a
"C" and it returns a 67. But in your code you're passing it a boolean value: the "firstValue
>= secondValue" phrase is true if firstValue is greater or equal to secondValue,
otherwise it is false. That means getValue will be assigned the result of either ord(true)
or ord(false), and it's been too long for me to remember what Pascal would do with that.
(I suspect it would be either one or zero, since that's what true and false usually
evaluate to, but I'm not 100% certain.)
At any rate, the REALbasic version of this is similar. Something like this is a literal
translation:
dim getValue, firstValue, secondValue as integer
getValue = asc(firstValue >= secondValue)
However, if you try this, you'll get a "Type Mismatch" error. That's because REALbasic's
asc() function is expecting a string and you're passing it a boolean. asc(true) and
asc(false) don't work.
The way around this, if this is what you are wanting, would be to frame the code within
an if-then statement like this:
dim getValue, firstValue, secondValue as integer
if (firstValue >= secondValue) then
getValue = 1
else
getValue = 0
end if
This is a little more convoluted, of course, but it's doable. If this is a function you expect
to use often, I'd suggest you make it into a method. Let's call it ascBool and set it up like
this:
function ascBool(bool as boolean) as integer
if bool then
return 1
else
return 0
end if
end function
If you create the above method, then this code works:
getValue = ascBool(firstValue >= secondValue)
.
REALbasic University: Column 061
Monotab: Part II
In our previous lesson, we got Monotab's interface set up, but hadn't started writing the
conversion code. We'll finish the program today.
Importing a File
Our first task is to bring in a text file so we can convert it. We already set up listBox1 to
accept a dropped file: now we just have to do something with it.
Go to listBox1's DropObject event and add this code:
if obj.folderItemAvailable then
addText(obj.folderItem)
end if
This simply checks to see if a folderItem is available, and if so, sends it to a routine
called addText. Let's add that method now.
Create a new method (Edit menu, "New Method") and name it addText. First we'll make
sure it's a valid file and try to open it as a text file. If either of those operations fail, we'll
exit the routine without doing anything. If the file is successfully opened as a text file,
we'll extract the text and add it to listBox1.
Here's the code:
dim t as textInputStream
dim s as string
dim colNum, i as integer
if f = nil then
return
end if
if f.directory then
return
end if
t = f.openAsTextFile
if t = nil then
return
end if
listBox1.deleteAllRows
s = t.readLine
colNum = countFields(s, chr(9))
listBox1.ColumnCount = colNum
for i = 0 to colNum
listBox1.heading(i) = nthField(s, chr(9), i + 1)
next // i
while not t.EOF
s = t.readLine
listBox1.addRow nthField(s, chr(9), 1)
for i = 2 to colNum
listBox1.cell(listBox1.lastIndex, i - 1) = nthField(s, chr(9),
i)
next // i
wend
t.close
Note that we first delete all the rows in listBox1. That's in case the user has already
dropped one file on to fill up the listbox: this cleans it up.
Next, we read in just the first line of the text file. From that first line we set up how many
fields the file will contain. It's therefore important that the first line of your text file
include the same number of fields as all other lines. In fact, we assume that this first line
contains the names of each column type (the headers). Obviously, for a commercial or
professional program we might not want to assume this, but for a program we're writing
for our own use, this is a valid assumption.
To count the fields in the first line, we use the countFields command and tell it the
fields are separated by tabs. We then set the headings for listBox1 by extracting them
from the first line.
Once the headings have been established, all that's left is to read in the data. We start a
while-wend loop which lasts as long as the file has stuff left to read. For each pass
through the loop, we read in a single line of text. Then we parse that line with the
nthField command (which extracts the text of individual fields by number), and
carefully add each field's text to a cell in listBox1.
(Remember, since listBox1 has several columns, we can't just use the addRow
command: that only adds to the first column in the row. The data for the other columns
must be added cell by cell.)
When we've finished all that, we close the file and the method is done. The file has been
imported and is now in our program's memory.
Converting the Text
In order to convert the text, we'll need a couple special routines. The first is simple: a
function that will return a string of n spaces (where n is an integer).
Create a new method (Edit menu, "New Method") and call it padSpaces. Give it n as
integer as its parameter, and a return type of string like this:
Here's the code for the method:
dim i as integer
dim s as string
s = ""
for i = 1 to n
s = s + " "
next // i
return s
As you can see, this is a pretty simple function. It just adds n spaces to s, effectively
returning a string of n spaces.
The second routine we'll need to handle the conversion is a little more complicated. Last
week we explored the conversion task and discovered that for us to know how wide (in
characters) a column must be, we must know the width of the largest item within that
column of data. The example I used was the list of MLS teams, where the top team was
the "San Jose Earthquakes," which, while long, isn't as long as the "New York/New
Jersey Metrostars."
To figure out this calculation, we need to examine the length of each item within a field.
Since we need to save this information, we need a place to store it. Since we'll need a
value saved for each column, it makes sense to store this value inside an array with an
element for each column.
Let's add a property to window1. Go to the Edit menu and choose "New Property." Add
an array like this: colMax(0) as integer.
Now create a new method (Edit menu, "New Method") and call it CalcMax. This routine
will go through and figure out the largest (widest) string for each column and store that
within the colMax array.
The first thing the routine does is initialize colMax to the number of fields within the
current data file. Then it starts up two nested loops: the outer one (col) steps through
each column, while the inner one (row) steps through each line (record) of the text and
sets n to the width of that field. Then it checks n to see if it's bigger than whatever value
has been stored in colMax(col). If it is, then we store n inside colMax(col), replacing
what used to be there.
Here's the code:
dim row, col, n as integer
redim colMax(listBox1.columnCount - 1)
for col = 0 to (listBox1.columnCount - 1)
colMax(col) = 0
for row = 0 to (listBox1.listCount - 1)
n = len(listBox1.cell(row, col))
if n > colMax(col) then
colMax(col) = n
end if
next // row
next // col
Note that after this routine runs, colMax contains the maximum lengths of each field.
That all the routine does: build the colMax array.
Now let's create a new method called exportText. This is where the actual conversion
takes place. We basically need to do two things: create a new file where we saved the
converted text, and convert the tabs to spaces.
The first part isn't difficult. We ask the user to name and save the new file, and then we
try to create it (with the .createTextFile function). If that all works, we're set to
convert the text.
Here's the code:
dim
dim
dim
dim
row, col as integer
s, tcell as string
f as folderItem
t as textOutputStream
f = getSaveFolderItem("", "Converted")
if f = nil then
beep
return
end if
t = f.CreateTextFile
if t = nil then
beep
return
end if
calcMax
s = ""
for col = 0 to (listBox1.columnCount - 1)
s = s + listBox1.heading(col)
s = s + padSpaces(colMax(col) - len(listBox1.heading(col)) +
padSlider.value)
next // col
s = s + chr(13)
for row = 0 to (listBox1.listCount - 1)
for col = 0 to (listBox1.columnCount - 1)
tcell = listBox1.cell(row, col)
s = s + tcell
if col < (listBox1.columnCount - 1) then
s = s + padSpaces(colMax(col) - len(tcell) + padSlider.value)
end if
next // col
s = s + chr(13)
next // row
t.write s
t.close
msgBox "All done!"
Before we begin the conversion, we first call calcMax to set the colMax array. Next, we
examine the headers of listBox1 and build the first row of s, which will become our
exported file. Note that the amount we send padSpaces (the number of spaces between
fields) uses the following calculation formula:
C minus L minus P
where C is the size of the largest item within that field, L is the length of the current field
value, and P is the value of padSlider.
After adding our headers to s we add a carriage return (chr(13)), then we start some
for-next loops. The outer loop is row, in which row counts through each row of
listBox1. The inner loop is col, which steps through each column of data. As we
encounter each field, we add the appropriate number of spaces to pad out the column.
After each row, we add a return character to s to finish off the line.
When we've finished the loops, we write s to the file to save it, and close it. We display a
"Done" message so the user knows the file has been saved, and we're all finished.
Before our program will work, however, there's one more thing to do: we must call
exportText from our exportButton. Add this line to exportButton's Action event.
exportText
There! We're all finished. How does it work? Let's test it using the sample file I gave out
last week (Option-click to save it). Here's the result:
Date
May 31
June 1
June 1
June 1
June 2
June 2
June 2
June 2
June 3
June 3
June 3
June 4
June 4
June 4
June 4
June 4
June 5
June 5
June 5
June 5
June 5
June 6
June 6
June 6
June 6
June 6
June 7
June 7
June 7
June 7
June 8
June 8
June 8
June 8
June 9
Match
France vs. Senegal
Uruguay vs. Denmark
Germany vs. Saudi Arabia
Rep. of Ireland vs. Cameroon (tape)
Argentina vs. Nigeria
Paraguay vs. South Africa
Spain vs. Slovenia
England vs. Sweden (tape)
Croatia vs. Mexico
Brazil vs. Turkey
Italy vs. Ecuador
China vs. Costa Rica
Japan vs. Belgium
Korea Republic vs. Poland
Brazil vs. Turkey (replay)
Italy vs. Ecuador (replay)
Russia vs. Tunisia
United States vs. Portugal
Germany vs. Rep. of Ireland
United States vs. Portugal (replay)
Korea Republic vs. Poland (replay)
Denmark vs. Senegal
Cameroon vs. Saudi Arabia
France vs. Uruguay
United States vs. Portugal (replay)
Germany vs. Rep. of Ireland (replay)
Sweden vs. Nigeria
Spain vs. Paraguay
Argentina vs. England
France vs. Uruguay (replay)
South Africa vs. Slovenia
Italy vs. Croatia
Brazil vs. China
Argentina vs. England (replay)
Mexico vs. Ecuador
Time
7:25 a.m. ET
4:55 a.m. ET
7:25 a.m. ET
3:30 p.m. ET
1:25 a.m. ET
3:25 a.m. ET
7:25 a.m. ET
3:30 p.m. ET
2:25 a.m. ET
4:55 a.m. ET
7:25 a.m. ET
2:25 a.m. ET
4:55 a.m. ET
7:25 a.m. ET
1:00 p.m. ET
3:00 p.m. ET
2:25 a.m. ET
4:55 a.m. ET
7:25 a.m. ET
3:00 p.m. ET
3:00 p.m. ET
2:25 a.m. ET
4:55 a.m. ET
7:25 a.m. ET
1:00 p.m. ET
3:00 p.m. ET
2:25 a.m. ET
4:55 a.m. ET
7:25 a.m. ET
12:00 p.m. ET
2:25 a.m. ET
4:55 a.m. ET
7:25 a.m. ET
1:00 p.m. ET
2:25 a.m. ET
Channel
ESPN2
ESPN2
ESPN
ABC
ESPN2
ESPN
ESPN
ABC
ESPN2
ESPN2
ESPN2
ESPN2
ESPN2
ESPN2
Classic
Classic
ESPN2
ESPN2
ESPN2
ESPN2
Classic
ESPN2
ESPN2
ESPN2
Classic
Classic
ESPN2
ESPN2
ESPN2
ESPN2
ESPN2
ESPN2
ESPN
ABC
ESPN2
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
June
July
9
9
10
10
10
10
11
11
11
11
11
11
12
12
12
12
12
13
13
13
13
13
13
13
14
14
14
14
14
15
15
15
15
16
16
16
17
17
18
18
18
18
19
19
21
21
22
22
22
22
25
25
26
26
26
27
29
29
29
30
30
3
Costa Rica vs. Turkey
Japan vs. Russia
Korea Republic vs. United States
Tunisia vs. Belgium
Portugal vs. Poland
Korea Republic vs. United States (replay)
Denmark vs. France
Senegal vs. Uruguay
Cameroon vs. Germany
Saudi Arabia vs. Rep. of Ireland
Korea Republic. vs. United States (replay)
Portugal vs. Poland (replay)
Sweden vs. Argentina
Nigeria vs. England
South Africa vs. Spain
Slovenia vs. Paraguay
Denmark vs. France (replay)
Costa Rica vs. Brazil
Turkey vs. China
Mexico vs. Italy
Ecuador vs. Croatia
Sweden vs. Argentina (replay)
Mexico vs. Italy (replay)
Costa Rica vs. Brazil (replay)
Tunisia vs. Japan
Belgium vs. Russia
Portugal vs. Korea Repbulic
Poland vs. United States
Mexico vs. Italy (replay)
1st E vs. 2nd B (1)
1st A vs. 2nd F (5)
Poland vs. United States (replay)
Round of 16 match (tape)
1st F vs. 2nd A (6)
1st B vs. 2nd E (2)
Round of 16 match (tape)
1st G vs. 2nd D (3)
1st C vs. 2nd H (7)
1st H vs. 2nd C (8)
1st D vs. 2nd G (4)
Round of 16 (replay)
1st D vs. 2nd G (replay)
Round of 16 (replay)
Round of 16 (replay)
Winner (5) vs. Winner (7) (C)
Winner (1) vs. Winner (3) (A)
Winner (2) vs. Winner (4) (B)
Winner (6) vs. Winner (8) (D)
Quarterfinal match (tape)
Quarterfinal match (replay)
Winner (A) vs. Winner (B)
Semifinal #1 (replay)
Winner (C) vs. Winner (D)
Semifinal #1 (replay)
Semifinal #2 (replay)
Semifinal #2 (replay)
Semifinal #1 (replay)
Semifinal #2 (replay)
Third place match (tape)
World Cup final
World Cup final (replay)
World Cup final (replay)
4:55 a.m. ET
7:25 a.m. ET
2:25 a.m. ET
4:55 a.m. ET
7:25 a.m. ET
2:00 p.m. ET
2:25 a.m. ET
2:25 a.m. ET
7:25 a.m. ET
7:25 a.m. ET
1:00 p.m. ET
3:00 p.m. ET
2:25 a.m. ET
2:25 a.m. ET
7:25 a.m. ET
7:25 a.m. ET
1:00 p.m. ET
2:25 a.m. ET
2:25 a.m. ET
7:25 a.m. ET
7:25 a.m. ET
1:00 p.m. ET
3:00 p.m. ET
7:00 p.m. ET
2:25 a.m. ET
2:25 a.m. ET
7:25 a.m. ET
7:25 a.m. ET
1:00 p.m. ET
2:25 a.m. ET
7:25 a.m. ET
1:00 p.m. ET
3:30 p.m. ET
2:25 a.m. ET
7:25 a.m. ET
1:30 p.m. ET
2:25 a.m. ET
7:25 a.m. ET
2:25 a.m. ET
7:25 a.m. ET
12:00 p.m. ET
2:00 p.m. ET
1:00 p.m. ET
3:00 p.m. ET
2:25 a.m. ET
7:25 a.m. ET
2:25 a.m. ET
7:25 a.m. ET
1:30 p.m. ET
9:30 p.m. ET
7:25 a.m. ET
3:00 p.m. ET
7:25 a.m. ET
1:00 p.m. ET
3:00 p.m. ET
1:00 p.m. ET
2:30 a.m. ET
4:30 a.m. ET
1:30 p.m. ET
6:30 a.m. ET
12:30 p.m. ET
2:30 p.m. ET
ESPN2
ESPN
ESPN2
ESPN2
ESPN2
ESPN2
ESPN
ESPN2
ESPN
ESPN2
Classic
Classic
ESPN
ESPN2
ESPN
ESPN2
Classic
ESPN
ESPN2
ESPN
ESPN2
Classic
ESPN
ESPN2
ESPN
ESPN2
ESPN2
ESPN
Classic
ESPN2
ESPN
ABC
ABC
ESPN2
ESPN
ABC
ESPN2
ESPN2
ESPN2
ESPN2
Classic
ESPN2
Classic
Classic
ESPN2
ESPN2
ESPN2
ESPN
ABC
ESPN2
ESPN2
ESPN2
ESPN2
Classic
ESPN2
Classic
ESPN2
ESPN2
ABC
ABC
ABC
ESPN2
Looks pretty good, eh? Note that we don't take into account the overall length of each
line, so this could create lines too long for email (most email programs wrap text longer
than 65 or 70 characters). An improved Monotab would wrap text cells to keep the
overall line length within a user-specified maximum. Perhaps we'll do that in a future
RBU -- at present I explored creating that version, but it's surprisingly complicated and I
never finished it.
If you would like the complete REALbasic project file for this week's tutorial (including
resources), you may download it here.
Bonus Program: Convert Times
Just to show you how I'm a pathalogical user of REALbasic, I'm going to let you look a
the source for another program I wrote along the lines of Monotab, called Convert Times.
Convert Times was written specifically to look at the World Cup TV schedule above and
convert the times from Eastern to Pacific (I live in California). Obviously that's useless
for other purposes, but it is interesting to look at the code, and the program presented
some interesting challenges. For instance, converting from Eastern to Pacific is the
"simple" matter of subtracting three hours: but what happens when a show starts at 2:30
a.m. ET?
If you'd like the project file for Convert Times, you can download it here.
Next Week
We'll have a mini-review of the latest version of REALbasic, 4.5, released last week at
the Macworld Expo in New York.
News
REALbasic Developer Magazine Launched!
Speaking of Macworld Expo New York, yours truly was there in person to hand out
flyers and printed copies of the premiere issue of REALbasic Developer. The results were
outstanding: everyone was excited to see the first printed copies, and the balance of
articles seemed to appeal to potential readers.
For people who've already subscribed to REALbasic Developer, your copies are being
mailed right now. (International orders will take a little bit longer, due to red tape with
the U.S. postal service.) Those who've ordered digital (PDF) subscriptions will receive an
email shortly with instructions on how to download your copy.
If you'd still like to subscribe, it's not too late: we'll be doing a second mailing of the
premiere issue at the end of August (keep in mind this is the August/September issue) to
catch any late subscri