Step 14: Making an OLE container

Back Home Up Next

The next step in the ObjectWindows tutorial shows you how to make an OLE 2 container from the Drawing Pad application. Object Linking and Embedding (OLE) is an extension to Windows that lets the user seamlessly combine several applications into a single workspace. An OLE container application can host server objects, providing additional workspace where the user of your application can expand your application with the capabilities provided by OLE-server-enabled application.

The code for the example used in this chapter is contained in the files STEP14.CPP, STEP14DV.CPP, STEP14.RC, and STEP14DV.RC in the EXAMPLES/OWL/TUTORIAL directory where your compiler is installed.

How OLE works

Two different types of application are necessary for basic OLE operations:

A container can have other applications or objects embedded within it, presenting the data from the embedded object as part of the container's own data set.
A server can be embedded within a container application and can be used to manipulate the data that the server displays in the container's work space.

What is a container?

In this step of the tutorial, you'll make your Doc/View Drawing Pad application into an OLE container. Making Drawing Pad into an OLE container has some important ramifications: the application is no longer limited to displaying a set of lines, but can also display any kind of data that can be presented by any server users embed within their drawings. Although line drawing capability is still in the application and producing line drawings is still the main function of the application, users can now spice up their drawings with bitmaps, spreadsheet charts, even sound files.

Although you can do many of the same tasks by using the Clipboard to transfer data, it's easier to use OLE. Using the Clipboard, your application has to be able to accept the type of data stored there. This means if you want to accept bitmaps in the Drawing Pad application, you have to build the functionality required to accept and display bitmaps. This in no way prepares the application to accept spreadsheet charts, database tables, or or data in other graphic formats. To include another type of data requires implementing more functionality to interpret and display that data.

Using OLE, your application can display any type of data that is supported by an available OLE server. As far as your application is concerned, a bitmap looks exactly like a spreadsheet chart, a database table, or any other kind of object; that is, they all look like OLE server objects.

Also, using the Clipboard, you can build the ability to display a bitmap into your application. But modifying the bitmap after it's been pasted in requires more functionality to be built into your application.

Using OLE, the embedded server handles its embedded data whenever the user wants to modify or change it. The type of data used in the server is of no consequence to the container.

Implementing OLE in ObjectWindows: ObjectComponents

There is a price to pay for the advantages OLE provides for your application: programming an OLE implementation has historically been very messy and time consuming. You needed to modify your code to conform to OLE specifications. Even more than this, OLE doesn't follow the event-based paradigm that Windows applications were previoiusly based on. Instead it implements a new interface-based paradigm, requiring an understanding of standard OLE interfaces, reference counting, and other OLE specifications.

ObjectWindows implements OLE through the ObjectComponents Framework. You can use ObjectComponents to make your application an OLE container or server with only minor modifications to your code. You can use ObjectComponents with the following application types:

Doc/View ObjectWindows applications
Non-Doc/View ObjectWindows applications
Non-ObjectWindows C++ applications

The fewest modifications are required for Doc/View ObjectWindows applications, which is shown in this chapter. Implementing OLE with ObjectComponents in non-Doc/View ObjectWindows applications and non-ObjectWindows C++ applications is described in the Borland C++ Programmer's Guide in Part 5, "ObjectComponents Programmer's Guide.''

The following steps are required to convert your Doc/View ObjectWindows application to an OLE container application:

Include the proper header files.
Register your application and Doc/View objects in the system registration database.
Create a TOcApp object and associating it with your application object.
Change your frame window class to an OLE-aware frame window class.
Change your document and view classes's base class to OLE-enabled classes.

The ObjectComponents objects used in this chapter are explained as you add them to the Drawing Pad application. The ObjectComponents Framework is described in detail in Part 5 of the Borland C++ Programmer's Guide and in the ObjectWindows Reference in Part 2, ``ObjectComponents Programmer's Reference.''

Adding OLE class header files

You need to add new headers to your files to use the ObjectComponents classes. ObjectComponents adds OLE capabilities by adding deriving new OLE-enabled classes from existing classes.

