Document/View and you(Cover OWL 2.5)Table of contents:Introduction IntroductionHere are the basics to Doc/View, as far as I can figure them. I suspect they are pretty hard to figure out, as none of the aftermarket BC4 books explain them, except Swans. They seem to be discussed only in the OWL manuals, in the on-line help, and with comments in the header files and source code itself. The source code seems to be the place to look to figure out Doc/View, as the other material is a little sparse at best. To explain where a bit of information comes from, I have used the following convention. "1)" means copied directly from the help text, "2)" means from the source code, and "3)" means my own explanation. This document assumes that you are an OWL user and have compiled and run the DOCVIEW example on the BC4 disks and have worked through the tutorial. It was at that point that I started pulling my hair out trying to figure out TDocument and TDocument and it is there that I begin:
Doc/View class referenceDocument and View classes
Introduction to Doc/ViewAdopting the Document/View model makes it easier to develop large, flexible, document-centric GUI applications. Doc/View users get a big win developing programs which either have multiple windows open on the same set of data or the ability to switch the format of the screen dramatically over the same set of data. If you have never seen what Doc/View does, compile and run the \bc45\examples\owl\owlapi\docview.ide example program. Open up a "Draw Line File", then select Window|Add View and add two more line draw views and also add a drawlist view. Tile the windows, and start scribbling in one of the drawing views. By using the Edit menu items in the DrawList view, each lines properties can be edited individually. Being able to work with data more than one way can be very satisfying. Data can be viewed as both a graph or a spreadsheet, for example. The "data" would be considered the Document, and the graph and the spreadsheet both would be kinds of views. This terminology is unfortunately confusing. A word processing application wants to give the user the feeling of working directly with the document but it can feel at first as if instead the user is removed from the document and working with a view. In a well designed program, though, the user feels as if he is directly manipulating the document in each view, which is of course what is actually happening. The DOCVIEW.IDE example is an outgrowth of the OWL tutorial. There is a lot of really good stuff in the tutorial, and if you are new to Doc/View I recommend working on the step where Doc/View is added to the tutorial. How does it work?In the scribble application, pressing down on the mouse button starts drawing, and moving the mouse with the button down adds points to the line. When the mouse button is released, this function is called: The only thing new about this function is the call to TDrawDocument::AddLine(). This function looks like this: First, the new line is added to the documents line data structure, as the lines are kept in the document as opposed to in the window. The document is marked as "dirty," meaning it is different than it was when it was initially loaded/created. Finally, NotifyViews() is called, sending the custom vnDrawAppend message to each view of the document. To define such a custom message, three lines are needed: const int vnDrawAppend = vnCustomBase+0; NOTIFY_SIG(vnDrawAppend, unsigned int) #define EV_VN_DRAWAPPEND VN_DEFINE(vnDrawAppend, VnAppend, int) And a line of code needs to be added to the DEFINE_RESPONSE_TABLEx for the view class. The ClassExpert doesnt know about custom messages, so you need to add it by hand, after the "//{{TMyViewRSP_TBL_END}}" comment so you dont interfere with the ClassExpert, like this: //{{TDrawViewRSP_TBL_END}} EV_VN_DRAWAPPEND, END_RESPONSE_TABLE; Finally, you write the function that a view uses to handle getting this update from its document: bool
TDrawView::VnAppend(unsigned int index) The views Paint() needs to be modified to get data from the document as well: void
TDrawView::Paint(TDC& dc, BOOL, TRect&) One thing that is interesting about this Paint() method is that all of the information is held in the document. The view doesnt send a message to the document to have it call line->Draw() on the view, because the document is supposed to be 100% isolated from anything do to with the view, and it should be possible to add new views without changing the document at all. The view, on the other hand, is required to know about the document. It needs to know how to handle every message the document is going to send. It is also allowed, for efficiency reasons, to keep information about the document cached inside itself. For example, lets see what happens when a line is deleted. TDrawView doesnt cache anything about the document, and when it gets a notification that a line has deleted it simply invalidates itself and lets its Paint() method redraw all of the lines: bool
TDrawView::VnDelete(unsigned int /*index*/) TDrawListView, the listbox of line information takes a different approach. Because it is a listbox all the text about the lines is saved inside a Windows data structure. When it sees a line has been deleted, it just deletes that particular line. The TListBox::DeleteString() can set up and do the redraw without a Paint() member at all and no calls to the document: bool
TDrawListView::VnDelete(unsigned int index) When I first began developing with Doc/View, I had a lot of difficulty figuring out how to structure my program, and understanding the often inaccurate and incomplete help files. To aid the Doc/View programmer, I have extracted a good deal of the Doc/View help from the OWL help file and added my comments, and I have written a FAQ on how to do certain things with Doc/View. 1) TDocument is an abstract base class that serves as an interface between the document, its views, and the document manager (the TDocManager class). TDocument creates, destroys, and sends messages about the view. For example, if the user changes a document, TDocument tells the view that the document has been updated. In order to send messages to its associated views, the document maintains a list of all the views existing for that document and communicates with the views using ObjectWindows event-handling mechanism. Rather than using the function SendMessage, the document accesses the view's event table. The views can update the document's data by calling the member functions of the particular document. Views can also request streams, which are constructed by the document. Both documents and views have lists of properties for their applications to use. When documents and views are created or destroyed, messages are sent to the application, which can then query the properties to determine how to process the document or view. It is the document manager's responsibility to determine if a particular view is appropriate for the given document. Because the property attribute functions are virtual, a derived class (which is called first) might override the properties defined in a base class. Each derived class must implement its own property attribute types of either string or binary data. If the derived class duplicates the property names of the parent class, it should provide the same behavior and data type as the parent. Although documents are usually associated with files, they do not necessarily have to be files; they can also consist of database tables, mail systems, fax or modem transmissions, disk directories, and so on.
TDocument Members (public): virtual ~TDocument() DESTRUCTOR virtual TInStream* InStream(int mode, const char far* strmId=0) virtual TOutStream* OutStream(int mode, const char far* strmId=0) virtual bool Open(int mode, const char far* path=0) virtual bool Close(); virtual bool Commit(bool force=FALSE); virtual bool Revert(bool clear=FALSE); virtual TDocument& RootDocument(); TDocManager& GetDocManager() void SetDocManager(TDocManager& dm); TDocument* GetParentDoc() TDocTemplate* GetTemplate() bool SetTemplate(TDocTemplate* tpl); virtual bool SetDocPath(const char far* path) 3) Filename of the document. This can be 0 in the case of File|New. virtual bool IsDirty(); void SetDirty(bool dirty = TRUE) virtual bool IsOpen() virtual bool CanClose(); bool HasFocus(HWND hwnd); bool NotifyViews(int event, long item=0, TView* exclude=0); TView* QueryViews(int event, long item=0, TView* exclude=0); virtual UINT PostError(UINT sid, UINT choice = MB_OK) The following functions deal with properties. virtual int PropertyFlags(int index); virtual const char* PropertyName(int index); virtual int GetProperty(int index, void far* dest, int textlen=0); virtual bool SetProperty(int index, const void far* src); TStream* NextStream(const TStream* strm); TView* NextView(const TView* view); int GetOpenMode(); void SetOpenMode(int mode); TView* InitView(TView* view); // called from template InitView bool IsEmbedded() {return Embedded;} void SetEmbedded(bool embed) {Embedded = embed;} virtual bool InitDoc() {return true;} TView* GetViewList() {return ViewList;} void far* Tag; List ChildDoc; TDocument Members (protected): bool Embedded; virtual void static int UntitledIndex; AttachStream(TStream& strm); virtual void DetachStream(TStream& strm); 1) Derived virtually from both TEventHandler and TStreamableBase, TView is the interface presented to a document so it can access its client views. Views then call the document functions to request input and output streams. Views own the streams and are responsible for attaching and deleting them. Instead of creating an instance of TView, you create a derived class that has access to TViews virtual functions. The derived class must have a way of knowing the associated window (provided by GetWindow), of describing the view (provided by GetViewName), and of displaying a range of data (GetSelection). The view must also be able to restore the view later (SetSelection) and to display the document title in its window (SetDocTitle). TView uses several event handler functions to query views, commit, and close views. For example, to query views to find the one with the focus, you would use the vnIsWindow function, passing a handle to the current window. View classes can take various forms. For example, a view class can be a window (through inheritance), can contain a window (an embedded object), can reference a window, or can be contained within a window object. A view class might not even have a window, as in the case of a voice mail or a format converter. Some remote views (for example, those displayed by OLE 2.0 or DDE servers) might not have local windows. Public members: virtual ~TView() DESTRUCTOR TDocument& GetDocument() {return *Doc;} unsigned GetViewId() {return ViewId;} TMenuDescr* GetViewMenu() {return ViewMenu;} void SetViewMenu(TMenuDescr* menu); bool IsOK() static unsigned GetNextViewId() TView* GetNextView() {return NextView;} // static const char far* StaticName() {return "name of view";} virtual const char far* GetViewName()=0; virtual TWindow* GetWindow() virtual bool SetDocTitle(const char far* docname, int index) virtual int PropertyCount() {return NextProperty - 1;} virtual int FindProperty(const char far* name);// return property index virtual int PropertyFlags(int index); // pfXxxxx bit array virtual const char* PropertyName(int index); // locale invariant name virtual int GetProperty(int index, void far* dest, int textlen=0); virtual bool SetProperty(int index, const void far* src) {return FALSE;} Protected members: void NotOK()
3) This class simply mixes TWindow and TView. It is a good base class to use if you aren't making an edit window (TEditView) or a list box (TListView) and sometimes is a good base to use even if you are. It defines the necessary stuff to make a view act like a window, and has a single response function, VnIsWindow()
3) This class does everything that TWindowView does, and LOTS more things. We will look only at the extra functionality. Would you ever use TListView? You really don't want to use it unless you are doing something similar to what it does, scrolling over text. For example, the TDrawListView in the drawing example doesn't use TListView, it goes right to TListBox and TView directly, because TListView wants to do too much. Public members: bool Create() bool DirtyFlag; protected members: int MaxWidth; void SetExtent(const char far* str) bool LoadData(int top, int sel) The following members respond to choices on the Edit menu that was loaded in the constructor. void CmEditCut(); void CmEditCopy(); void CmEditPaste(); void CmEditDelete(); void CmEditClear(); void CmEditAdd(); void CmEditItem(); UINT EvGetDlgCode(MSG far*) bool VnDocClosed(int omode) bool VnCommit(bool force) bool VnRevert(bool clear) bool VnIsWindow(HWND hWnd) {return HWindow == hWnd;} bool VnIsDirty() void CmSelChange() 3) This class does basically the same thing that TListView does, except it does it with TEditSearches instead of TListBoxes, so you get nice Find/Next dialogs and stuff. Use this only if you are writing an editor or something...
1) Derived from TWindowView [not! from TOleWindow] and TView, TOleView supports the View half of the Doc/View pair and creates a window with a view that can display an associated document. Documents use views to display themselves to a user. Regardless of whether a view belongs to a server or a container, TOleView sets up a corresponding TOcDocument object (an entire compound document). In the case of an OLE-enabled container application, view refers to the window where the container application draws the compound document, which may consist of one or more linked and embedded objects. To display these objects in different formats, a container can be associated with more than one view. Similarly, to display the data properly, each embedded object can also have its own view. Each container view creates a corresponding ObjectComponents TOcView object. If the view belongs to an OLE-enabled server application, TOleView creates a remote view on the server's document (a TOcRemView object). TOleView takes care of transmitting messages from the server to the container, specifically in the case of merging menus and redrawing embedded objects, and supports merging the server's and the container's pop-up menu items to form a composite menu. Because it knows the dimensions of the server's view, TOleView is responsible for telling the container how to redraw the embedded object. Similarly to TView, TOleView supports the creation of views and provides several event handling functions that allow the view to query, commit, and close views. TOleView also manages the writing to storage of documents that belong to a container or a server. 3) I havent used this class enough to be able to say much about it, except that OLE apps dont need TOleView, they only need TOleWindow. TOleView only adds the Doc/View view stuff to TOleWindow, so if you are having trouble with the user interface and such you need to look at TOleWindow. For example, if you look at the STEP14 step in the tutorial, you see a simple Doc/View OLE container. When you Edit|Insert Object, your new object shows up about 1/2" from the upper left hand corner. The code that puts it exactly there, drags it around etc. is entirely in TOleWindow. Your code just does the same old line stuff you have been doing since the early examples. TOleView apps can also use the header file OLEVIEW.RH and pick up the following constants: #define CM_EDITPASTESPECIAL 24311 #define CM_EDITPASTELINK 24312 #define CM_EDITINSERTOBJECT 24313 #define CM_EDITLINKS 24314 #define CM_EDITOBJECT 24370 #define CM_EDITFIRSTVERB 24371 // 20 verbs at most #define CM_EDITLASTVERB 24390 #define CM_EDITCONVERT 24391 #define CM_EDITSHOWOBJECTS 24392 // // Menu ID // #define IDM_OLEPOPUP 32405 #define IDM_OLEVIEW 32406 #define IDM_OLEVIEWEMBED 32407 // // String ID // #define IDS_EDITOBJECT 32600 #define IDS_EDITCONVERT 32601 #define IDS_CLOSESERVER 32602 #define IDS_EXITSERVER 32603 // // Accelerator IDs // #define IDA_OLEVIEW 32551 There is also an OLEVIEW.RC which defines these items. Contstructor/Destructor Public Member Functions virtual TWindow* GetWindow(); virtual bool SetDocTitle(const char far* docname, int index); static const char far* StaticName(); virtual bool OleShutDown(); Protected Member Functions virtual void CleanupWindow(); bool OtherViewExists(); bool EvOcViewAttachWindow(bool attach); bool EvOcViewBreakLink(TOcLinkView& view); bool EvOcViewInsMenus(TOcMenuDescr far& sharedMenu); bool EvOcViewClose(); bool EvOcViewLoadPart(TOcSaveLoad far& ocLoad); bool EvOcViewOpenDoc(const char far*); virtual bool EvOcViewPartInvalid(TOcPart far& changeInfo); bool EvOcViewSavePart(TOcSaveLoad far& ocSave); bool EvOcViewSetLink(TOcLinkView& view); bool VnInvalidate(TRect& rect); bool VnDocOpened(int omode); bool VnDocClosed(int omode); TMenuDescr* GetViewMenu(); bool VnInvalidateRect(LPARAM p); 1) Derived from TView, TOleLinkView provides embedding and linking support for a portion of a document instead of an entire document. With the added functionality of TOleLinkView, a container gains the ability to embed or link to a selection within the server document. The main purpose of a class derived from TOleLinkView is to attach a view to a portion of a document whenever a link is created to a selection within a server document. After this link is established, any changes made to the linked selection in the server document are sent to the container via the following sequence of steps:
Non-Doc/View servers need to maintain a list of the TOleLinkViews attached to the document so that change notifications can be sent to each one of the views. 3) This class does not appear in the help files ObjectWindows hierarchy chart. Public Constructor and Destructor Public Member Functions virtual const char far* GetViewName(); static const char far* StaticName(); virtual bool UpdateLinks(); virtual bool VnLinkMoniker(TString moniker); virtual bool VnLinkView(TOcLinkView& view); Protected Data Member 1) Derived from TDocument, TFileDocument opens and closes views and provides stream support for views. Streams are created on top of DOS files using Windows file services. TFileDocument has member functions that continue to process FileNew and FileOpen messages after a view is constructed. You can add support for specialized file types by deriving classes from TFileDocument. TFileDocument makes this process easy by hiding the actual process of storing file types. public members: ~TFileDocument() bool Open(int mode, const char far* path=0) bool Close(); TInStream* InStream(int mode, const char far* strmId=0) TOutStream* OutStream(int mode, const char far* strmId=0); bool Commit(bool force = FALSE) bool Revert(bool clear = FALSE); bool IsOpen() All of the property functions are redefined: Protected members void CloseThisFile(HFILE fhdl, int omode); 1) Derived from TDocument, TStorageDocument supplies functionality that supports OLE's compound file structure. A compound file structure is a file-management system that stores files in a hierarchical structure within a root file. This storage structure is analagous [sic] to the directory or folder and file scheme used on a disk, except that a directory is called a storage and a file is called a stream. In addition, TStorageDocument provides support for OLE's compound document mechanism. A compound document can store many different kinds of embedded objects, for example, spreadsheets as well as bitmaps. Basically, TStorageDocument supports having a document read and write its own storage. In order to provide this functionality, TStorageDocument overrides several virtual methods from TDocument and, following TDocument's strategy, also applies property lists both to documents and to their views. In this way, documents can use these attributes to read files in from storage and write files out to storage Messages are sent to the application, which queries the properties in order to determine how to process the document or view. Each derived class must implement its own property attribute types - either string or binary data. Type Definitions Public ~TStorageDocument(); DESTRUCTOR virtual bool ReleaseDoc(); implement virtual methods of TDocument bool Close(); bool Commit(bool force = false); bool CommitTransactedStorage(); bool Revert(bool clear = false); bool SetDocPath(const char far* path); bool IsOpen() {return (StorageI != 0);} int FindProperty(const char far* name); // return index int PropertyFlags(int index); const char* PropertyName(int index); int PropertyCount() {return NextProperty - 1;} int GetProperty(int index, void far* dest, int textlen=0); bool SetProperty(int index, const void far* src); additional methods for obtaining IStorage virtual bool SetHandle(int omode, HANDLE hGlobal, bool create = false, bool remember = false); virtual bool GetHandle(HGLOBAL* handle); IStorage* GetStorage() {return StorageI;} protected 1) Derived from TStorageDocument, TOleDocument implements the document half of the Doc/View pair. It manages the document's data while the corresponding TOleView object determines how the data is displayed on the screen. Basically, TOleDocument is a TStorageDocument with a knowledge of TOcDocument through its pointer to TOcDocument. TOleDocument is responsible for creating compound documents, closing documents, reading documents from storage, and writing documents to storage. In the case of a server, the document consists of a single object. In the case of a container, the document can consist of one or more embedded objects (also referred to as parts). To accomplish these tasks, TOleDocument talks to the underlying ObjectComponents classes through the use of functions such as GetOcApp, GetOcDoc, and SetOcDoc. 3) TOleDocument adds two private members to TStorageDocument, TOcDocument* OcDoc and bool Closing. This is a relatively small class that manages those variables and calls TStorageDocument.
Public Members Accessors Overridables virtual bool CommitSelection(TOleWindow& oleWin, void* userData) {return true;} virtual bool Open(int mode, const char far* path = 0); virtual bool Close(); bool Revert(bool clear); virtual void PreOpen(); virtual bool Write(); virtual bool Read(); virtual bool SetStorage(IStorage* stg, bool remember = true); virtual bool RestoreStorage(); virtual bool CanClose(); virtual bool ReleaseDoc(); virtual bool InitDoc(); virtual IStorage* GetNewStorage(); Utilities void OleViewClose(); 1) TDocManager creates a document manager object that manages the list of current documents and registered templates, handles standard file menu commands, and displays the user-interface for file and view selection boxes. To provide support for documents and views, an instance of TDocManager must be created by the application and attached to the application. The document manager normally handles events on behalf of the documents by using a response table to process the standard CM_FILENEW, CM_FILEOPEN, CM_FILECLOSE, CM_FILESAVE, CM_FILESAVEAS, and CM_VIEWCREATE File menu commands. In response to a CM_FILENEW or a CM_FILEOPEN command, the document manager creates the appropriate document based on the user's selections. In response to the other commands, the document manager determines which of the open documents contains the view associated with the window that has focus. The menu commands are first sent to the window that is in focus and then through the parent window chain to the main window and finally to the application, which forwards the commands to the document manager. When you create a TDocManager or a derived class, you must specify that it has either a multi-document (dmMDI) or single-document (dmSDI) interface. In addition, if you want the document manager to handle the standard file commands, you must OR these with dmMenu. You can also enable or disable the document manager menu options by passing dmSaveEnable or dmNoRevert in the constructor. If you want to enable the File|Save menu option if the document is unmodified, pass the dmSaveEnable flag in the constructor. To disable the "Revert to Saved" menu option, pass dmNoRevert in the constructor. When the application directly creates a new document and view, it can attach the view to its frame window, create MDI children, float the window, or create a splitter. However, when the document manager creates a new document and view from the File|Open or File|New menu selection, the application doesn't control the process. To give the application control, the document manager sends messages after the new document and view are successfully created. Then, the application can use the information contained in the template to determine how to install the new document or view object. 3) It shouldn't be too hard to subclass the manager to change some small thing about it, such as using BWCC message boxes for errors and such, but I wouldn't want to rewrite it... public members: virtual ~TDocManager(); virtual TDocument* CreateAnyDoc(const char far* path, long flags = 0); virtual TView* CreateAnyView(TDocument& doc,long flags = 0); TDocument* CreateDoc(TDocTemplate* tpl, const char far* path, TDocument* parent=0, long flags=0); TView* CreateView(TDocument& doc); TDocument* InitDoc(TDocument* doc, const char far* path, long flags); bool SelectSave(TDocument& doc); virtual TDocTemplate* SelectAnySave(TDocument& doc, bool samedoc = TRUE); virtual TDocTemplate* MatchTemplate(const char far* path); virtual TDocument* GetCurrentDoc(); // return doc with focus, else 0 virtual bool FlushDoc(TDocument& doc); // attempt to update changes TDocument* FindDocument(const char far* path); // 0 if not found TApplication* GetApplication() {return Application;} bool IsFlagSet(int flag) {return (Mode & flag) != 0;} void RefTemplate(TDocTemplate&); // add template ref void UnRefTemplate(TDocTemplate&); // drop template ref void DeleteTemplate(TDocTemplate&); // remove from list void AttachTemplate(TDocTemplate&); // append to list TDocTemplate* GetNextTemplate(TDocTemplate* tpl) {return tpl ? tpl->NextTemplate : TemplateList;} // primary event handlers, public to allow direct invocation from app // virtual void CmFileOpen(); virtual void CmFileNew(); virtual void CmFileClose(); virtual void CmFileSave(); virtual void CmFileSaveAs(); virtual void CmFileRevert(); virtual void CmViewCreate(); // overrideable document manager UI functions // virtual UINT PostDocError(TDocument& doc, UINT sid, UINT choice = MB_OK); virtual void PostEvent(int id, TDocument& doc); // changed doc status virtual void PostEvent(int id, TView& view); // changed view status 1) TDocManager uses the dnxxxx message constants to indicate that a document or view has been created or closed. You can set up response table entries for these messages using the EV_OWLVIEW or EV_OWLDOCUMENT macros. Constant Meaning ------------ ------------------------------------------------------- dnCreate A new document or view has been created. dnClose A document or view has been closed. 3) An application uses receives these dnxxxx events by putting in a response table something like this, from the docviewx example: EV_OWLVIEW(dnCreate, EvNewView), EV_OWLVIEW(dnClose, EvCloseView), Now, EvNewView() and EvCloseView() are not defined in the header file for TApplication, the names are just place holders, but these are the names generated by the AppExpert for Doc/View programs. In a standard Doc/View program, the EvNewView() either (mdi) creates a new mdi child and puts the view as the child's client or (sdi) puts the new view as the main window's client. The docviewx example program uses this to modify the menu, as well. EvCloseView() does nothing under mdi, as mdi takes care of it, under sdi it should set the main window's client to 0, and can alter the menu, title, etc. bool EvCanClose(); void EvWakeUp(); protected members: virtual int SelectDocType(TDocTemplate** tpllist, int tplcount); virtual int SelectViewType(TDocTemplate** tpllist, int tplcount); Note: 3) There are private members for all of the Cmxxx members: virtual void CmEnableNew(TCommandEnabler& hndlr); virtual void CmEnableOpen(TCommandEnabler& hndlr); virtual void CmEnableSave(TCommandEnabler& hndlr); virtual void CmEnableSaveAs(TCommandEnabler& hndlr); virtual void CmEnableRevert(TCommandEnabler& hndlr); virtual void CmEnableClose(TCommandEnabler& hndlr); virtual void CmEnableCreate(TCommandEnabler& hndlr); These members dim illegal menu choices.
TDocTemplate and TDocTemplateT<D, V> 1) TDocTemplate is an abstract base class that contains document template functionality. TDocTemplate classes create documents and views from resources and handle document naming and browsing. The document manager maintains a list of the current template objects. Each document type requires a separate document template. 3) When AppExpert made your program, it set up something like this: DEFINE_DOC_TEMPLATE_CLASS(TFileDocument, TWindowView, DocType1); DocType1 __dvt1("All Files (*.*)", "*.*", 0, "TXT", dtAutoDelete | dtUpdateDir); This is called a document template. FNOTE: Document Templates have nothing to do with C++ templates! Document templates are data items that contain a view and a three letter file name extension for each document. DEFINE_DOC_TEMPLATE has three parameters: docClass, viewClass, and tplClass. If you are used to the container classes, this is kind of like an association, it is saying that there is a type "DocType1". It might be more readable like this: // (This code wont compile, it is imaginary.) DEFINE_A_TYPE_OF_VIEW(TfooDocument, TbarWindowView, TfoobarDocViewPair); TfoobarDocViewPair dummy("All Files (*.*)", "*.*", 0, "TXT", dtAutoDelete | dtUpdateDir); Where 'foo' is your own special I/O routines for saving whatever kind of objects your document holds, and 'bar' is your viewer class. Sometimes you want more than one viewer class, like in the line drawing example there was a graphical viewer and a text viewer, so there were two DEFINE_DOC_TEMPLATE_CLASSes and __dvts. You can have as many as you want. When you select "Window|Add View" you get a menu of the StaticName()s of all views which have the same document class as the current view. DEFINE_DOC_TEMPLATE_CLASS is supposed to hide from you a bunch of code that it writes that creates a TDocTemplateT<D,V>. While most of the Doc/View stuff becomes clearer when you look at the code, TDocTemplateT<D,V> doesn't, so just use the macro. The strings in the template define what will appear in the common dialog when a file is being opened. The dtxxxx flags are discussed below. TDocTemplate public members: virtual TView* ConstructView(TDocument& doc) = 0; virtual TDocument* CreateDoc(const char far* path, long flags = 0) = 0; virtual TView* CreateView(TDocument& doc, long flags = 0) = 0; TDocument* InitDoc(TDocument* doc,const char far* path,long flags); TView* InitView(TView* view); virtual TDocument* IsMyKindOfDoc(TDocument& doc) = 0; virtual TView* IsMyKindOfView(TView& view) = 0; virtual const char far* GetViewName() = 0; bool SelectSave(TDocument& doc); bool IsVisible(); // displayable in file select dialogs virtual TDocTemplate* Clone(TModule* module, TDocTemplate*& phead=DocTemplateStaticHead)=0; TDocManager* GetDocManager() const {return DocManager;} void SetDocManager(TDocManager* dm) {DocManager = dm;} const char far* GetFileFilter() const {return FileFilter;} void SetFileFilter(const char far*); const char far* GetDescription() const {return Description;} void SetDescription(const char far*); const char far* GetDirectory() const {return Directory;} void SetDirectory(const char far*); void SetDirectory(const char far*, int len); const char far* GetDefaultExt() const {return DefaultExt;} void SetDefaultExt(const char far*); long GetFlags() const {return Flags;} bool IsFlagSet(long flag) {return (Flags & flag) != 0;} void SetFlag(long flag) {Flags |= flag;} void ClearFlag(long flag){Flags &= ~flag;} bool IsStatic() {return (RefCnt & 0x8000) != 0;} TModule*& GetModule() {return *ModulePtr;} void SetModule(TModule* module){ModuleDeref = module; ModulePtr = &ModuleDeref;} TDocTemplate protected members: ~TDocTemplate(); TDocument* InitDoc(TDocument* doc, const char far* path, long flags); TView* InitView(TView* view); TDocTemplateT<D, V> public members: TDocTemplateT* Clone(TModule* module, TDocTemplate*& phead = DocTemplateStaticHead); D* ConstructDoc(TDocument* parent = 0); V* ConstructView(TDocument& doc); // The following [two] functions are maintained for backward compatibility (OWL 2.5 // comment) TView* CreateView(TDocument& doc, long flags=0); D* IsMyKindOfDoc(TDocument& doc); // returns 0 if template can't support V* IsMyKindOfView(TView& view); // returns 0 if template incompatible virtual const char far* GetViewName() {return V::StaticName();}
TStream, TInStream, TOutStream
TStream 1) An abstract base class, TStream provides links between streams and documents, views, and document files. 3) This class isnt abstract in the usual C++ sense with =0 pure virtual functions, it is made abstract because its only constructor is protected. Public members Protected members 3) TInStream and TOutStream simply add public constructors and mix in istream and ostream. Related classes TStorageInStream, TStorageOutStream, TStorageStreamBase, and TStorageBuf are used by TStorageDocument and TOleDocument but are declared in the .cpp files and so cannot be used. Support for Doc/View in other classes: TDocManager* GetDocManager() returns the document manager. This is used when you want to programmatically cause Doc/View things to happen, such as loading a document on startup. From any TWindow, you can call GetApplication()->GetDocManager() to get the document manager. In addition, your application will need an EvNewView() and possibly an EvCloseView() member in your application class. This class is used to alter an applications menu. It is normally used with Doc/View to merge menu items specific to a view with menu items which are always available. See the on-line help and the docview.ide sample program. Let's take a look at how the standard events are defined. They are defined in two pieces: First, the argument types to the various functions are defined, then the EV_VN_ messages are defined, like this: NOTIFY_SIG(vnViewOpened,TView*) NOTIFY_SIG(vnViewClosed,TView*) NOTIFY_SIG(vnDocOpened, int) NOTIFY_SIG(vnDocClosed, int) NOTIFY_SIG(vnCommit, BOOL) NOTIFY_SIG(vnRevert, BOOL) NOTIFY_SIG(vnIsDirty, void) NOTIFY_SIG(vnIsWindow, HWND) #define EV_VN_VIEWOPENED VN_DEFINE(vnViewOpened,VnViewOpened,pointer) #define EV_VN_VIEWCLOSED VN_DEFINE(vnViewClosed,VnViewClosed,pointer) #define EV_VN_DOCOPENED VN_DEFINE(vnDocOpened, VnDocOpened, int) #define EV_VN_DOCCLOSED VN_DEFINE(vnDocClosed, VnDocClosed, int) #define EV_VN_COMMIT VN_DEFINE(vnCommit, VnCommit, int) #define EV_VN_REVERT VN_DEFINE(vnRevert, VnRevert, int) #define EV_VN_ISDIRTY VN_DEFINE(vnIsDirty, VnIsDirty, void) #define EV_VN_ISWINDOW VN_DEFINE(vnIsWindow, VnIsWindow, int) Here is a handy table of the meanings of the constants: Constant Meaning vnCommit 1) Changes are committed to the document. 2) document is committing, flush cached changes 3) The document is about to write changes to the file. If the view is keeping changes from the document for efficiency reasons, it has to tell the document about them all when it gets this notification. vnCustomBase 1) Base event for document notifications. 2) base of document class specific notifications 3) Has the value of 100. Document classes you write are supposed to use vnCustomBase+x for each of their custom notifications. Documents don't send events to views that are not their own, so you don't need to guarantee unique #s among documents. Define these in your document and not your views so they can be shared. vnDocOpened 1&2) Document has been opened. vnDocClosed 1&2) Document has been closed. vnIsDirty 1&2) Is TRUE if uncommitted changes are present. vnIsWindow 1&2) Is TRUE if the HWND passed belongs to this view. vnRevert 1) Document's previous data is reloaded and overwrites the view's current data. 2) document has reverted, reload data from doc vnViewOpened 1) A new view has been constructed. vnViewClosed 2) A view is about to be destroyed.
Now, in the Borland supplied line drawing example, three additional events were defined: Append a line, Delete a line, and Modify a line. Each of these worked with an unsigned int which was the # of the line. These events were defined thusly: 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) When I first started trying to make Doc/View apps, I didn't realize that I could put any old type I wanted into NOTIFY_SIG, because I didn't understand how it figured out what to do. But you can, provided it fits into 32 bits. Just follow the examples from DOCVIEW.H above. (DOCVIEW.H has a bunch of secret stuff in it to make this all work, it is quite hard to read...)
The document manager is lazy; it doesn't open your document. Why? Well lets say you are writing the Borland C++ IDE with Doc/View. Now, you are going to need the .IDE, the .EXE (for debugging and browsing) and the .APX (for Class Expert) when a project is opened up. But loading in the whole .EXE might be a waste, so the system waits until it needs it. If you look at the line drawing example it does this to show you how to do it, although you don't get an actual benefit out of it in this case. (I'm not sure though if you wouldn't model a system like the C++ IDE with the .IDE file being a parent document, and the .EXE and .APX being children though...) What the DOCVIEW.IDE line drawing example does is have the following code at the top of its GetLine() method: if (!IsOpen() && !Open(ofRead | ofWrite)) return 0; Paint() calls GetLine() which will call Open() the first time through. If you dont like this you can add the bit dtAutoOpen in your DocTypeN declaration. If you decide to add dtAutoOpen to an existing project, be warned that your Open() will not be called for File|New operations as it was when Open() was called in the document.
You copied the CanClose() member from the line drawing example: bool CanClose() {return TListBox::CanClose() && Doc->CanClose();} This bit of code asks the document if it can close when the view closes, even though the document is not going to close. If your view doesn't hold any data, you don't want to call TDocument::CanClose() unless you are the last view on the screen: bool CanClose() { return TListBox::CanClose() && (Doc->NextView(this) || Doc->NextView(0) != this || Doc->CanClose()); }
SetDocTitle doesn't do anything except call the parent document, and there is rarely one of those. The only SetDocTitle() in OWL that does anything interesting is the one in TFrameWindow (which wasnt in the BC 4.0x help). What it does is takes the frame's title, the document name, and the index number, and make a window caption. If you want to set the frame's title, you can do it in TMyApp::EvNewView by replacing the call to your MDI child constructor with something like this: TMyMDIChild* child = new TMyMDIChild(*mdiClient, view.GetViewName(), // caption view.GetWindow());
You have to write a VnIsWindow() member AND you must put a EV_VN_ISWINDOW in the response table. If you don't the document manager thinks that your view isn't the active view, even when it has the focus. See TListBox::VnIsWindow() for an example.
If your view has controls on it (for example if you are deriving from TView and TDialog), the example VnIsWindow() will not work for you, because the document manager is passing around the window with the focus, and you have given the focus to a control. Try this: bool TMyDialog::VnIsWindow(HWND hWnd) { return ((HWindow == hWnd) || IsChild(hWnd)); } This is frustrating for projects created by the "Dialog main window" option of BC 4.5x, because the AppExpert will generate a VnIsWindow() like this: bool VnIsWindow(HWND hWnd) {return HWindow == hWnd;} which will NEVER work for a dialog.
I haven't found a clean way to do this, although a property could probably take care of it. In your EvNewView(), set the icon based upon the view class: if (TYPESAFE_DOWNCAST(&view, TAlphaView)) child->SetIcon(this, IDI_ALPHA); else if (TYPESAFE_DOWNCAST(&view, TBetaView)) child->SetIcon(this, IDI_BETA); else if (TYPESAFE_DOWNCAST(&view, TGammaView)) child->SetIcon(this, IDI_GAMMA); (When I suggested this method on the OWL mailing list, I was blasted for the non-object oriented nature of this solution. Some people think that all the views should inherit from a single view with a GetDesiredIcon() method, and the view should be TYPESAFE_DOWNCAST into that type and its GetDesiredIcon() method called. This is a good solution, although it would be better if the stock TView had something like this or even better if the ClassExpert could derive classes from your own base classes.)
Changing the menus is your code's job, not the document manager's. In your EvNewView() member you want to add the following code before the call to setting the icon: if (view.GetViewMenu()) child->SetMenuDescr(*view.GetViewMenu()); This of course assumes that you are setting the view menu up correctly in the constructor for your view. Once this has been done, TMDIChild::EvMDIActivate() will call TFrameWindow::MergeMenu() when the active view changes. I believe if you want to change the speed bar for different window types as the Borland IDE does that you would need to do this in EvMDIActivate for your TMDIChild descendent but I have not tried this yet.
This is done in your InitInstance() member of your application class AFTER the call to TApplication::InitInstance(), like this: // If a command line argument was given, try to open it as a file if (_argc == 2) GetDocManager()->CreateDoc(&__dvt1, _argv[1], 0, 0); __dvt1 is a doc/view template given at the top of your application .cpp. If you have more than one document type, you need to get the file extension from _argv[1] and use that to determine which __dvtx variable to use. If you have more than one view, you need to specify the one you want to open by default. F Note that this method doesnt work with OWL 2.0.
There is no notification when a Document closes, but you can monitor for view closings and if you see the last view for a document closing add that item to the menu. Here is a code sample for an EvCloseView() method. Two variables, bool fAddedSeparator and the array char szFileCache[5][MAXPATH] were added to the application class, as well as changes in InitInstance() and ~TApplication() to read and write the cache list for the next time the program was started: void myApp::EvCloseView (TView& view) { // Only update menu if this is the last view. TDocument::GetViewList() and // TView::GetNextView() are undocumented public inline functions. if (view.GetDocument().GetViewList()->GetNextView()) return; const char *psz = view.GetDocument().GetDocPath(); // Don't update if there is no filename (After File|New...) if (!psz) return; AddCacheTop(psz); // Erase the existing menu items TMenu menu(GetSubMenu(GetMainWindow()->GetMenu(), 0)); for (i = 0; i < nelems(szFileCache); i++) menu.RemoveMenu(CM_FILE0+i, MF_BYCOMMAND); // Put 'em on the menu AddCachedFilenames(menu);
I dont know if this is the best solution for updating this menu (other choices are requiring all of your documents to call a function when they are closed/destroyed and subclassing the document manager. nelems is a handy #define from K&R C which gives the number of elements in an array: #define nelems(a) (sizeof (a)/sizeof (a)[0]) You will need to #define CM_FILE0 .. CM_FILEn in your .rh file, and you will need to write functions and response table entries to handle the menu items. (This example function only handles the first menu item, you would probably modify this to use a subroutine or a catch-all menu command function instead). void myApp::CmFileCache0() { TryLoadDocument(szFileCache[0]); } I have not been able to find a good place to get the list of documents out of the document manager when the document manager is shutting down. I put this code in my applications CanClose which is generally called right before shutdown: TDocument* doc = GetDocManager()->DocList.Next(0); while (doc) { AddCacheTop(doc->GetDocPath()); doc = GetDocManager()->DocList.Next(doc); } This code uses three subroutines I wrote: void myApp::TryLoadDocument(const char *psz) { TDocument *doc; if ((doc = GetDocManager()->FindDocument(psz)) != 0) { GetDocManager()->PostDocError(*doc, IDS_DUPLICATEDOC); return ; } GetDocManager()->CreateDoc(&__dvt2, psz, 0, 0); } void myApp::AddCachedFilenames(TMenu &menu) { // Add the menu items char szBuffer[MAXPATH + 10]; for (int i = 0; i < nelems(szFileCache); i++) if (szFileCache[i][0]) { if (!fAddedSeparator) { menu.AppendMenu(MF_BYCOMMAND|MF_SEPARATOR); fAddedSeparator = TRUE; } wsprintf(szBuffer, "&%d %s", i+1, szFileCache[i]); menu.AppendMenu(MF_BYCOMMAND|MF_ENABLED, CM_FILE0+i, szBuffer); } } void myApp::AddCacheTop(const char *psz) { // Don't cache unnamed (File|New) documents if (!psz) return; // psz must now be made first in szFileCache for (int n = 0; n < nelems(szFileCache); ) if (stricmp(szFileCache[n++], psz) == 0) break; // Now copy the files down to allow the latest file to be first for (int i = n-1; i > 0; i--) strcpy(szFileCache[i], szFileCache[i-1]); // Put the newly closed menu item at the top strcpy(szFileCache[0], psz); } This routine adds the items under the last choice on the File menu, as the Borland IDE does. Newer programs like Microsoft Word 95 seem to be adding the files before the exit item, so you may want to update this code. F Note that this method doesnt work with OWL 2.0.
Class Expert might know a lot about dialogs and such, but it doesn't know much about Doc/View and if you think it does you will make certain common errors:
I/O is done by TInStream and TOutStream classes. TFileDocument knows two things: How to open and close files, and how to create those streams. The actual I/O is going to be done by either a TDocument descendent or a TView descendent. TFileDocument isn't going to be doing any I/O.
TInStream and TOutStream are for writing out structs, not objects. All of the type information falls off your class as you write it to disk. You either need to prepend a word of type information before writing and look up the word in a table to call the correct constructor, or you need to use Borland's pstream, which do that for you. However, pstreams being what they are they don't mix well with regular streams. (You don't have to worry about this if you are writing out all of the same kind of object together, like the lines in the drawing example.)
First, do an experiment. Bring up the DOCVIEW example, and create a new line document. Set your pen to something thick, like 50, and draw some lazy meandering lines. Then use Window|Add View to add two copies of the view, and a single copy of line list view. Tile the windows, and then draw another line. Changes go to all of the views nicely. Then go to the line list view and change the color of a line. There is an ugly redraw. In this case, the ugly redraw is unavoidable, because we changed the line and we want to see the change in all of the views. There is no way to "un-draw" a line easily. What you want to do is to make sure that you don't get this symptom unnecessarily. Keeping the data in the view keeps the system fast, but people like to be able to work on a single document in several different ways at the same time.
You forgot to set the style bits of your listbox, and it is trying to maintain an integral height. TListView uses the following modifications to the style bits: Attr.Style &= ~(LBS_SORT); Attr.Style |= (WS_HSCROLL | LBS_NOINTEGRALHEIGHT);
The standard way to not open a document is to return FALSE in ::Open(). However, that will not work unless you have the dtAutoOpen flag set, you will end up with an empty view instead of the "could not open document" message. dtAutoOpen set up, you can display your own MessageBox and return TRUE if you want to accept the part of the file you could read, or FALSE if you want to fail the open. If you need to throw exceptions when you fail, you probably want to catch them and return false so that your objects get cleared up correctly. The following seems to work inside open (you need a TRY, too): CATCH( (xmsg& x) { GetDocManager().GetApplication()->Error(x, 0, 0); return FALSE; } ) Not clearing up objects can cause the document manager to refuse to try to load them again if you aren't using dtAutoOpen.
In your EvNewView(), check for TDialog descendants: bool fIsDialog = (TYPESAFE_DOWNCAST(&view, TDialog)) ? TRUE : FALSE; myMDIChild* child = new myMDIChild(*mdiClient, view.GetViewName(), // caption view.GetWindow(), fIsDialog); (This sets the frame to shrink to the view's initial size.) if (fIsDialog) child->Attr.Style &= ~(WS_THICKFRAME|WS_MAXIMIZEBOX); (This makes the frame look like a modeless dialog's frame.)
TOcDocument and TOcView are used by TOleDocument and TOleView. By themselves, they are not Doc/View classes. TOcDocument isnt derived from anything, and TOcView is derived from TUnknown, IBContainer, IBContains, and IBDropDest.
You need to subclass the document manager. Write a constructor and copy the function TDocManager::SelectDocPath(). Under Win95, but NOT under NT, or the flag member of the ofn structure with OFN_EXPLORER (0x80000). (Under NT this seems to prevent the box from coming up at all. I havent tried this in an environment in which more than only template exists, so I am not sure if the hook still works.)
Parent (All of the examples use 0 for a parent, when would I put something else in?) The idea behind the parent is that documents can contain other documents as objects. For example, you can often insert a .BMP file by reference directly into a word processor. Because you aren't inserting the bits, you have another file, and you might want to have a graphical tool and be drawing on the picture in one window, and have the picture in a page of text in another. When you close the .BMP file you want to keep the word processing document up, but when you close the word processing document you want the .BMP to go away. The word processing document would then be considered the parent of the .BMP. Property Properties are facts about the document, like its filename and size. A lot of tricks are used with enums to get properties to inherit correctly. Properties also have the annoying problem of being any type, so you have to know what you want before you can get it. Properties defined in the base class TDocument are: DocumentClass, // text TemplateName, // text ViewCount, // int StoragePath, // text DocTitle, // text TFileDocument adds: CreateTime, // FILETIME ModifyTime, // FILETIME AccessTime, // FILETIME StorageSize, // unsigned long FileHandle, // platform file handle (HFILE if Windows) TStorageDocument adds: CreateTime, // FILETIME ModifyTime, // FILETIME AccessTime, // FILETIME StorageSize, // unsigned long IStorageInstance, // IStorage* TOleDocument doesnt add any. The example TDrawDocument for drawing lines adds: LineCount, Description, etc. Mix class This is a class which is multiply derived from more than one class. It isnt a Doc/View concept, but it is used in the documentation. An example is TWindowView, which mixes the functionality of TWindow and TView. 1) This enum defines the document and open sharing modes used for constructing streams and storing data. Any constants that have the same functionality as those used by OLE 2.0 docfiles are indicated in the following table; for example, STGM_TRANSACTED, STGM_CONVERT, STGM_PRIORITY, and STGM_DELETEONRELEASE. Although files are typically used for data storage, databases or spreadsheets can also be used. I/O streams rather than DOS use these bit values. Documents open the object used for storage in one of the following modes: Constant Value Meaning ofParent 0 A storage object is opened using the parent's mode. ofRead 1 A storage object is opened for reading. ofWrite 2 A storage object is opened for writing. ofReadWrite 1|2 A storage object is opened for reading and writing. ofAtEnd 4 Seek to end-of-file when opened originally. ofAppend 8 Data is appended to the end of the storage object. ofTruncate 10 An already existing file is truncated. ofNoCreate 20 Open fails if file doesn't exist. ofNoReplace 40 Open fails if file already exists. ofBinary 80 Data is stored in a binary, not text, format. Carriage returns. are not stripped. ofIosMask FF All of the above bits are used by the ios class. ofTransacted 1000 Changes to the storage object are preserved until the data is either committed to permanent storage or discarded. (STGM_TRANSACTED) ofPreserve 2000 Backs up previous storage data using before creating a new storage object with the same name. (STGM_CONVERT) ofPriority 4000 Supports temporary, efficient reading before opening the storage. (STGM_PRIORITY) ofTemporary 8000 The storage or stream is automatically destroyed when it is destructed. (STGM_DELETEONRELEASE) 3) These modes use used all over the place by the TDocument descendants and the document manager to figure out what it is supposed to be doing. So, when you look at a function, you have to figure out not only where it is called from and when it is called, but what modes were passed into it.
Appendix 2: Template constants 1) dtxxxx constants are used by TDocument and TDocTemplate to create templates. Several constants are equivalent to the OFN_xxxx constants defined by Windows in commdlg.h.
(All of the OFN_ flags relate to common dialog stuff. Look at the common dialog information in your help text.)
pfGetText Property is accessible in a text format.
Appendix 4: Borland C++ and OWL versions Borland C++ 3.x included OWL 1.x. OWL 1.x did not include Doc/View. Borland C++ 4.0x included OWL 2.0x. OWL 2.0 included Doc/View, but not the OLE classes. It also didnt have an AppExpert that could generate "Dialog Main Window" applications. Borland C++ 4.5x and Turbo C++ 4.5 included OWL 2.5x. OWL 2.5 includes the OLE classes for Doc/View. It also supports a new built-in type, "bool". This replaces the old type "BOOL". The new type has two values, true and false. The old bool had TRUE and FALSE. Because this document was started in the old OWL 2.0 days, it uses bool for classes which existed in OWL 2.0. Note: The old BOOL is compatible with the new bool, so old programs will still compile. New programs should use "bool" unless they need to be distributed with source to people who are still using version 4.02. Borland C++ 5.0 is to include OWL 3.0. This has not yet been released. At the time of this writing (9/13/95), OWL 3.0 promises "DocView enhancements for improved extensibility" but no details are available. There is an OWL mailing list. Details can be found on the WWW at http://www.mind.net/jfs/owl.html There is an OWL newsgroup, comp.os.ms-windows.programmer.tools.owl The only book I have found which discusses Doc/View is Object Windows 2.0 Programming by Tom Swan. Unlike some of the authors out there, Tom Swan knows OWL well, he even wrote some of the classes in OWL 2.0 such as TPrintout. Regarding copyright, assume that everything marked with a 3) (that is, the stuff I wrote). is public domain. I'm hoping everything marked with a 1) or a 2) is considered fair-use of Borland's copyrighted works... Ed Snible esnible@goodnet.com Revised: December 04, 1999. |
|