| |||||||||||||||||||||||||||||
|
| |||||||||||||||||||||||||||||
Print This Article REALbasic University: Column 092
OOP University: Part SixteenLast 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 InterfaceWe'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:
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:
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 FilesThe 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 StandardHowever, 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 MenusTo 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:
Then add the three new menu handlers (Edit menu, "New Menu Handler"). For FileNew put this:
Here's the code for FileOpen:
And for FileSave:
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 MethodsLet'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:
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:
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() 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.
Wrapping UpUnfortunately, 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 WeekWe really really finish SuperDraw, I promise!
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's question is from Darius Monaghan, who writes with a question about modem transfers.
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 See the REALbasic University Archives
REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.
| |||||||||||||||||||||||||||||