To add new headers to your files so you can use ObjectComponents classes:

  1. In STEP14.CPP, instead of using TDecoratedMDIFrame, you'll use TOleMDIFrame, which is an OLE-enabled decorated MDI frame. All OLE frame windows, whether they're MDI or SDI, must be able to handle decorations, since many embedded OLE servers provide their own tool bars. The TOleMDIFrame class is declared in the owl/olemdifr.h header file. Your list of include statements in STEP14.CPP should now look something like this:
    #include <owl/applicat.h>
    #include <owl/appdict.h>  // Needed for DEFINE_APP_DICTIONARY   
    #include <owl/dialog.h>
    #include <owl/controlb.h>
    #include <owl/buttonga.h>
    #include <owl/statusba.h>
    #include <owl/docmanag.h>
    #include <owl/olemdifr.h>
    #include <stdlib.h>
    #include <string.h>
    #include "step14.rc"
               
  2. In STEP14DV.CPP, you need to include OLE-enabled document and view classes. These classes, TOleDocument and TOleView, provide standard Doc/View functionality along with the ability to support OLE. They're declared in the header files owl/oledoc.h and owl/oleview.h. Your list of include statements in STEP14DV.CPP should now look something like this:
    #include <owl/chooseco.h>
    #include <owl/dc.h>
    #include <owl/docmanag.h>
    #include <owl/gdiobjec.h>
    #include <owl/inputdia.h>
    #include <owl/listbox.h>
    #include <owl/oledoc.h>
    #include <owl/oleview.h>
    #include <classlib/arrays.h>
    #include "step14dv.rc"
    
               

Registering the application for OLE

For OLE to keep track of the applications running on a particular system, any application that wants to use OLE must register in the system-wide OLE registration database. You need to provide a unique identifier number and a description of the application. You also need to create objects that let your application communicate with OLE.

ObjectComponents simplifies the process of registering your application through a set of registration macros. These macros create an object of type TRegList, known as a registration table, which contains the information required by the OLE registration database. The macros are the same ones you use when creating a Doc/View template, but you use more of the capabilities available in the TRegList class.

Once you've created a registration table, you need to pass it to a connector object. A connector object provides the channel through which a Doc/View application communicates with ObjectComponents and, by extension, with OLE. The registration table is passed to an object of type TOcApp (ObjectComponents connector objects all begin with TOc).

Later, you'll modify the declaration of your TDrawApp class to be derived from both TApplication and TOcModule. Your application object initilizes the TOcApp connector object during the application object's construction. The connector object is then accessed through a pointer contained in the TOcModule class.

Creating the registration table

You use the REGDATA macro to create a container application's registration table. This is the same macro you used earlier to register your default document extension and file name filter. For your purposes now, you need the following key values:

Key values and meanings
 
Key value Meaning
clsid String representation of a 16-byte number called a globally unique ID or GUID. This number must be unique to the application. It is used to distinguish your application from every other application on the system. This value is for internal system use only.
description Application description for the system user to see. This string appears in the OLE registration list.

Your registration table should look something like this:

REGISTRATION_FORMAT_BUFFER(100)

BEGIN_REGISTRATION(AppReg)
  REGDATA(clsid, "{383882A1-8ABC-101B-A23B-CE4E85D07ED2}")
  REGDATA(description,"OWL Drawing Pad 2.0")
END_REGISTRATION
       

Note: You must select a unique GUID for your application. There are a number of ways to get a unique identifier for your application. Generating a GUID and describing your application is presented in detail in "Turning an application into an OLE server" in the ObjectWindows Programmer's Guide. For this tutorial, you can use the GUIDs provided in the tutorial examples. Do not use these same numbers when you create other applications.

Other macros can go into your registration table. Those for creating AppReg are the bare minimum for a container application object. You'll get to see a more complicated table when you create the registration table for your document class.

Also, because AppReg is created in the global name space of your application, it's safer and more informative to refer to it inside your classes and functions using the global scoping qualifier. So instead of:

void
MyClass::MyFunc()
{
  OtherFunc(AppReg);
}
       

you would write:

void
MyClass::MyFunc()
{
  OtherFunc(::AppReg);
}
       

Creating a class factory

A class factory is pretty much what it sounds like--it's an object that can make more objects. It is used in OLE to provide objects for linking and embedding. When an application wants to embed your application's objects in itself, it's the class factory that actually produces the embedded object.

ObjectWindows makes it easy to create a class factory with the TOleDocViewFactory template. All you need to do is create an instance of the template with the application class you want to produce as the template type. In this case, you want to produce instances of TDrawApp with your factory. Creating the template would look like this:

TOleDocViewFactory<TDrawApp>();
       

You need to pass an instance of this template as the second parameter of the TOcRegistrar constructor. You can see how this looks in the sample OwlMain below. The objects themselves are created in the factory using the same Doc/View templates used by your application when it's run as a stand-alone application.

TOleDocViewFactory is the class factory template for Doc/View ObjectWindows applications. There are other class factory templates for different types of applications. These are discussed in Chapter 36 in the ObjectWindows Reference.

Creating a registrar object

The registration table contains information about your application object for the system. The registrar object, which is of type TOcRegistrar, takes the registration table and registers the application with the OLE registration database. It also parses the application command line looking for OLE-related options.

