REALbasic University Resources:

RBU: Glossary Defines common REALbasic programming terms
  Archives Previously published columns
Translations: Dutch Courtesy of Floris van Sandwijk
  Japanese Courtesy of Kazuo Ishizuka
  Chinese Courtesy of Dong Li
  RBU Translation Guide Information on Translating RBU into other languages
Books: Matt's Book (2nd Edition!) Ideal for experienced programmers
  Erick's Book Best for beginning programmers
Websites: Mother Ship The publisher of REALbasic
  RB Webring Links to hundreds of REALbasic websites
  RESExcellence Another REALbasic programming column
  REALbasic Developer Magazine The premiere source for REALbasic instruction.

REALbasic University is Sponsored by

Make your Mac do what YOU want it to. Create games, utilities, cool Mac OS X tricks. Download REALbasic now and create your own software.


Print This Article

REALbasic University: Column 092

OOP University: Part Sixteen

Last time we began polishing SuperDraw and today we'll continue that effort with a interface enhancement and adding the ability to load and save drawings.

I had intended that this would be our last SuperDraw lesson, but this portion took more time than I anticipated, so we won't quite finish it today. I'm actually a week behind on publishing RBU, however, so I'll publish two this week and catch up and you won't have to wait long for the second part of today's lesson.

Improving the Interface

We'll begin with the simplest addition. For simplicities' sake, when I began SimpleDraw, I created my pushButton-based "tool palette" on the same window as the drawing. This worked fine for an example project, but for a more full-featured drawing program like SuperDraw, it makes more sense to have a real tools palette as a separate window. We can also improve how that tool palette looks.

First, we're going to add a new window (File menu, "New Window") and give it these properties:

Next, download this folder of icons. You can just drag the entire folder into your REALbasic project folder to import the images. Then place six bevelButtons on objectPalette, making sure to create them as a control array (they all should have the same name property). The index property of the first should be 0, the second is 1, the third is 2, etc. For each bevelButton, set its caption property to the appropriate text and its icon to the correct picture. When you're finished it should look something like this:

Yes, the artwork isn't exactly fancy, but it's better than the plain text, and this should give you an idea of how to enhance this. If you want to make your version more elaborate or prettier, be my guest.

If you're in a hurry to create this window, but you'd still like to follow along with the rest of the project (versus downloading the completed work), you can grab just the window here.

The code we're going to add to objectPalette is simple enough. We simply adapt the code used for the same buttons on window1. Put this in the Action event of objectPalette's addObjectButton:

  
dim l, t, w, h as integer

// Generate random size and location
w = round(rnd * 50) + 10
h = round(rnd * 50) + 10
l = rnd * (window1.canvas1.width - w)
t = rnd * (window1.canvas1.height - h)

window1.canvas1.addObject(index, l, t, w, h)

This simply generates a random size and location within window1's canvas1 and tells that object to add a new drawing object at that size and location. Easy!

Technically we're done here -- the program and palette should work -- but there still are a couple things needed to make this look and work better.

First, open window1 and delete the pushButtons we have there. Then make window1 square (325 x 325) with canvas1 set to 300 x 300. Now our drawing area is on its own window. Since we've done that, we can lock the four lock properties (lockleft, locktop, etc.) of canvas1 and set window1's growBox property to true. That way the user can resize the drawing area.

Finally, let's add some code to window1's Open event:

  
objectPalette.top = 50
objectPalette.show

me.left = objectPalette.left + objectPalette.width + 25

This will ensure that objectPalette and window1 open up in the correct places.

Good. Enough with that, let's get to saving drawings!

Saving and Opening Files

The most important limitation of SuperDraw is that there is no way to save or retrieve drawings. But since our drawing is really nothing more than a list of objects, we can easily save that to a file.

Creating a File Format Standard

However, it's important to note that while our object-oriented approach makes adding new kinds of objects and capabilities to our program, it also makes coming up with a "final" drawing file format difficult. For example, if SuperDraw supports five kinds of objects and you create a save routine for those five, adding a sixth object type will break your save routine. You also need to modify your load routine to support the new object as well.

