Step 13: Moving the DOC/View application to MDI
The Doc/View model is much more useful when it is used in a
multiple-document interface (MDI) application. The ability to have multiple child windows
in a frame lets you open more than one view for a document. You can find the source for
Step 13 in the files STEP13.CPP, STEP13.RC, STEP13DV.CPP, and STEP13DV.RC in the directory
EXAMPLES\OWL\TUTORIAL.
In Step 13, you'll add MDI capability to the application. This
requires new functionality in the TDrawDocument and TDrawView classes. In
addition, you'll add new features such as the ability to delete or modify an existing line
and the ability to undo changes. You'll also create a new view class called TDrawListView
to take advantage of the ability to display multiple views. TDrawListView shows an
alternate view of the drawing stored in TDrawDocument, displaying it as a list of
line information.
Supporting MDI in the application
STEP13.CPP contains the code for the application object and the
definition of the main window. The application object provides a framework for the
Doc/View classes defined in STEP13DV.CPP. This section discusses the changes to the TDrawApp
class that are required to provide MDI support for your Doc/View application. The OwlMain
function remains unchanged.
Changing to a decorated MDI frame
To support an MDI application, you need to change the TDecoratedFrame
you've been using to a TDecoratedMDIFrame. Then, inside the decorated MDI frame,
you need to create an MDI client window with the class TMDIClient. To easily locate
the client window later, add a TMDIClient * to your TDrawApp class. Call the
pointer Client. This client window contains the MDI child windows that display the
various views.
The parameters for the constructor in this case are different from
the parameters used in creating the decorated MDI frame used in Step 11.
| There's no menu resource for this
window. Instead, you'll construct a TMenuDescr, just as you did for Step 12.
|
| You need to create the client
window explicitly so that you can assign it to the Client data member. Unlike Step
11, where you used a custom client window class derived from TMDIClient, in this
step you can use a TMDIClient object directly. The functionality that was added to
the TDrawMDIClient class, such as opening files, creating new drawings, and so on,
is now handled by the document manager. Thus, TMDIClient is sufficient to handle
the chore of managing the MDI child windows.
|
| Lastly, you should turn menu
tracking on.
|
The window constructor
should look like this:
TDecoratedMDIFrame* frame = new TDecoratedMDIFrame("Drawing Pad", 0,
*(Client = new TMDIClient)true);
Changing the hint mode
You might have noticed in Step 12 that the hint text for control bar
buttons didn't appear until you actually press the button. You can change the hint mode so
that the text shows up when you just run the mouse over the top of the button.
To make this happen, call the control bar's SetHintMode
function with the TGadgetWindow::EnterHints parameter:
cb->SetHintMode(TGadgetWindow::EnterHints);
This causes hints to be displayed when the
cursor is over a button, even if the button isn't pressed. You can reset the hint mode by
calling SetHintMode with the TGadgetWindow::PressHints parameter. You can
also turn off menu tracking altogether by calling SetHintMode with the TGadgetWindow::NoHints
parameter.
Setting the main window's menu
You need to change the SetMenuDescr call a little. The
COMMANDS menu resource has been expanded to provide placeholder menus for the document
manager's and views' menu descriptors. Also, the decorated MDI frame provides window
management functions, such as cascading or tiling child windows, arranging the icons of
minimized child windows, and so on.
The call to the SetMenuDescr function should now look like
this:
GetMainWindow()->SetMenuDescr(TMenuDescr("COMMANDS"));
Setting the document manager
You also need to change how you create the document manager in an
MDI application. The only change you need to make in this case is to change the dmSDI
flag to dmMDI. You need to keep the dmMenu flag:
SetDocManager(new TDocManager(dmMDI | dmMenu));
InitInstance function
You need to make one change to the InitInstance function:
remove the call to CmFileNew. This makes the frame open with no untitled documents.
In the SDI application, opening the frame with an untitled document was OK. If the user
opened a file, the untitled document was replaced by the new document. But in an MDI
application, if the user opens an existing document, the untitled document remains open,
requiring the user to close it before it'll go away.
Opening a new view
When you open a new view, you must provide a window for the view. In
Step 12, EvNewView used the same client window again and again for every document
and view. In an MDI application, you can open numerous windows in the EvNewView
function. Each window you open inside the client area should be a TMDIChild. You
can place your view inside the TMDIChild object by calling the view's GetWindow
function for the child's client window.
Once you've created the TMDIChild object, you need to set its
menu descriptor, but only if the view has a menu descriptor itself. After setting the menu
descriptor, call the MDI child's Create function.
The EvNewView function should now look something like this:
void
TDrawApp::EvNewView(TView& view)
{
TMDIChild* child = new TMDIChild(*Client, 0, view.GetWindow());
if (view.GetViewMenu())
child->SetMenuDescr(*view.GetViewMenu());
child->Create();
}
Modifying drag and drop
In the SDI version of the tutorial application, you had to check to
make sure the user didn't drop more than one file into the application area. But in MDI,
if the user drops in more than one file, you can open them all, with each document in a
separate window. Here's how to implement the ability to open multiple files dropped into
your application:
- Find the number of files dropped into the application. Use the DragQueryFileCount
function. Use a for loop to iterate through the files.
- For each file, get the length of its path and allocate a char array with enough
room. Call the DragQueryFile function with the file's index (which you can track
using the loop counter), the char array, and the length of the path.
- Once you've got the file name, you can call the document manager's MatchTemplate
function to get the proper template for the file type. This is done the same way as in
Step 12.
- Once you've located a template, call the template's CreateDoc function with the
file path as the parameter to the function. This creates a new document and its
corresponding view, and opens the file into the document.
- Once all the files have been opened, call the DragFinish function. This function
releases the memory that Windows allocates during drag and drop operations.
Here's how the new EvDropFiles function should look:
void
TDrawApp::EvDropFiles(TDropInfo dropInfo)
{
int fileCount = dropInfo.DragQueryFileCount();
for (int index = 0; index < fileCount; index++) {
int fileLength = dropInfo.DragQueryFileNameLen(index)+1;
char* filePath = new char [fileLength];
dropInfo.DragQueryFile(index, filePath, fileLength);
TDocTemplate* tpl = GetDocManager()->MatchTemplate(filePath);
if (tpl)
GetDocManager()->CreateDoc(tpl, filePath);
//Old tpl->CreateDoc(filePath); // TDocTemplate::CreateDoc() is obsolete, use TDocManager::CreateDoc() instead
delete filePath;
}
dropInfo.DragFinish();
}
Closing a view
In Step 12, when you wanted to close a view, you had to remove the
view as a client window, restore the main window's menu, and reset the main window's
caption. You no longer need to do any of this, because these tasks are handled by the MDI
window classes. Here's how your EvCloseView function should look:
void
TDrawApp::EvCloseView(TView& /*view*/)
{ // nothing needs to be done here for MDI
}
Changes to TDrawDocument and TDrawView
You need to make the following changes in the TDrawDocument
and TDrawView classes. These changes include defining new events, adding new
event-handling functions, adding document property functions, and more.
Defining new events
First you need to define three new events to support the new
features in the TDrawDocument and TDrawView classes. These view notification
events are vnDrawAppend, vnDrawDelete, and vnDrawModify. These events
should be const ints, and defined as offsets from the predefined value vnCustomBase.
Using vnCustomBase ensures that your new events don't overlap any ObjectWindows
events.
Next, use the NOTIFY_SIG macro to specify the signature of the
event-handling function. The NOTIFY_SIG macro takes two parameters, the event name (such
as vnDrawAppend or vnDrawDelete) and the parameter type to be passed to the
event-handling function. The size of the parameter type can be no larger than a long;
if the object being passed is larger than a long, you must pass it by pointer. In
this case, the parameter is just an unsigned int to pass the index of the affected
line to the event-handling function. The return value of the event-handling function is
always void.
Lastly, you need to define the response table macro for each of these
events. By convention, the macro name uses the event name, in all uppercase letters,
preceded by EV_VN_. Use the #define macro to define the macro name. To define the
macro itself, use the VN_DEFINE macro. Here's the syntax for the VN_DEFINE macro:
VN_DEFINE(eventName, functionName, paramSize)
where:
The full definition of the
new events should look something like this:
const int vnDrawAppend = vnCustomBase+0;
const int vnDrawDelete = vnCustomBase+1;
const int vnDrawModify = vnCustomBase+2;
NOTIFY_SIG(vnDrawAppend, unsigned int)
NOTIFY_SIG(vnDrawDelete, unsigned int)
NOTIFY_SIG(vnDrawModify, unsigned int)
#define EV_VN_DRAWAPPEND VN_DEFINE(vnDrawAppend, VnAppend, int)
#define EV_VN_DRAWDELETE VN_DEFINE(vnDrawDelete, VnDelete, int)
#define EV_VN_DRAWMODIFY VN_DEFINE(vnDrawModify, VnModify, int)
Changes to TDrawDocument
TDrawDocument adds some new protected data members:
The TDrawDocument
constructor should be modified to initialize UndoLine to 0 and UndoState to UndoNone.
The TDrawDocument destructor is modified to delete UndoLine.
You need to modify the Open function slightly to read the file
information string from the document file and use it to initialize the FileInfo
member. If the document doesn't have a valid document path, initialize FileInfo
using the string resource IDS_FILEINFO.
Modify the AddLine function to notify any other views when a
line has been added to the drawing. You can use the NotifyViews function with the vnDrawAppend
event. The second parameter to the NotifyViews call should be the new line's array
index. You also need to set UndoState to UndoAppend. The AddLine
function should now look like this:
int
TDrawDocument::AddLine(TLine& line)
{
int index = Lines->GetItemsInContainer();
Lines->Add(line);
SetDirty(true);
NotifyViews(vnDrawAppend, index);
UndoState = UndoAppend;
return index;
}
Property functions
Every document has a list of properties. Each property has an
associated value, defined as an enum, by which it is identified. The list of enums
for a derived document object should always end with the value NextProperty. The
list of enums for a derived document object should always start with the value PrevProperty,
which should be set to the NextProperty member of the base class, minus 1.
Each property also has a text string describing the property
contained in an array called PropNames and an int containing
implementation-defined flags in an array called PropFlags. The property's enum
value can be used in an array index to locate the property string or flag for a particular
property.
TDrawDocument adds two new properties to its document
properties list: LineCount and Description. The enum definition
should look like this:
enum {
PrevProperty = TFileDocument::NextProperty-1,
LineCount,
Description,
NextProperty,
};
By redefining PrevProperty and NextProperty,
any class that's derived from your document class can create new properties without
overwriting the properties you've defined.
TDrawDocument also adds an array of static char
strings. This array contains two strings, each containing a text description of one of the
new properties. The array definition should look like this:
static char* PropNames[] = {
"Line Count",
"Description",
};
Lastly, TDrawDocument adds an array
of ints called PropFlags, which contains the same number of array elements
as PropNames. Each array element contains one or more document property flags ORed
together, and corresponds to the property in PropNames with the same array index.
The PropFlags array definition should look like this:
static int PropFlags[] = {
pfGetBinary|pfGetText, // LineCount
pfGetText, // Description
};
TDrawDocument overrides a number of
the TDocument property functions to provide access to the new properties. You can
find the total number of properties for the TDrawDocument class by calling the PropertyCount
function. PropertyCount returns the value of the property enum NextProperty,
minus 1.
You can find the text name of any document property using the PropertyName
function. PropertyName returns a char *, a string containing the property
name. It takes a single int parameter, which indicates the index of the parameter
for which you want the name. If the index is less than or equal to the enum PrevProperty,
you can call the TFileDocument function PropertyName. This returns the name
of a property defined in TFileDocument or its base class TDocument. If the
index is greater than or equal to NextProperty, you should return 0; NextProperty
marks the last property in the document class. If the index has the same or greater value
than NextProperty, the index is too high to be valid. As long as the index is
greater than PrevProperty but less than NextProperty, you should return the
string from the PropNames array corresponding to the index. The code for this
function should look like this:
const char*
TDrawDocument::PropertyName(int index)
{
if (index <= PrevProperty)
return TFileDocument::PropertyName(index);
else if (index < NextProperty)
return PropNames[index-PrevProperty-1];
else
return 0;
}
The FindProperty function is
essentially the opposite of the PropertyName function. FindProperty takes a
single parameter, a const char *. It tries to match the string passed in with the
name of each document property. If it successfully matches the string with a property
name, it returns an int containing the index of the property. The code for this
function should look like this:
int
TDrawDocument::FindProperty(const char far* name)
{
for (int i=0; i < NextProperty-PrevProperty-1; i++)
if (strcmp(PropNames[i], name) == 0)
return i+PrevProperty+1;
return 0;
}
The PropertyFlags function takes a
single int parameter, which indicates the index of the parameter for which you want
the property flags. These flags are returned as an int. If the index is less than
or equal to the enum PrevProperty, you can call the TFileDocument
function PropertyName. This returns the name of a property defined in TFileDocument
or its base class TDocument. If the index is greater than or equal to NextProperty,
you should return 0; NextProperty marks the last property in the document class. If
the index has the same or greater value than NextProperty, the index is too high to
be valid. As long as the index is greater than PrevProperty but less than NextProperty,
you should return the member of the PropFlags array corresponding to the index. The
code for this function should look like this:
int
TDrawDocument::PropertyFlags(int index)
{
if (index <= PrevProperty)
return TFileDocument::PropertyFlags(index);
else if (index < NextProperty)
return PropFlags[index-PrevProperty-1];
else
return 0;
}
The last property function is the GetProperty
function, which takes three parameters. The first parameter is an int, the index of
the property you want. The second parameter is a void *. This should be a block of
memory that is used to hold the property information. The third parameter is an int
and indicates the size in bytes of the block of memory.
There are three possibilities the GetProperty function should
handle:
The code for the GetProperty
function should look like this:
int
TDrawDocument::GetProperty(int prop, void far* dest, int textlen)
{
switch(prop)
{
case LineCount:
{
int count = Lines->GetItemsInContainer();
if (!textlen) {
*(int far*)dest = count;
return sizeof(int);
}
return wsprintf((char far*)dest, "%d", count);
}
case Description:
char* temp = new char[textlen]; // need local copy for medium model
int len = FileInfo.copy(temp, textlen);
strcpy((char far*)dest, temp);
return len;
}
return TFileDocument::GetProperty(prop, dest, textlen);
}
New functions in TDrawDocument
Step 13 adds a number of new functions to TDrawDocument.
These functions let you modify the document object by deleting lines, modifying lines,
clearing the document, and undoing changes.
The first new function is DeleteLine. As its name implies, the
purpose of this function is to delete a line from the document. DeleteLine takes a
single int parameter, which gives the array index of the line to be deleted.
- Delete should check that the index passed in to it is valid. You can check this
by calling the GetLine function and passing the index to GetLine. If the
index is valid, GetLine returns a pointer to a line object. Otherwise, it returns
0.
- Once you have determined the index is valid, you should set UndoLine to the line
to be deleted and set UndoState to UndoDelete. This saves the old line in
case the user requests an undo of the deletion.
- You should then detach the line from the document using the container class Detach
function. This function takes a single int parameter, the array index of the line
to be deleted.
- Turn the IsDirty flag on by calling the SetDirty function.
- Lastly, notify the views that the document has changed by calling the NotifyViews
function. Pass the vnDrawDelete event as the first parameter of the NotifyViews
call and the array index of the line as the second parameter.
The code for the DeleteLine function should look like this:
void
TDrawDocument::DeleteLine(unsigned int index)
{
const TLine* oldLine = GetLine(index);
if (!oldLine)
return;
delete UndoLine;
UndoLine = new TLine(*oldLine);
Lines->Detach(index);
SetDirty(true);
NotifyViews(vnDrawDelete, index);
UndoState = UndoDelete;
}
The ModifyLine function takes two
parameters, a TLine & and an int. The int is the array
index of the line to be modified. The affected line is replaced by the TLine &.
- As with the DeleteLine function, you need to set up the undo data members before
replacing the line. Copy the line to be replaced to UndoLine and set UndoState
to UndoModify. You also need to set UndoIndex to the index of the affected
line.
- Set the line to the TLine object passed into the function.
- Turn the IsDirty flag on by calling the SetDirty function.
- Lastly, notify the views that the document has changed by calling the NotifyViews
function. Pass the vnDrawModify event as the first parameter of the NotifyViews
call and the array index of the line as the second parameter.
The code for this function should look like this:
void
TDrawDocument::ModifyLine(TLine& line, unsigned int index)
{
delete UndoLine;
UndoLine = new TLine((*Lines)[index]);
SetDirty(true);
(*Lines)[index] = line;
NotifyViews(vnDrawModify, index);
UndoState = UndoModify;
UndoIndex = index;
}
The Clear function is fairly
straightforward. It flushes the TLines array referenced by Lines, then
forces the views to update by calling NotifyViews with the vnRevert
parameter. When the views are updated, there's no data in the document, causing the views
to clear their windows. The function should look something like this:
void
TDrawDocument::Clear()
{
Lines->Flush();
NotifyViews(vnRevert, true);
}
The Undo function has three
different types of operations to undo: append, delete, and modify. It determines which
type of operation it needs to undo by the value of the UndoState variable:
Here's how the code for the
Undo function should look:
void
TDrawDocument::Undo()
{
switch (UndoState) {
case UndoAppend:
DeleteLine(Lines->GetItemsInContainer()-1);
return;
case UndoDelete:
AddLine(*UndoLine);
delete UndoLine;
UndoLine = 0;
return;
case UndoModify:
TLine* temp = UndoLine;
UndoLine = 0;
ModifyLine(*temp, UndoIndex);
delete temp;
}
}
Each operation uses one of these new
modification functions. That way, each undo operation can itself be undone.
Changes to TDrawView
TDrawView modifies a number of its functions, including
deleting the GetPenSize function. This function should be moved to the TLine
class, so that the pen size is set in the line itself. You can call the TLine::GetPenSize
function from the CmPenSize function. The same thing should be done with the CmPenColor
function; move the functionality of this function to the TLine::GetPenColor
function. You can call the TLine::GetPenColor function from the CmPenColor
function.
To accommodate the new editing functionality in the TDrawDocument
and TDrawView classes, you need to add menu choices for Undo and Clear. These
choices should post the events CM_CLEAR and CM_UNDO. The menu requires a change in the
menu resource to group the menus properly. The call should look like this:
SetViewMenu(new TMenuDescr(IDM_DRAWVIEW));
You can redefine the right button behavior
by changing the EvRButtonDown function (there are now two other ways to change the
pen size, the Tools|Pen Size menu command and the Pen Size control bar button). You can
use the right mouse button as a shortcut for an undo operation. The EvRButtonDown
function should look like this:
void
TDrawView::EvRButtonDown(uint, TPoint&)
{
CmUndo();
}
New functions in TDrawView
Step 13 adds a number of new functions to TDrawDocument.
These functions implement an interface to access the new functionality in TDrawDocument.
You need to override the TView virtual function GetViewName.
The document manager calls this function to determine the type of view. This function
should return a const char * referencing a string containing the view name.
This function should look like this:
const char far* GetViewName() { return StaticName(); }
After adding the new menu items Clear and
Undo to the Edit menu, you need to handle the events CM_CLEAR and CM_UNDO. Add the
following lines to your response table:
EV_COMMAND(CM_CLEAR, CmClear),
EV_COMMAND(CM_UNDO, CmUndo),
You also need functions to handle the
CM_CLEAR and CM_UNDO events. If the view receives a CM_CLEAR message, all it needs to do
is to call the document's Clear function:
void
TDrawView::CmClear()
{
DrawDoc->Clear();
}
If the view receives a CM_UNDO message,
all it needs to do is to call the document's Undo function:
void
TDrawView::CmUndo()
{
DrawDoc->Undo();
}
The other new events the view has to
handle are the view notification events, vnDrawAppend, vnDrawDelete, and vnDrawModify.
You should add the response table macros for these events to the view's response table:
DEFINE_RESPONSE_TABLE1(TDrawView, TWindowView)
EV_VN_DRAWAPPEND,
EV_VN_DRAWDELETE,
EV_VN_DRAWMODIFY,
END_RESPONSE_TABLE;
The event-handling functions for these
macros are VnAppend, VnDelete, and VnModify. All three of these
functions return a bool and take a single parameter, an int indicating which line
in the document is affected by the event.
The VnAppend function gets notification that a line was
appended to the document. It then draws the new line in the view's window. It should
create a device context, get the line from the document, call the line's Draw
function with the device context object as the parameter, then return true. The code for
this function looks like this:
bool
TDrawView::VnAppend(unsigned int index)
{
TClientDC dc(*this);
const TLine* line = DrawDoc->GetLine(index);
line->Draw(dc);
return true;
}
The VnModify function forces a
repaint of the entire window. It might seem more efficient to just redraw the affected
line, but you would need to paint over the old line, repaint the new line, and restore any
lines that might have crossed or overlapped the affected line. It is actually more
efficient to invalidate and repaint the entire window. So the code for the VnModify
function should look like this:
bool
TDrawView::VnModify(unsigned int /*index*/)
{
Invalidate(); // force full repaint
return true;
}
The VnDelete function also forces a
repaint of the entire window. This function faces the same problem as VnModify;
simply erasing the line will probably affect other lines. The code for the VnDelete
function should look like this:
bool
TDrawView::VnDelete(unsigned int /*index*/)
{
Invalidate(); // force full repaint
return true;
}
TDrawListView
The purpose of the TDrawListView class is to display the
data contained in a TDrawDocument object as a list of lines. Each line will display
the color values for the line, the pen size for the line, and the number of points that
make up the line. TDrawListView will let the user modify a line by changing the pen
size or color. The user can also delete a line.
TDrawListView is derived from TView and TListBox.
TView gives TDrawListView the standard view capabilities. TListBox
provides the ability to display the information in the document object in a list.
Creating the TDrawListView class
The TDrawListView constructor takes two parameters, a TDrawDocument
& (a reference to the view's associated document) and a TWindow * (a
pointer to the parent window). The parent window defaults to 0 if no value is supplied.
The constructor passes the first parameter to the TView constructor and initializes
the DrawDoc member to point at the document passed as the first parameter.
TDrawListView has two data members, one protected TDrawDocument
* called DrawDoc and one public int called CurIndex. DrawDoc
serves the same purpose in TDrawListView as it did in TDrawView, namely to
reference the view's associated document object. CurIndex contains the array index
of the currently selected line in the list box.
The TDrawListView constructor also calls the TListBox
constructor. The first parameter of the TListBox constructor is passed the parent
window parameter of the TDrawListView constructor. The second parameter of the TListBox
constructor is a call to the TView function GetNextViewId. This function
returns a static unsigned that is used as the list box identifier. The view
identifier is set in the TView constructor. The coordinates and dimensions of the
list box are all set to 0; the dimensions are filled in when the TDrawListView is
set as a client in an MDI child window.
The constructor also sets some window attributes, including the Attr.Style
attribute, which has the WS_BORDER and LBS_SORT attributes turned off, and the Attr.AccelTable
attribute, which is set to the IDA_DRAWLISTVIEW accelerator resource defined in
STEP13DV.RC.
The constructor also sets up the menu descriptor for TDrawListView.
Because TDrawListView has a different function from TDrawView, it requires a
different menu. Compare the menu resource for TDrawView and the menu resource for TDrawListView.
Here's the code for the TDrawListView constructor:
TDrawListView::TDrawListView(TDrawDocument& doc,TWindow *parent)
: TView(doc), TListBox(parent, GetNextViewId(), 0,0,0,0), DrawDoc(&doc)
{
Attr.Style &= ~(WS_BORDER | LBS_SORT);
Attr.AccelTable = IDA_DRAWLISTVIEW;
SetViewMenu(new TMenuDescr(IDM_DRAWLISTVIEW));
}
TDrawListView has no dynamically
allocated data members. The destructor therefore does nothing.
Naming the class
Like the TDrawView class, TDrawListView should define
the function StaticName to return the name of the view class. Here's how the StaticName
function might look:
static const char far* StaticName() {return "DrawList View";}
Overriding TView and TWindow virtual functions
The document manager calls the view function GetViewName to
determine the type of view. You need to override this function, which is declared virtual
function in TView. This function should return a const char * referencing a
string containing the view name. This function should look like this:
const char far* GetViewName() { return StaticName(); }
The document manager calls the view
function GetWindow to get the window associated with a view. You need to override
this function also, which is declared virtual function in TView. It should
return a TWindow * referencing the view's window. This function should look like
this:
TWindow* GetWindow() { return (TWindow*) this; }
You also need to supply a version of the CanClose
function. This function should call the TListBox version of CanClose and
also call the document's CanClose function. This function should look like this:
bool CanClose() {return TListBox::CanClose() && Doc->CanClose();}
You also need to provide a version of the Create
function. You can call the TListBox version of Create to actually create the
window. But you also need to load the data from the document into the TDrawListView
object. To do this, call the LoadData function. You'll define the LoadData
function in the next section of this step. The Create function should look
something like this:
bool
TDrawListView::Create()
{
TListBox::Create();
LoadData();
return true;
}
Loading and formatting data
You need to provide functions to load data from the document object
to the view document and to format the data for display in the list box. These functions
should be protected so that only the view can call them.
The first function is LoadData. To load data into the list
box, you need to first clear the list of any items that might already be in it. For this,
you can call the ClearList function, which is from the TListBox base class.
After that, get lines from the document and format each line until the document runs out
of lines. You can tell when there are no more lines in the document; the GetLine
function returns 0. Lastly, set the current selection index to 0 using the SetSelIndex
function. This causes the first line in the list box to be selected. The code for the LoadData
function looks something like this:
void
TDrawListView::LoadData()
{
ClearList();
int i = 0;
const TLine* line;
while ((line = DrawDoc->GetLine(i)) != 0)
FormatData(line, i++);
SetSelIndex(0);
}
The FormatData function takes two
parameters. The first parameter is a const TLine * that references the line
to modified or added to the list box. The second parameter contains the index of the line
to modified.
The code for FormatData should look something like this:
void
TDrawListView::FormatData(const TLine* line, int unsigned index)
{
char buf[80];
TColor color(line->QueryColor());
wsprintf(buf, "Color = R%d G%d B%d, Size = %d, Points = %d",
color.Red(), color.Green(), color.Blue(),
line->QueryPenSize(), line->GetItemsInContainer());
DeleteString(index);
InsertString(buf, index);
SetSelIndex(index);
}
Event handling in TDrawListView
Here's the response table for TDrawListView:
DEFINE_RESPONSE_TABLE1(TDrawListView, TListBox)
EV_COMMAND(CM_PENSIZE, CmPenSize),
EV_COMMAND(CM_PENCOLOR, CmPenColor),
EV_COMMAND(CM_CLEAR, CmClear),
EV_COMMAND(CM_UNDO, CmUndo),
EV_COMMAND(CM_DELETE, CmDelete),
EV_VN_ISWINDOW,
EV_VN_COMMIT,
EV_VN_REVERT,
EV_VN_DRAWAPPEND,
EV_VN_DRAWDELETE,
EV_VN_DRAWMODIFY,
END_RESPONSE_TABLE;
This response table is similar to TDrawView's
response table in some ways. The two views share some events, such as the CM_PENSIZE and
CM_PENCOLOR events and the vnDrawAppend and vnDrawModify view notification
events.
But each view also handles events that the other view doesn't. This
is because each view has different capabilities. For example, the TDrawView class
handles a number of mouse events, whereas TDrawListView handles none. That's
because it makes no sense in the context of a list box to handle the mouse events; those
events are used when drawing a line in the TDrawView window.
TDrawListView handles the CM_DELETE event, whereas TDrawView
doesn't. This is because, in the TDrawView window, there's no way for the user to
indicate which line should be deleted. But in the list box, it's easy: just delete the
line that's currently selected in the list box.
TDrawListView also handles the vnIsWindow event. The vnIsWindow
message is a predefined ObjectWindows event, which asks the view if its window is the same
as the window passed with the event.
The CmPenSize function is more complicated in the TDrawListView
class than in the TDrawView class. This is because the TDrawListView class
doesn't maintain a pointer to the current line the way TDrawView does. Instead, you
have to get the index of the line that's currently selected in the list box and get that
line from the document. Then, because the GetLine function returns a pointer to a const
object, you have to make a copy of the line, modify the copy, then call the document's ModifyLine
function. Here's how the code for this function should look:
void
TDrawListView::CmPenSize()
{
int index = GetSelIndex();
const TLine* line = DrawDoc->GetLine(index);
if (line) {
TLine* newline = new TLine(*line);
if (newline->GetPenSize())
DrawDoc->ModifyLine(*newline, index);
delete newline;
}
}
The interesting aspect of this function
comes in the ModifyLine call. When the user changes the pen size using this
function, the pen size in the view isn't changed at this time. But when the document
changes the line in the ModifyLine call, it posts a vnDrawModify event to
all of its views:
NotifyViews(vnDrawModify, index);
This notifies all the views associated
with the document that a line has changed. All views then call their VnModify
function and update their displays from the document. This way, any change made in one
view is automatically reflected in other open views. The same holds true for any other
functions that modify the document's data, such as CmPenColor, CmDelete, CmUndo,
and so on.
The CmPenColor function looks nearly same as the CmPenSize
function, except that, instead of calling the line's GetPenSize function, it calls GetPenColor:
void
TDrawListView::CmPenColor()
{
int index = GetSelIndex();
const TLine* line = DrawDoc->GetLine(index);
if (line) {
TLine* newline = new TLine(*line);
if (newline->GetPenColor())
DrawDoc->ModifyLine(*newline, index);
delete newline;
}
}
The CM_DELETE event indicates that the
user wants to delete the line that is currently selected in the list box. The view needs
to call the document's DeleteLine function, passing it the index of the currently
selected line. This function should look like this:
void
TDrawListView::CmDelete()
{
DrawDoc->DeleteLine(GetSelIndex());
}
You also need functions to handle the
CM_CLEAR and CM_UNDO events for TDrawListView. If the user chooses the Clear menu
command, the view receives a CM_CLEAR message. All it needs to do is call the document's Clear
function:
void
TDrawListView::CmClear() {
DrawDoc->Clear();
}
If the user chooses the Clear menu
command, the view receives a CM_UNDO message. All it needs to do is call the document's Undo
function:
void
TDrawListView::CmUndo()
{
DrawDoc->Undo();
}
These functions are identical to the TDrawView
versions of the same functions. That's because these operation rely on TDrawDocument
to actually make the changes to the data.
Like the TDrawView class, TDrawListView's VnCommit
function always returns true. In a more complex application, this function would add any
cached data to the document, but in this application, the data is added to the document as
each line is drawn.
The VnRevert function calls the LoadData function to
revert the list box display to the data contained in the document:
bool
TDrawListView::VnRevert(bool /*clear*/)
{
LoadData();
return true;
}
The VnAppend function gets a single
unsigned int parameter, which gives the index number of the appended line. You need
to get the new line from the document by calling the document's GetLine function.
Call the FormatData function with the line and the line index passed into the
function. After formatting the line, set the selection index to the new line and return.
The function should look like this:
bool
TDrawListView::VnAppend(unsigned int index)
{
const TLine* line = DrawDoc->GetLine(index);
FormatData(line, index);
SetSelIndex(index);
return true;
}
The VnDelete function takes a
single int parameter, the index of the line to be deleted. To remove the line from
the list box, call the TListBox function DeleteString:
bool
TDrawListView::VnDelete(unsigned int index)
{
DeleteString(index);
HandleMessage(WM_KEYDOWN,VK_DOWN); // force selection
return true;
}
The call to HandleMessage ensures
that there is an active selection in the list box after the currently selected string is
deleted.
The VnModify function takes a single int parameter,
the index of the line to be modified. You need to get the line from the document using the
GetLine function. Call FormatData with the line and its index:
bool
TDrawListView::VnModify(unsigned int index)
{
const TLine* line = DrawDoc->GetLine(index);
FormatData(line, index);
return true;
}
Where to find more information
Here's a guide to where you can find more information on the topics
introduced in this step:
|