To create a registrar object:

  1. Create a global static pointer to a TOcRegistrar object. You can do this using the TPointer template, defined in the osl\geometry.h header file (this file is already included by a number of the ObjectWindows header files, so you don't need to include it again). This should look something like this:
    static TPointer<TOcRegistrar> Registrar;
                   

    Using TPointer instead of a simple pointer, such as TOcRegistrar* Registrar, provides automatic deletion when the object referred to is destroyed or goes out of scope. The full range of operations available with regular pointers is available in TPointer, while some of the traditional dangers of using pointers are eliminated.

  2. Create the actual registrar object. The TOcRegistrar constructor takes four parameters:
    A reference to a registration table object
    A pointer to a callback function of type TComponentCreate
    A string containing the application's command line
    An instance handle indicating the application instance the registrar is for; this parameter defaults to _hInstance, the current application instance

    For these parameters, you can pass the following arguments when constructing the registrar object.

    For the first parameter, pass your registration table object.
    For the second parameter, pass in your class factory.
    For the third parameter, pass the application's command line. You can get the command line by calling TApplication's GetCmdLine function.
    You don't need to specify the fourth parameter, an instance handle; just let that parameter take its default value.

    For example,

    ::Registrar = new TOcRegistrar(AppReg, TOleDocViewFactory<TDrawApp>(),
                                   TApplication::GetCmdLine());
               
  3. Call the Run function. However, instead of calling the application object's Run function (which you couldn't do at this point if you wanted to, since you haven't created an application object), call the registrar object's Run function. TOcRegistrar provides a Run function that is called just like TApplication's Run function. However, any ObjectWindows OLE application should call the registrar object's Run function. This function performs some checks and actions required for your OLE application.

Your OwlMain function should now look something like this:

int
OwlMain(int /*argc*/, char* /*argv*/ [])
{
  ::Registrar = new TOcRegistrar(AppReg, TOleDocViewFactory<TDrawApp>(),
                                 TApplication::GetCmdLine());
  return ::Registrar->Run();
}
       

Creating an application dictionary

The application dictionary is an object that helps coordinate associations between processes or tasks and TApplication pointers. Before diving into OLE, this was relatively simple: a TApplication object was pretty much synonymous with a process. With OLE, the environment becomes confused: there can be multiple tasks and processes in a single application, with a container application, a number of embedded servers, possibly more servers embedded within those servers--the neighborhood's gotten a little more crowded.

To deal with this, ObjectWindows provides application dictionaries with the TAppDictionary class. The best thing about TAppDictionary is that, in order to use it for our purposes here, you don't have to know a whole lot about it. ObjectWindows also provides a macro, DEFINE_APP_DICTIONARY, that creates and initializes an application dictionary object for you.

DEFINE_APP_DICTIONARY takes a single parameter, the name of the object you want to create. You should place this near the beginning of your source file in the global name space. You must at least place it before TDrawApp's constructor, since that's where you'll use it.

Your application dictionary definition should look something like this:

DEFINE_APP_DICTIONARY(AppDictionary);
       

Changes to TDrawApp

You need change the TDrawApp class to support ObjectComponents. These changes are fairly standard when you're creating a Doc/View application in an OLE container.

Changing the class declaration
Changing the class functionality, including
Creating an OLE MDI frame
Setting the OLE MDI frame's application connector
Adding a tool bar identifier

Changing the class declaration

You need to make the following changes to the declaration of the TDrawApp class:

  1. Derive TDrawApp from both TApplication and the TOcModule class. TOcModule provides the interface your application object uses to communicate with OLE through the ObjectComponents Framework. Both TApplication and TOcModule should be public bases.
  2. Change the constructor so that you pass the TApplication constructor a single parameter. You should initialize the name of the application object with the value of description from AppReg. To make this easier, the TRegList class overloads the square bracket operators ([]) to return the string associated with the key value passed between the brackets. So to get the string associated with the description key, call AppReg["description"].

Your TDrawApp declaration should now resemble the following code:

class TDrawApp : public TApplication, public TOcModule
{
  public:
    TDrawApp() : TApplication(::AppReg["description"]) {}

  protected:
    TMDIClient* Client;

    // Override methods of TApplication
    void InitInstance();
    void InitMainWindow();

    // Event handlers
    void EvNewView(TView& view);
    void EvCloseView(TView& view);
    void EvDropFiles(TDropInfo dropInfo);
    void CmAbout();

  DECLARE_RESPONSE_TABLE(TDrawApp);
};
       

Changing the class functionality

You need to change the main window to a TOleMDIFrame object and properly initialize it as follows:

Creating an OLE MDI frame
Setting the OLE MDI frame's application connector
Adding a tool bar identifier

Creating an OLE MDI frame

Next, you need to change your InitMainWindow function by changing your frame window object from a decorated MDI frame to an OLE-aware decorated MDI frame (note that all OLE-aware ObjectWindows frame window classes are decorated). The window class to use for this is TOleMDIFrame. TOleMDIFrame is based on TMDIFrame, which provides MDI support, and TOleFrame, which provides the ability to work with ObjectComponents. Here's the constructor for TOleMDIFrame:

TOleMDIFrame(const char far* title,
             TResId menuResId,
             TMDIClient& clientWnd = *new TMDIClient,
             bool trackMenuSelection = false,
             TModule* module = 0);
       

The parameters to the TOleMDIFrame constructor are the same as those for TDecoratedMDIFrame. This makes the conversion simple: all you need to do is change the name of the class when you create the frame window object.

Setting the OLE MDI frame's application connector

In order for the OLE MDI frame to be able to handle embedded OLE objects, it needs to know how to communicate with the ObjectComponents mechanism. This is accessed through the TOcApp object associated with the application object. The frame window must be explicitly associated with this object.

To do this, TOleMDIFrame provides a function (inherited from TOleFrame) called SetOcApp. SetOcApp returns void and takes a pointer to a TOcApp object. For the parameter to SetOcApp, you can just pass OcApp.

Adding a tool bar identifier

OLE servers often provide their own tool bar to replace yours while the server is functioning. The mechanics of this are handled by ObjectComponents, but to put the server's tool bar in place of yours, ObjectWindows must be able to find your tool bar.

ObjectWindows tries to locate your tool bar by searching through the list of child windows owned by the OLE MDI frame window and checking each window's identifier. Up until now, your tool bar hasn't actually had an identifier, which would cause ObjectWindows to not find the tool bar. In order for ObjectWindows to identify the container's tool bar, the container must use the IDW_TOOLBAR as its window ID (the Id member of the tool bar's Attr member object).