The same problems occur if you enhance the existing objects, giving them color settings or other new properties. If you're creating a program that will be used by other people, you need to keep in mind that some users might try to open a more recent file with an older version of your application (as well as vice versa).

My save and load routines will work for my basic objects, but if you've modified yours, you'll need to modify my routines to support your objects.

If you don't solve for this situation, your program could lose (fail to save) objects or certain object characteristics, or it could crash on load or save (for instance, if you try to assign or get a characteristic of a drawing object when that property doesn't exist).

There are several ways around this problem, none of which SuperDraw implements, but I will briefly describe some of the methodologies in case you're interested in making the SD file format more robust.

The simplest "solution" is to include a version number in every file save, and then make your file load routine ignore files it can't handle. In other words, if your file routine is up to version 10 (or whatever), it will only open "10" files. Files with other version numbers won't open since the file load routine isn't sure what those formats are. Of course this isn't very convenient for the end-user, who might have files from different versions.

A slightly more advanced version of this is to include multiple "load" routines in your program, one for each file version you support. Then, based on the version detected, you can call the appropriate load method. This is more work for you, but transparent for the end-user.

A more powerful approach is to create a dynamic, "limitless" object-oriented file format. This can be tremendously complex, but the basic approach is to include enough description information in the file format that the program's load routine can identify and load the appropriate objects. For example, let's say an object has a integer property. Well, if the file format tells us it's an integer property and what that value is, the load routine can check to make sure that the property it is setting is also a integer property (therefore doing a little error-checking to make sure we aren't trying to assign the wrong data type to a property, which would cause a crash).

Of course there is no file format that can teach a program how to handle objects it wasn't programmed to understand. However, adding extra information could allow your program to better identify and load objects it does know how to support and to ignore objects it doesn't understand. To the user, this could mean opening a newer drawing in an older version of the program wouldn't cause a crash or an error, but only load an incomplete drawing -- but for the user that might be better than nothing. Obviously a warning to the user of this potential side-effect is appropriate.

Top commercial programs use a combination of these techniques. For instance, Adobe Illustrator will warn you when you save a drawing (via the "Save As" command) in an older format that not all elements may be preserved. And you can often open newer Illustrator files in an older version of the program, though it will tell you that some objects or elements may be missing. Watch how other programs support these kinds of problems to get ideas of how you should handle it.

Adding Menus

To support file saving, we'll need more menu commands, so open the Menu object in your project file and on the File menu add FileNew, FileOpen, and FileSave. Their CommandKey properties should be set to N, O, and S, respectively.

Within window1's enableMenuItems event, put this:

  
if canvas1.objectCount > 0 then
editSelectAll.enabled = true
fileSave.enabled = true
end if
fileNew.enabled = true
fileOpen.enabled = true

Then add the three new menu handlers (Edit menu, "New Menu Handler"). For FileNew put this:

  
canvas1.eraseAllObjects

Here's the code for FileOpen:

  
canvas1.loadFile(getOpenFolderItem("superdraw"))

And for FileSave:

  
canvas1.saveFile

As you can see, all we do here is pass messages on to canvas1, telling it to open or save a file.

Finally, you'll notice in the FileOpen routine we use a file type called "superdraw" -- we need to define that for our project. On the Edit menu, go to "File Types..." and add a new one with these settings:

Excellent. Now we're ready to write some code!

Save and Load Methods

Let's begin by opening drawCanvasClass. It is here, in our drawing canvas, where we'll add methods to do the file loading and saving.

The first method we'll add (Edit menu, "New Method") is one called eraseAllObjects. Here's the code:

  
redim objectList(0)
me.refresh

As you can see, this simply erases the current drawing. The refresh command redraws the window. This command is used at two different times: when the user creates a new file (via the "New" menu command) and when a new file is being opened (the current drawing is erased).

