OLE (Object Linking and Embedding)
The Document-Centered Paradigm
In the document-centered paradigm users create compound documents that contain embedded objects (sound clips, pictures, animations, diagrams, charts, tables, text, worksheets, web pages, etc.) that are created, edited, loaded, and saved by a variety of background applications. The user does not need to be particularly aware of these background applications; the compound document is in the foreground of his attention. When the user edits an embedded worksheet, Excel menus appear at the top of the document's window. If the user next edits a block of embedded text, Word menus replace the Excel menus. The document can be opened initially using Word or Excel. The Document-Centered paradigm replaces the Application-Centered paradigm.
OLE Architecture
OLE is a client-server framework using the ActiveX architecture. Recall that ActiveX is an ORB architecture (Object Request Broker) based on COM (Common Object Model). The OLE client contains remote views of documents maintained by OLE servers. For this reason OLE clients are called container applications.
Object Linking
Select Object from MS Word's Insert menu. Select the "Create From File" tab on the dialog box that appears. You are prompted for a file. The Result box reads:
Inserts the contents of the file into your document so that you can edit it later using the application which created the source file.
Check the "Link to file" box and read the Result box:
MS Word is an OLE container application (it's also an OLE server application!). It can contain links to objects that are created, edited, loaded, and saved by various server applications. If the server application edits an object, then the change will appear in all container applications that contain a link to the object.
Activating the object in the container application by double clicking on it automatically starts the server application.
Object Embedding
Select Object from MS Word's Insert menu. By default, the "Create New" tab is selected. A list box displays all objects that can be created by registered server applications and inserted (i.e. embedded) into a Word document:
It is the container application's responsibility to "hold" its embedded objects (in memory and in files), although it calls upon appropriate server applications to perform edits and serialization/deserialization. A container's embedded and native objects are stored in a special type of compound file.
Activating the object in the container application by double clicking on it either starts the server application, or causes the container application's menus to temporarily change to the menus of the server application. This is called in-place activation. An active embedded object may have a different appearance (i.e. view) than the same object in its inactive state.
Because the container holds its embedded objects, the server doesn't need to run as a stand-alone application (full server). A mini-server, for example, is a server application that doesn't ever hold its own data. It depends on container applications to hold its data, but it provides the editing logic. A third possibility is to use Windows dynamic linking technology (dll) to dynamically link the server into the container's address space. In this case we call the server an in-process server.
A Pure OLE Container Application
Create an MDI application called Container. In the "Compound Document Support" dialog choose Container. Accept all other defaults. The following classes are generated:
class CAboutDlg : public CDialog { ... };
class CChildFrame : public CMDIChildWnd { ... };
class CContainerApp : public CWinApp { ... };
class CContainerCntrItem : public COleClientItem { ... };
class CContainerDoc : public COleDocument { ... };
class CContainerView : public CView { ... };
class CMainFrame : public CMDIFrameWnd { ... };
The surprises are the base class of CContainerDoc, which is COleDocument, a derived class of CDocument, and a new class: CContainerCntrItem derived from COleClientItem. An OLE client item is essentially a container-side remote proxy. A COleServerItem object is a server-side remote proxy. Both classes are derived from CDocItem. An OLE document is a client or server item container.
The basic OLE document member functions allow users to add, remove, and traverse server and client items:
class COleDocument: public CDocument
{
virtual void AddItem( CDocItem* pItem );
virtual void RemoveItem( CDocItem* pItem );
virtual POSITION GetStartPosition( );
// these automatically update pos
COleClientItem* GetNextClientItem( POSITION& pos );
COleServerItem* GetNextServerItem( POSITION& pos );
virtual CDocItem* GetNextItem( POSITION& pos );
COleClientItem* GetInPlaceActiveItem( CWnd* pWnd );
// etc.
};
The client item has a Run() member function that runs its remote server. It can also ask its server to perform specific operations using the DoVerb() function. An item can be in one of five states. In the empty state, a client item is not yet completely an item. Memory has been allocated for it, but it has not yet been initialized with the OLE item's data.
In the second step, performed through a call to COleClientItem::CreateFromFile or another CreateFromxxxx function, the item is completely created. The OLE data (from a file or some other source, such as the Clipboard) has been associated with the COleClientItem-derived object. Now the item is in the loaded state.
When an item has been opened in the server's window rather than opened in place in the container's document, it is in the open (or fully open) state. In this state, a cross-hatch usually is drawn over the representation of the item in the container's window to indicate that the item is active elsewhere.
When an item has been activated in place, it passes, usually only briefly, through the active state. It then enters the UI active state, in which the server has merged its menus, toolbars, and other user-interface components with those of the container. The presence of these user-interface components distinguishes the UI active state from the active state. Otherwise, the active state resembles the UI active state. If the server supports Undo, the server is required to retain the OLE item's undo-state information until it reaches the loaded or open state.
class COleClientItem : public CDocItem
{
public:
COleClientItem(COleDocument* pContainerDoc = NULL);
// Item state
enum ItemState
{ emptyState, loadedState, openState, activeState, activeUIState };
void Run(); // insure item is in running state
BOOL Draw(CDC* pDC, LPCRECT lpBounds,
DVASPECT nDrawAspect = (DVASPECT)-1);
virtual BOOL DoVerb(
LONG nVerb, CView* pView, LPMSG lpMsg = NULL);
void Activate(LONG nVerb, CView* pView, LPMSG lpMsg = NULL);
virtual void Serialize(CArchive& ar);
// data valid when in-place activated
CView* m_pView; // view when object is in-place activated
COleFrameHook* m_pInPlaceDoc; // doc window when in-place
HWND m_hWndServer; // HWND of in-place server window
// etc.
};
Build and test the application. Select "Insert New Object" from the Edit menu. Select Microsoft Word Picture. Create a simple picture then press the close button. Your picture should appear in the container's view window. Now select "Edit" from the "Picture Object" sub menu under the Edit menu.
Let's add some enhancements to make the container application more useful.
CContainerCntrItem
Add a CRect to hold the container item's position in the view:
class CContainerCntrItem : public COleClientItem
{
public:
CRect m_rect; // item's poisition in the view
// etc.
};
Initialize the rectangle in the constructor:
CContainerCntrItem::CContainerCntrItem(CContainerDoc* pContainer)
: COleClientItem(pContainer)
{
m_rect = CRect(10, 10, 210, 210);
}
Save and restore the rectangle in the serialize function:
void CContainerCntrItem::Serialize(CArchive& ar)
{
ASSERT_VALID(this);
COleClientItem::Serialize(ar);
if (ar.IsStoring())
{
ar << m_rect;
}
else
{
ar >> m_rect;
}
}
CContainerView
The document contains a list of client items, each with a rectangle indicating its position. The view's OnDraw() function traverses this list, drawing each client item inside its rectangle:
POSITION pos = pDoc->GetStartPosition();
while(pos)
{
CContainerCntrItem* pCurrentItem =
(CContainerCntrItem*) pDoc->GetNextClientItem(pos);
pCurrentItem->Draw(pDC, pCurrentItem->m_rect);
}
The Selected Item
Assume the document contains multiple client items. When the user selects "Edit Object" from the Edit menu, how does the application know which object to edit? The App wizard has added a variable to the view class that points at the selected client item:
class CContainerView : public CView
{
public:
CContainerCntrItem* m_pSelection;
// etc.
};
This is the item selected for editing by the "Edit Object" selection. We could add a left button handler to the view class that would set the selected client item to the one containing the mouse position at the time of the click:
POSITION pos = pDoc->GetStartPosition();
while(pos)
{
CContainerCntrItem* pCurrentItem =
(CContainerCntrItem*) pDoc->GetNextClientItem(pos);
if (pCurrentItem->m_rect.PtInRect(point))
{
m_pSelection = pCurrentItem;
}
}
Trackers
How will users know which client item is the selected one? How will users resize it and move it around the view window? MFC solves these problem with trackers. A tracker is a temporary user interface frame that can be associated with a rectangle. A tracker can be resized or moved using the mouse. This resizes or moves the associated rectangle. The tracker's border can be solid, dashed, or hatched to indicate its "state". We can associate a tracker with rectangle of a client item:
Add three member functions to the view class:
class CContainerView : public CView
{
public:
void SetSelection(CContainerCntrItem* item);
CContainerCntrItem* HitTest(CPoint point);
void SetupTracker(CContainerCntrItem* item,
CRectTracker* track);
// etc.
};
SetupTracker() associates a tracker with a client item. If the client item is embedded, the tracker's border is solid. If the client item is linked, the tracker's border is dashed. If the client item is active (i.e., it is currently being edited by the server application), then hatching is added to the border. If the client item is the selected item, then resize handles are added to the border:
void CContainerView::SetupTracker(CContainerCntrItem * item,
CRectTracker * track)
{
track->m_rect = item->m_rect;
// allow resizing from inside border
if (item == m_pSelection)
{
track->m_nStyle |= CRectTracker::resizeInside;
}
if (item->GetType() == OT_LINK)
{ // linked items have dotted borders
track->m_nStyle |= CRectTracker::dottedLine;
}
else
{ // embedded items have solid borders
track->m_nStyle |= CRectTracker::solidLine;
}
// active or open items have hatched inner borders
if (item->GetItemState() == COleClientItem::openState ||
item->GetItemState() == COleClientItem::activeUIState)
{
track->m_nStyle |= CRectTracker::hatchInside;
}
}
HitTest() searches the document's list of client items for one containing a given point:
CContainerCntrItem* CContainerView::HitTest(CPoint point)
{
CContainerDoc* pDoc = GetDocument();
CContainerCntrItem* pHitItem = NULL;
POSITION pos = pDoc->GetStartPosition();
while(pos)
{
CContainerCntrItem* pCurrentItem =
(CContainerCntrItem*) pDoc->GetNextClientItem(pos);
if (pCurrentItem->m_rect.PtInRect(point))
{
pHitItem = pCurrentItem;
}
}
return pHitItem;
}
SetSelection() sets m_pSelection to a given item. We assume items can be in-place activated. (I.e., the server application's menus appear across the top of the container application.) If the selected item is not the currently active item, the SetSelection() deactivates the active item and closes its associated server application:
void CContainerView::SetSelection(CContainerCntrItem * item)
{
if (item == NULL || item != m_pSelection)
{
COleClientItem* pActive =
GetDocument()->GetInPlaceActiveItem(this);
if(pActive != NULL && pActive != item)
{
pActive->Close();
}
}
Invalidate();
m_pSelection = item;
}
The view's OnDraw() function draws each client item. It also creates a tracker, associates it with the selected item, then draws it:
void CContainerView::OnDraw(CDC* pDC)
{
CContainerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
pDC->TextOut(0, 0, "This is an OLE container.");
// TODO: also draw all OLE items in the document
POSITION pos = pDoc->GetStartPosition();
while(pos)
{
CContainerCntrItem* pCurrentItem =
(CContainerCntrItem*) pDoc->GetNextClientItem(pos);
pCurrentItem->Draw(pDC, pCurrentItem->m_rect);
// draw tracker around selected item
if (pCurrentItem == m_pSelection)
{
CRectTracker trackrect;
SetupTracker(pCurrentItem, &trackrect);
trackrect.Draw(pDC);
}
}
}
Use the Class Wizard to add a handler to the view class for WM_LBUTTONDOWN messages. The handler calls HitTest() to determine which client item was clicked. It calls SetSelection() to make this item the selected item. The handler associates a tracker with the selected item and calls the tracker's Track() member function. This displays the tracker's border and handles all mouse events that occur on or inside the border. Pressing the escape key or calling the button handler again makes the tracker go away:
void CContainerView::OnLButtonDown(UINT nFlags, CPoint point)
{
CContainerCntrItem* pHitItem = HitTest(point);
SetSelection(pHitItem);
if (pHitItem == NULL) return;
CRectTracker track;
SetupTracker(pHitItem, &track);
UpdateWindow();
if(track.Track(this, point)) // hit <esc> to return false
{
Invalidate();
pHitItem->m_rect = track.m_rect;
GetDocument()->SetModifiedFlag();
}
}
Build and test the application. Insert an MS Word document into the container. Notice how the Word menus appear replace the container's menus. This is what is meant by in-place activation.
Further Enhancements
Add a handler to the view class for the WM_SETCURSOR message. The handler associates a tracker with the selected item and calls its SetCursor() function. This causes the cursor to change when it is inside the tracker:
BOOL CContainerView::OnSetCursor(
CWnd* pWnd, UINT nHitTest, UINT message)
{
if (pWnd == this && m_pSelection != NULL)
{
CRectTracker tracker;
SetupTracker(m_pSelection, &tracker);
if (tracker.SetCursor(this, nHitTest))
{
return TRUE;
}
}
return CView::OnSetCursor(pWnd, nHitTest, message);
}
Add a handler to the view class for the WM_LMOUSEDBLCLK message. Double left clicking an item makes it the selected item (by calling OnLButtonDown()), then asks the client item's server application to edit the item. Of course the container doesn't know what type of object the selected item is. It doesn't know what the server application is, and therefore it doesn't know what "editing" means for the server application. Does it mean run? Play? Edit? This is where COM comes in. All OLE servers implement a COM interface that has a DoVerb() function that can either take the name of a specific operation, such as OPEN, or a generic operation such as PRIMARY. The client item communicates with its remote server by calling DoVerb():
void CContainerView::OnLButtonDblClk(UINT nFlags, CPoint point)
{
OnLButtonDown(nFlags, point);
if (m_pSelection)
{
if (GetKeyState(VK_CONTROL) < 0)
{
m_pSelection->DoVerb(OLEIVERB_OPEN, this);
}
else
{
m_pSelection->DoVerb(OLEIVERB_PRIMARY, this);
}
}
CView::OnLButtonDblClk(nFlags, point);
}
An OLE Full Server (Drawing Program)
Create an SDI application called Draw. In step 3, select the Full Server option. The following seven classes are generated:
class CDrawDoc : public COleServerDoc { ... };
class CAboutDlg : public CDialog { ... };
class CDrawApp : public CWinApp { ... };
class CDrawSrvrItem : public COleServerItem { ... };
class CDrawView : public CView { ... };
class CInPlaceFrame : public COleIPFrameWnd { ... };
class CMainFrame : public CFrameWnd { ... };
CDrawDoc
#include <afxtempl.h> // needed for CArray
class CDrawDoc : public COleServerDoc
{
public:
CArray<CPoint, CPoint&> m_points;
};
void CDrawDoc::Serialize(CArchive& ar)
{
m_points.Serialize(ar);
}
CDrawView
Add a Boolean m_drawing variable to CDrawView:
class CDrawView : public CView
{
private:
bool m_drawing;
}
As usual, m_drawing should be initialized to false in the constructor. Add a handler for WM_LBUTTONDOWN that sets m_drawing to true, and a handler for WM_LBUTTONUP that sets m_drawing to false.
The mouse move handler adds points to the documents m_points array, invalidates the appropriate rectangle, sets the documents modified flag, and alerts other possible views:
void CDrawView::OnMouseMove(UINT nFlags, CPoint point)
{
if (m_drawing)
{
CDrawDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDoc->m_points.Add(point);
InvalidateRect(
CRect(point.x, point.y, point.x + 5, point.y + 5));
pDoc->SetModifiedFlag();
pDoc->UpdateAllViews(this);
}
CView::OnMouseMove(nFlags, point);
}
The WM_RBUTTONDOWN handler empties the document's array:
void CDrawView::OnRButtonDown(UINT nFlags, CPoint point)
{
CDrawDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDoc->m_points.RemoveAll();
Invalidate();
pDoc->SetModifiedFlag();
pDoc->UpdateAllViews(this);
CView::OnRButtonDown(nFlags, point);
}
Use the Class Wizard to add a function to prepare the device context. It selects a red brush:
void CDrawView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)
{
CBrush *newBrush = new CBrush(RGB(255, 0, 0));
pDC->SelectObject(newBrush);
CView::OnPrepareDC(pDC, pInfo);
}
OnDraw() simply traverses the document's array, drawing tiny circles for each point:
void CDrawView::OnDraw(CDC* pDC)
{
CDrawDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
for(int i = 0; i < pDoc->m_points.GetSize(); i++)
{
int x = pDoc->m_points[i].x;
int y = pDoc->m_points[i].y;
pDC->Ellipse(x, y, x + 5, y + 5);
}
}
CDrawSrverItem
Add the same code to the Server item's OnDraw() function:
BOOL CDrawSrvrItem::OnDraw(CDC* pDC, CSize& rSize)
{
// Remove this if you use rSize
UNREFERENCED_PARAMETER(rSize);
CDrawDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: set mapping mode and extent
// (The extent is usually the same as
// the size returned from OnGetExtent)
pDC->SetMapMode(MM_ANISOTROPIC);
pDC->SetWindowOrg(0,0);
pDC->SetWindowExt(3000, 3000);
for(int i = 0; i < pDoc->m_points.GetSize(); i++)
{
int x = pDoc->m_points[i].x;
int y = pDoc->m_points[i].y;
pDC->Ellipse(x, y, x + 5, y + 5);
}
return TRUE;
}
You must run the application once in stand alone mode. This makes an entry in the registry. Now start any OLE container application. MS Word will do. I used the container application just developed. I selected Draw Document from the list of objects to insert:
We revisit the static structure of this collaboration below:
Problem
Applications can be both OLE servers and OLE containers (for example MS Word and MS Excel). Redo the polygon program as an OLE Container and Server. (Recall this program draws a polygon in the middle of the view port. Mouse clicks increment the number of sides and menu selections change the color.)