Your InitMainWindow function should now look something like this:

void 
TDrawApp::InitMainWindow()
{
  // Construct OLE-enabled MDI frame
  TOleMDIFrame* frame;
  frame = new TOleMDIFrame(GetName(), 0, *(Client = new TMDIClient), true);

  // Set the frame's OcApp to OcApp
  frame->SetOcApp(OcApp);

  // Construct a status bar
  TStatusBar* sb = new TStatusBar(frame, TGadget::Recessed);

  // Construct a control bar
  TControlBar* cb = new TControlBar(frame);
  cb->Insert(*new TButtonGadget(CM_FILENEW, CM_FILENEW,
          TButtonGadget::Command));
  cb->Insert(*new TButtonGadget(CM_FILEOPEN, CM_FILEOPEN,
          TButtonGadget::Command));
  cb->Insert(*new TButtonGadget(CM_FILESAVE, CM_FILESAVE,
          TButtonGadget::Command));
  cb->Insert(*new TButtonGadget(CM_FILESAVEAS, CM_FILESAVEAS,
          TButtonGadget::Command));
  cb->Insert(*new TSeparatorGadget);
  cb->Insert(*new TButtonGadget(CM_PENSIZE, CM_PENSIZE,
          TButtonGadget::Command));
  cb->Insert(*new TButtonGadget(CM_PENCOLOR, CM_PENCOLOR,
          TButtonGadget::Command));
  cb->Insert(*new TSeparatorGadget);
  cb->Insert(*new TButtonGadget(CM_ABOUT, CM_ABOUT,
          TButtonGadget::Command));
  cb->SetHintMode(TGadgetWindow::EnterHints);
  
  // Set the control bar's id.  Required for OLE tool bar merging
  cb->Attr.Id = IDW_TOOLBAR;

  // Insert the status bar and control bar into the frame
  frame->Insert(*sb, TDecoratedFrame::Bottom);
  frame->Insert(*cb, TDecoratedFrame::Top);

  // Set the main window and its menu
  SetMainWindow(frame);
  GetMainWindow()->SetMenuDescr(TMenuDescr("MDI_COMMANDS",1,1,0,0,1,1));

  // Install the document manager
  SetDocManager(new TDocManager(dmMDI | dmMenu));
}
       

Changes to the Doc/View classes

There are a number of changes you need to make to your TDrawDocument and TDrawView classes to support OLE containers. For your document class, you need to

  1. Add more information to the registration table for creating TDrawDocument document templates.
  2. Change the base class to TOleDocument.
  3. Modify the constructor .
  4. Add two new functions, GetLine and IsOpen.
  5. Modify the file access functions to store and load OLE objects.

For your view class, you need to

  1. Change the base class to TOleView.
  2. Remove the DragDC member.
  3. Modify the constructor and destructor to remove statements with DragDC.
  4. Modify the Paint function to call the base class Paint function.
  5. Modify the mouse action commands to check when the user selects an embedded OLE object.

Changing document registration

The registration table you created earlier contains information necessary for the creation of a basic document template. This functions fine when the only thing using the document template is the document manager. But the way that ObjectComponents uses the Doc/View classes requires some more information:

An identifier string. For this identifier, you want to use the REGDATA macro with the progid key value. This is a three part identifier. Each part of the identifier should be a text description, with each part separated by a period. There should be no whitespace or non-alphabetic character in this string other than the period delimiters.
The first part of the identifier should be descriptive of the overall application. For example, in the sample code, the first part of the identifier is DrawPad.
The second part should describe the part of the application contained in the module associated with the registration table. For the application registration table in the sample code, this part of the identifier is Application. For the document registration table, it's Document.
The third part should be a number. In the sample code, this number is 1. If your application supports multiple document types, use a different number for each document type.

Note that this isn't meant for the users of your application to see. It's entered in the system's OLE registration database and should be unique for every application.

A description of the document class. For this, you want to use the REGDATA macro with the description key value. This value is intended for the users of your application to see; this is the string that appears in the OLE registration database when someone is inserting an object into their container.
A list of the types of data the container application can pass on to the Clipboard. To register Clipboard formats, use the REGFORMAT macro. This macro takes five parameters:
Format priority. The lower the value, the higher the priority. 0 indicates that the format is the highest priority format. When the user tries to paste data into your application, the Clipboard tries to paste it in as the highest priority format that is consistent with the format of the data in the Clipboard.
Data format.
Presentation aspect used to display data (for example, a bitmap could be displayed as a bitmap, as formatted information about the bitmap such as its dimensions and number of colors, as a hex dump, and so on) or an object might be presented in iconic form.
How the data is transferred when not otherwise specified (for example, when data is transferred by a drag-and-drop transaction, the server might prefer to pass the data to the container by means of a temporary file).
Whether the document can provide as well as receive this type of data.

Every OLE application must specify that it can handle the ocrEmbedSource and ocrMetafilePict formats. By default, ObjectComponents always registers ocrLinkSource. You'll usually want to register ocrLinkSource yourself, though, so that you can set its priority lower. In addition, you can register ocrBitmap and ocrDIB. Note these formats indicate the type of data your application can pass to the Clipboard, not the type of data your application can accept. Pasting this data to the Clipboard is handled by ObjectComponents. The exact meaning of each of these values is described in the ObjectWindows Reference.

The following registration table shows how your registration table should look. The values for the REGFORMAT macro are described in the ObjectWindows Reference.

BEGIN_REGISTRATION(DocReg)
  REGDATA(progid, "DrawContainer")
  REGDATA(description,"OWL Drawing Pad 2.0 Document")
  REGDATA(extension, "PTS")
  REGDATA(docfilter, "*.pts")
  REGDOCFLAGS(dtAutoOpen | dtAutoDelete | dtUpdateDir | dtCreatePrompt |
    dtRegisterExt)
  REGFORMAT(0, ocrEmbedSource,  ocrContent,  ocrIStorage,  ocrGet)
  REGFORMAT(1, ocrMetafilePict, ocrContent,  ocrMfPict,    ocrGet)
  REGFORMAT(2, ocrBitmap, ocrContent,  ocrGDI|ocrStaticMed, ocrGet)
  REGFORMAT(3, ocrDib, ocrContent,  ocrHGlobal|ocrStaticMed, ocrGet)
  REGFORMAT(4, ocrLinkSource, ocrContent,  ocrIStream,    ocrGet)
END_REGISTRATION
       

Changing TDrawDocument to handle embedded OLE objects

You need to make a few changes to TDrawDocument to support embedded OLE objects. These changes mainly affect reading and writing documents that contain OLE objects. The changes are fairly simple; most of the capabilities required to handle embedded OLE objects are handled in the new base class TOleDocument. Here's a summary of the changes required.

  1. Change TDrawDocument's base class to TOleDocument.
  2. Modify TDrawDocument's constructor to improve performance.
  3. Remove the IsOpen function.
  4. Add some function calls to the Commit and Open functions; these function calls read and write OLE objects embedded in the document.

Changing TDrawDocument's base class to TOleDocument

To get your document class ready to work in an ObjectComponents environment, you need to change the base class from TFileDocument to TOleDocument. TOleDocument is based on the TStorageDocument class, which is in turn based on TDocument. TStorageDocument provides the ability to manage and store compound documents. Compound documents provide a way to combine multiple objects into a single disk file, without having to worry about where each of the individual objects are stored or how they are written out or read in. On top of TStorageDocument's capabilities, TOleDocument adds the ability to interface with an OLE object, control and display the OLE object, and read and write the object to and from storage.

To change your base class from TFileDocument to TOleDocument, you first need to change all references from TFileDocument to TOleDocument. This is fairly simple, since all that needs to change is the actual name; all the function signatures, including the base class constructor's, are the same.

Constructing and destroying TDrawDocument

The only change you need to make to the constructor for TDrawDocument (other than changing the base class to TOleDocument) basically serves to enhance the performance of the Drawing Pad application, and is not connected to its OLE functionality.

  1. Remove the Lines member from the constructor's initialization list.
  2. Initialize Lines in the constructor body, with an initial size of 100, lower boundary of 0, and a delta of 5.

