[N.A.B.G. picture]

Symantec Think Class Library Tutorial


2 ListMaker

2.1 A Program that saves data to file

This example creates a simplified version of the kind of "database" component that you might find in a basic "Office System" or "Works" program. The program allows records to be added to (and deleted from) a collection, and also supports searches for records that contain specified words. Collections of records can be saved to disk files and reloaded in later runs of the program. The program is simplified in that the only permitted form for a record is a pair of strings. Further, it is assumed that there will be no difficulty in holding all records in memory. Another simplification is that there is no support for printing.

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.
[SYM2.1]

Figure 2.1 Interaction window of the "ListMaker" program.

2.2 Specification and design

Specification

The program is to:

Design

Inheriting a design!
There is almost no design work necessary! When you use a framework class library like TCL, you "inherit" a complete design.
Application and Document

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.

Interface

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 responsibilities

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.

DataItem

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.
};
"List"

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.

CArray and related classes

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.

Interactions with user interface

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.

CArrayPane and our CMyArrayPane subclass

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.

Searches

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.

"Finalized design"

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.
[SYM2.2]

Figure 2.2 Some of classes used in ListMaker program.

2.3 Building the Interface

Start Symantec project manager, and create a new "VA Application" project named ListMaker.

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).

Set window size etc

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.

Add Labels

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.

Add data entry panes

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.)

Changing edit text parameters

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.

List/Table tool

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.
[SYM2.3]

Figure 2.3 Editing the pane information for the CArrayPane.

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.

Defining the class CMyArrayPane

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.)
[SYM2.4]

Figure 2.4 The class definition dialog.

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.

Changing the class of the "Data Display",pane

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.

Popup Menu

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.

Editing the menu

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).

Changing Application properties?

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".
[SYM2.5]

Figure 2.5 Editing "Application" parameters.

Finder interface parameters

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).

This dialog don't work!

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.

Forget about the "Finder interface"

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".

Generating the code

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.

2.4 Code

2.4.1 The generated files

More files are created for the ListMaker example than were generated for Dice. The extra files, such as ItsContents_CMain.h and CSaver_CMain.cpp contain code, templates, and macros related to file handling. Eventually we will use all the generated files. Initially, we implement only a part of the program's intended functionality and we can focus on much the same subset of files as were used in the Dice example. The CMain group will be the most important.
Generated files
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.)

CMain.cp, CMain.h and x_CMain.cp, x_CMain.h

As in the case of the Dice example, the CMain group of files provide most of the skeleton code that we must expand. The header file x_CMain.h contains the declaration of the class with data members for the links to the various visual elements and the command handling functions DoCADD() etc:
#define x_CMain_super	CSaver

class 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);
};
Complex hierarchy involving template classes

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, CSaverWe will make proper use the i/o system and template scheme in section 2.X. To begin with, we can ignore the complexities and continue as if the CSaver classes were not in the hierarchy (as if x_CMain were directly derived from CDocument).

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.

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).

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.)

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.

Changed start of CMain.cp file
#include "CMain.h"
//#include "AppCommands.h"

#include 
#include "DataItem.h"

TCL_DEFINE_CLASS_M1(CMain, x_CMain);
...

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)
{
}

We should implement the "add" function first. A partial implementation is:

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

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:

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.

If there are valid entries in both the "Location" and "Subject" edit text objects, these data are read into a local, automatic, DataItem variable:

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).

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.

Placing a breakpoint prior to program 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.)
[SYM2.6]

Figure 2.6 Placing a breakpoint prior to program execution.

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).
[SYM2.7]

Figure 2.7 Window displayed by ListMaker program.

Enter a string in one field only and try the Add option in the popup menu.

Error alert generated by standard code

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.

Transfer to debugger

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.
[SYM2.8]

Figure 2.8 Debugger display with call stack.

Looking at the call stack

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).

Inspecting data

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.
[SYM2.9]

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

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.

Private CArray* data member in class CMain

Change file CMain.h. Add a CArray* data member, fDataStore. You must also remember to #include ; put the include along with the others before the class declaration.

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.)

Creating the array in ICMain()
void CMain::ICMain()
{
	Ix_CMain();

	fDataStore = new CArray(sizeof(DataItem));
}

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():

	fDataStore->Add(&d);	
Test using debugger

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.
[SYM2.10]

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.

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.

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?

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:

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".)

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.)
[SYM2.11]

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.

The new definition for GetCellText() has to go in our CMyArrayPane class. The function prototype is

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 illustrates the type of display that you should get after making these changes.
[SYM2.12]

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.

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.)

Defining the second column

The following additional code should go at the end of CMain::MakeNewContents():

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.

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:

Displaying both data members of a record
if(aCell.h == 0) TCLpstrcpy(itsText, d.fLoc);
else
if(aCell.h == 1) TCLpstrcpy(itsText, d.fSubj);

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.

Fixing up the "clip"

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.)

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.

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.)

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;
		}
	...
}

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:

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 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:

if(fnd == 0) { SysBeep(1); return; }

Cell which;
which.v = fnd - 1;
which.h = (checkAddress) ? 0 : 1;


fMain_DataDisplay->SelectCell(which, FALSE, TRUE);

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.

Interaction problem when entering search request

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:

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.

The constraint has not been removed, and so still applies. If the pane is cleared, it will fail its "validation" test.

Validation checks are built into framework

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.

"Focus" for input activity

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:

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();
}

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.

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.

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.

Using the "Dependency" mechanism

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.

CCollaborator hierarchy

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:

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.

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).

Handling a change in the CArrayPane

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.

Overridding CMain::ProviderChanged

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:

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);
	
}

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.)

Selection "region"

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".

Quickdraw "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.

Getting the selection from the region

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:

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.

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.

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.

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:

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

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:

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

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:

void CFileStream::PutDouble(double value)
{
	out.write(&value, sizeof(value));
}

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:

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.

The corresponding input routine is:

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()

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:

void CMain::ContentsToWindow()

{
	fMain_DataDisplay->SetArray(fDataStore, FALSE);
	fMain_DataDisplay->AddCol(1,1);
	fMain_DataDisplay->SetColWidth(0, 200);
	fMain_DataDisplay->SetColWidth(1,200);
}

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.

"Revert to Saved"

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

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().

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:

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.)

Make the suggested changes to your program and check that it now does handle file input and output.

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.

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:

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.

The ReadContents() and WriteConents() functions of CMain can therefore be simplified:

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<>

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

CSaver

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:

/***********************************************************

 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

#endif
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

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.

fDataStore becomes ÒitsContentsÓ

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).

Creating the actual CArray

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().

Providing the macros needed for some object i/o

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:

#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.

If you recompile the resulting code it should again run.

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:

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.


Last modified April 1996. Please email questions to nabg@cs.uow.edu.au