The drawing area is to be reasonably large (e.g. 600 pixels wide by 800 deep, a bit larger than an A4 page); this will be bigger than can be displayed within a window so some scrolling mechanism will be required.
Of course pictures should be saved to file and be capable of being elaborated in later editing sessions.
The first version of the program will use a menu to define the choice of picture components. Components added to the picture will be displayed at a fixed size. Later versions add features like the ability to change the size of a picture component when it is added. Another extension is the use of a "picture palette" - a second scrolling pane that is used to display the available picture components.
After removing the default picture and "Hello World" message, you should edit the parameters that define the main window (View/View Info dialog). Windows are defined with fixed maximum sizes; the default maximum is a bit small (unless you are working on a classic small screen Macintosh). You should make it something like 600 wide by 400 deep. While you are editing this window, remove the scroll bars. We will have scroll bars attached instead to the main drawing area.
We will need our own special subclass of CPanorama. Use the Edit/Classes ... menu and, in the associated dialog, define a new class, DrawPane, based on CPanorma. The dialog lets you add data members. We will need some record of the "current picture component" - a short integer identifier will suffice. Use the dialog to add this data member, see Figure 3.1.
The dialog has two check boxes - GetFrom and PutTo. The default is for GetFrom to be set. These check boxes control the code generated for the x_DrawPane class invented by the Visual Architect. The function x_DrawPane:: GetFrom() loads the definition of a DrawPane object from the view resource file; this function is called when your program opens its windows. The corresponding PutTo() function is not often needed. The settings of the check boxes determine whether the data for the extra members are transferred in these functions. If you want to control transfers in your own code (i.e. in function DrawPane::GetFrom() etc rather than the x_DrawPane functions) you can change the settings. Normally, you should leave the defaults. If you introduce an inconsistency between the Visual Architect's view resource definition and your code, then your program will crash when it tries to open its windows.
Once you have finished defining the DrawPane class, use the Panorama tool from the Tools palette to add a Panorama to your window. You should place the Panorama so that it starts at coordinate 0,0 and you should make it one or two pixels wider and deeper than the enclosing window.
After using the Panorma tool to place the pane in the window, use the Pane/Class menu to change its class to DrawPane.
Use the Pane/Scroll Pane Info menu to get a dialog that lets you change details of the enclosing scrolling mechanism. The default is to have just a vertical scroll bar. You should change the settings so that there is also a horizontal scroll bar.
Next use the Pane/Pane Info menu to get the dialog that changes Pane properties. You might want to change the name (the default will probably be "Pano3"). In addition, you want to change one of the many "size" attributes.
If you work through the dialog, you will find width and height attributes defined for CPane, and for CPanorama. It is the CPanorama size that you should change to something large like 600 pixels wide by 800 deep ("bounds" attribute, "right" and "bottom" fields). The CPanorama bounds define the "size of the paper" used for drawing; the CPane size defines the "size of the viewing" window that displays part of the "drawing paper".
The CPane size should relate to the size of the enclosing window used by the program. The size values placed here will have been chosen by Visual Architect. It used the size of the window as currently drawn in its own editing area and deducted some amounts to allow for the scroll bars in the CScroller that will enclose the DrawPane. But what happens when the window is resized?
By default, nothing happens! If you enlarge the window, you will get an odd looking arrangement with a pane and its scroll bars left in the top left corner of an otherwise empty window.
You have to tell Visual Architect that you want this Pane to grow. In the CPane section of the Pane/Info dialog you will find "hsizing" and "vsizing" attributes. Both default to "sticky", you must change them to "elastic".
Before finishing with this dialog, go up to the CView level and change the check box so that the DrawPane can be the "gopher". (This change will allow a DrawPane to get the chance to handle things like menu commands.)
Next you need to define a menu that offers a choice of predefined picture components. This extra menu can be created by using Edit/Menu Bar to get a dialog that lets you add a new menu "Pictures" and then, via Edit Menu Items, lets you define the menu entries.
I wasn't particularly ambitions at this stage. I decided to use just three predefined picture elements - a cat, a dog, and a train. So my menu has entries Cat, Dog, and Train. (As explained later, ResEdit is used to actually copy the prototype pictures into the program's collection of resources).
Command numbers have to be associated with these commands - so I ended up with commands cPICTURE1 (512, my cat), cPICTURE2 (513), and cPICTURE3 (514).
The code needed to handle these commands is rather different from that normally generated and so will have to be hand coded. However, it is useful to get Visual Architect to provide a suggestion as to the required code (even though this automatically generated code will need to be deleted later). For one of your commands (my cPICTURE1), use the dialog controls to specify that this is command is to be handled by a call to a function in class DrawPane.
Draw some pictures in your favourite drawing program. Make them all 100x100 pixels in size. Copy each separately to the Scrapbook. When you have your pictures, open the file Project Resources.rsrc. This will start ResEdit. You can then take pictures from the Scrapbook and paste them into ResEdit. It will give the default numbers like 128, 129. You should change these defaults (Get Resource Info dialog). Add names to the picture resources and renumber them to start from 1000 (low resource numbers should be avoided, they may clash with "standard" system resources). Figure 3.2 illustrates editing in ResEdit.
Now use "Generate All" to get the skeleton code generated. Leave Visual Architect.
Visual Architect has generated some initial menu handling code in x_DrawMane.cp:
#define cPICTURE1 512
extern CBartender *gBartender; /* Manages all menus */
void x_DrawPane::DoCommand(long theCommand)
{
switch (theCommand)
{
case cPICTURE1:
DoCPICTURE1();
break;
default:
CPanorama::DoCommand(theCommand);
}
}
void x_DrawPane::UpdateMenus()
{
CPanorama::UpdateMenus();
gBartender->EnableCmd(cPICTURE1);
}
void x_DrawPane::DoCPICTURE1()
{
// Subclass must override this function to
// handle the command
}
Although the code is not what is needed in this case, it does illustrate several important aspects.
First, you need consistency between the constants defined in resource files (like the resource data structure that defines the Pictures menu) and the code. Visual Architect has added a #define for CPICTURE1. One could simply #include the "AppCommands.h" file which lists all the commands. Although conceptually tidier, a #include of the AppCommands.h file is more costly as this code would have to be recompiled every time any command number was changed.
Next there is an extern reference to gBartender. This is a global pointer to one of a number of "helper" objects that gets created by the Application object. The "Bartender" object looks after the menu bar, dealing with things like enabling and disabling commands.
A special DoCommand() function has been generated. The code picks up the cPICTURE1 command code and calls the DoCPICTURE1() function; other commands are handled by the inheritted default mechanism.
As you will know from use of other applications, menu choices are frequently only conditionally available. For example, in a word processor or draw program the editing options like "Cut" or "Copy" are typically only enabled when there is some text or a graphic element selected. If no data are selected, these editing options are dimmed out when you pick the Edit menu. Obviously something has to chose whether a particular menu option is available; that something is the object that handles that menu choice.
In the Visual Architect, we specified that command CPICTURE1 was to be handled by the DrawPane class. This lead to the generation of the UpdateMenus() code. UpdateMenus() is called when the mouse button is pressed in the menu bar before the corresponding menu is shown. Here the code gets the Bartender to unconditionally activate the cPICTURE1 option.
Finally, Visual Architect has generated a skeletal routine for handling this command.
All this code has to be deleted and replaced by our own hand written code in class DrawPane. (If you go back to Visual Architect and make changes there, this x_DrawPane code will get regenerated. If you do have to change something in Visual Architect, you should also cancel the "handled by a call to a DrawPane" function option that we specified for cPICTURE1 command.)
The reason we want to change things is that we don't want separate DoCPICTURE1(), DoCPICTURE2(), ... functions. All would be very similar, they just have to change the value in fPicNum. A single function is tidier.
So:
/** Commands **/ virtual void DoCommand(long theCommand); virtual void UpdateMenus(); virtual void DoCPICTURE1(void);from class x_DrawPane and change class DrawPane to declare the following
virtual void DoCommand(long theCommand); virtual void UpdateMenus(); virtual void DoCPICTURES(int);
Add the following implementations for the DrawPane functions:
#define cPICTURE1 512
#define cPICTURE2 513
#define cPICTURE3 514
extern CBartender *gBartender;
void DrawPane::DoCommand(long theCommand)
{
switch (theCommand)
{
case cPICTURE1:
case cPICTURE2:
case cPICTURE3:
DoCPICTURES(theCommand - cPICTURE1);
break;
default:
CPanorama::DoCommand(theCommand);
}
}
void DrawPane::UpdateMenus()
{
CPanorama::UpdateMenus();
gBartender->EnableCmd(cPICTURE1);
gBartender->EnableCmd(cPICTURE2);
gBartender->EnableCmd(cPICTURE3);
}
void DrawPane::DoCPICTURES(int val)
{
fPicNum = val;
}
This code enables all the Pictures menu options, and uses the same function to set fPicNum to a number in range 0... (max number of pictures - 1).
Our DrawPane is supposed to draw a picture component when it gets a mouse click. The code will have to go in DrawPane::DoClick(). This virtual function is declared at the CView level in the hierarchy. An overridding function must be declared in the DrawPane class and an implementation provided:
void DrawPane::DoClick(
Point hitPt,
short modifierKeys,
long when)
{
Rect r;
r.left = hitPt.h;
r.right = r.left + 100;
r.top = hitPt.v;
r.bottom = r.top + 100;
PicHandle ph = GetPicture(1000+fPicNum);
if(ph == NULL)
return;
HLock((Handle)ph);
DrawPicture(ph, &r);
HUnlock((Handle)ph);
}
The Point argument identifies the position of the mouseclick; the other arguments are of no interest in this example.
We want to draw pictures in a fixed size area below and to the right of the click point. A rectangle of size 100x100 is defined. The GetPicture() function is part of the Macintosh Toolbox; its documented in the Quickdraw section of "Inside Macintosh" (and is described in Think/Reference's data file). GetPicture() can load one of the picture resources that we added using ResEdit. It uses the picture identifier number to determine which picture to load.
Function GetPicture() returns a "PicHandle" - a type specific "Handle" that must point to a Picture data structure. A "Handle" is a kind of pointer (it is actually a double indirection pointer). If you program at Macintosh Toolbox level you have to learn all about Handles and moveable memory structures. But you don't need such detailed knowledge when working with the TCL framework. In this case the PicHandle returned may be NULL (the picture requested was missing from the program's resource file) or will reference the picture data structure.
Function DrawPicture() is a rather smart function that can rescale any picture and redraw it within a specified rectangle.
The HLock() and HUnlock() functions relate to the old Mac OS's habit of moving data structures around in the heap. The Mac OS may chose to move heap based structures; this isn't wise if they are being used. So the picture data structure is locked while it is being drawn.
There is no code to free the picture after it has been drawn! In fact, this isn't a problem. The picture data structure (resource) is really still owned by the "Resource Manager" component of the Mac OS. The Resource Manager will get rid of the picture when appropriate.
That completes the initial edits and makes it time for the first compilation of the entire project. Leave it to run overnight.
Careful examination will reveal quite a few "glitches":
If you scroll the window or change its size, none of the components gets redrawn (again, this is reasonable, we haven't saved any record of the components that we drew).
The third glitch requires the least effort to correct. It is due to our failure to set up the complete "command handler" chain.
The Application object picks up information from the Macintosh OS concerning events like the user picking a menu option. It converts such an event into a "command number" and then has to pass the resulting command number to the "appropriate handler object". But how does it know which object is the appropriate handler?
It isn't practical for the Application object to know about all the other "command handler" objects and the commands that they handle. Instead, it relies on a mechanism known as the "command handler chain".
Command handlers (mostly objects that are instances of specialized subclasses of View, Document etc) are linked together in a chain. The Application object creates a command to represent a user's menu request, or mouse click, or keystroke. The Application object gives this command to the handler at the front of the command handler chain. If this object can handle the command, processing is completed. If the first object in the chain cannot handle the command, it passes it to the next entry in the chain.
Most of the work involved in setting up a command handler chain is dealt with by code in the network. But the standard code does not know about the specific View objects displayed in a program's windows and so can't deal with everything.
The standard code arranges that when a new document is created and opens windows etc, then the first entry in the command handler chain is the Document object itself. (In fact, the chain will only have two entries, the Document and the Application.)
A View object gets inserted into the chain if it receives a mouse click.
The program's behaviour is defined by these standard framework mechanisms. Initially, the command handler chain has only the Document and Application object. When the mouse button is first clicked in the menu bar, the effect is similar to having a "Get your menu ready" command being sent to the chain. The Document object prepares its menus, and the Application prepares its menu items. But because it is not in the handler chain, the DrawPane doesn't get the chance to prepare its menus and so they are disabled.
Clicking on the DrawPane to add the first picture component also has the effect of adding the DrawPane to the handler chain. Subsequently, all menu activations also get considered by the DrawPane and its menus are enabled.
We want the DrawPane to be part of the command handler chain from the moment the program opens a new document window. The object at the front of the chain is identified by a global variable gGopher. (Symantec's library authors used the name "gopher" for the first handler object because as commands arrive this object typically has to "go for this" and then "go for that".) Simply resetting gGopher does not work, because that doesn't get the DrawPane properly linked into the chain.
The mechanism required to set the "gopher" correctly is already built in to the framwork code for the Document class. The class has a data member, itsGopher, that should be set to identify the View object that is to go at the front of the chain.
If you change the empty CMain::MakeNewContents() fu nction to:
void CMain::MakeNewContents()
{
itsGopher = fMain_Pano3;
}
then the document's gopher is set. (Remember to #include "DrawPane.h" in CMain.cp before compiling! Also, if you changed the name of the pane when in Visual Architect, you will have to use the name as generated rather than the default Pano3.) When the window with the DrawPane is opened, the DrawPane becomes the "gopher" at the front of the handler chain.
The data that must be saved for each "picture item" consists of the picture identifier number and the rectangle where it is drawn. A struct should be defined to hold these data:
struct PicItem {
short fPicNum;
Rect fPlace;
};
This struct should be defined in a header file ("DItem.h") that gets #included in the other files that need to know about PicItems.
The storage structure used to hold the data might as well be a simple dynamic array as was used in the ListMaker example. The file ItsContents_CMain.h has to be edited to change the references to CCollaborator, replacing them with CArray:
#include "CArray.h" #define ITSCONTENTS_CMain CArray #ifdef GENERATE_TEMPLATE #pragma template CSaver< CArray>The special CStream_CArray.cpp file with the template declarations for the input output functions can be copied from the ListMaker project. The actual array has to be created in CMain::MakeNewContents().
Class CMain will have to have two extra member functions. One will be used to add a new PicItem to its collection:
void CMain::AddPicItem(const PicItem& p)
{
itsContents->Add((void*)&p);
}
The other function will draw all the items currently in the collection:
void CMain::DrawItems(Rect *area)
{
int num = itsContents->GetNumItems();
for(int i=1; i <= num; i++) {
PicItem p;
itsContents->GetArrayItem(&p,i);
Rect common;
Optimize drawing
if(!SectRect(area,&p.fPlace,&common))
continue;
PicHandle ph = GetPicture(1000+p.fPicNum);
if(ph == NULL)
continue;
HLock((Handle)ph);
DrawPicture(ph, &p.fPlace);
HUnlock((Handle)ph);
}
}
Both these functions will have to be added to the public interface for the class.
The DrawItems() function illustrates a minor optimization of the drawing process. When a pane is told to redraw its contents, it is given a Rect (rectangle structure) specifying the area that must be redrawn. It is only necessary to redraw components within this rectangle. Redrawing all components won't cause any difficulties; it is just that a full redraw will slow the program, so selective redrawing is preferrable. Here it is easy to arrange. Each picture component has a defining rectangle. A function, SectRect() (for "interSect rectangles"), exists in the Macintosh Toolbox; this determines whether two rectangles have any points in common. This SectRect() function can be used as shown to avoid redrawing picture components that don't intersect with the update area.
Class DrawPane must also be modified. It is the DrawPane object that can create the PicItem data and which knows when to get the data redrawn. It has to call the AddPicItem() and Draw() functions in the corresponding CMain object.
The DrawPane object has to be given a link to its CMain object. The framework code doesn't provide such a link, so it is an extra that we will have to provide. We will need a data member to store the link and a function to set it:
class DrawPane : public x_DrawPane
{
public:
TCL_DECLARE_CLASS
void SetLink(class CMain *mydoc);
void Draw(Rect *area);
...
private:
class CMain *fMyDoc;
};
void DrawPane::SetLink(class CMain *mydoc)
{
fMyDoc = mydoc;
}
This link will be used to pass a newly created PicItem to the CMain object:
void DrawPane::DoClick(
Point hitPt,
short modifierKeys,
long when)
{
Rect r;
r.left = hitPt.h;
r.right = r.left + 100;
// code as shown earlier
...
DrawPicture(ph, &r);
HUnlock((Handle)ph);
PicItem p;
p.fPlace = r;
p.fPicNum = fPicNum;
fMyDoc->AddPicItem(p);
}
The inheritted Draw() function must also be redefined. Class DrawPane's Draw() function passes the draw request to its associated CMain object:
void DrawPane::Draw(Rect *area)
{
fMyDoc->DrawItems(area);
}
The link between the CMain and DrawPane objects can be set in CMain's MakeNewContents() function:
void CMain::MakeNewContents()
{
itsGopher = fMain_Pano3;
itsContents = new CArray(sizeof(PicItem));
fMain_Pano3->SetLink(this);
}
These details should be sufficient for you to complete the implementation of this part of the program, getting a version that redraws all components whenever a window is scrolled or resized. (If you get odd compilation error messages, e.g. "Can't cast from DrawPane* to CBureaucrat*" or "Function CMain::DrawItems() does not exist" then it is probable that you forgot to include one of the header files that you require.)
You should note that there is an inconsistency between the treatment of the data members fPicNum and fMyDoc in class DrawPane. The fPicNum field was added when we were using Visual Architect (and actually ended up being declared in the x_DrawPane class). This data member is defined in the resources created by Visual Architect and transferred in the x_DrawPane::GetFrom() function. The fMyDoc data member is only defined in the code. It does not exist in the Visual Architect resources. This kind of inconsistency does not matter with the current version of the Think class library, but it could be a problem in future. Probably it would be best to go back to Visual Architect and define the extra fMyDoc data member there.
Unlike the ListMaker example where the edit text interaction fields confused things a bit, here are no interfering commands that appear to change the document. The normal "dirty" flag in the Document object can be used to indicate when changes have been made and, consequently, the File/Save option should be available. Simply add a call, SetChanged(TRUE), in the CMain::AddPicItem() function.
The ContentsToWindow() function has to do the same work when restoring existing documents as is done in the MakeNewContents() function. Here we need to set the Document's gopher, and establish a link from the DrawPane back to the document:
void CMain::ContentsToWindow()
{
itsGopher = fMain_Pano3;
fMain_Pano3->SetLink(this);
}
After making these changes, you should be able to save and restore pictures made up from sets of components.
The THINK framework library has support for such reversible actions integrated through the code. So, it is easy to make addition of a picture component a reversible action.
Reversible actions are represented by instances of specialized classes derived from the framework class CTask. A CTask object has to be able to represent the data needed to change the document between new and old states. The CTask class defines three functions: Do(), Undo(), and Redo(). The Do() action is to work with the Document (CMain) object to change the data; Undo() reverses the change; Redo() restores the change.
The framework code already picks up Edit/Undo and Edit/Redo commands. These are forwarded to the appropriate handler object (usually CMain) which checks whether it has an associated CTask object. If there is a CTask object, the Edit/Undo and Edit/Redo commands are forwarded to it.
The framework code also has all the code to "commit" an editing action. This will involve deletion of any CTask object.
If we want this mechanism, we need to define our own specialised subclass of CTask and add some extra public member functions to CMain so as to allow a CTask object to talk to its CMain. The CTask object will be created by the DrawPane.
Our specialized CTask class will have a declaration like the following:
class DrawTask : public CTask {
public:
DrawTask(const PicItem& d, CMain* mydoc);
~DrawTask();
virtual void Do();
virtual void Undo();
virtual void Redo();
private:
PicItem fdata;
CMain *fdoc;
};
It has two data members; one represents the new PicItem picture component that may be getting added to the document, and the other is the link to the CMain document that is to be changed. This class has to be defined in a header file (DrawTask.h); the file should #include the headers for CMain and CTask.
The implementation is:
const int kDRWTASKNUM = 7;
DrawTask::DrawTask(const PicItem& d, CMain* mydoc)
: CTask(kDRWTASKNUM)
{
fdata = d;
fdoc = mydoc;
}
Before initialising its own data members from its arguments, the DrawTask constructor calls the constructor of its base class.
The constructor for class CTask has to be given an integer argument. This integer is supposed to identify a message string; the string will appear in the Undo/Redo menu. Standard message strings exist for actions like typing (so you will see entries in the Edit menu like "Undo Typing"). These standard message strings are defined in an STR# resource. This STR# resource (it is number 130) is defined in the (ResEdit) project resources file. It has six entries. ResEdit has to be used to add a seventh ("Drawing"). The integer used to initialize the CTask base class is just this index number in the list of strings.
There is no real need to define a special destructor in this case. I defined it so that I could put a breakpoint there and so check that my task objects were really getting deleted by the framework code when no longer required.
DrawTask::~DrawTask()
{
}
The three functions that actually get the CMain document object to change its data are:
void DrawTask::Do()
{
fdoc->AddPicItem(fdata);
}
void DrawTask::Undo()
{
fdoc->RemoveLastItem();
}
void DrawTask::Redo()
{
fdoc->AddPicItem(fdata);
}
Class CMain already has an AddPicItem() function but this needs to be extended slightly and the new RemoveLastItem() has to be defined:
void CMain::AddPicItem(const PicItem& p)
{
itsContents->Add((void*)&p);
SetChanged(TRUE);
fMain_Pano3->Refresh();
}
void CMain::RemoveLastItem()
{
int last = itsContents->GetNumItems();
if(last == 0)
return; // should never happen!
itsContents->DeleteItem(last);
fMain_Pano3->Refresh();
}
Whenever an editing action is undone, the picture has to be redrawn without the picture component; when the action is redone (or done for the first time) the picture must be drawn with the element present.
The call fMain_Pano3->Refresh() gets the draw pane to redraw all its data. (There is a RefreshRect() call that specifies the subpart that has to be redrawn. It would actually be more efficient to use that function, specifying a redraw area that was just a little larger than the area occupied by the picture component being added/removed.)
The RemoveLastItem() represents a rather crude solution to the problem of undoing the edit. See if you can come up with something neater.
The other change required is in the DrawPane::DoClick() function. Instead of passing the PicItem directly to the CMain object, the DrawPane creates the new editing task and then "notifies" the CMain object of this associated task:
void DrawPane::DoClick(
Point hitPt,
short modifierKeys,
long when)
{
Rect r;
r.left = hitPt.h;
r.right = r.left + 100;
r.top = hitPt.v;
r.bottom = r.top + 100;
// PicHandle ph = GetPicture(1000+fPicNum);
// if(ph == NULL)
// return;
// HLock((Handle)ph);
// DrawPicture(ph, &r);
// HUnlock((Handle)ph);
PicItem p;
p.fPlace = r;
p.fPicNum = fPicNum;
// fMyDoc->AddPicItem(p);
DrawTask *d = new DrawTask(p, fMyDoc);
d->Do();
fMyDoc->Notify(d);
}
(Note that the DrawPane no longer draws the picture item. It is going to get told to redraw its contents anyway as soon as a consequence of CMain telling it to do a Refresh() operation.)
The call to the d->Do() gets the editing process started.
If you make these changes, the program should work with each individual editing step being "undoable" (at least until a commitment action is made).
The click is supposed to be handled by a specialized CMouseTask object. Class CMouseTask is a specialization of CTask. Like a CTask, it has data members that represent details of how the document's data should be changed and has member functions like Do(), Undo(), and Redo(). In addition, it has functions that handle the visual feedback.
A Pane object creates a CMouseTask in response to the first mouse click. Then the Pane object "tracks the mouse" until the mouse button is released. As it tracks the mouse, it sends requests to the CMouseTask object asking it to provide visual feedback. This visual feedback may be quite limited, e.g. a circle may be drawn on the screen surrounding the current mouse position.
We can use a specialized CMouseTask to make different sized versions of our picture components. We will know the start point where the mouse was first clicked, and the end point where the mouse was released. We can make a rectangle that has these points at opposite corners and use this rectangle to define the size for the picture component. The visual feedback could take the form of a displayed outline for the rectangle.
To achieve this we must first redefine our DrawTask. This must now be defined as a specialized CMouseTask rather than a specialized CTask; it must also provide some additional functions, and it does require extra data members. The new class declaration is:
class DrawTask : public CMouseTask {
public:
DrawTask(int thePicNum, CMain* mydoc, DrawPane* myPane);
~DrawTask();
virtual void Do();
virtual void Undo();
virtual void Redo();
virtual void BeginTracking(LongPt *startPt);
virtual void KeepTracking(LongPt *currPt,
LongPt *prevPt, LongPt *startPt);
virtual void EndTracking(LongPt *currPt,
LongPt *prevPt, LongPt *startPt);
private:
PicItem fdata;
CMain *fdoc;
DrawPane *fMyPane;
int fPicNum;
};
The constructor now takes as arguments the picture number (rather than a completed PicItem) and a pointer to the DrawPane; extra data members are defined to hold these data, their values are set in the constructor. (The link to the DrawPane is needed so that this mouse-based drawing task can ask its pane to scroll as needed while drawing.)
Three "tracking" functions are declared in CMouseTask; these usually have to be redefined in subclasses. Function BeginTracking() is called to start the feedback process; here we have nothing special to do, so the function is empty:
void DrawTask::BeginTracking(LongPt *startPt)
{
}
Function EndTracking() is called when the mouse button is released. Here we need to complete the definition of the PicItem that will be added to the document in the Do() and Redo() functions:
void DrawTask::EndTracking(LongPt *currPt, LongPt *prevPt,
LongPt *startPt)
{
Rect r;
Point p1;
Point p2;
p1.h = startPt->h;
p1.v = startPt->v;
p2.h = prevPt->h;
p2.v = prevPt->v;
Pt2Rect(p1, p2, &r);
fdata.fPicNum = fPicNum;
fdata.fPlace = r;
}
You will note that we have variables of type LongPt as well as ordinary Point variables. LongPt (and similar LongRect) variables use 32-bit coordinates while normal Points and Rects use 16-bit coordinates. The Quickdraw functions built into the Macintosh ROM use 16-bits. Of course, 16-bit coordinates limit the size of views; the maximum coordinate being approx. 32000. With normal sized pixels, the maximum view size is a mere 30 feet by 30 feet. Some people require bigger views and so must work with 32-bit coordinates. Most of the Symantec functions take LongPt (and LongRect) arguments allowing use of 32-bit coordinates.
If you are using 32-bit coordinates, you have to perform extra "house-keeping" operations to determine which part of the coordinate space is currently being displayed and so sort out a mapping onto 16-bit coordinates. If your views are only a few hundred pixels in size (like our DrawPane) you can ignore such complexities and just copy the horizontal and vertical coordinates from the LongPt to a Point variable.
Function Pt2Rect() used in EndTracking() is provided by the Quickdraw library built into the Macintosh ROM. It sets the third argument, the rectangle, to enclose the two point arguments (this is the easiest way to define a rectangle from point data because you don't have to bother sorting out the leftmost coordinate, the top coordinate etc).
The KeepTracking() function provides visual feedback. You have to be able to show the new component on the screen, either by actually drawing it or by showing an outline. As the user moves the mouse, you have to keep redrawing a changing display. You have to avoid leaving any extraneous picture components behind; so you must erase any feedback drawn previously. The rectangle that will be occupied by the picture component is defined by the initial click point and the current position of the mouse.
It would be possible to draw the picture component inside the current rectangle and erase it a moment later. Most users dislike the flickering screen that results. Drawing and erasing an outline rectangle is usually adequate feedback and there are fewer problems with screen flicker. There is a trick method for drawing and erasing that has some advantages. You set the "pen" in Xor mode and just draw the rectangle twice. The first drawing action puts its perimiter bits on the screen, the second drawing action removes the same bits. This is faster (and less flickery) than a draw and erase operation. It also means that any existing components drawn on the screen are left unchanged.
The KeepTracking() function also has to ask its panorama (the DrawPane) to scroll to keep the current mouse position in view.
void DrawTask::KeepTracking(LongPt *currPt, LongPt *prevPt,
LongPt *startPt)
{
fMyPane->AutoScroll(currPt);
PenMode(patXor);
PenPat(&qd.gray);
Rect r;
Point p1;
Point p2;
p1.h = startPt->h;
p1.v = startPt->v;
p2.h = currPt->h;
p2.v = currPt->v;
Pt2Rect(p1, p2, &r);
FrameRect(&r);
FrameRect(&r);
PenNormal();
}
This code contains several calls to standard Quickdraw functions (and also uses the main qd data structure that contains screen related data). The PenMode() call puts the "pen" into Xor mode; the PenPat() call changes the pen to "use gray ink" (defined by a data field in the main qd structure). The PenNormal() call resets the pen.
The function DrawPane::DoClick() must also be modified. The new code is:
void DrawPane::DoClick(
Point hitPt,
short modifierKeys,
long when)
{
DrawTask *d = new DrawTask(fPicNum, fMyDoc, this);
LongPt startPt;
startPt.h = hitPt.h;
startPt.v = hitPt.v;
this->TrackMouse(d, &startPt, &bounds);
d->Do();
fMyDoc->Notify(d);
}
This code creates a DrawTask object and then does some coordinate conversion before calling TrackMouse(). The third argument to TrackMouse() is a LongRect that defines bounds on the area where drawing can occur. Here we use the DrawPane's own bounds data member (declared in class Panorama) as drawing can occur anywhere. However, many other programs would need to define more restricted areas.
You should use the class browser to have a look at the TrackMouse() code (its in CPane). You will see the loop with the calls to KeepTracking() etc.
When TrackMouse() returns, use the Do() function to get the DrawTask object to add its data to the document.
With these extensions, the program should be able to produce pictures with the components drawn in arbitrary sized rectangles, see Figure 3.4.
The contents of the "picture palette" itself can be created automatically by the program checking for 'PICT' resources in its own file. This makes the program modifiable and extandable. ResEdit can be used to change its repertoire of picture components.
It is probably best to start again rather than try to hack the existing version of the program. Some of the existing functions can be copied to the new version.
Use the "Table tool" to place the palette at the left side of the window; it should be about 115 pixels wide. Use the Pane menu options to set the class and to define the pane properties. This pane should "accept clicks", should not be the "gopher", should be fixed in width but elastic in height. The pane should be given a proper name like "palette". In the CTable part, arrange that the size of items be 100x100 pixels, and set it so that only a single cell can be selected.
Use the "Panorma tool" to place the draw pane so that it occupies the rest of the window. This pane should be elastic in both width and height, its CPanorama bounds should be set to something large like 600x800, it accepts clicks and can be the gopher (though in this version of the program we won't really need that feature). Again, this pane should be given a proper name, like "drawing"; renaming will necessitate a few more edits to existing code but this actually helps make certain that everything has been updated.
There are no special menus required, so you can proceed immediately by saving the Visual Architect.rsrc file and then using the "Generate All" option to get the initial code.
Class DrawTask (both header and implementation) should be copied from the previous version and added to the new project (this code does not need to be changed). You should also copy the template file CArray_CStream.cpp, the header DItem.h with the declaration of the PicItem struct, and you should edit the ItsContents_CMain.h file changing the default reference from CCollaborator to CArray.
Functions like CMain::AddPicItem(), and the draw pane's DoClick() function can also be copied to the new files. However most of these functions will require changes.
The Application object will check its resource file for 'PICT' resources. It can find how many there are, and get details like their identifier numbers (these may not be sequential). The information defining the number of pictures and their resource identifiers can be stored in a CArray object; this might as well be made public so that it can be accessed later by the Palette objects that exist for the various windows. The declaration in CApp.h will have to be extended:
#include < CDialog.h>
#include < CArray.h>
class CApp : public x_CApp
{
public:
TCL_DECLARE_CLASS
void ICApp(void);
...
CArray *fPictures;
};
The code to get the windows should go in CApp::ICApp(). It will be something like the following:
fPictures = new CArray(sizeof(short));
UseResFile(CurResFile());
int numpics = Count1Resources('PICT');
for(int i=1; i <= numpics; i++) {
Handle h = Get1IndResource('PICT', i);
short id;
ResType rType;
Str255 rName;
GetResInfo(h,&id,&rType,rName);
fPictures->Add(&id);
}
The array, fPictures, is only to store the picture identifier numbers so its element size is sizeof(short). The UseResFile() call, specifying the current resource file, is really redundant here. In more complex situations you might need to specify the use of other files.
As explained in the Resource Manager documentation, a call to get resource data can consider all open resource files or just the first file. The manager includes calls like Count1Resources() and CountResources(); we want the versions that are restricted to a single file.
The Count1Resources() file takes a ResType argument; we specify 'PICT' so as to get details of picture resources. The loop loads each of the 'PICT' resources in turn. If you were confident that the pictures were small, and that there weren't going to be too many, you could "lock" them in memory (HLockHi(h);); that would make subsequent operations slightly faster. But its probably more sensible just to leave things to the resource manager; it will try to keep resources in memory but if memory gets short it will arrange to fetch them from disk when they are again needed.
At this stage, we need only the identifier numbers. The identification number, name, and other data can be obtained using the resource manager call GetResInfo(). The identifier numbers are stored in the array.
We should really provide a destructor to get rid of the array, but it isn't that important.
The class declaration should be extended. It needs a declaration for this extra access function and has to declare that it will overwrite the DrawCell() function:
class PalettePane : public x_PalettePane
{
public:
TCL_DECLARE_CLASS
/** Object I/O **/
virtual void PutTo(CStream &aStream);
virtual void GetFrom(CStream &aStream);
int SelectedPicture();
protected:
virtual void DrawCell(Cell theCell, Rect *cellRect);
};
The DrawCell() routine is given the cell to draw (as point with horizontal and vertical coordinates) and a drawing rectangle. Since the palette has only a single column, the "horizontal coordinate" is meaningless; the cell is defined by its vertical coordinate. This has to be converted into an index number indentifying the element from the array.
void PalettePane::DrawCell(Cell theCell, Rect *cellRect)
{
Finding the array element
int which = theCell.v + 1;
short picnum;
itsArray->GetArrayItem(&picnum, which);
Getting the corresponding picture
PicHandle ph = GetPicture(picnum);
if(ph == NULL)
return;
Drawing the picture
HLock((Handle)ph);
DrawPicture(ph, cellRect);
HUnlock((Handle)ph);
}
Once the element is known, the picture identification number is extracted from the array and an attempt is made to actually load the picture. If the picture can't be loaded, the draw request is ignored. Otherwise, the picture resource is temporarily locked whil it is drawn in the specified rectangle. As previously explained, there is no need to free the picture; it still belongs to the resource manager who should free it if memory is short.
As a specialized table, the palette has a RgnHandle data member itsSelection (pointer to pointer to a "region" data structure). The region structure defines the selected cells; since we specified that only a single cell be selected, we can find the cell from the coordinates of the region's bounding box rectangle. Again, once we have the cell, we can convert it into an array index number and access the picture identifier.
int PalettePane::SelectedPicture()
{
int row = (*itsSelection)->rgnBBox.top + 1;
short picnum;
itsArray->GetArrayItem(&picnum, row);
return picnum;
}
The palette panes have to be initialized when new document's are created or existing documents are opened. Each document is displayed in a separate window, and each window has its own palette. But all the palettes share the same CArray with picture numbers. They have to be given a pointer to this palette. The same code can go in the CMain::MakeNewContents() and ContentsToWindow() functions:
CArray *a = ((CApp*)gApplication)->fPictures; fMain_Palette->SetArray(a, FALSE); Point z; z.h = z.v = 0; fMain_Palette->SelectCell(z, FALSE, FALSE);(You must remember to #include the header files "CApp.h", and "PalettePane.h" in CMain.cp. You will also need the external declaration for the gApplication; this can be copied from x_CMain.cp.)
As well as the call to SetArray() that establishes a link from the palette to the shared array, the code shown has a call to SelectCell() so that the top most picture in the palette is the initial selection.
Consequently, the drawing pane now has a PalettePane* data member along with its CMain* data member.
class DrawPane : public x_DrawPane
{
public:
TCL_DECLARE_CLASS
void SetLink(class CMain *mydoc, class PalettePane *pal);
void Draw(Rect *area);
void DoClick(Point hitPt, short modifierKeys,
long when);
...
private:
class CMain *fMyDoc;
class PalettePane *fPalette;
};
The code for the functions is similar to that of the corresponding functions in the previous version. The DoClick() function now has a call to the collaborating palette to find the picture number of the currently selected picture:
void DrawPane::DoClick(
Point hitPt,
short modifierKeys,
long when)
{
int aPicNum;
aPicNum = fPalette->SelectedPicture();
DrawTask *d = new DrawTask(aPicNum, fMyDoc, this);
LongPt startPt;
startPt.h = hitPt.h;
startPt.v = hitPt.v;
this->TrackMouse(d, &startPt, &bounds);
d->Do();
fMyDoc->Notify(d);
}
(Remember to #include the necessary header files, PalettePane.h etc, when compiling DrawPane.cp. If you omit a header file you get spurious error messages like "PalettePane does not have a SelectedPicture() member function".)
void CMain::DrawItems(Rect *area)
{
int num = itsContents->GetNumItems();
for(int i=1; i <= num; i++) {
PicItem p;
itsContents->GetArrayItem(&p,i);
Rect common;
if(!SectRect(area,&p.fPlace,&common))
continue;
PicHandle ph = GetPicture(p.fPicNum);
if(ph == NULL)
continue;
HLock((Handle)ph);
DrawPicture(ph, &p.fPlace);
HUnlock((Handle)ph);
}
}
The member functions AddPicItem() and RemoveLastItem() are unchanged. The MakeNewContents() and ContentsToWindow() function just require extra code, as noted earlier, to initialize the palette in addition to the code needed to set links, and create the itsCotents array if needed:
void CMain::MakeNewContents()
{
itsGopher = fMain_Drawing;
itsContents = new CArray(sizeof(PicItem));
fMain_Drawing->SetLink(this, fMain_Palette);
CArray *a = ((CApp*)gApplication)->fPictures;
fMain_Palette->SetArray(a, FALSE);
Point z;
z.h = z.v = 0;
fMain_Palette->SelectCell(z, FALSE, FALSE);
}
The framework includes a variety of specialized classes descended from CSelector. These are intended to handle tasks like picking a tool from a palette (they are designed for a fixed palette not a dynamically created one like that used in this program). Rework the program to use one of these classes. Note that you ought to be able to create a program that has a "tear off palette" (like the Tools palette in the Visual architect). This tear off palette will belong to the CApp object. The individual documents won't need their own copies.