Note that currently SuperDraw gives no warning to the user that the current drawing will be lost: it's just immediately erased. Obviously in a commercial program a warning is essential.

The ideal way to support this is to add a new event to drawCanvasClass called eraseExistingDrawing with a boolean return value. If the returned value is false (the default), you continue with the erase, otherwise you stop. You'd change the eraseAllObjects code to look like this:

  
if not eraseExistingDrawing then
redim objectList(0)
me.refresh
end if

Then it would be up to you to add code to canvas1, within the eraseExistingDrawing event, that would ask the user for confirmation as to the delete. For FileNew, the cancel result wouldn't effect anything, but for the FileOpen routine, it's essential the file open operation be canceled if the erase all objects didn't happen. I'll leave both of those processes as exercises for you.

All right, let's get saving. First, since we want to remember the file that was previously saved (we don't want to ask the user for a file name every time the drawing is saved), we need to add a property to drawCanvasClass to remember the file. So add this property (Edit menu, "New Property"): theDocument as folderItem.

Next, here's the saveFile method. Note that our file format is very simple. The very first thing in our file is an integer that indicates how many objects we are going to be saving. After that, we loop through each object in our data structure and save an integer which tells us the kind of object, followed by the left, top, width, height, and lineSize properties (these are properties of shapeClass, and therefore common to all object types). Then we save an odd or even integer which tells us whether the object is selected or not. Finally, we save extra info depending on the kind of object. For instance, for the text object we save the font used and the text specified.

 sub saveFile() 
dim binFile as binaryStream
dim i, n, kind as integer
dim o as shapeClass
dim s as string

if theDocument = nil then
theDocument = getSaveFolderItem("superdraw", "untitled drawing.sd")
end if

if theDocument <> nil then
binFile = theDocument.createBinaryFile("superdraw")
if binFile <> nil then
o = new shapeClass
n = objectCount
binFile.WriteLong n

for i = 1 to n
o = objectList(i)
kind = -1
if o isa circleClass then
kind = 0
elseif o isa rectClass then
kind = 1
elseif o isa triangleClass then
kind = 2
elseif o isa polygonClass then
kind = 3
elseif o isa pictClass then
kind = 4
elseif o isa textClass then
kind = 5
end if

// Save object kind
binFile.writeLong kind
// Save standard shapeClass properties (common to all subclasses)
binFile.writeLong o.left
binFile.writeLong o.top
binFile.writeLong o.width
binFile.writeLong o.height
binFile.writeLong o.getLineSize
if o.selected then
binFile.writeByte 1
else
binFile.writeByte 0
end if

select case kind
case 0 // Circle
// No additional properties
case 1 // Rectangle
// No additional properties
case 2 // Triangle
// No additional properties
case 3 // Polygon
// No additional properties
case 4 // Picture
s = getPictureData(pictClass(o).image)
binFile.writeLong len(s)
binFile.write s
case 5 // Textblock
binFile.writeLong len(textClass(o).text)
binFile.write textClass(o).text
binFile.writeLong len(textClass(o).textFont)
binFile.write textClass(o).textFont
end select
next // i

else
beep
msgBox "Sorry, could not create the file."
end if // binFile = nil
end if // theDocument = nil
end sub

If you aren't that familiar with the binaryStream file object, it's ideal for this kind of data. Each type of info that is saved is a standard size (in bytes). For instance, a writeLong method writes a four-byte number. When we use the opposite command, readLong, it always reads in a four-byte number. As long as your save and load routines match data type for data type, your files will read correctly. If you get them out of sync, however -- reading or writing a byte when you should have read or written a long -- your file is toast. This type of file format requires extreme precision; there is no room for error. Computers are great at this kind of repetitive work and will do it over and over perfectly, without a complaint. But you must write the load and save routines correctly, so they balance each other perfectly, or it won't work.

Now examine the case 5 section above. This is where we make use of the binaryStream's write command, which simply lets us write any data we want. There's no pre-defined length for this kind of data, so we must save the length of this data before we write it so that the read routine knows how much to read.