Your constructor should now look something like this:

TDrawDocument(TDocument* parent) :
TOleDocument(parent), UndoLine(0), UndoState(UndoNone)
{
  Lines = new TLines(100, 0, 5);
}
       

You don't need to make any changes to the destructor.

Removing the IsOpen function

You need to remove the IsOpen function from the TDrawDocument class. This function is made obsolete by the change you made to the constructor, since the function tests the validity of the Lines member, and Lines now always points to a valid object.

TStorageDocument provides an IsOpen function that tests whether the document object has a valid IStorage member. IStorage is an OLE 2 construct that manages compound file storage and retrieval. A compound file is a basically a file that contains references to objects in a number of other locations. To the user, the compound file appears to be a single document. In reality, the different elements of the file are stored in various areas determined by the system and managed through the IStorage object. By constructing an OLE container, you're venturing into supporting compound documents in your application. However, since the support is provided through the OLE-enabled ObjectComponents classes, you don't need to worry about managing the compound documents yourself.

Along with removing the IsOpen function declaration and definition from TDrawDocument, you need to eliminate any references to the IsOpen function. This function is called only once, in the GetLine function. In this case, you can simply remove the entire statement that contains the call to IsOpen. This statement checks the validity of the document's TLine object referenced by the Lines data member, but the change you made to the constructor, which ensures that each document object is always associated with a valid TLine object, makes the check unnecessary. Your GetLine function should now look something like this:

TLine*
TDrawDocument::GetLine(uint index)
{
  return index < Lines->GetItemsInContainer() ? &(*Lines)[index] : 0;
}
       

The TDrawDocument class declaration should now look something like this:

class _DOCVIEWCLASS TDrawDocument : public TOleDocument
{
  public:
    enum {
      PrevProperty = TFileDocument::NextProperty-1,
      LineCount,
      Description,
      NextProperty,
    };
    enum {
      UndoNone,
      UndoDelete,
      UndoAppend,
      UndoModify
    };
    TDrawDocument(TDocument* parent = 0);
   ~TDrawDocument() { delete Lines; delete UndoLine; }

    // implement virtual methods of TDocument
    bool Open(int mode, const char far* path=0);
    bool Close();
    bool Commit(bool force = false);
    bool Revert(bool clear = false);

    int FindProperty(const char far* name);  // return index
    int PropertyFlags(int index);
    const char far* PropertyName(int index);
    int PropertyCount() {return NextProperty - 1;}
    int GetProperty(int index, void far* dest, int textlen=0);

    // data access functions
    TLine* GetLine(uint index);
    int AddLine(TLine& line);
    void DeleteLine(uint index);
    void ModifyLine(TLine& line, uint index);
    void Clear();
    void Undo();

  protected:
    TLines* Lines;
    TLine* UndoLine;
    int UndoState;
    int UndoIndex;
    string FileInfo;
};
       

Reading and writing embedded OLE objects

The last change you need to make to your document class provides the ability to save and load OLE objects embedded in a document. This is contained in two functions provided by TOleDocument. The functions are named Open and Commit. As you can probably guess, Open reads in the OLE objects contained in the document and Commit writes them out, that is, it commits the changes to disk.

To add these changes to your document class:

  1. Add the call to TOleDocument::Commit in the Commit function right before you create the TOutStream object by calling the OutStream function.
  2. Add the call to the TOleDocument::Open function in TDrawDocument's Open function right before you create the TInStream object by calling the InStream function.
  3. At the end of the procedure, call TOleDocument::CommitTransactedStorage to make your changes permanent. By default, TOleDocument uses the transacted mode (ofTransacted) to buffer changes in temporary storages until they are committed permanently.

That's all you need to do read and store OLE objects in your document! Your Commit function should now look something like this:

bool TDrawDocument::Commit(bool force)
{
  TOleDocument::Commit(force);

  TOutStream* os = OutStream(ofWrite);

  if (!os)
    return false;

  // Write the number of lines in the figure
  *os << Lines->GetItemsInContainer();

  // Append a description using a resource string
  *os << ' ' << FileInfo << '\n';

  // Get an iterator for the array of lines
  TLinesIterator i(*Lines);

  // While the iterator is valid (i.e. we haven't run out of lines)
  while (i)
    // Copy the current line from the iterator and increment the array.
    *os << i++;

  delete os;

  // Commit the storage if it was opened in transacted mode
  TOleDocument::CommitTransactedStorage();
  SetDirty(false);
  return true;
}
       

Your Read function should look something like this:

bool TDrawDocument::Open(int mode, const char far* path)
{
  char fileinfo[100];
  TOleDocument::Open(mode, path);
  if (GetDocPath()) {
    TInStream* is = (TInStream*)InStream(ofRead);

    if (!is)
      return false;

    unsigned numLines;
    *is >> numLines;
    is->getline(fileinfo, sizeof(fileinfo));

    while (numLines--) {
      TLine line;      *is >> line;
      Lines->Add(line);
    }

    delete is;

     FileInfo = fileinfo;
  } else {
    FileInfo = string(*::Module,IDS_FILEINFO);
  }
  SetDirty(false);
  UndoState = UndoNone;
  return true;
}
       

Changing TDrawView to handle embedded OLE objects

You need to make a few changes to TDrawView to support embedded OLE objects. These changes mainly affect handling OLE objects through the mouse, including dragging the objects and activating the object's server. The changes are fairly simple; most of the capabilities required to handle embedded OLE objects are handled in the new base class TOleView. Here's a summary of the changes required.

  1. Change the base class of TDrawView to TOleView.
  2. Remove the DragDC member; TOleView supplies a TDC pointer called DragDC.
  3. Modify the constructor and destructor to remove initialization and deletion of DragDC.
  4. Remove the EvRButtonDown function.
  5. Modify the Paint function to call TOleView::Paint to force embedded objects to paint themselves.
  6. Modify the mouse action functions to deal with user interaction with embedded OLE objects.
  7. Modify the class declaration to reflect changes in the view class.

Modifying the TDrawView declaration

Here's the class declaration for TDrawView. The modifications to it will be explained in the following sections.

class _DOCVIEWCLASS TDrawView : public TOleView
{
  public:
    TDrawView(TDrawDocument& doc, TWindow* parent = 0);
   ~TDrawView() {delete Line;}
    static const char far* StaticName() {return "Draw View";}
    const char far* GetViewName() {return StaticName();}

  protected:
    TDrawDocument* DrawDoc;  // same as Doc member, but cast to derived 
      class TPen* Pen;
    TLine* Line;             // To hold a single line sent or received 
      from document

    // Message response functions
    void EvLButtonDown(uint, TPoint&);
    void EvMouseMove(uint, TPoint&);
    void EvLButtonUp(uint, TPoint&);
    void Paint(TDC&, bool, TRect&);
    void CmPenSize();
    void CmPenColor();
    void CmClear();
    void CmUndo();

    // Document notifications
    bool VnCommit(bool force);
    bool VnRevert(bool clear);
    bool VnAppend(uint index);
    bool VnDelete(uint index);
    bool VnModify(uint index);

  DECLARE_RESPONSE_TABLE(TDrawView);
};
       

Here's the response table for TDrawView.

DEFINE_RESPONSE_TABLE1(TDrawView, TOleView)
  EV_WM_LBUTTONDOWN,
  EV_WM_MOUSEMOVE,
  EV_WM_LBUTTONUP,
  EV_COMMAND(CM_PENSIZE, CmPenSize),
  EV_COMMAND(CM_PENCOLOR, CmPenColor),
  EV_COMMAND(CM_EDITCLEAR, CmClear),
  EV_COMMAND(CM_EDITUNDO, CmUndo),
  EV_VN_COMMIT,
  EV_VN_REVERT,
  EV_VN_DRAWAPPEND,
  EV_VN_DRAWDELETE,
  EV_VN_DRAWMODIFY,
END_RESPONSE_TABLE;
       

Changing TDrawView's base class to TOleView

To get your view class ready to work in an ObjectComponents environment, you need to change the base class from TWindowView to TOleView. TOleView is itself based on the TWindowView class. TOleView provides the ability required to manipulate and move OLE objects and activate an object's server.

To change your base class from TWindowView to TOleView, you first need to change all references from TWindowView to TOleView. This is fairly simple, since all that needs to change is the actual name; all the function signatures, including the base class constructor's, are the same.

Removing DragDC

This change is relatively straightforward. TOleView provides a pointer to a TDC called DragDC, obviating the need for this member in the TDrawView class. You'll also need to remove a lot of the actions you previously took with DragDC. Many of these, such as creating a device context object when the left mouse button is clicked, is taken care of by TOleView. These changes are discussed in the next section.

Constructing and destroying TDrawView

The only change you need to make to the TDrawView constructor is to remove the initialization of the DragDC member. Although DragDC was removed from the TDrawView class declaration, it is still a class member; it is provided by TOleView. But TOleView also handles initializing DragDC, since TOleView needs to check for OLE actions that the user might have taken.

Note that the TOleView constructor signature is the same as that of TWindowView, meaning all you have to do is change the name and nothing else. Here's how your TDrawView constructor should look.

TDrawView::TDrawView(TDrawDocument& doc, TWindow* parent) :
  TOleView(doc, parent), DrawDoc(&doc)
{
  Line = new TLine(TColor::Black, 1);
  SetViewMenu(new TMenuDescr(IDM_DRAWVIEW));
}
       

