| |||||||||||||||||||||||||||
|
| |||||||||||||||||||||||||||
Print This Article REALbasic University: Column 093
OOP University: Part SeventeenIn our last lesson I didn't quite finish explaining how SuperDraw loads and saves files, so we'll wrap that up today.
Opening FilesNow 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.
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 PicturesThe 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):
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:
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.
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 FixesSuperDraw, 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 BugFirst, 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 PicturesAnother 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 LayersFinally, 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 UpDepending 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 WeekWe get back to some good old fashioned OOP lecturing!
SuperDraw Object ContestSuperDraw'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
Winners will be announced in May 2003, so get coding!
LettersToday we've got a letter from Mel Defrancesco who has a pertinent question about file saving.
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:
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:
The problem here is that if you look in your "save" and "load" code you'll find this line:
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:
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:
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:
FileSave Menu Handler:
FileOpen Menu Handler:
You don't need to do any checking within the FileSave menu handler because you should do that within EnableMenuItems:
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:
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:
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 See the REALbasic University Archives
REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.
| |||||||||||||||||||||||||||