So the first thing we do is write a long that represents the length of the text (in bytes). Then we write the text. We repeat this with the length of the font name and the font name itself. When we do our file load routine, we'll do the opposite of this, reading in a long and saving that value, and using it to tell the binaryStream how many bytes to read.

In Detail

You'll note that in this routine we make use of a very important REALbasic command, the isa operator:

  
if o isa circleClass then
...

This operator lets us determine what kind of object we are dealing with so we can set our kind variable. This is incredibly powerful and critical. It's critical because without it we wouldn't be able to know what kind of object we were dealing with (at least not without making a way for our object to tell us its kind).

But it's powerful because of the way object subclassing works. Remember, circleClass is a subclass of shapeClass. Because of that, we can almost use circleClass and shapeClass interchangeably. I say almost, because obviously the objects are different, so you must be careful: a crash would happen if you tried to access a unique property of circleClass when the object you were talking to was actually a different one like shapeClass or rectClass.

But because they share the same parent, you can do things like set up a method to accept a shapeClass as a parameter, and in the actual passing, send it a circleClass or rectClass or any other subclass of shapeClass. Because, in a sense, a circleClass is a shapeClass!

  
sub aMethod(theObject as shapeClass)
if theObject isa circleClass then
...

Important Note: this will not work the other direction, i.e. child to parent. If you set up aMethod to accept circleClass as the parameter type, you cannot then send it a shapeClass. That's because shapeClass is not a circleClass. Because circleClass inherits from shapeClass it is a shapeClass, but the opposite is not the case.

Using this is powerful because it lets you set up a single, generic method that can process different kinds of objects. Using it in combination with isa lets the method handle custom aspects of subobjects.

Keep this technique in mind, for it might save you a lot of code. As always, creating routines that work on generic objects is better than routines that work only on a specific object.

And remember, this works with any kind of subclassed object, so you could create a routine that requires and editField as a parameter, but actually pass your own subclass of a editField.

Wrapping Up

Unfortunately, we're out of time for today -- this column is already much longer than normal and I haven't even explained the complicated part of our file save and load routines!

The key problem is the pictClass object. We must save the actual picture data into the file, but REALbasic doesn't give us an easy way to access that data. So we'll implement a multi-step workaround. The code above won't run as you'll notice if you try -- we haven't written some of the critical functions required. I'll explain everything in the next lesson.

In the meantime, I'm going to give you the full SuperDraw project file, complete with everything you need. I'll just wait to explain it until the next 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 really really finish SuperDraw, I promise!

SuperDraw Object Contest

SuperDraw's object-oriented design makes it easy to add new object types and expand existing ones. The possibilities are endless. Have you added your own object types to SuperDraw? Do you have an idea for a new object type or an enhancement to an existing object? Send it to me!

REALbasic University is having a contest! Send in your SuperDraw objects and enhancements. We'll judge them and give out awards and prizes to the best entries. You could win a subscription to REALbasic Developer magazine, an RBD T-Shirt, or other cool prizes!

Contest Rules

  • All entries must be received by April 30th, 2003.
  • Entries may be either a new drawing object class for SuperDraw, or an enhanced version of SuperDraw with new object capabilities and features.
  • All entries must include entrant's Name, Email Address, and Mailing Address.
  • Multiple entries by the same entrant are acceptable.
  • All entries must be compressed in an archive and include the REAlbasic project file and all resources required to run the project.
  • Entries that don't run will be rejected.
  • Entries will be judged on originality, effectiveness, and code efficiency.
  • By submitting an entry, you give permission for REALbasic University to publish your name and your source code/REALbasic project in a future column on the RBU website.
  • All decisions by REALbasic University with regards to this contest are final.
  • Entries must be sent to: rbu-contest@stonetablesoftware.com

Winners will be announced in May 2003, so get coding!

Letters

Today's question is from Darius Monaghan, who writes with a question about modem transfers.

Hi there,

