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 093

OOP University: Part Seventeen

In our last lesson I didn't quite finish explaining how SuperDraw loads and saves files, so we'll wrap that up today.

Opening Files

Now that we've got our file save routine, let's add a new method for loading an existing file. This method simply takes a folderItem as a parameter and attempts to read in the SuperDraw picture saved in it.

  
sub loadFile(theFile as folderItem)
dim binFile as binaryStream
dim i, j, n, kind as integer

if theFile <> nil and theFile.exists then
binFile = theFile.openAsBinaryFile(false)
if binFile <> nil then

// Erase existing objects (if any)
eraseAllObjects

n = binFile.readLong
for i = 1 to n
// Read object kind
kind = binFile.readLong

addObject(kind, binFile.readLong, binFile.readLong, binFile.readLong, binFile.readLong)
objectList(i).setLineSize(binFile.readLong)

if binFile.readByte = 1 then
objectList(i).selected = true
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
j = binFile.readLong
pictClass(objectList(i)).image = setPictureData(binFile.read(j))
case 5 // Textblock
j = binFile.readLong
textClass(objectList(i)).text = binFile.read(j)
j = binFile.readLong
textClass(objectList(i)).textFont = binFile.read(j)
end select
next // i

binFile.close
theDocument = theFile

// Force redraw
me.refresh

else
beep
msgBox "Sorry, couldn't open the file."
end if // binFile = nil
end if // theFile = nil
end sub

Okay, the first thing we do is make sure the passed folderItem is valid (not nil) and it exists. Then we create a binaryStream object from it and read in the data. Our data read commands are exactly the opposite of the ones in the saveFile method.

You'll notice that the first item we read is a long which tells us how many objects we'll be loading. Then, for each object, we first read the number which tells us what kind of object we're reading. That's important because different objects might contain different amounts of data (objects like pictClass, for instance, contain extra data representing the picture).

For each object we read we set its properties according to the saved info, and eventually we have reloaded all the drawing objects that used to exist in the drawing!

Our final step is to redraw the screen so the new drawing is displayed.

Working with Pictures

The biggest obstacle with our file save and load routine is our pictClass object. Since this imports in a graphic, we need to save that graphic inside our file. In effect, our SuperDraw file format can contain multiple pictures!

But to do this we must be able to access the raw, binary data of the picture. Normally REALbasic doesn't give us access to that. When you load a graphic it is put into a picture data type and you really don't know anything about the actual data -- REALbasic handles all that behind the scenes.

So how do we get the raw picture data? Well, it's a little tricky, and it's a huge hack, but here's the basic principle. We know that when a graphic is stored on disk it exists as raw binary data, exactly what we want. Once REALbasic has loaded it as a picture, of course, that's too late for us to deal with it as data. So why can't we just read the picture from disk as binary data instead of as a picture? Why not indeed!

Here's the method we need to add (to drawCanvasClass):

  
function getPictureData(thePicture as picture) as string
dim f as folderItem
dim bf as binaryStream
dim s as string

if thePicture <> nil then
// Create temporary file
f = temporaryFolder.child("picture" + generateRandomString)
while f <> nil and f.exists
f = temporaryFolder.child("picture" + generateRandomString)
wend

f.saveAsPicture thePicture
bf = f.openAsBinaryFile(false)
if bf <> nil then
// Read back picture as binary string
s = bf.read(bf.length)
bf.close
f.delete // Delete temp file
return s
end if // bf = nil
end if // thePicture = nil

return ""
end function

As you can see, this function takes a picture as a parameter and returns a string (the raw binary data). To do this, it must save the graphic as a picture file to a temporary file, then read the file back in as binary data.