By the same token, the only modification needed to the destructor for TDrawView is to remove the statement deleting DragDC.

~TDrawView()
{
  delete Line;
}
       

Modifying the Paint function

You need to modify the Paint function to call TOleView::Paint. TOleView::Paint finds each linked or embedded object in the document (if there are any) and instructs each one to paint itself. Once this has been done, you can go on and paint the screen just as you did in Step 13. Your new Paint function should look something like this:

void
TDrawView::Paint(TDC& dc, bool erase, TRect&rect)
{
  TOleView::Paint(dc, erase, rect);

  // Iterates through the array of line objects.
  int j = 0;
  TLine* line;
  while ((line = const_cast<TLine *>(DrawDoc->GetLine(j++))) != 0)
    line->Draw(dc);
}
       

Selecting OLE objects

The next changes you need to make involve the functions dealing with mouse actions, namely EvLButtonDown, EvMouseMove, and EvLButtonUp. The changes you need to make in these functions involve checking whether the user's mouse actions involve an OLE object and what drawing mode is set. This is mostly handled by TOleView; for the most part, all you have to do is call the base class versions of the functions. The changes for each function are discussed in the following sections.

Modifying EvLButtonDown

You don't need to change the basic workings of the EvLButtonDown function as it exists in Step 13. What you do need to do is add a couple of extra steps to take into account OLE objects that might be in the view.

  1. The first thing you need to do is let the TOleView base class determine whether the user selected an OLE object. Do this by calling TOleView::EvLButtonDown. This function deactivates any currently selected OLE object, creates a new TOleDC object (TOleDC is derived from the TClientDC class you used in previous steps, adding the ability to handle embedded OLE objects), and checks to see if another OLE object was selected.
  2. To check whether the user wants to and is able to draw in the view, you need to check two things: whether a valid device context was created in the call to TOleView::EvLButtonDown and whether an OLE object was selected. You can check the validity of the device context simply by testing DragDC. You can find out whether an OLE object was selected by calling the SelectEmbedded function. SelectEmbedded returns true if an object was selected and false otherwise. If both these conditions weren't met, EvLButtonDown can just return.
  3. Assuming there is a valid device context and no OLE object was selected, you can go ahead and begin the drawing operation the same as you did in Step 13. The only change you need to make is removing the initialization of DragDC, since it's already set to a valid device context object.

Your EvLButtonDown function should look something like this:

void TDrawView::EvLButtonDown(uint modKeys, TPoint& point)
{
  TOleView::EvLButtonDown(modKeys, point);

  if (DragDC && !SelectEmbedded()) {
    SetCapture();
    Pen = new TPen(Line->QueryColor(), Line->QueryPenSize());
    DragDC->SelectObject(*Pen);
    DragDC->MoveTo(point);
    Line->Add(point);
  }
}
       
Modifying EvMouseMove

The changes needed to EvMouseMove are similar to those required by EvLButtonDown.

  1. Call the base class version of EvMouseMove.
  2. Check whether the device context is valid and whether an OLE object was selected.
  3. Continue the drawing operation the same way you did in Step 13.

Your EvMouseMove function should look something like this:

void TDrawView::EvMouseMove(uint modKeys, TPoint& point)
{
  TOleView::EvMouseMove(modKeys, point);

  if (DragDC && !SelectEmbedded()) {
    DragDC->LineTo(point);
    Line->Add(point);
  }
}
       
Modifying EvLButtonUp

With EvLButtonUp, you need to do the same things as you did in EvLButtonDown and EvMouseMove, but with a bit of a twist. In this case, call the base class version of the function last instead of first. TOleView::EvLButtonUp performs a number of cleanup operations, including deleting the device context object pointed to by DragDC.

  1. Check whether the device context is valid and whether an OLE object was selected.
  2. Perform the same operations as EvLButtonUp in Step 13, except for deleting and zeroing out DragDC.
  3. Call TOleView::EvLButtonUp.

Your EvLButtonUp function should look something like this:

void TDrawView::EvLButtonUp(uint modKeys, TPoint& point)
{
  if (DragDC && !SelectEmbedded()) {
    ReleaseCapture();
    if (Line->GetItemsInContainer() > 1) {
      DrawDoc->AddLine(*Line);
    }
    Line->Flush();
    delete Pen;
  }

  TOleView::EvLButtonUp(modKeys, point);
}
       

Where to find more information

Here's a guide to where you can find more information on the topics introduced in this step:

OLE and ObjectComponents containers are discussed in "Turning an application into an OLE and OCX container" in the ObjectWindows Programmer's Guide.
The ObjectComponents classes in general are discussed in more detail in Part 5 of the Borland C++ Programmer's Guide.
 


Copyright © 1998-2001 Yura Bidus. All rights reserved.