I'm fairly new to RB and have already found myself stuck on a problem. I am writing an app that dials via modem to another computer, logs into a terminal with user/name pass and receives files, all automatically....

So far I have got the app dialling, connecting, sending user/pass details. I am up to the stage of receiving files but don't know where to start....

Protocols? Download directories?

I don't know where to start...

Can you shed some light on my situation?

Thanks

Darzz

Interesting problem! From the way you are describing this, you're using a serial control and actually connecting via the modem (not via the Internet using TCP). Though once you're actually connected they both probably work very similarly.

If you're writing both ends of the equation (connector and connectee, a.k.a. server and client), you can make up whatever protocol you want. As long as you're consistent it should work.

However, the terminal program you are connecting to probably has it's own settings and protocol support, so it'd be up to you to support the same protocol. If it's a Mac, probably the files will be binhexed (in binhex format) which preserves the resource fork.

Rather than write support for the protocols yourself, you can use a third-party plug-in. Einhugur's e-CryptIt Engine supports converting to and from Base64, BinHex, MacBinary III, AppleSingle / Double, and UUCoding.

The terminal program also has its own commands for sending and receiving files, and you'd have to research those to discover what that terminal program supports.

While the above is feasible, it's definitely extra work. I'm not sure exactly what you're doing or why -- for most modern situations, communication via TCP makes far more sense than a direct modem connection. You could, for instance, use standard FTP commands and an FTP server to transfer files reliably, if that's all you're going for. You don't even have to know much about FTP if you use a third-party set of classes such as Pyramid Design's excellent FTP Suite.


About the Column
REALbasic University is a weekly instructional column on programming with REALbasic and is brought to you by REALbasic Developer, the magazine for REALbasic programmers.

Each week we answer select reader questions, and we're always open to ideas for future columns. Send your questions to . (Keep your questions simple and specific. General queries like "How do I write my own web browser?" will be neglected.) Your question won't be answered immediately, but will be answered in a future column. (If you don't want your correspondence published, just be sure to indicate that when you write. Otherwise it's fair game.)

About the Author
is an author, philosopher, graphic designer, photographer, film director, soccer fanatic, and programmer (among other things). He writes for MacOpinion, runs his own software company, Stone Table Software, which sells the revolutionary Z-Write word processor, and is Publisher and Editor of REALbasic Developer. He lives in Northern California with his cats, Mischief and Mayhem, and is rapidly running out of free time.

See the REALbasic University Archives


REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.

Email This Article - Comment On This Article

.

Reader Specials

Server Racks Online:
Apple Xserve CompatibleServer Racks and Universal Network Racks
42U KVM Switch Solutions:
High-End Mac and Multi-Platform KVM Matrix switching solutions!
Digital Camera Online:
Great prices on Digital Cameras and accessories!
KVM Switches Online:
Great prices on Mac KVM Switches from the leading manufacturers!
LCD Monitors Online:
Great prices on LCD Monitors from the leading manufacturers!
LCD Projectors Online:
Shop online for LCD Projectors from the leading manufacturers!
USB 2.0 Online:
Great prices on USB 2.0 products from the leading manufacturers

Serious Business Software:
Accounting, Sales, Inventory, CRM, Shipping, Payroll & more!

KVM Switch solutions for MACs:
DAXTEN is a KVM switch, KVM extender and monitor splitter specialist for PC, SUN and MAC applications from name brand manufacturers - offices worldwide.

The "Think Different Store: The iPod Accessories Store - iPod cases, iPod mini, iPod photo, speakers, itrip, inMotion, Soundstage and all other iPod accessories

Earn Cash with the ThinkDifferent Store Affiliates Program

Need A Web Site?
Applelinks Web Hosting Starting at 19.95 a Month

iTunes_RGB_9mm

.

iTunes_RGB_9mm

Cool Mac Gear


iPod 1G-2G
iPod 3G
iPod 4G
iPod Mini
PowerBook-iBook
Keyboard Skins
Garageband