So the first obstacle this routine has to overcome is to generate a temporary file for the graphic. We save this file inside the temporaryFolder, and we give it a random name (we use a routine called generateRandomString which we'll write in a minute). To make sure we don't erase any existing file, we make sure the file doesn't exist before we create it.

Once the folderItem is valid, we use REALbasic's saveAsPicture method to create the file on disk. Then we open that file as a binaryStream file and read in all the data. After we've closed and deleted our temporary file, we return the data we retrieved. If there was any kind of error, this function returns an empty string.

Our course for this to work, we need a name for our temp file, so let's add the generateRandomString method:

  
function generateRandomString() as string
return str(rnd * 9) + str(rnd * 9) + str(rnd * 9) + str(rnd * 9)
end function

This is very simple: it just returns a four-character string of four numbers, each between 0 and 9. This, combined with the code we wrote earlier, will mean our temp files will be named "picture 0123" (or some variation thereof).

We're making good progress, but we'll also need the opposite of getPictureData for our load/save methods to work. So let's add setPictureData, which takes a string of data as its parameter and returns a picture object.

  
function setPictureData(theData as string) as picture
dim f as folderItem
dim bf as binaryStream
dim p as picture

// Create temporary file
f = temporaryFolder.child("picture" + generateRandomString)
while f <> nil and f.exists
f = temporaryFolder.child("picture" + generateRandomString)
wend

bf = f.createBinaryFile("bin")
if bf <> nil then
// Save picture data
bf.write theData
bf.close

// Now reopen as picture
p = f.openAsPicture
f.delete // Delete temp file
return p
end if // bf = nil

// Error
return nil
end function

As you can see, this is very similar to its opposite routine. We first generate a randomly-named temporary file, then we save the picture data to it as a binaryStream. Once the file is written, we read it back as a picture object. We then delete the temp file and return the picture object. If anything in this routine fails, we return nil.

That's pretty much it for loading and saving files, though the cautions I mentioned last time regarding modifying the file format to support your modified and additional objects are important to remember.

I would recommend you make each of these three routines we just added -- getPictureData, setPictureData, and generateRandomString -- private (called "protected" in REALbasic 5 or better). This will prevent these methods from being available outside of drawCanvasClass, which is a good idea since these are not routines other objects need to access.

Finishing Up and Future Fixes

SuperDraw, for the purposes of RBU, is now finished. (Though we'll be unveiling the results of our SuperDraw contest in May -- see today's News for details on the contest.)

However, before we wrap things up, I will mention a few limitations and potential problems with SuperDraw in case you're thinking of expanding this into a more full-featured drawing program. This list isn't exhaustive, of course -- it's possible there are other more significant problems you'll encounter when you attempt to use the SuperDraw classes -- but thinking of these kinds of problems will hopefully help you in thinking about the kinds of problems that exist out there and what it takes to create a truly polished program.

Drag Bug

First, we've got a potential problem with our dragging system. Currently we're not allowed to drag an object outside of the drawing area -- that's because the mouseDrag event only works inside canvas1. However, because we can select more than one object at a time and move them as a group, we can cause one of the extra objects to be placed off the drawing area. Once that happens there's no way to get it back!

Well, the user can make the drawing area larger or smaller to reveal hidden objects, but at some point the size of the drawing area is limited to the size of the user's screen, and of course, objects can be pushed off that maximum size.

To fix this we'd need to add some code to prevent objects from being moved off the drawing area (outside canvas1).

Proportional Pictures

Another problem is that our pictClass isn't concerned with picture proportion. If the picture box is 50 x 50 and we import in a 50 x 100 picture, it is squished to 50 x 50. The user is free to adjust this, of course, and can stretch or squish the picture at will. But there's currently no way to ensure a picture is properly proportional to the original size.

The Macintosh standard is to constrain a picture to its proportional dimensions when it is resized while the Shift key is held down. Adding this feature would be highly recommended.

Object Layers

Finally, you may have seen that drawing objects in SuperDraw can appear on top of each other. Currently this happens based on the order in which the objects were added. Initial objects are on the bottom; later objects appear on top.

However, what if the user created these in the wrong order? Right now the user can only delete and start over. Professional drawing programs allow a user to move objects forward or backward in the layer (or all the way to the top or bottom). Adding support for object's layer positioning would be an excellent addition, and it wouldn't be that difficult.

Moving objects up or down a layer isn't difficult in principle: you simply move the object in the objectList array up or down. So objectList(10) becomes objectList(9). However, as you'll remember from earlier in this OOP University series, objects in REALbasic are linked by reference, not instance, so objectList(10) is a reference to an object, it is not the object itself. You cannot just reassign an object with a objectList(10) = tempObject command. No, you must copy the object, which means copying all its properties. The best way to do this is to add a copyTo method to shapeClass, and then override that method with additional property copies for subclasses with unique properties (like textClass).

You'll also need an interface to handle these layer moves. The most common is menu commands (or perhaps a contextual menu). There you'll find you'll tell drawCanvasClass to move the selected objects up or down or to top or bottom, so you'll have to have methods for all of those actions. It's not difficult, but it does require and number of steps.

An alternate approach to solving the layer problem is to simply make SuperDraw prevent objects from overlapping (running into each other). You'd have to do this in two places: in the addObject method, where you'd ensure that new objects don't overlap other ones, and during mouseDrag, ensuring that moved objects aren't moved into another one. Whichever way you go there are significant obstacles to overcome.

Wrap Up

Depending on what you're wanting to do with SuperDraw, these may not be things you want to bother fixing, or there might be others that are more important. It's up to you. But hopefully we've covered enough of the issues you'll have an idea of how to go about making SuperDraw do what you want.

If you would like the complete REALbasic project file for this week's tutorial (including resources), you may download it here.

Next Week

We get back to some good old fashioned OOP lecturing!

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 we've got a letter from Mel Defrancesco who has a pertinent question about file saving.

Hi Marc,

I enjoy your column and I hope you can help me with this problem. I hope its not a long problem but I can't seem to work it out.(I hope its not so ovbious that I look like a Neanderthal). Well here goes....

The following shows two methods where information in variable 'percentcomplete' is loaded and saved. The problem is if the 'filename' has been established, because a 'saveas' has already taken place, how do you save further changes where a 'save' routine finds the already created file and replaces it with the new updated file?

Thanks,
Mel

LOAD METHOD:

  
dim file as folderItem
dim binary as binaryStream
dim i as integer
redim percentcomplete(-1)

file=getopenFolderItem("filetype")
if file<>nil then
file.extensionVisible=true
filename=file.displayName
binary=file.openAsBinaryFile(false)
do until binary.eof
i=binary.readByte
percentcomplete.append val(binary.read(i))
loop
filehasbeensavedbefore=true
end if

SAVE METHOD:

  
dim binary as binaryStream
dim file as folderItem
dim i as integer

if filehasbeensavedbefore=false then
file = getsaveFolderItem("filetype",filename)
if file <> nil then
filename=file.displayName
binary=file.createBinaryFile("filetype")
for i = 0 to 22
binary.writeByte len(str(percentcomplete(i)))
binary.write str(percentcomplete(i))
next
binary.close
end if

else if filehasbeensavedbefore=true then
//this is the part that does not work
file = getFolderItem(filename)
if file.exists then
binary=file.createBinaryFile("file")
for i = 0 to 22
binary.writeByte len(str(percentcomplete(i)))
binary.write str(percentcomplete(i))
next
binary.close
end if
end if

filehasbeensavedbefore=true

Cool question, Mel. I hope you don't mind I saved it until today as it fits in perfectly with today's SuperDraw wrap-up.

First let me say I'm not sure exactly what problem you're encountering. You mention the "save as" portion of your code doesn't work, but you don't see why. Without creating a duplicate project or having your full code it's difficult for me to test this, but I do have various theories as I see several problems with your code.

I see two potential bugs in your code. In your "save as" code you've got this line:

  
binary=file.createBinaryFile("file")

where you use the file type "file" -- however, in your load and save routines you use a file type "filetype" -- that could be the problem.

Another potential problem is that in your "save as" routine you are attempting to recreate the file's folderItem with this code:

  
file = getFolderItem(filename)

The problem here is that if you look in your "save" and "load" code you'll find this line:

  
filename=file.displayName

The problem here is that this sets filename to the file's name, not its path. Even worse, this is only the displayName, which could be incomplete under Mac OS X if the user has extensions hidden.

If you put these two pieces of code together, your "save as" routine code really is saying something like this:

  
file = getFolderItem("a sample file")

The problem is that getFolderItem wants a full path to a file, not just a file name. It will take just a file name, but then it assumes the file is in the same folder as the application. So your routine might work if the file's in the same folder as the app, but it would certainly break if the file was elsewhere on your hard drive.

One solution to this would be to replace your filename=file.displayName with this:

  
filename=file.absolutePath

This would remember the full path to the file instead of just the name, and should solve that problem.

However, there's a more elegant solution that you might want to explore. I thought about explaining this as part of today's SuperDraw lesson but left it out as it's not really part of that program since it applies to almost all programs.

You'll notice in SuperDraw we only bothered to create a single "File Save" menu. A real Macintosh application ought to have two menus, a "Save" and a "Save As...". Presumably that's what your app has and that's where you're running into trouble.

Now in true object-oriented fashion, a document ought to know how to save itself. That's why in SuperDraw our drawCanvasClass has it's own save and load routines. To make it more complete, however, we should have included a "save as" routine as well.

In your code, you've put your "save" and "save as" code in the same routine. While there's nothing horribly wrong with that, you are duplicating code (look at how many lines of the two sections of code are identical). Here's a better way to do this.

Like we do in SuperDraw, add a folderItem property that is a link to the saved file (let's call it theDocument). This would replace the local file variable in all your code. If you do this, you can also get rid of your filehasbeensavedbefore variable. That's because theDocument is either going to point to the saved file or it will be nil. If it's nil, that means the file hasn't been saved yet (just like your filehasbeensavedbefore variable)

Then all you need is code similar to this (your program may require slightly different code, of course):

FileSaveAs Menu Handler:

  
if theDocument = nil then
theDocument = getsaveFolderItem("filetype", "untitled")
if theDocument <> nil then
doFileSave
end if
end if

FileSave Menu Handler:

  
doFileSave

FileOpen Menu Handler:

  
doFileLoad

You don't need to do any checking within the FileSave menu handler because you should do that within EnableMenuItems:

  
FileOpen.enabled = true // Always available
FileSaveAs.enabled = true // Always available
if theDocument <> nil then
// Only available if doc has been saved before
FileSave.enabled = true
end if

This will prevent the FileSave menu command from being available if the file hasn't been saved yet ("Save As..." will be the only available choice).

Because we're doing much of your error-checking outside of your save routine, your actual doFileSave method can be much shorter and generic:

doFileSave Method:

  
dim binary as binaryStream
dim i as integer

binary = theDocument.createBinaryFile("filetype")
for i = 0 to 22
binary.writeByte len(str(percentcomplete(i)))
binary.write str(percentcomplete(i))
next
binary.close

You'll notice that this routine doesn't even need to check to see if theDocument is nil or not, since this routine will never be called unless theDocument is valid. We also don't need to see if this is a "save as" or not, since this routine doesn't care: it does the same thing if this is a first save or the 100th.

So not only is your code simpler, but there's only one routine now, so if you change your file format you only have to update the code in one place.

Your file loading routine changes a little, but it's mostly the same. The main difference is that it stores the opened file's folderItem into theDocument instead of the local variable file:

doFileLoad Method:

  
dim binary as binaryStream
dim i as integer

redim percentcomplete(-1)
theDocument = getopenFolderItem("filetype")
if theDocument <> nil then
theDocument.extensionVisible = true
filename = theDocument.displayName
binary = theDocument.openAsBinaryFile(false)
do until binary.eof
i = binary.readByte
percentcomplete.append val(binary.read(i))
loop
end if

Hopefully this will solve your problem and improve your code. This type of thing is generic enough you should be able to use the same technique for all your programs. It's highly portable and the recommended approach for dealing with the "save as" problem. It's definitely a useful skill to know how to make this work.

Those of you who are interested should add a "save as" feature to SuperDraw using the above technique. Because SuperDraw encapsulates much of its code, the methodology will vary a little from the above example.

For instance, theDocument, drawCanvasClass' property, should be private -- so an external routine (in Window1 for example) would not be able to check if theDocument is nil or not. Instead you'll have to add a function within drawCanvasClass that will report if theDocument is nil or not (you could even call it hasFileBeenSaved and have it return a boolean).

Another method would be to move our file menu handlers to within drawCanvasClass, whereupon those handlers would have legitimate direct access to drawCanvasClass' properties.


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