The data files that the program creates might for example be used to hold URL addresses for pages on the World Wide Web, together with a comment that explains the content of each referenced page. The two fields in data records, and the data entry panes, are actually named "Location" and "Subject" to reflect this planned usage.
Figure 2.1 illustrates the form planned for the program's interaction window. There are two "edit text" data entry panes, a pane for a pop-up menu that offers options for the addition of a new record, searches and so forth, and a scrollable display showing some of the existing records All these panes are to be enclosed in a fixed size window. (The interface is not pretty; but it does allow illustration of a variety of controls.)
This program is more complex than Dice. The following sections illustrate a phased development process. An initial skeleton is generated and fleshed out with increasing functionality. Section 2.6 illustrates a simple approach for organizing data transfers between memory and files. In the following section, this simple mechanism is replaced with code that utilizes some of the more sophisticated facilities for object input and output that are built into the TCL framework.
Here we are taking the standard design for a program with an application object and document object etc and, by inheritance, we are creating a specialized version of that pre-designed program. The specialization is that our Document will own a collection of "data items". The Visual Architect will generate the initial declarations and implementations for our program specific CApp (application) and CMain (document) classes.
The framework code already implements a design for all the standard interactions that can occur between an application object and a document object. The application object will tell the document object when to read or write data from file, and when to construct a display. Our only responsibility is the low level design and implementation of the actual code for performing the data transfers.
The display structure of the interface will be created using the Visual Architect. Many of the components needed for the program are standard. For example, the interface uses instances of the framework classes CEditText, CStaticText, and CArrowPopupPane. These components deal with low-level details such as the handling of mouse-button clicks or typed characters. Some of these components will be associated with commands that must get passed to us so that we can handle them in our code. However, as illustrated in the Dice example, the Visual Architect knows the framework's design and can generate all the code needed to route a command to the handler object that we nominate. Again, all that we will have to deal with is some of the low-level design and implementation of the command handling code.
Our design decisions are pretty much limited to choosing: ¥ the form of the "data item" structure; ¥ the type of "list" used to store "data items" in memory; ¥ a mechanism for the memory to disk transfers, ¥ details interactions with display panes (command handling etc); ¥ a mechanism for doing searches.
The structure for a DataItem is trivial. It is to store two "Pascal-style" strings, and that is all. So it will be:
struct DataItem {
Str255 fLoc; // Str255 is the standard Macintosh
Str255 fSubj;// data type for a "Pascal-style" string.
};
When designing a program that is to be built using a class library, you always look out for existing library components that satisfy your requirements. We need a "list" that supports addition and deletion of items, and which will also let us do things like get an item from a specified position in the sequence.
The framework provides class CArray. It is a "dynamic array"; it starts space for some default number of elements, and grows if too many are added. Class CArray has all the functionality that we need; it even incorporates code for dealing with memory to disk transfers. Further there is a class (CArrayPane) that can be used to display the contents of a CArray in a scrollable pane. We will use a CArray for the collection.
An edit text is activated by clicking the mouse within its region. Once activated, it accepts character input. We can simply leave the edit text panes to do their own thing. The only interaction needed will be when our CMain (Document) object asks an edit text for its contents. The Visual Architect will generate the code needed to set the links between the CMain object and the edit text panes. Our work is limited to organizing a few requests for current values and, possibly, we might want some form of input validation.
A CArrayPane does most of the work involved in displaying the contents of an associated CArray. There is one function that we will have to override. This function is the one used to "draw" the contents of a data item. We may need a specialized CMyArrayPane that knows that it is displaying those data items with text strings.
Apart from that draw function, the CArrayPane seems to do almost everything. It will handle scrolling. It will handle mouse clicks. Clicking the mouse button in a CArrayPane changes its idea of "the currently selected item". There is a mechanism that allows a CArrayPane to announce a change to "the currently selected item" to any interested object. Other objects can tell a CArrayPane to change or highlight its selection.
Most of the commands sent to our CMain object will be sent by the CArrowPopupPane. This handles menu selection and so will be sending CMain the "add", "search", and "delete" requests entered by the user.
An example of the type of search that we might want would be "Find a web page in our collection that belongs to a government organization" (i.e. find a location string that includes the substring ".gov"). The user is supposed to enter search strings into the two edit text fields (one of the two fields might be blank). If we limit the program to finding the first match, search is easy. We simply need a loop that accesses successive data items from the array and checks for the search string(s) in the data fields. The loop can stop when a match is found.
We don't even have to do much design or coding for the substring check. The standard C string library contains a function that can find a substring in a C string. We can use that function provided we convert the Pascal strings to C strings.
The program will utilize instances of the classes illustrated in Figure 2.2. These are mostly either standard library classes or classes whose initial declarations and definitions will be generated by the Visual Architect. We will only need to write a few functions for CMain and CMyArrayPane.
The first steps are similar to those for the Dice example. You sublaunch the Visual Architect program (by double clicking on the Visual Architect.rsrc file in the listing in the project window). When Visual Architect is ready, you edit the "Main" view resource. Once again, the "petals" and greeting in the default view must be removed, and the window type must be converted to a fixed size window (the View/View Info ... menu option).
This time the window does have to be enlarged. You should make it at least 400 units wide and 300 deep. You should uncheck the "Print" checkbox, but leave the "Use File" checkbox switched on.
Place a rule line right across the window (the "straight line tool" from the Tools palette) about 70 units down. This rule line is actually an (inactive) button! Its role is simply decorative. Use the static text tool (the big A) to add the two labels, Location and Subject. They will be given names like Stat4 and Stat5. There is no need to rename these panes; we won't want to change them in the program and since there is therefore no need to reference them it doesn't matter what they are called.
Next use the "edit text tool" (the small A in an outlined box) to add two edit text panes to the main view. Get these aligned with the static text labels added earlier (Pane/Align menu). The new panes will be named automatically by Visual Architect; but the names will be meaningless things like "Edit7". Since we will need to reference these panes from our own code we had better give them more informative names such as EditLoc and EditSubj. Use the Pane/Pane Info menu get the dialog box used for editing Pane info. (The display will show that these edit text data entry fields are actually instances of CDialogText.)
The dialog will display the current name for the pane; enter the replacement name. The dialog also has tags that allow examination of other properties that an edit text pane, some of these need to be changed. Open the entry for CDialogText and change the "maxValidLength" to a more reasonable value like 255. The "isRequired" and "validateOnResign" properties can be left. The program will change these as explained later. It might be worth looking at the various properties that an "edit text" entry pane inherits from superclasses; this will give you some feel for the class hierarchy. None of these other properties would be changed for this example.
When you have finished editing the two "edit text" panes, use the "List/Table" tool to add the pane that will be used to display the data. Again, the new pane should be renamed (give it the name DataDisplay) and several parameters have to be set. Use the Pane/Info dialog to do this, see Figure 2.3. The parameters that must be changed are in the CTable part. The default width of a column in the table display (defColWidth) should be increased from 50 to 200 units. We want to be able to select only a single item at a time, so change the selectionFlags to selOnlyOne. We would like ruled lines separating rows and columns; set the rowBorders and colBorders "thickness" values to 1.
The "List/Table" tool has created the pane as a CArrayPane. As noted earlier, we will need it to be an instance of a specialized subclass, CMyArrayPane, because we have to override the function used to draw the contents of a cell in the array.
The new class CMyArrayPane must be defined. Use the Edit/Classes ... menu option to bring up the class definition dialog (shown in Figure 2.4). Enter the name of the new class, CMyArrayPane. (As was the case of command definition illustrated earlier, you must start by typing a return or the Visual Architect program will assume that you are trying to rename an existing entry.)
The "Base Class" pop-up menu can be used to identify the base class for the new class. Naturally, in this case this is class CArrayPane. There is a subsiduary dialog that can be used to define additional data members. None are needed here.
Once the class name CMyArrayPane has been added to the list maintained by Visual Architect, you can change the class for the "DataDisplay" pane. Select the pane by clicking it, then use the Pane/Class menu option. You should get a submenu listing the possible choices as CArrayPane and CMyArrayPane. Make the class CMyArrayPane.
By default, a CArrayPane is displayed in a scrolling pane that only has a vertical scroll bar. You would use the ScrollPane Info menu option if you needed a horizontal scroll bar as well.
Place a PoupMenu using the Pop-up tool. and then use the Pane/Class dialog to change its class from the default CStdPopupPane to CArrowPopupPane. Use Pane Info to give it a proper name (PopUp).
TheVisual Architect creates a default menu for a pop up control; the first (or only) pop up gets menu #128, the second gets menu #129 etc. These menus all start the same; they have three entries - First, Second, and Third. There are no commands associated with these menu entries.
The next step is to edit the menus. Use the Edit/Menus option. You should edit the File menu. The file handling commands like New should be left; but because we aren't supporting printing, the Print and Page Setup entries should be removed.
The record for "Popup menu 128" must also be changed. You have to change the names of each of the three entries and associate commands with them. Select "First" and change the name to Add. Change its "Mark" field to none. Then call up the Commands dialog (same as shown in Figure 1.9) and define a new command cADD that is to be handled by a call to a member function of CMain. Similarly, change the "Second" and "Third" entries to Delete (with command cDELETE) and Search (with command cSEARCH).
Because this program creates files, it would be appropriate to change some of the parameters related to the overall application. The Edit/Application ... menu option can be used to call up a dialog (see Figure 2.5) that has fields for properties such as the Application "Signature" and the "File Ids".
These parameters are used by the "Finder" part of the Macintosh OS. Every file on a Macintosh disk is tagged with details of the program used to create it, and with other type information. It is these details that allows the Macintosh OS to use distinctive icons to represent different types of files and help it determine which program to start when you double click on a file icon.
Every program is supposed to use a distinct four character "signature" (used to identify the creator of a file) and four character identifiers for different file types. One file type is used for the application itself. The others are for data files that it creates. Commercial developers have to register their program signatures with Apple Corporation. In later example where we do use signatures etc, you can just pick your own four character combinations (you may experience minor difficulties if you happen to pick a combination that belongs to one of the programs that you already have on your disk).
Although this dialog exists and has the necessary data entry fields, it is a documented feature of the current Symantec 8 system that it doesn't actually change the appropriate information records elsewhere in the project.
Some of the changes needed to build a full "Finder interface" can be made using other dialogs while others are done by editing generated code files. The ResEdit program has to be used to create distinctive icons and define other Finder related resources. Since it is all rather complex, we omit these steps for this project. The program will still be able to work with files, it just won't have a proper "Finder interface".
The changed resource file should be saved. You might want to "try out" the window. Not much will happen. You will see that you can enter text into the edit text panes and the pop up menu will pop up. When ready, invoke the "Generate All" option.
Exit from Visual Architect and return to the main Symantec Project program.
AppCommands.h CApp.cp CApp.h CMain.cp CMain.h CMyArrayPane.cp CMyArrayPane.h CSaver_CMain.cpp ItsContents_CMain.h main.cp MainItems.h References.cp References.h x_CApp.cp x_CApp.h x_CMain.cp x_CMain.h x_CMyArrayPane.cp x_CMyArrayPane.h(Why the different file extension for CServer_CMain? The convention seems to be that the extension .cp is used for ordinary code files, and .cpp for a file that contains simply a template that must be expanded. The file CServer_CMain.cpp has a couple of #includes and a template expansion macro; the initial compilation steps applied to this file yield an intermediate file with the code resulting from the template expansion.)
#define x_CMain_super CSaverclass CFile; class x_CMain : public x_CMain_super { public: TCL_DECLARE_CLASS // Pointers to panes in window CLine *fMain_Line3; CStaticText *fMain_Stat4; CStaticText *fMain_Stat5; CDialogText *fMain_EditLoc; CDialogText *fMain_EditSubj; CMyArrayPane *fMain_DataDisplay; CArrowPopupPane *fMain_Popup; void Ix_CMain(void); virtual void DoCommand(long theCommand); virtual void UpdateMenus(void); protected: virtual void MakeNewWindow(void); virtual void FailOpen(FSSpec *spec); virtual void PositionWindow(void); virtual void DoCADD(void); virtual void DoCDELETE(void); virtual void DoCSEARCH(void); };
You will have noticed that the class hierarchy is a good bit more complex. In the Dice example, the x_CMain class was simply derived from class Document. Here we have something that is derived from a template class, CSaver If you look, you will see that class x_CMyArrayPane is essentially a CArrayPane with nothing added or changed. Similarly, a CMyArrayPane is just a x_CMyArrayPane and again has nothing added. Both generated classes declare redefined versions of the function used to load information about a CArrayPane from file. However, the implementations change nothing; a CMyArrayPane is loaded in the same way a a CArrayPane (which is what you would expect because we didn't add any data fields or make any other significant changes).
This DataItem.h header file has to be #included by every file where DataItems get referenced. For now, that is only going to be in CMain.cp; so put the #include "DataItem.h" there. Later, when we do a "proper" implementation of i/o, we will need to change this temporary arrangement.
We should implement the "add" function first. A partial implementation is:
Any new data item should have text data in both the "Location" and "Subject" fields; neither of these text strings should exceed 255 characters. Class CDialogText has a SetConstraints() function to specifify constraints and a Validate() member function that checks input data against specified constraints:
If there are valid entries in both the "Location" and "Subject" edit text objects, these data are read into a local, automatic, DataItem variable:
If you managed the edits without any typing errors, the program will compile successfully and then can be linked. Use the Project/Run with Debugger option to start execution.
Your Symantec system manuals contain full details of how to use the debugger. When you "Run with Debugger", you code is not started immediately; instead both it and the debugger rprogram are loaded into memory and control is given to the debugger. You have the opportunity to set breakpoints before commencing execution. Place a breakpoint at the end of the CMain::DoCADD() function that we have just defined, see Figure 2.6. (Switch temporarily from Symantec Debugger to Symantec Project Manager; open the file CMain.cp; invoke the menu option Project/Debug file; scroll to the required collection and click the mouse on the diamond marker at the end of the function.)
Once the breakpoint has been placed, start the program (Go button on the floating control window). The program should display its window (see Figure 2.7).
Enter a string in one field only and try the Add option in the popup menu.
If there is only data in one edit text field, the validation test on the other will fail. Part of the standard Validate() code arranges for the display of the error alert that you should have just seen. The behaviour typifies the advantages of using a framework. You never even thought to add error handling code - but it is there and it works.
If you put data in both fields and try the Add command, the program will reach the breakpoint and transfer control to the debugger. The debugger display is shown in Figure 2.8.
Part of the debugger window shows the call stack. Later you will find this call stack information useful in that it can help you learn how the framework code really works. If you look at the display here, you will see that the program started in main(), called the Run() member of the application object. Our mouse click on the pop up menu was picked up in HandleEvent() and passed via many intermediaries to the pop up menu object. This invoked command handling functions thus arriving eventually at CMain:: DoCADD(). You can get details of arguments at each function call (including the "this" parameter that identifies the specific object performing the function).
When the program is stopped at a breakpoint, you can select a data element in the text of current function (usual Macintosh style mouse-based selection) and use Edit/Copy To Data. Switch to the Data window. You will be able to view the contents of the selected data variable. (See Figure 2.9).
Remove the breakpoint and resume the program. Try some of the other options. All should work (even saves to files, though nothing real gets written to file). Quit from the ListMaker and debugger programs and get back to the Symantec Project Manager.
The framework's class CArray defines a dynamic (resizable) array of data elements (not pointers to data elements). The constructor has to be told the size of the data element so that it can allocate sufficient and so that, later, copying operations will use the correct number of bytes.
Change file CMain.h. Add a CArray* data member, fDataStore. You must also remember to #include The actual array should be created in the ICMain() initialization function. (We don't bother to get rid of the array. If feel this is untidy, add a destructor to your CMain class.)
A call to CArray::Add() will copy a data item into the array (enlarging the array if necessary). The call should go at the end of CMain::DoCADD():
Once again, the only way to test this code is to run the program using breakpoints and the data viewing facilities in the debugger. Figure 2.10 illustrates output from the debugger, showing information about the array after two data items had been added. (The debugger cannot display all the data items in the array; the compiler leaves insufficient symbol table information. However you can determine the number of items added, and view at least part of the first item.
A CArrayPane has to be told:
¥ the identity of the array whose contents are to be displayed;
¥ the way to draw an entry from the array.
The call to SetArray(
) will be in one of the functions in CMain. It cannot be ICMain() because this function is executed before the code used to create the window and set the links between the document object and the display elements. The function CMain::MakeNewContents() is the appropriate point. This function is supposed to contain code for finalizing the set up of the display structures. (When you first begin to work with a framework, you often face problems of deciding exactly where to put in additional or overriding code. The correct place will depend on the order in which the framework code performs particular actions. Often such details are poorly documented. But you can always read the framework code, or use the debugger and set breakpoints in various functions and so determine the sequence in which the framework makes its calls.)
Addition of the code:
If you run this code and add a few data items, you will get a display like that shown in Figure 2.11. The main data display pane is drawn using the default output function. This is CTable::DrawCell(); the CArrayPane (and hence the CMyArrayPane) inherits this from the CTable class. CTable::DrawCell() outputs a cell identifier. (Note that there is an inconsistency here. CArray uses indices 1...N; the first element in the array has index 1. The CTable::DrawCell() function uses a more typical C-like scheme with a 0-base, so the first cell is 0,0 etc.)
The new definition for GetCellText() has to go in our CMyArrayPane class. The function prototype is
Figure 2.12 illustrates the type of display that you should get after making these changes.
The CArrayPane can be told to use more columns. This has to be done in the code. (If you are using an earlier version of the Symantec system, you may see a "number of columns" field in the Visual Architect dialog used when adding a CArrayPane. This data field does not actually change the number of columns. It has been removed from the current versions of the dialog.)
The following additional code should go at the end of CMain::MakeNewContents():
The code for CMyArrayPane::GetCellText() must also be adjusted. The "horizontal" field of the aCell argument can be used to select between the fLoc and fSubj fields of the data item:
After making these changes and recompiling the code, your program should have a two-column display showing both fields of the records that have been added.
However, if you try entering a long text string in the Location edit text, you will find that it overwrites the display for the Subject field of that record. There is another controlling parameter, clipToCells, in the CTable part of the CArrayPane. Restart Visual Architect, open the edit window for the Main view, and use the Pane/Info dialog to change the setting of this parameter for the array pane. Use Visual Architects Generate menu option to update the appropriate files and rebuild the program. (You can always go back and make changes in Visual Architect if you find that the interface isn't quite right.)
The code can be implemented entirely as the body of CMain::DoCSEARCH(). The resulting function is largish; if the code were any larger, it would be appropriate to split it using some auxiliary private member functions. Implementation of the search function also identifies the need to make a small change in DoCADD().
The code is for the most part straightforward. The contents of the two edit text panes have to be fetched into a local dataitem and their lengths checked. If both are zero length, there isn't anything to be done. (The type Boolean is not the "boolean" proposed for the new C++ standard. It is defined, along with TRUE, FALSE constants, in one of the "Macintosh" header files.)
The code of the actual search loop is made slightly messy by the need to copy the Pascal strings of the records into character arrays in (null-terminated) C string format. (You could omit this and use the string comparison functions on the string part of a Pascal string, but this might be risky; the Pascal string might not end with a '\0' as needed by the C string functions.) The actual substring checks are done using strstr() from the C library:
If no match is found, the function returns after "beeping" the user. A match results in a request to the array pane asking it to highlight the selected field:
When you have made these edits and recompiled, you should find that the program "more or less" works. You can do searches for a record with data specified in both Location and Subject fields, or with either one of the fields blank. But you will encounter a problem when making a request where only one of Location or Subject is specified.
For example, if you want to do a search where you intend only to check the Location data, you can enter your search string into the Location edit text pane, then switch to the Subject pane and blank it out, and then invoke the Search. However, if you try first blanking out the Subject pane and then switching to the Location pane, you will get an error alert saying "This is a required field and cannot be left empty."
The problem is caused by the rather simple way that we arranged for the "Add" command to validate its data. In section 2.4.2, we used code like:
The constraint has not been removed, and so still applies. If the pane is cleared, it will fail its "validation" test.
But you can't see any validation test in the code given for DoCSEARCH()! The validation check that can result in an error alert is not in the program specific code, it is in a function built into the framework.
When you click the mouse in an edit text pane to activate it for input, that pane is told that it owns any subsequent input characters - it is the "focus" for activity. When you click the mouse in a different edit text pane, it becomes the "focus". But before the second pane is activated, the framework tells the previous focus pane that it is losing control. The framework code for an edit text pane includes a "validation" check at the point where it loses the "focus".
It is this built in validation check that causes problems for the search command. You can blank out an edit text pane and use the pop-up menu because the pop-up doesn't take the focus for character input. You can't blank out an edit text and switch to another if the first edit text has a "requires input" constraint.
The simple solution is to add some code at the end of the DoCADD() function. This code cancels the requirement for non-empty inputs in the Location and Subject panes:
Another "cosmetic" change can be made to the input routine at the same time. When another data item gets added, the display should be scrolled so that the new item is shown. The extra code needed to set the selection, then get the selected item displayed, is as shown.
Our program should do more than just highlight a cell. If a cell is selected in the display, the contents of the corresponding record (both cells) should be copied to the Location and Subject edit text panes.
This process can be largely automated by utilizing the "dependency" mechanism that is already built into the code of the framework. The "dependency" mechanism is intended to simplify processes that make data consistent after the user changes something. The object that is changed announces this fact in a "changed" message; this message gets passed on to all other objects that might be interested.
The code for doing things like announcing a change, and responding to an announcement of a change in another object, are provided in the framework's CCollaborator class. This defines functions like:
A large proportion of the classes in the THINK framework are derived from class CCollaborator and therefore have the ability to work together using the dependency mechanisms. Class derived from CCollaborator include CApplication, CDocument (hence, CMain), CView (hence specializations like CPane, and CArrayPane), as well as the CCollection classes.
In the general case, the programmer is responsible for setting up dependency relationships. An object that needs to know about changes that might occur in some other source object must be registered as a "dependent" of that source object. The framework does however insert quite a few "standard" dependency relationships. For example, a "document" (i.e. CMain) object is by default made to be a dependent on any data entry element in a Window that it works with. Consequently, the CMain object is automatically informed of every change to an edit text pane, or a change like altering the "selected" item in a CArrayPane.
The default handling of a "changed" message will be to ignore it. Some change messages have effective code to handle them. For example, when the CMain object receives a "I've been changed" message from an edit text pane, it assumes that its own data have probably been changed and marks itself as "dirty". When a document is closed, the "clean/dirty" status is checked. If a document is "dirty", the user is asked if the document should be saved to file. (It is this automatic response to changes in the edit text panes that have caused the "save to file" dialog to be posed by the document in the code that we have developed so far).
We need to extend the default handling of changes. An overridding CMain::ProviderChanged() has to be defined. This overriding functions should deal with change messages from the CArrayPane; other messages should be handled in whatever is the normal fashion as defined in the framework code.
The arguments to ProviderChanged() include a pointer that identifies the object whose change necessitates the update process, and an integer code that specifies the particular change experienced. Classes that broadcast details of changes typically define a set of constants that can be used to characterise the different possible types of change. Thus the CTable.h header file contains a definition of tableSelectionChanged.
Class CMain must be extended. The class declaration must include a declaration for function ProviderChanged() (as always, an overriding definition should have the same access status as the original function in the base class). The function must be defined in the CMain.cp implementation file:
If the change message is a not a "tableSelectionChanged" then it is one of the other change messages used by the framework. Such a message should be handled in "the standard way", so it is passed to the inherited handling function (x_CMain:: ProviderChanged()).
Since there is only one table associated with the CMain object, there is no need to check the source of the message; it is going to be the main data display. (If you needed to check the source, you could compare the address in the pointer argument to the function with the addresses of known collaborators as held in pointer data members.)
The main data display has to be asked which "cell" is selected. The CArrayPane class is actually rather sophisticated. It is not restricted to having a single cell selected. It can have a group of adjacent cells selected. It can even have disjoint groups (using standard Macintosh "shift-click" operations to add cells to an existing group). Consequently, the "selection" isn't defined in terms of an individual point; instead it is given as a "region".
"Regions" are supported directly by the Quickdraw graphics package that forms part of the Macintosh toolbox. They can become quite complex. A region for a selection in a spreadsheet could consist of several separate rectangles. The "region" data structure would describe each of these individual parts, and also provide an overall "bounds" rectangle that encloses all the separate parts. A program that allowed multiple selections would have to interpret the region information to identify each separate part.
But this case is simpler. We specified that the table allow only single cell selection (by setting the appropriate control when we were using Visual Architect). The "bounds" rectangle will identify the one cell that can be selected.
Although a Quickdraw region would normally define pixels, the region that we get given is defined in terms of table cells. The coordinates of the region's bounding box define the row and column of the selected cell. We do have to change from the 0-based scheme that is used to define the table layout to the 1-based indexing scheme used for a "Table"; then we can get the data item from the table:
The code for dealing with deletion is "left as an exercise for the reader". The DoCDELETE() function can get the selection from the arraypane, and ask the data store to delete the corresponding item.
Test your code. You should find that there is a problem when you delete the only item in an array. Use the debugger call display to determine how the problem is caused and hence work out a fix.
The framework incorporates all the code to pick up a menu command like File/Save and convert this into a request for the "document" object to save its data. The routines that actually transfer the data are:
The functions take CFileStream* pointer arguments. Class CFileStream is defined in the THINK library; it is concrete class based on the frameworks abstract CStream class. At a first approximation, a CFileStream is similar to an fstream from the iostream/fstream standard C++ libraries. A CFileStream object can be asked to write out (or read in) different types of data. Thus, there are member functions like:
CFileStream and fstream differ in how they perform data transfers. The standard C++ fstream will convert internal binary data into a character stream. The basic input and output operations with a CFileStream are done in binary. You can imagine an implementation something like:
In a "WriteContents()" function, we can write out any data values that we need to have saved to file. A possible implementation for our program is:
The corresponding input routine is:
When opening an existing file, it isn't sufficient to just read in the data. The existing data must be displayed. This is basically the role of the function CMain:: ContentsToWindow(); this is one of the CMain functions that was created, as an empty "stub" routine, by the Visual Architect.
The work needed is in this case very similar to that required in the MakeNewContents() function. In fact, the same code can be used:
There is just one more thing that must be fixed before the program will work "correctly" with files. This extra fix relates the "revert" mechanism.
Applications on a Macintosh are expected to support the command "Revert to Saved". This abandons all changes made since the contents were last saved to disk. The contents of the last saved version of the file are used to refill memory.
You should use the browser to see the standard revert mechanism; a slightly simplified version of the main DoRevert() function is
We have a problem here in that we are not yet using the standard framework supported mechanisms for looking after a document's contents. Instead we are using that CArray object created in CMain::ICMain(). By default, this array isn't going to get thrown away and replaced by a new array.
We will have to define some extra code to deal with our data array:
Make the suggested changes to your program and check that it now does handle file input and output.
But you can often simplify things. Many of the components defined in the framework, such as class CArray, already have functions that support input and output via CFileStreams. Class CArray has the functions:
The ReadContents() and WriteConents() functions of CMain can therefore be simplified:
This is the role of the CSaver<>template class that comes between CDocument and x_CMain classes in the hierarchy. CSaver<> does everything. We have just got to tell it the type of our data.
Actually, we have to do a bit more. As with any template class of function, we have to arrange to instantiate all template functions needed by CSaver<>. Further, the object input/ouput mechanisms that are built into frameworks require many support functions. Most of these functions take very standardized forms. Rather than require that the programmer write the code for these standardized functions for each data type, a framework can provide ÒmacrosÓ that are used to generate the required functions. Of course, we have to include ÒcallsÓ to these macros in our own code
In our case, the data belonging to the document is really the CArray that we've been calling fDataArray. We can set things up "correctly" by using the class CArray as the parameter for Csaver.
Using class Csaver<> necessitates many small changes throughout our code. Some of these changes add the new template related items and macros, other changes remove code that is no longer appropriate.
First, change the ItsContents_CMain.h file. This is where you specify the class of the data storage object, in our case Carray. The file ItsContents_CMain.h was originally generated by the Visual Architect. Its original contents were:
A number of the functions that we defined for Cmain are no longer needed because we can utilize the framework provided functions. So, remove functions DoRevert(), WriteContents(), and ReadContents() from CMain.h and Cmain.cp. We will use the standard implementations for these functions.
You should also remove the declaration fDataArray from CMain.h and rename it as "itsContents" throughout the body of Cmain.cp. The standard framework code expects the documentÕs Òdata objectÓ to be accessed via the data member itsContents (declared in class CSaver).
The data object is still an array, and the data member itsContents must be set to point to a dynamically created instance of class Carray. However, the place where the array is created must now change. Previously it was done in the ICMain() function and the array remained in existence for the same time as the document object. The framework code assumes that a data object is created either (explicitly) in the MakeNewContent() function (for a new document) or (implicitly) in part of the input routine called when a documentÕs data are read.
Remove the call new Carray(...) from CMain::ICMain(); replace it by a similar call at the start of MakeNewContents().
Finally, we do have to provide a few macros that are needed for the object i/o to run. Create a new file CStream_CArray.cpp:
If you recompile the resulting code it should again run.
These ÒglitchesÓ relate to the mechanism that a document uses to determine whether it has been changed. A document has a ÒdirtyÓ flag as one of its members; this gets accessed and modified via the member functions GetChanged() and SetChanged(). (Try exploring with the browser, the debugger, and by multi-file searches to find the class where these functions are defined and the places that they are called.)
Changes to the edit text fields are causing the document to set its dirty flag. The document isn't dealing with changes to the array.
Try inventing a mechanism that alters the way that the Cmain class handles changes so as to get a more appropriate behaviour.
CMyArrayPane group
We didn't tell Visual Architect much about the CMyArrayPane, simply that it was derived from class CArrayPane. Visual Architect couldn't guess what we wanted so it generated some very "vanilla" class declarations.
2.4.2 Initial edits
DataItems
The first thing we have to do is define a DataItem. The struct declaration might as well go in a separate file DataItem.h:
struct DataItem {
Str255 fLoc;
Str255 fSubj;
};
(The definition of type Str255, "Pascal string type", is in one of the header files that always gets scanned.)
Changed start of CMain.cp file
#include "CMain.h"
//#include "AppCommands.h"
#include
The Commands cADD, cSEARCH, and cDELETE
The command handling functions like DoCADD() are declared in class x_CMain where they are given empty definitions (i.e. { } ). We have to redeclare them in class CMain and start to provide their implementation. So, you should edit the class declaration in CMain.h, adding the function declarations (with protected access status). "Empty" functions should be provided for DoCDELETE() and DoCSEARCH():
void CMain::DoCDELETE(void)
{
}
void CMain::DoCSEARCH(void)
{
}
void CMain::DoCADD(void)
{
fMain_EditLoc->SetConstraints(TRUE, 255);
if(! fMain_EditLoc->Validate())
return;
fMain_EditSubj->SetConstraints(TRUE, 255);
if(! fMain_EditSubj->Validate())
return;
DataItem d;
fMain_EditLoc->GetTextString((unsigned char*) &d.fLoc);
fMain_EditSubj->GetTextString((unsigned char*) &d.fSubj);
}
This function will get called when the user activates the pop up menu and picks the "Add" menu item. (If you look in x_CMain.cp, you will see code to dispatch commands to functions like DoCAdd(); the code is very similar to that illustrated in section 1.4.1 for the Dice program.)
Checking constraints
fMain_EditLoc->SetConstraints(TRUE, 255);
if(! fMain_EditLoc->Validate())
return;
When the CMain object executes this code, it gets the associated "Location" edit text entry object to check that a reasonable length string has been enetered.
Getting the text strings from the edit text panes
DataItem d;
fMain_EditLoc->GetTextString((unsigned char*) &d.fLoc);
fMain_EditSubj->GetTextString((unsigned char*) &d.fSubj);
That is as far as it goes in this initial fragment. It is enough. We will be able to check correct operation by using the debugger.
2.4.3 Compilation and test run using debugger
Compiling
The program must now be compiled; all 160 files of it. Save the files and invoke Project/Bring Up To Date. You will have time to do something else (for example, approximately 30Km on an exercise bicycle).
Placing a breakpoint prior to program execution
Figure 2.6 Placing a breakpoint prior to program execution.
Figure 2.7 Window displayed by ListMaker program.
Error alert generated by standard code
Transfer to debugger
Figure 2.8 Debugger display with call stack.
Looking at the call stack
Inspecting data
Figure 2.9 Debugger displays the contents of a data variable.
2.5 Adding capabilities
2.5.1. A temporary storage structure
This step adds a CArray object to store the data items that are entered. The arrangement is temporary. It is sufficient to show how things like storage, data display, and file transfers work. However, the framework is designed around a more sophisticated approach for organizing data storage and transfers. Consequently, some of the code developed in this section will get changed later.
CArray
Private CArray* data member in class CMain
Creating the array in ICMain()
void CMain::ICMain()
{
Ix_CMain();
fDataStore = new CArray(sizeof(DataItem));
}
fDataStore->Add(&d);
Test using debugger
Figure 2.10 Using the debugger to examine the array fDataStore.
2.5.2 Displaying the data in the array
The next extension activates the CArrayPane so that data in the array gets displayed.
Linking the array and the display pane
Class CArrayPane has a member function, SetArray(), that takes a pointer to the CArray whose contents are to be displayed. Function SetArray() has a second boolean argument that indicates whether the array-pane object "owns" this array. This function has to be passed the address of our CArray. (The boolean argument is FALSE; the array continues to be "owned" by the CMain document object.)
Where to perform an initialization?
void CMain::MakeNewContents()
{
fMain_DataDisplay->SetArray(fDataStore, FALSE);
}
will result in some data (not the actual data entered) appearing in the data display. (As well as changing the body of MakeNewContents(), you must remember to #include "CMyArrayPane.h". If you forget this header, you will get one of those confusing messages like "CMyArrayPane does not have a SetArray() function".)
Figure 2.11 Data display with default output functions.
Changing the output function
The default implementation of function CTable::DrawCell() consists of a call to GetCellText(). If we needed a table of pictures, we would have to completely replace DrawCell(). However, since our data does consist of text strings, it is sufficient to override th CTable::GetCellText() function.
void GetCellText(Cell aCell, short availableWidth,
StringPtr itsText);
its access status is protected. (A StringPtr is a pointer to a Str255 Pascal-style string; a Cell is an alternative typedef name for a Point, a Cell has two integer data member h and v.) The function is supposed to copy the text into This GetCellText() function has to be redeclared in the class declaration in CMyArrayPane.h, and a definition has to be provided in CMyArrayPane.cp:
void CMyArrayPane::GetCellText(Cell aCell,
short availableWidth, StringPtr itsText)
{
DataItem d;
int itemnum = aCell.v + 1; // correct from 0-base to 1-base
itsArray->GetArrayItem(&d, itemnum);
TCLpstrcpy(itsText, d.fLoc);
}
The function CArray::GetArrayItem() copies data from a CArray (at location specified by itemnum argument) into a data item. Function TCLpstrcpy() is one of the THINK library utilities for working with Pascal style strings. The code copies the "fLoc" string into the displayed string. (The parameter availableWidth can be ignored; it is the width, in pixels, of the available space in the display.)
Figure 2.12 Displaying part of the entered data.
2.5.3 Multicolumn data
By default, a CArrayPane expects to work with a one dimensional array that it display in a column. This is fine when you want something like a list of filenames for a file dialog. Here we need something more like the kind of grid that you get with a spreadsheet.
Defining the second column
fMain_DataDisplay->AddCol(1,1);
fMain_DataDisplay->SetColWidth(0, 200);
fMain_DataDisplay->SetColWidth(1, 200);
Class CArrayPane inherits functions AddCol() and SetColWidth() from class CTable; they are documented in the CTable section of the library guide.
Displaying both data members of a record
if(aCell.h == 0) TCLpstrcpy(itsText, d.fLoc);
else
if(aCell.h == 1) TCLpstrcpy(itsText, d.fSubj);
Fixing up the "clip"
2.5.4 Handling the Search command
The "Search" requirement are simple. When the "Search" command is invoked, we have to first check that at least one of the Location or Subject entry fields has data. If there are search data, we must search linarly through the items in the array until we find one with matching data. If there is no matching item, we should probably use something like a call to SysBeep() to give some audible indication of failure. If a match is found, we make this the "selected item" (as shown by highlighting in the array pane). The specification doesn't require use to do anything more than find the first matching item.
void CMain::DoCSEARCH(void)
{
DataItem d;
fMain_EditLoc->GetTextString((unsigned char*) &d.fLoc);
fMain_EditSubj->GetTextString((unsigned char*) &d.fSubj);
Boolean checkSubject, checkAddress;
checkAddress = (fMain_EditLoc->GetLength() != 0);
checkSubject = (fMain_EditSubj->GetLength() != 0);
if(!(checkAddress || checkSubject)) {
SysBeep(1);
return;
}
...
}
int fnd = 0;
char buff0[256];
char buff1[256];
char buff2[256];
char buff3[256];
TCLptocstrcpy((char*)&buff0, d.fLoc);
TCLptocstrcpy((char*)&buff1, d.fSubj);
for(int i = 1; i <= fDataStore->GetNumItems(); i++) {
DataItem ref;
fDataStore->GetArrayItem(&ref,i);
TCLptocstrcpy((char*)&buff2, ref.fLoc);
TCLptocstrcpy((char*)&buff3, ref.fSubj);
Boolean matchAddress =
(!checkAddress) ? TRUE :
(strstr((char const*) buff2,
(char const*) buff0) != NULL) ;
Boolean matchSubject =
(!checkSubject) ? TRUE :
(strstr((char const*) buff3,
(char const*) buff1) != NULL);
if(matchAddress && matchSubject) {
fnd = i;
break;
}
}
...
Functions like TCLptocstrcpy (converting Pascal to C string) are documented in the Think Reference.
if(fnd == 0) { SysBeep(1); return; }
Cell which;
which.v = fnd - 1;
which.h = (checkAddress) ? 0 : 1;
fMain_DataDisplay->SelectCell(which, FALSE, TRUE);
Interaction problem when entering search request
fMain_EditLoc->SetConstraints(TRUE, 255);
if(! fMain_EditLoc->Validate())
return;
This adds the constraint that there must be some data in the Location edit text pane, and then immediately gets the pane to check this constraint.
Validation checks are built into framework
"Focus" for input activity
void CMain::DoCADD(void)
{
...
fDataStore->Add(&d);
fMain_EditLoc->SetConstraints(FALSE, 255);
fMain_EditSubj->SetConstraints(FALSE, 255);
int place = fDataStore->GetNumItems();
Cell which;
which.v = place - 1;
which.h = 0;
fMain_DataDisplay->SelectCell(which, FALSE, TRUE);
fMain_DataDisplay->ScrollToSelection();
}
2.5.5 Responding when the user selects displayed items
The standard code for the CArrayPane handles mouse clicks (and arrow keys) by highlighting a selected cell.
Using the "Dependency" mechanism
CCollaborator hierarchy
void CCollaborator::DependUpon(CCollaborator *aProvider);
void CCollaborator::CancelDependency(CCollaborator *aProvider);
to set up, or cancel, a depency relationship; the function
void CCollaborator::BroadcastChange(long reason, void* info);
that informs all current dependents of a change in an object's state; and, the function
void CCollaborator::ProviderChanged(CCollaborator *aProvider,
long reason, void* info);
that gets called when an object gets informed of a change in something on which it is dependent.
Handling a change in the CArrayPane
Overridding CMain::ProviderChanged
void CMain::ProviderChanged(CCollaborator *aProvider,
long reason, void* info)
{
if(reason == tableSelectionChanged) {
RgnHandle r = fMain_DataDisplay->GetSelection();
int row = (*r)->rgnBBox.top + 1;
DataItem d;
fDataStore->GetArrayItem(&d, row);
fMain_EditLoc->SetTextString(d.fLoc);
fMain_EditSubj->SetTextString(d.fSubj);
}
else x_CMain::ProviderChanged(aProvider, reason, info);
}
Selection "region"
Quickdraw "region"
Getting the selection from the region
RgnHandle r = fMain_DataDisplay->GetSelection();
int row = (*r)->rgnBBox.top + 1;
DataItem d;
fDataStore->GetArrayItem(&d, row);
Once we have the data item, we can use its values to set the contents of the edit text panes:
fMain_EditLoc->SetTextString(d.fLoc);
fMain_EditSubj->SetTextString(d.fSubj);
2.5.6 Dealing with deletions
The user should be able to delete the "currently selected" data item from the table. We have now dealt with selection, so we can handle deletion.
2.6 simple ways to save data to file
2.6.1 Saving and loading
The "correct" way to save a document's data is moderately elaborate. This section illustrates a simpler approach that will suffice.
CSaver<...>::ReadContents(CFileStream *aStream);
CSaver<...>::WriteContents(CFileStream *aStream);
These have effective definitions and will perform the input and output tasks if the program is making proper use of the framework's capabilities. However, the functions are virtual and we can redefine them if we want to use non-standard mechanisms.
CFileStream
virtual void PutStr255(const unsigned char* string);
virtual void PutChar(char value);
virtual void PutBoolean(Boolean value);
virtual void PutInt(int value);
virtual void PutDouble(double value);
...
virtual void GetStr255(unsigned char* string);
virtual char GetChar();
virtual Boolean GetBoolean();
virtual short GetShort();
virtual double GetDouble();
virtual void GetPoint(Point& p);
virtual void GetRect(Rect& r);
Operator function variants are defined for some of these basic routines:
friend CStream& operator << (CStream& s, long v)
{ s.PutLong(v); return s; }
friend CStream& operator << (CStream& s, double v)
{ s.PutDouble(v); return s; }
friend CStream& operator << (CStream& s, Rect& r)
{ s.PutRect(r); return s; }
...
friend CStream& operator >> (CStream& s, Boolean& v)
{ v = s.GetBoolean(); return s; }
friend CStream& operator >> (CStream& s, short& v)
{ v = s.GetShort(); return s; }
Binary transfers
void CFileStream::PutDouble(double value)
{
out.write(&value, sizeof(value));
}
void CMain::WriteContents(CFileStream *aStream)
{
int num = fDataStore->GetNumItems();
*aStream << num;
for(int i=1; i<= num; i++) {
DataItem d;
fDataStore->GetArrayItem(&d, i);
aStream->PutStr255(d.fLoc);
aStream->PutStr255(d.fSubj);
}
}
This code saves the number of data items, then gets the contents of each data item written to file. You can use similar code to save any kind of data to a file.
void CMain::ReadContents(CFileStream *aStream)
{
int num;
*aStream >> num;
for(int i = 1; i<= num; i++) {
DataItem d;
aStream->GetStr255(d.fLoc);
aStream->GetStr255(d.fSubj);
fDataStore->Add(&d);
}
}
This code would read the counter and then read in each data item and add it to the array.
ContentsToWindow()
void CMain::ContentsToWindow()
{
fMain_DataDisplay->SetArray(fDataStore, FALSE);
fMain_DataDisplay->AddCol(1,1);
fMain_DataDisplay->SetColWidth(0, 200);
fMain_DataDisplay->SetColWidth(1,200);
}
"Revert to Saved"
delete the window
delete the "contents" data structure
if (itsFile != NULL)
ReadDocument();
else
NewFile();
Function ReadDocument() uses the standard ReadContents() function to create a new "contents" data structure, while NewFile() does things like call MakeNewConents().
void CMain::DoRevert()
{
delete fDataStore;
fDataStore = new CArray(sizeof(DataItem));
x_CMain::DoRevert();
}
This gets rid of the existing array, allocates a new one, and then gets on with whatever is the normal DoRevert() behaviour as defined by our parent class x_CMain() (and ultimately by the CSaver<> class). (Function DoRevert() has to be redeclared in the public interface of the CMain class. Most of the other functions that we've added were part of the protected interface.)
2.6.2 Basic use of framework supported object i/o
The code just given to save the contents of the data array is actually correct and it illustrates the way that you would have to write something more general if your data consisted of many separate structures.
virtual void PutTo(CStream& stream);
virtual void GetFrom(CStream& stream);
The default implementation is equivalent to the code that was given earlier. The PutTo() function writes the number of items, then in a single write operation transfers the contents of the array. Function GetFrom() performs the corresponding input operation.
void CMain::WriteContents(CFileStream *aStream)
{
fDataArray->PutTo(*aStream);
}
2.7 Using class CSaver
The TCL framework expects a Document to have data that it is to save. Of course, Symantec's authors don't know what your data are going to be. But they can handle any data by defining a template class that has a data parameter of some type T* (pointer to an object of Òclass TÓ). Standard code can be written that saves the T object before closing a document, and creates a T object when reading a document.
CSaver<>
CSaver
/***********************************************************
ItsContents_CMain.h
....
YOU NEED TO MODIFY THIS FILE:
Change every instance of CCollaborator below to the
name of the actual class of your itsContents object
***********************************************************
*/
#pragma once
#include "CCollaborator.h"
#define ITSCONTENTS_CMain CCollaborator
#ifdef GENERATE_TEMPLATE
#pragma template CSaver
Those references to Ccollaborator have to be changed to CArray:
#define ITSCONTENTS_CMain CArray
#ifdef GENERATE_TEMPLATE
#pragma template CSaver
Removing our own functions from CMain
fDataStore becomes ÒitsContentsÓ
Creating the actual CArray
Providing the macros needed for some object i/o
#include "CStream.h"
#include "CArray.h"
#pragma template_access public
#pragma template PutObject(CStream&, CArray*)
#pragma template GetObject(CStream&, CArray*&)
#pragma template PutObjectReference(CStream&, CArray*)
#include "CStream.tem"
and add this to the project. This calls PutObject(), GetObject() and PutObject Reference() are ÒmacroÓ calls that result in the generation of the various special functions needed to transfer CArray data objects. The functions generated for GetObject() actually create a new object of the required type before getting it to read its data.
2.8 Chasing "Glitches"
The program should work - sort of. Actually, there are quite a few ÒglitchesÓ where the program does not work quite correctly. For example, trye the following:
Last modified April 1996. Please email questions to
nabg@cs.uow.edu.au