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.
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.
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:
The following steps are required to convert your Doc/View ObjectWindows application to an OLE container application:
To add new headers to your files so you can use ObjectComponents classes:
#include <owl/applicat.h> #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"
#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"
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.
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_REGISTRATIONNote: 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); }
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.
To create a registrar object:
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.
::Registrar = new TOcRegistrar(AppReg, TOleDocViewFactory<TDrawApp>(), TApplication::GetCmdLine());
int OwlMain(int /*argc*/, char* /*argv*/ []) { ::Registrar = new TOcRegistrar(AppReg, TOleDocViewFactory<TDrawApp>(), TApplication::GetCmdLine()); return ::Registrar->Run(); }
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);
[]
) 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"]
.
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); };
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.
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.
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)); }
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
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.
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.
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; };
To add these changes to your document class:
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; }
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;
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.
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; }
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); }
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); } }
void TDrawView::EvMouseMove(uint modKeys, TPoint& point) { TOleView::EvMouseMove(modKeys, point); if (DragDC && !SelectEmbedded()) { DragDC->LineTo(point); Line->Add(point); } }
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); }