Chapter 23

Building an ActiveX Container Application


CONTENTS

You can get a rudimentary ActiveX container by asking AppWizard to make you one, but it will have a lot of shortcomings. A far harder task is to understand how an ActiveX container works, and what you have to do to really use it. In this chapter, by turning the ShowString application of earlier chapters into an ActiveX container and then making it a truly functional container, you get a backstage view of ActiveX in action. Adding drag-and-drop support brings your application into the modern age of intuitive, document-centered user interface design.

Changing ShowString

ShowString was built originally in Chapter 17, "Building Menus and Dialogs," and has no ActiveX support. You could make the changes by hand to implement ActiveX container support, but there would be over 30 changes. It's quicker to build a new ShowString application, this time asking for ActiveX container support, and then make changes to that code to get the ShowString functionality again.

AppWizard Generated ActiveX Container Code

Build the new ShowString in a different directory, making almost exactly the same AppWizard choices as before: call it ShowString, choose an MDI application, no database support, OLE container, a docking toolbar, status bar, printing and print preview, context-sensitive Help, and 3D controls. Finally, select source file comments and a shared DLL.

NOTE
Even though the technology is now called ActiveX, the AppWizard dialogs still refer to OLE. Many of the class names that are used throughout this chapter have Ole in their names, and comments refer to OLE. While Microsoft has changed the name of the technology, it has not propagated that change throughout Visual C++ yet. You will have to live with these contradictions until the next release of Visual C++.

There are a lot of differences between the application you have just built and a do-nothing application without ActiveX container support. The remainder of this section describes and explains these differences and their effects.

Menus  There's another entire menu, called IDR_SHOWSTTYPE_CNTR_IP, shown in Figure 23.1. The name refers to a container whose contained object is being edited in place. During in-place editing, the menu bar is built from the container's in-place menu and the server's in-place menu. The pair of vertical bars in the middle of IDR_SHOWSTTYPE_CNTR_IP are separators; the server menu items will be put between them. This is discussed in more detail in Chapter 24, "Building an ActiveX Server Application."
Figure 23.1 : AppWizard adds another menu for editing in place.

The Edit menu, as shown in Figure 23.2, has four new items:

Figure 23.2 : AppWizard adds items to the Edit menu of the IDR_SHOWSTTYPE resource.

Figure 23.3 : The Insert Object dialog box can be used to embed new objects.

Figure 23.4 : The Insert Object dialog box can be used to embed or link objects that are in a file.

Figure 23.5 : The Links dialog box controls the way linked objects are updated.

Figure 23.6 : Each object type adds a cascading menu item to the Edit menu when it has focus.

CShowStringApp  CShowStringApp::InitInstance() has several changes from the InitInstance() method provided by AppWizard for applications that are not ActiveX containers. The lines in Listing 23.1 initialize the ActiveX (OLE) libraries.

Listing 23.1  Excerpt from ShowString.cpp-Library Initialization

// Initialize OLE libraries

if (!AfxOleInit())

{

     AfxMessageBox(IDP_OLE_INIT_FAILED);

     return FALSE;

}


Still in CShowStringApp::InitInstance(), after the MultiDocTemplate is initialized, but before the call to AddDocTemplate(), this line is added to register the menu used for in-place editing:


pDocTemplate->SetContainerInfo(IDR_SHOWSTTYPE_CNTR_IP);

CShowStringDoc  The document class, CShowStringDoc, now inherits from COleDocument rather than CDocument. As well, this line is added at the top of ShowStringDoc.cpp:

#include "CntrItem.h"

CntrItem.h describes the container item class, CShowStringCntrItem, discussed later. Still in ShowStringDoc.cpp, the macros in Listing 23.2 have been added to the message map.


Listing 23.2  Excerpt from ShowString.cpp-Message Map Additions

ON_UPDATE_COMMAND_UI(ID_EDIT_PASTE, 

›;COleDocument::OnUpdatePasteMenu)

ON_UPDATE_COMMAND_UI(ID_EDIT_PASTE_LINK, 

›;COleDocument::OnUpdatePasteLinkMenu)

ON_UPDATE_COMMAND_UI(ID_OLE_EDIT_CONVERT, 

›;COleDocument::OnUpdateObjectVerbMenu)

ON_COMMAND(ID_OLE_EDIT_CONVERT, 

›;COleDocument::OnEditConvert)

ON_UPDATE_COMMAND_UI(ID_OLE_EDIT_LINKS, 

›;COleDocument::OnUpdateEditLinksMenu)

ON_COMMAND(ID_OLE_EDIT_LINKS, 

›;COleDocument::OnEditLinks)

ON_UPDATE_COMMAND_UI(ID_OLE_VERB_FIRST, 

›;COleDocument::OnUpdateObjectVerbMenu)


These enable and disable the following menu items:

The new macros also handle Convert and Edit Links. Notice that the messages are handled by functions of COleDocument and don't have to be written by you.

The constructor, CShowStringDoc::CShowStringDoc(), has a line added:


EnableCompoundFile();

This turns on the use of compound files. CShowStringDoc::Serialize() has a line added as well:


COleDocument::Serialize(ar);

This call to the base class Serialize() takes care of serializing all the contained objects, with no further work for you.

CShowStringView  The view class, CShowStringView, includes CntrItem.h just as the document does. The view class has these new entries in the message map:
ON_WM_SETFOCUS()
ON_WM_SIZE()
ON_COMMAND(ID_OLE_INSERT_NEW, OnInsertObject)
ON_COMMAND(ID_CANCEL_EDIT_CNTR, OnCancelEditCntr)

These are in addition to the messages caught by the view before it was a container. These catch WM_SETFOCUS, WM_SIZE, the menu item Edit, Insert New Object, and the cancellation of editing in place. An accelerator has already been added to connect this message to Esc.

In ShowStringView.h, a new member variable has been added, as shown in Listing 23.3.


Listing 23.3  Excerpt from ShowStringView.h-m_pSelection

     // m_pSelection holds the selection to the current 

     // CShowStringCntrItem. For many applications, such 

     // a member variable isn't adequate to represent a 

     // selection, such as a multiple selection or a selection

     // of objects that are not CShowStringCntrItem objects.  

     // This selection mechanism is provided just to help you 

     // get started.



     // TODO: replace this selection mechanism with one appropriate to your app.

        CShowStringCntrItem* m_pSelection;


This new member variable shows up again in the view constructor, Listing 23.4, and the revised OnDraw(), Listing 23.5.


Listing 23.4  ShowStringView.cpp-Constructor

CShowStringView::CShowStringView()

{

     m_pSelection = NULL;

     // TODO: add construction code here

}



Listing 23.5  ShowStringView.cpp- CShowStringView::OnDraw()

void CShowStringView::OnDraw(CDC* pDC)

{

     CShowStringDoc* pDoc = GetDocument();

     ASSERT_VALID(pDoc);



     // TODO: add draw code for native data here

     // TODO: also draw all OLE items in the document



     // Draw the selection at an arbitrary position.  This code should be

     //  removed once your real drawing code is implemented.  This position

     //  corresponds exactly to the rectangle returned by CShowStringCntrItem,

     //  to give the effect of in-place editing.



     // TODO: remove this code when final draw code is complete.



     if (m_pSelection == NULL)

     {

          POSITION pos = pDoc->GetStartPosition();

          m_pSelection = (CShowStringCntrItem*)pDoc->GetNextClientItem(pos);

     }

     if (m_pSelection != NULL)

          m_pSelection->Draw(pDC, CRect(10, 10, 210, 210));

}


The code supplied for OnDraw() draws only a single contained item. It doesn't draw any native data, in other words, elements of ShowString that are not contained items. At the moment there is no native data, but after the string is added to the application, OnDraw() is going to have to draw it. What's more, this code only draws one contained item, and it does so in an arbitrary rectangle. OnDraw() is going to see a lot of changes as you work through this chapter.

The view class has gained a lot of new functions. They are as follows:

Each of these new functions is discussed in the subsections that follow.

OnInitialUpdate()  OnInitialUpdate() is called the very first time the view is to be displayed. The boilerplate code (Listing 23.6) is pretty dull:

Listing 23.6  ShowStringView.cpp-CShowStringView::OnInitialUpdate()

void CShowStringView::OnInitialUpdate()

{

     CView::OnInitialUpdate();



     // TODO: remove this code when final selection 

     // model code is written

     m_pSelection = NULL;    // initialize selection



}


The base class OnInitialUpdate() calls the base class OnUpdate(), which calls Invalidate(), requiring a full repaint of the client area.

IsSelected()  IsSelected() can't really work right now, because the selection mechanism is so rudimentary. Listing 23.7 shows what you get, and it works, sort of:

Listing 23.7  ShowStringView.cpp-CShowStringView::IsSelected()

BOOL CShowStringView::IsSelected(const CObject* pDocItem) const

{

     // The implementation below is adequate if your selection consists of

     //  only CShowStringCntrItem objects.  To handle different selection

     //  mechanisms, the implementation here should be replaced.



     // TODO: implement this function that tests for a selected OLE client item



     return pDocItem == m_pSelection;

}


This function is passed a pointer to a container item. If that is the same as the current selection, it returns TRUE.

OnInsertObject()  OnInsertObject() is called when the user chooses Edit, Insert New Object. It's quite a long function, so it is presented in parts. The overall structure is presented in Listing 23.8.

Listing 23.8  ShowStringView.cpp-CShowStringView::OnInsertObject()

void CShowStringView::OnInsertObject()

{

     //display the Insert Object dialog box



     CShowStringCntrItem* pItem = NULL;

     TRY

     {

          // Create new item connected to this document.

          // Initialize the item

          // set selection and update all views

     }

     CATCH(CException, e)

     {

          // handle failed create

     }

     END_CATCH



     // tidy up

}


Each comment here is replaced with a small block of code, discussed in the remainder of this section. The TRY and CATCH statements, by the way, refer to exception handling, discussed in Chapter 30, "Power-User C++ Features."

First, this function displays the Insert Object dialog box, as shown in Listing 23.9.


Listing 23.9  ShowStringView.cpp-Display the Insert Object Dialog Box

// Invoke the standard Insert Object dialog box to obtain information

//  for new CShowStringCntrItem object.

COleInsertDialog dlg;

if (dlg.DoModal() != IDOK)

     return;

BeginWaitCursor();


If the user clicks Cancel, this function returns and nothing is inserted. If the user clicks OK, the cursor is set to an hourglass while the rest of the processing occurs.

To create a new item, the code in Listing 23.10 is inserted.


Listing 23.10  ShowStringView.cpp-Create a New Item

// Create new item connected to this document.

CShowStringDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

pItem = new CShowStringCntrItem(pDoc);

ASSERT_VALID(pItem);


This code makes sure there is a document, even though the menu item is only enabled if there is, and then creates a new container item, passing it the pointer to the document. As you see later, container items hold a pointer to the document that contains them.

The code in Listing 23.11 initializes that item.


Listing 23.11  ShowStringView.cpp-Initializing the Inserted Item

// Initialize the item from the dialog data.

if (!dlg.CreateItem(pItem))

     AfxThrowMemoryException();  // any exception will do

ASSERT_VALID(pItem);

// If item created from class list (not from file) then launch

//  the server to edit the item.

if (dlg.GetSelectionType() == COleInsertDialog::createNewItem)

     pItem->DoVerb(OLEIVERB_SHOW, this);



ASSERT_VALID(pItem);


The code in Listing 23.11 calls the CreateItem() function of the dialog class, ColeInsertDialog. That may seem a strange place to keep such a function, but the function needs to know all the answers that were given on the dialog box, so if it was a member of another class, it would have to interrogate the dialog for the type, file name, was it link or embedded, and so on. It calls member functions of the container item like CreateLinkFromFile(), CreateFromFile(), CreateNewItem(), and so on. So it's not that the code to actually fill the object from the file is in the dialog box, but rather that the work is partitioned between the objects, instead of passing information back and forth between them.

Then, one question is asked of the dialog box: was this a new item? If so, the server is called to edit it. Objects created from a file can just be displayed.

Finally, the selection is updated and so are the views, as shown in Listing 23.12.


Listing 23.12  ShowStringView.cpp-Update Selection and Views

// As an arbitrary user interface design, this sets the selection

//  to the last item inserted.



// TODO: reimplement selection as appropriate for your application



m_pSelection = pItem;   // set selection to last inserted item

pDoc->UpdateAllViews(NULL);


If the creation of the object failed, execution ends up in the CATCH block, shown in Listing 23.13.


Listing 23.13  ShowStringView.cpp-CATCH Block

CATCH(CException, e)

{

     if (pItem != NULL)

     {

     ASSERT_VALID(pItem);

     pItem->Delete();

     }

     AfxMessageBox(IDP_FAILED_TO_CREATE);

}

END_CATCH


This deletes the item that was created and gives the user a message box.

Finally, that hourglass cursor can go away:


EndWaitCursor();

OnSetFocus()  OnSetFocus(), shown in Listing 23.14, is called whenever this view sets focus.

Listing 23.14  ShowStringView.cpp-CShowStringView::OnSetFocus()

void CShowStringView::OnSetFocus(CWnd* pOldWnd)

{

     COleClientItem* pActiveItem = GetDocument()->GetInPlaceActiveItem(this);

     if (pActiveItem != NULL &&

          pActiveItem->GetItemState() == COleClientItem::activeUIState)

     {

          // need to set focus to this item if it is in the same view

          CWnd* pWnd = pActiveItem->GetInPlaceWindow();

          if (pWnd != NULL)

          {

               pWnd->SetFocus();   // don't call the base class

               return;

          }

     }



     CView::OnSetFocus(pOldWnd);

}


If there is an active item and its server is loaded, then that active item sets focus. If not, focus remains with the old window, and, to the user, it appears that the click was ignored.

OnSize()  OnSize(), shown in Listing 23.15, is called when the application is resized.

Listing 23.15  ShowStringView.cpp-CShowStringView::OnSize()

void CShowStringView::OnSize(UINT nType, int cx, int cy)

{

     CView::OnSize(nType, cx, cy);

     COleClientItem* pActiveItem = GetDocument()->GetInPlaceActiveItem(this);

     if (pActiveItem != NULL)

          pActiveItem->SetItemRects();

}


This resizes the view using the base class function, and then, if there is an active item, onsize () tells it to adjust to the resized view.

OnCancelEditCntr()  OnCancelEditCntr() is called when a user who has been editing in place presses Esc. The server must be closed and the object stops being active. The code is shown in Listing 23.16.

Listing 23.16  ShowStringView.cpp-CShowStringView::OnCancelEditCntr()

void CShowStringView::OnCancelEditCntr()

{

     // Close any in-place active item on this view.

     COleClientItem* pActiveItem = 

          GetDocument()->GetInPlaceActiveItem(this);

     if (pActiveItem != NULL)

     {

          pActiveItem->Close();

     }

     ASSERT(GetDocument()->GetInPlaceActiveItem(this) == NULL);

}


CShowStringCntrItem  The container item class is a completely new addition to ShowString. It describes an item that is contained in the document. As you've already seen, the document and the view use this object quite a lot, primarily through the m_pSelection member variable of CShowStringView. It has no member variables other than those inherited from the base class, COleClientItem. It has overrides for a lot of functions though. They are as follows:

The constructor simply passes the document pointer along to the base class. The destructor does nothing. GetDocument() and GetActiveView() are inline functions that return member variables inherited from the base class by calling the base class function with the same name and casting the result.

OnChange() is the first of these functions that has more than one line of code. It's in Listing 23.17.


Listing 23.17  CntrItem.cpp-CShowStringCntrItem::OnChange()

void CShowStringCntrItem::OnChange(OLE_NOTIFICATION nCode, 

     DWORD dwParam)

{

     ASSERT_VALID(this);



     COleClientItem::OnChange(nCode, dwParam);



     // When an item is being edited (either in-place or fully open)

     // it sends OnChange notifications for changes in the state of the

     // item or visual appearance of its content.



     // TODO: invalidate the item by calling UpdateAllViews

     // (with hints appropriate to your application)



     GetDocument()->UpdateAllViews(NULL);

          // for now just update ALL views/no hints

}


Okay, so it has three lines of code. The comments are actually more useful than the code. When the user changes the contained item, the server notifies the container. Calling UpdateAllViews() is a rather drastic way of refreshing the screen, but it gets the job done.

OnActivate() (Listing 23.18) is called when a user double-clicks an item to activate it and edit it in place. ActiveX objects are usually outside-in, which means that a single-click of the item selects it but does not activate it. Activating an outside-in object requires a double-click, or a single-click followed by choosing the appropriate OLE verb from the Edit menu.


Listing 23.18  CntrItem.cpp-CShowStringCntrItem::OnActivate()

void CShowStringCntrItem::OnActivate()

{

    // Allow only one inplace activate item per frame

    CShowStringView* pView = GetActiveView();

    ASSERT_VALID(pView);

    COleClientItem* pItem = GetDocument()->GetInPlaceActiveItem(pView);

    if (pItem != NULL && pItem != this)

        pItem->Close();

    

    COleClientItem::OnActivate();

}


This code makes sure that the current view is valid, closes the active item, if any, and then activates this item.

OnGetItemPosition() (Listing 23.19) is called as part of the in-place activation process.


Listing 23.19  CntrItem.cpp-CShowStringCntrItem:: OnGetItemPosition()

void CShowStringCntrItem::OnGetItemPosition(CRect& rPosition)

{

     ASSERT_VALID(this);



     // During in-place activation, 

     // CShowStringCntrItem::OnGetItemPosition

     // will be called to determine the location of this item.  

     // The default implementation created from AppWizard simply 

     // returns a hard-coded rectangle.  Usually, this rectangle 

     // would reflect the current position of the item relative 

     // to the view used for activation. You can obtain the view 

     // by calling CShowStringCntrItem::GetActiveView.



     // TODO: return correct rectangle (in pixels) in rPosition



     rPosition.SetRect(10, 10, 210, 210);

}


Like OnChange(), the comments are more useful than the actual code. At the moment, the View's OnDraw() function draws the contained object in a hard-coded rectangle, so this function returns that same rectangle. You are instructed to write code that asks the active view where the object is.

OnDeactivateUI() (Listing 23.20) is called when the object goes from being active to inactive.


Listing 23.20  CntrItem.cpp-CShowStringCntrItem:: OnDeactivateUI()

void CShowStringCntrItem::OnDeactivateUI(BOOL bUndoable)

{

     COleClientItem::OnDeactivateUI(bUndoable);



    // Hide the object if it is not an outside-in object

    DWORD dwMisc = 0;

    m_lpObject->GetMiscStatus(GetDrawAspect(), &dwMisc);

    if (dwMisc & OLEMISC_INSIDEOUT)

        DoVerb(OLEIVERB_HIDE, NULL);

}


While the default behavior for contained objects is outside-in, as discussed earlier, you can write inside-out objects. These are activated simply by moving the mouse pointer over them; clicking in the object has the same effect that clicking in that region has while editing the object. For example, if the contained item is a spreadsheet, clicking might select the cell that was clicked. This can be really nice for the user, who can completely ignore the borders between the container and the contained item, but it is harder to write.

OnChangeItemPosition() is called when the item is moved during in-place editing. It, too, contains mostly comments, as shown in Listing 23.21.


Listing 23.21  CntrItem.cpp-CShowStringCntrItem:: OnChangeItemPosition()

BOOL CShowStringCntrItem::OnChangeItemPosition(const CRect& rectPos)

{

     ASSERT_VALID(this);



     // During in-place activation 

     // CShowStringCntrItem::OnChangeItemPosition

     // is called by the server to change the position 

     // of the in-place window.  Usually, this is a result 

     // of the data in the server document changing such that 

     // the extent has changed or as a result of in-place resizing.

     //

     // The default here is to call the base class, which will call

     //  COleClientItem::SetItemRects to move the item

     //  to the new position.



     if (!COleClientItem::OnChangeItemPosition(rectPos))

          return FALSE;



     // TODO: update any cache you may have of the item's rectangle/extent



     return TRUE;

}


This code is supposed to handle moving the object, but it doesn't really. That's because OnDraw() always draws the contained item in the same place. You'll fix that later.

AssertValid() and Dump() are debug functions that simply call the base class functions. The last function in CShowStringCntrItem is Serialize(), which is called by COleDocument:: Serialize(), which in turn is called by the document's Serialize(), as you've already seen. It is shown in Listing 23.22.


Listing 23.22  CntrItem.cpp-CShowStringCntrItem:: Serialize()

void CShowStringCntrItem::Serialize(CArchive& ar)

{

     ASSERT_VALID(this);



     // Call base class first to read in COleClientItem data.

     // Since this sets up the m_pDocument pointer returned from

     //  CShowStringCntrItem::GetDocument, it is a good idea to call

     //  the base class Serialize first.

     COleClientItem::Serialize(ar);



     // now store/retrieve data specific to CShowStringCntrItem

     if (ar.IsStoring())

     {

          // TODO: add storing code here

     }

     else

     {

          // TODO: add loading code here

     }

}


All this does at the moment is call the base class function. COleDocument:: Serialize() stores or loads a number of counters and numbers to keep track of several different contained items, then calls helper functions called WriteItem() or ReadItem() to actually deal with the item. These functions and the helper functions they call are a bit too "behind-the-scenes" for most people, but if you'd like to take a look at them, they are in the MFC source folder (C:\MSDEV\MFC\SRC on many installations) in the file olecli1.cpp. They do their job, which is to serialize the contained item for you.

Shortcomings of This Container  This container application isn't ShowString yet, of course, but it has more important things wrong with it. It isn't a very good container, and that's a direct result of all those TODO tasks that haven't been done. Still, the fact that it is a functioning container is a good measure of the power of the MFC classes COleDocument and COleClientItem. So why not build the application now and run it? After it's running, choose Edit, Insert New Object and insert a Bitmap image. Now that you've seen the code, it shouldn't be a surprise that Paint is immediately launched to edit the item in place, as you see in Figure 23.7.
Figure 23.7 : The boilerplate container can contain items and activate them for in-place editing, like this bitmap image being edited in Paint.

Click outside the bitmap to unselect the item and return control to the container, and nothing happens. Click outside the document and again nothing happens. Are you even still in ShowString? Choose File, New, and you see that you are. The Paint menus and toolbars go away, and a new ShowString document is created. Click the bitmap item again, and you are still editing it in Paint. How can you insert another object into the first document, when the menus are those of Paint? Press Esc to cancel in-place editing, and the menus are ShowString menus again. Insert an Excel chart into the container, and the bitmap disappears as the new Excel chart is inserted, as shown in Figure 23.8. Obviously, this container leaves a lot to be desired.

Figure 23.8 : Inserting an Excel chart gets you a default chart, but it completely covers the old bitmap.

Press Esc to cancel the in-place editing and notice that the view changes a little, as shown in Figure 23.9. That's because CShowStringView::OnDraw() draws the contained item in a 200´200 pixel rectangle, so the chart has to be squeezed a little to fit into that space. It is the server, Excel, in this case, that decides how to fit the item into the space given to it by the container.

Figure 23.9 : Items can look quite different when they are not active.

As you can see, there's a lot to be done to make this feel like a real container. But first, you have to turn it back into ShowString.

Returning the ShowString Functionality

This section is a quick summary of the steps in Chapter 17, "Building Menus and Dialogs." Open the files from the old ShowString as you go so that you can copy code and resources wherever possible. Follow these steps:

  1. In ShowStringDoc.h, add the private member variables and public Get functions to the class.
  2. In CShowStringDoc::Serialize() paste in the code that saves or restores these member variables. Leave the call to COleDocument::Serialize() in place.
  3. In CShowStringDoc::OnNewDocument() paste in the code that initializes the member variables.
  4. In CShowStringView::OnDraw() add the code that draws the string before the code that handles the contained items. Remove the TODO about drawing native data.
  5. Copy the entire Tools menu from the old ShowString to the new container ShowString: Choose File, Open to open the old ShowString.rc, open the IDR_SHOWSTTYPE menu, click the Tools menu, and choose Edit, Copy. Open the new ShowString's IDR_SHOWSTTYPE menu, click the Window menu, and choose Edit, Paste. Do not paste it into the IDR_SHOWSTTYPE_CNTR_IP menu.
  6. Add the accelerator Ctrl+T for ID_TOOLS_OPTIONS as described in Chapter 17, "Building Menus and Dialogs." Add it to the IDR_MAINFRAME accelerator only.
  7. Delete the IDD_ABOUTBOX dialog box from the new ShowString. Copy IDD_ABOUTBOX and IDD_OPTIONS from the old ShowString to the new.
  8. While IDD_OPTIONS has focus, choose View, Class Wizard. Create the COptionsDialog class as in the original ShowString. Remember not to add it to the Component Gallery.
  9. Use Class Wizard to arrange for CShowStringDoc to catch the ID_TOOLS_OPTIONS command.
  10. In ShowStringDoc.cpp, replace the Class Wizard version of CShowStringDoc::OnToolsOptions() with the OnToolsOptions() from the old ShowString, that puts up the dialog box.
  11. In ShowStringDoc.cpp, add #include "OptionsDialog.h" after the #include statements already present.
  12. Use Class Wizard to connect the dialog controls to COptionsDialog member variables, as described in Chapter 17.

Build the application, fix any typos or other simple errors, then execute it. It should run as before, saying Hello, world! in the center of the view. Convince yourself that the Options dialog box still works and that you have restored all the old functionality. Then resize the application and the view as large as possible, so that when you insert an object it does not land on the string. Insert an Excel chart as before, and press Esc to stop editing in place. There you have it: a version of ShowString that is also an ActiveX container. Now it's time to get to work making it a good container.

Moving, Resizing, and Tracking

The first thing you want to do, even when there is only one item contained in ShowString, is to allow the user to move and resize that item. It makes life simpler for the user if you also provide a tracker rectangle, a hashed line around the contained item. This is easy to do with the MFC class CRectTracker.

The first step is to add a member variable to the container item (CShowStringCntrItem) definition in CntrItem.h, to hold the rectangle occupied by this container item. Use the MFC class CRect, like this:


public:

     CRect m_rect;

Why public? Doesn't that break encapsulation? Yes, but implementing both Set and Get functions, or making other classes friends, does too. This makes the code easier to type and read, without any real loss of information hiding.

This needs to be initialized in a function that is called when the container item is first used and then never again. While view classes have OnInitialUpdate() and document classes have OnNewDocument(), container item classes have no such called-only-once function except the constructor. So, initialize the rectangle in the constructor, as shown in Listing 23.23.


Listing 23.23  CntrItem.cpp-Constructor

CShowStringCntrItem::CShowStringCntrItem(CShowStringDoc* pContainer)

     : COleClientItem(pContainer)

{

     m_rect = CRect(10,10,210,210);     

}


The numerical values used here are those in the boilerplate OnDraw() provided by AppWizard. Now, you need to start using the m rect member variable, and setting it. The functions affected are presented in the same order as in the earlier section on CShowStringView.

First, CShowStringView::OnDraw(). Find this line:


m_pSelection->Draw(pDC, CRect(10, 10, 210, 210));

Replace it with this:


m_pSelection->Draw(pDC, m_pSelection->m_rect);

Next, you will change CShowStringCntrItem::OnGetItemPosition(), which needs to return this rectangle. Take away all the comments and the old hardcoded rectangle (leave the ASSERT_VALID macro call), and add this line:


rPosition = m_rect;

The partner function, CShowStringCntrItem::OnChangeItemPosition(), is called when the user moves the item. Here is where m_rect gets changed from the initial value. Remove the comments and add code immediately after the the call to the base class function, COleClientItem::OnChangeItemPosition(). The code to add is:


m_rect = rectPos;

     GetDocument()->SetModifiedFlag();

     GetDocument()->UpdateAllViews(NULL);

Finally, the new member variable needs to be incorporated into CShowStringCntrItem::Serialize(). Remove the comments and add lines in the storing and saving blocks so that the function looks like Listing 23.24.


Listing 23.24  CntrItem.cpp-CShowStringCntrItem::Serialize()

void CShowStringCntrItem::Serialize(CArchive& ar)

{

     ASSERT_VALID(this);



     // Call base class first to read in COleClientItem data.

     // Since this sets up the m_pDocument pointer returned from

     //  CShowStringCntrItem::GetDocument, it is a good idea to call

     //  the base class Serialize first.

     COleClientItem::Serialize(ar);



     // now store/retrieve data specific to CShowStringCntrItem

     if (ar.IsStoring())

     {

          ar << m_rect;

     }

     else

     {

          ar >> m_rect;

     }

}


Build and execute the application, insert a bitmap, and scribble something in it. Press Esc to cancel editing in place, and your scribble shows up in the top-right corner as well. Choose Edit, Bitmap Image Object and then Edit. (Choosing Open allows you to edit it in a different window.) Use the resizing handles that appear to drag the image over to the left, then press Esc to cancel editing in place. The image is drawn at the new position, as expected.

Now for the tracker rectangle. The Microsoft tutorials recommend writing a helper function, SetupTracker(), to handle this. Add these lines to CShowStringView::OnDraw(), just after the call to m_pSelection->Draw():


CRectTracker trackrect;

SetupTracker(m_pSelection,&trackrect);

trackrect.Draw(pDC);

(The one-line statement after the if was not in brace brackets before; don't forget to add them.)

Add the following public function to ShowStringView.h (inside the class definition):


void SetupTracker(CShowStringCntrItem* item, 

CRectTracker* track);

Add the code in Listing 23.25 to ShowStringView.cpp immediately after the destructor.


Listing 23.25  ShowStringView.cpp-CShowStringView::SetupTracker()

void CShowStringView::SetupTracker(CShowStringCntrItem* item, 

     CRectTracker* track)

{

     track->m_rect = item->m_rect;



     if (item == m_pSelection)

     {

          track->m_nStyle |= CRectTracker::resizeInside;

     }



     if (item->GetType() == OT_LINK)

     {

          track->m_nStyle |= CRectTracker::dottedLine;

     }

     else

     {

          track->m_nStyle |= CRectTracker::solidLine;

     }

     if (item->GetItemState() == COleClientItem::openState ||

          item->GetItemState() == COleClientItem::activeUIState)

     {

          track->m_nStyle |= CRectTracker::hatchInside;

     }

}


This code first sets the tracker rectangle to the container item rectangle. Then it adds styles to the tracker. The styles available are as follows:

This code first compares the pointers to this item and the current selection. If they are the same, this item is selected and it gets resize handles. It's up to you whether they go on the inside or the outside. Then it asks the item whether it is linked (dotted line) or not (solid line). Finally, it adds hatching to active items.

Build and execute the application, and try it out. You still cannot edit the contained item by double-clicking it: choose Edit from the cascading menu added at the bottom of the Edit menu. You can't move and resize an inactive object, but if you activate it you can resize it while active, and when you press Esc, it is drawn at its new position.

Handling Multiple Objects and Object Selection

The next step is to catch mouse clicks and double-clicks so that the item can be resized, moved, and activated more easily. This involves testing to see if a click is over a contained item or not.

Hit Testing

You need to write a helper function that returns a pointer to the contained item that the user clicked, or NULL if the user clicked in an area of the view that has no contained item. This function runs through all the items contained in the document. Add the code in Listing 23.26 to ShowStringView.cpp immediately after the destructor.


Listing 23.26  ShowStringView.cpp-CShowStringView::SetupTracker()

CShowStringCntrItem* CShowStringView::HitTest(CPoint point)

{

     CShowStringDoc* pDoc = GetDocument();

     CShowStringCntrItem* pHitItem = NULL;



     POSITION pos = pDoc->GetStartPosition();

     while (pos)

     {

          CShowStringCntrItem* pCurrentItem = 

               (CShowStringCntrItem*) pDoc->GetNextClientItem(pos);

          if ( pCurrentItem->m_rect.PtInRect(point) )

          {

               pHitItem = pCurrentItem;

          }

     }



     return pHitItem;

}


TIP
Don't forget to add the declaration of this public function to the header file.

This function is given a CPoint that describes the point on the screen where the user clicked. Each container item has a rectangle, m_rect, as you've seen earlier, and the CRect class has a member function called PtInRect() that takes a CPoint and returns TRUE if the point is in the rectangle, FALSE if it is not. This code simply loops through the items in this document, using the OLE document member function GetNextClientItem(), and calls PtInRect() for each.

What happens if there are several items in the container, and the user clicks at a point where two or more overlap? The one on top is selected. That's because GetStartPosition() returns a pointer to the bottom item, and GetNextClientItem() works its way up through the items.If two items cover the spot where the user clicked, pHitItem is set to the lower one first, and then, on a later iteration of the while loop, it is set to the higher one. It is the pointer to the higher item that is returned.

Drawing Multiple Items

While that code to loop through all the items is still fresh in your mind, why not fix CShowStringView::OnDraw() so it draws all the items? Leave all the code that draws the string, and replace the code in Listing 23.27 with that in Listing 23.28.


Listing 23.27  ShowStringView.cpp-Lines in OnDraw() to Replace

     // Draw the selection at an arbitrary position.  This code should 

     // be removed once your real drawing code is implemented.  This 

     // position corresponds exactly to the rectangle returned by 

     // CShowStringCntrItem, to give the effect of in-place editing.



     // TODO: remove this code when final draw code is complete.



     if (m_pSelection == NULL)

     {

          POSITION pos = pDoc->GetStartPosition();

          m_pSelection = (CShowStringCntrItem*)pDoc->GetNextClientItem(pos);

     }

     if (m_pSelection != NULL)

     {

          m_pSelection->Draw(pDC, m_pSelection->m_rect);

          CRectTracker trackrect;

          SetupTracker(m_pSelection,&trackrect);

          trackrect.Draw(pDC);

     }



Listing 23.28  ShowStringView.cpp-New Lines in OnDraw()

     POSITION pos = pDoc->GetStartPosition();

     while (pos)

     {

          CShowStringCntrItem* pCurrentItem = 

               (CShowStringCntrItem*) pDoc->GetNextClientItem(pos);

          pCurrentItem->Draw(pDC, pCurrentItem->m_rect);



          if (pCurrentItem == m_pSelection )

          {

               CRectTracker trackrect;

               SetupTracker(pCurrentItem,&trackrect);

               trackrect.Draw(pDC);

          }

     }


Now each item is drawn, starting from the bottom and working up, and if it is selected it gets a tracker rectangle.

Handling Single Clicks

When the user clicks in the client area of the application, a WM_LBUTTONDOWN message is sent. This message should be caught by the view. Because you are editing the view source now, use the WizardBar to add a handler for this message. Click the Messages box in the WizardBar and scroll down until you find WM_LBUTTONDOWN, then select it. A message box appears like the one in Figure 23.10, asking if you want to add a function to handle this message. Click Yes, and a function is added to the class for you and the file scrolls to the skeleton implementation.

Figure 23.10 : Using the WizardBar makes adding a function as simple as selecting a message that is not yet handled.

Add the code in Listing 23.29 to the empty OnLButtonDown() that the Wizard bar generated.


Listing 23.29  ShowStringView.cpp-CShowStringView::OnLButtonDown()

void CShowStringView::OnLButtonDown(UINT nFlags, CPoint point) 

{

     CShowStringCntrItem* pHitItem = HitTest(point);

     SetSelection(pHitItem);

     if (pHitItem)

     {

          CRectTracker track;

          SetupTracker(pHitItem, &track);

          UpdateWindow();

          if (track.Track(this,point))

          {

               Invalidate();

               pHitItem->m_rect = track.m_rect;

               GetDocument()->SetModifiedFlag();

          }

     }

     

     CView::OnLButtonDown(nFlags, point);

}


This determines which item has been selected and sets it. (SetSelection() isn't written yet.) Then, if something has been selected, it draws a tracker rectangle around it and calls CRectTracker::Track(), which allows the user to resize the rectangle. After the resizing, the item is sized to match the tracker rectangle and is redrawn.

SetSelection() is pretty straightforward. Add the definition of this public member function to the header file, ShowStringView.h, and the code in Listing 23.30 to ShowStringView.cpp.


Listing 23.30  ShowStringView.cpp-CShowStringView:: SetSelection()

void CShowStringView::SetSelection(CShowStringCntrItem* item)

{

     // if an item is being edited in place, close it

     if ( item == NULL || item != m_pSelection)

     {

          COleClientItem* pActive = 

               GetDocument()->GetInPlaceActiveItem(this);

          if (pActive != NULL && pActive != item)

          {

               pActive->Close();

          }

     }

     Invalidate();

     m_pSelection = item;

}


This closes the item being edited in place if a different item, or no item, has been selected. Then it calls for a redraw, and sets m_pSelection. Build and execute ShowString, insert an object, move it away, insert another-you should see something like Figure 23.11. Notice the resizing handles around the bitmap, indicating that it is selected.

Figure 23.11 : ShowString can now hold multiple items, and the user can move and resize them intuitively.

You may have noticed that the cursor doesn't change as you move or resize. That's because you didn't tell it to. Luckily, it's easy to tell it to: CRectTracker has a SetCursor() member function, and all you need to do is call it when a WM_SETCURSOR message is sent. Again, it should be the view that catches this message, and, again, Wizard Bar is the quickest way to catch it. Choose WM_SETCURSOR from the Messages box on the Wizard Bar (scroll to the very bottom), and say Yes when asked if you want a new function. Add the code in Listing 23.31 to the empty function that was generated for you.


Listing 23.31  ShowStringView.cpp-CShowStringView:: OnSetCursor()

BOOL CShowStringView::OnSetCursor(CWnd* pWnd, UINT nHitTest, 

     UINT message) 

{

     if (pWnd == this && m_pSelection != NULL)

     {

          CRectTracker track;

          SetupTracker(m_pSelection, &track);

          if (track.SetCursor(this, nHitTest))

          {

               return TRUE;

          }

     }

     

     return CView::OnSetCursor(pWnd, nHitTest, message);

}


This code does nothing unless the cursor change involves this view and there is a selection. It gives SetCursor() a chance to change the cursor, since the tracking object knows where the rectangle is and whether the cursor is over a boundary or sizing handle, and, if SetCursor() didn't deal with it, it lets the base class handle it. Build and execute ShowString and you should see cursors that give you feedback as you move and resize.

Handling Double-Clicks

When a user double-clicks a contained item, the primary verb should be called. For most objects, the primary verb is to edit in place, but for some, such as sound files, it is Play. Use Wizard Bar to catch the WM_LBUTTONDBLCLK message, and add the code in Listing 23.32 to the new function:


Listing 23.32  ShowStringView.cpp-CShowStringView:: OnLButtonDblClk()

void CShowStringView::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);

}


First, this function handles the fact that this item has been clicked; calling OnLbuttonDown() draws the tracker rectangle, sets m_pSelection, and so on. Then, if the user holds down Ctrl while double-clicking, the item is opened, otherwise the primary verb is called. Finally, the base class function is called. Build and execute ShowString and try double-clicking. Insert an object, press Esc to stop editing it, move it, resize it, and double-click it to edit in place.

Implementing Drag and Drop

The last step to make ShowString a completely up-to-date ActiveX container application is to implement drag and drop. The user should be able to grab a contained item and drag it out of the container, or hold down Ctrl while dragging to drag out a copy and leave the original behind. The user should also be able to drag items from elsewhere and drop them into this container just as though they had been inserted through the Clipboard. In other words, the container should operate as a drag source and a drop target.

Implementing a Drag Source

Because CShowStringCntrItem inherits from COleClientItem, implementing a drag source is really easy. When a user clicks a contained object, he may start to drag it away. Add these lines at the end of CShowStringView::OnLButtonDown() just before the call to the base class function:


if (m_pSelection)

{

    CPoint newpoint = point - m_pSelection->m_rect.TopLeft();

    if (m_pSelection->DoDragDrop(m_pSelection->m_rect, 

         newpoint) == DROPEFFECT_MOVE)

    {

         Invalidate();

         delete m_pSelection;

         m_pSelection = NULL;

    }

}

This code just calls the DoDragDrop() member function of CShowStringCntrItem, inherited from COleClientItem and not overridden. It returns DROPEFFECT_MOVE if the item was moved out of the container and needs to be deleted. Build and execute ShowString, insert a new object, press Esc to stop editing in place, then drag the inactive object to an ActiveX container application such as Microsoft Excel.

Implementing a Drop Target

It is harder to make ShowString a drop target (it could hardly be easier). If you dragged a contained item out of ShowString and dropped it into another container, try dragging that item back into ShowString. The cursor changes to a circle with a slash through it, meaning "you can't drop that here." In this section, you make the necessary code changes that allow you to drop it there after all.

You need to register your view as a place that things can be dropped. Next, you need to handle the following four events that can occur:

Registering the View as a Drop Target

To register the view as a drop target, add a COleDropTarget member variable to the view. In showstringview.h, add this line to the class definition:


COleDropTarget m_droptarget;

To handle registration, override OnCreate() for the view, called when the view is created. Use ClassWizard or the Wizard bar to catch the WM_CREATE message. Add the code in Listing 23.33 to the empty function generated for you.


Listing 23.33  ShowStringView.cpp-CShowStringView::OnCreate()

int CShowStringView::OnCreate(LPCREATESTRUCT lpCreateStruct) 

{

     if (CView::OnCreate(lpCreateStruct) == -1)

          return -1;

     

     if (m_droptarget.Register(this))

     {

          return 0;

     }

     else

     {

          return -1;

     }

}


OnCreate() returns 0 if everything is going well, and -1 if the window should be destroyed. This code calls the base class function, then uses COleDropTarget::Register() to register this view as a place to drop things.

Setting Up Function Skeletons and Adding Member Variables

The four events that happen in your view correspond to four virtual functions you must override: OnDragEnter(), OnDragOver(), OnDragLeave(), and OnDrop(). Use the Wizard bar to add overrides of these functions. The function names appear before the actual messages in the Messages drop-down input box.

OnDragEnter() sets up a focus rectangle that shows the user where the item would go if it were dropped here. This is maintained and drawn by OnDragOver(). But first, a number of member variables related to the focus rectangle must be added to CShowStringView. Add these lines to ShowStringView.h:


CPoint m_dragpoint;

CSize m_dragsize;

CSize m_dragoffset;

A data object contains a great deal of information about itself, in various formats. There is, of course, the actual data, as text, device independent bitmap (DIB), or whatever other format is appropriate. But there is also information about the object itself. If you request data in the "Object Descriptor" format, you can find out the size of the item and where on the item the user originally clicked, and the offset from the mouse to the upper-left corner of the item. These formats are generally referred to as Clipboard formats because they were originally used for cut and paste through the Clipboard.

To ask for this information, you call the data object's GetGlobalData() member function, passing it a parameter that means "Object Descriptor, please." Rather than building this parameter from a string every time, you build it once and store it in a static member of the class. When a class has a static member variable, every instance of the class looks at the same memory location to see that variable. It is initialized (and memory is allocated for it) once, outside the class.

Add this line to showstringview.h:


static CLIPFORMAT m_cfObjectDescriptorFormat;

In showstringview.cpp, just before the first function, add these lines:


CLIPFORMAT CShowStringView::m_cfObjectDescriptorFormat =

     (CLIPFORMAT) ::RegisterClipboardFormat("Object Descriptor");

This makes a CLIPFORMAT from the string "Object Descriptor" and saves it in the static member variable for all instances of this class to use. Using a static member variable speeds up dragging over your view.

Your view does not accept any and all items that get dropped on it. Add a BOOL member variable to the view that indicates whether it accepts the item that is now being dragged over it:


BOOL m_OKtodrop;

There is one last member variable to add to CShowStringView. As the item is dragged across the view, a focus rectangle is repeatedly drawn and erased. Add another BOOL member variable that tracks the status of the focus rectangle:


BOOL m_FocusRectangleDrawn;

Initialize this, in the view constructor, to FALSE:


CShowStringView::CShowStringView()

{

     m_pSelection = NULL;

     m_FocusRectangleDrawn = FALSE;

}

OnDragEnter

OnDragEnter() is called when the user first drags an item over the boundary of the view. It sets up the focus rectangle and then hands over to OnDragOver(). As the item continues to move, OnDragOver() is called repeatedly until the user drags the item out of the view or drops it in the view. The overall structure of OnDragEnter() is shown in Listing 23.34.


Listing 23.34  ShowStringView.cpp-CShowStringView::OnDragEnter()

DROPEFFECT CShowStringView::OnDragEnter(COleDataObject* pDataObject, 

DWORD dwKeyState, CPoint point) 

{

     ASSERT(!m_FocusRectangleDrawn);

     

     // check that the data object can be dropped in this view

     // set dragsize and dragoffset with call to GetGlobalData

     // convert sizes with a scratch dc

     // hand off to OnDragOver

     return OnDragOver(pDataObject, dwKeyState, point);

}


First, you check that whatever pDataObject carries is something you can make a COleClientItem (and therefore a CShowsStringCntrItem) from. If not, the object cannot be dropped here, and you return DROPEFFECT_NONE, as shown in Listing 23.35.


Listing 23.35  ShowStringView.cpp-Can Object Be Dropped?

     // check that the data object can be dropped in this view

     m_OKtodrop = FALSE;

     if (!COleClientItem::CanCreateFromData(pDataObject))

          return DROPEFFECT_NONE;



     m_OKtodrop = TRUE;


Now the weird stuff starts. The GetGlobalData() member function of the data item that is being dragged into this view is called to get the object descriptor information mentioned earlier. It returns a handle of a global memory block. Then the SDK function GlobalLock() is called to convert the handle into a pointer to the first byte of the block and prevent any other object from allocating the block. This is cast to a pointer to an object descriptor structure (the undyingly curious can check about 2,000 lines into oleidl.h, in the C:\MSDEV\include folder for most installations, to see the members of this structure) so that the sizel and pointl elements can be used to fill the m_dragsize and m_dragoffset member variables.

TIP
There is not a number 1 at the end of those structure elements, but a lowercase letter L. And the elements of the sizel structure are cx and cy, but the elements of the pointl structure are x and y. Don't get carried away cutting and pasting.

Finally GlobalUnlock() reverses the effects of GlobalLock(), making the block accessible to others, and GlobalFree() frees the memory. It ends up looking like Listing 23.36.


Listing 23.36  ShowStringView.cpp-Set dragsize and dragoffset

     // set dragsize and dragoffset with call to GetGlobalData

     HGLOBAL hObjectDescriptor = pDataObject->GetGlobalData(

          m_cfObjectDescriptorFormat);

     if (hObjectDescriptor)

     {

          LPOBJECTDESCRIPTOR pObjectDescriptor = 

               (LPOBJECTDESCRIPTOR) GlobalLock(hObjectDescriptor);

          ASSERT(pObjectDescriptor);

          m_dragsize.cx = (int) pObjectDescriptor->sizel.cx;

          m_dragsize.cy = (int) pObjectDescriptor->sizel.cy;

          m_dragoffset.cx = (int) pObjectDescriptor->pointl.x;

          m_dragoffset.cy = (int) pObjectDescriptor->pointl.y;     

          GlobalUnlock(hObjectDescriptor);

          GlobalFree(hObjectDescriptor);

     }

     else

     {

          m_dragsize = CSize(0,0);

          m_dragoffset = CSize(0,0);

     }


NOTE
Global memory, also called shared application memory, is allocated from a different place than the memory available from your process space. It is the memory to use when two different processes need to read and write the same memory, and so it comes into play when using ActiveX.
For some ActiveX operations, global memory is too small-imagine trying to transfer a 40MB file through global memory. There is a more general function than GetGlobalData, called (not surprisingly) GetData, which can transfer the data through a variety of storage medium choices. Since the object descriptors are small, asking for them in global memory is a sensible approach.

If the call to GetGlobalData() didn't work, set both member variables to zero by zero rectangles. Next, convert those rectangles from OLE coordinates (which are device-independent) to pixels:


// convert sizes with a scratch dc

CClientDC dc(NULL);

dc.HIMETRICtoDP(&m_dragsize);

dc.HIMETRICtoDP(&m_dragoffset);

HIMETRICtoDP() is a very useful function that happens to be a member of CClientDC, which inherits from the familiar CDC of Chapter 11, "Drawing on the Screen." You create an instance of CClientDC just so you can call the function.

OnDragEnter() closes with a call to OnDragOver(), so that's the next function to write.

OnDragOver

This function returns a DROPEFFECT. As you saw earlier in the drag source section, if you return DROPEFFECT_MOVE the source deletes the item from itself. Returning DROPEFFECT_NONE rejects the copy. It is OnDragOver() that deals with preparing to accept or reject a drop. The overall structure of the function looks like this:


DROPEFFECT CShowStringView::OnDragOver(COleDataObject* pDataObject, 

DWORD dwKeyState, CPoint point) 

{

     // return if dropping is already rejected

     // determine drop effect according to keys depressed

     // adjust focus rectangle

}

First, check to see if OnDragEnter() or an earlier call to OnDragOver() already rejected this possible drop:


// return if dropping is already rejected

if (!m_OKtodrop)

{

     return DROPEFFECT_NONE;

}

Next, look at the keys that the user is holding down now, available in the parameter passed to this function, dwKeyState. The code you need to add (Listing 23.37) is pretty straightforward.


Listing 23.37  ShowStringView.cpp-Determine Drop Effect

     // determine drop effect according to keys depressed

     DROPEFFECT dropeffect = DROPEFFECT_NONE;



     if ((dwKeyState & (MK_CONTROL|MK_SHIFT) )

          == (MK_CONTROL|MK_SHIFT))

     {

          // Ctrl+Shift force a link

          dropeffect = DROPEFFECT_LINK;

     }



     else if ((dwKeyState & MK_CONTROL)     == MK_CONTROL)

     {

          // Ctrl forces a copy

          dropeffect = DROPEFFECT_COPY;

     }

     else if ((dwKeyState & MK_ALT) == MK_ALT)

     {

          // Alt forces a move

          dropeffect = DROPEFFECT_MOVE;

     }

     else

     {

          // default is to move

          dropeffect = DROPEFFECT_MOVE;

     }


NOTE
This code has to be a lot more complex if the document might be smaller than the view, as can happen when you are editing a bitmap in Paint, and especially if the view can scroll. The OLE container sample included on the CD handles these contingencies. Look in the CD folder MSDVEV\SAMPLES\MFC\OLE\DRAWCLI for the file drawvw.cpp and compare that code for OnDragOver to this code.

If the item has moved since the last time OnDragOver() was called, the focus rectangle has to be erased and redrawn at the new location. Because the focus rectangle is a simple XOR of the colors, drawing it a second time in the same place removes it. The code to adjust the focus rectangle is in Listing 23.38.


Listing 23.38  ShowStringView.cpp-Adjust the Focus Rectangle

     // adjust focus rectangle



     point -= m_dragoffset;

     if (point == m_dragpoint)

     {

          return dropeffect;

     }

     

     CClientDC dc(this);



     if (m_FocusRectangleDrawn)

     {

          dc.DrawFocusRect(CRect(m_dragpoint, m_dragsize));

          m_FocusRectangleDrawn = FALSE;

     }



     if (dropeffect != DROPEFFECT_NONE)

     {

          dc.DrawFocusRect(CRect(point, m_dragsize));

          m_dragpoint = point;

          m_FocusRectangleDrawn = TRUE;

     }


The first time OnDragOver() is called, m_dragpoint is uninitialized. That doesn't matter, because m_FocusRectangleDrawn is FALSE, and an ASSERT in OnDragEnter() guarantees it. When m_FocusRectangleDrawn is set to TRUE, m_dragpoint gets a value at the same time.

Finally, replace the return statement that was generated for you with one that returns the calculated DROPEFFECT:


return dropeffect;

OnDragLeave

Sometimes a user drags an item right over your view and out the other side. OnDragLeave() just tidies up a little by removing the focus rectangle, as shown in Listing 23.39.


Listing 23.39  ShowStringView.cpp-ShowStringView::OnDragLeave()

void CShowStringView::OnDragLeave() 

{

     CClientDC dc(this);

     if (m_FocusRectangleDrawn)

     {

          dc.DrawFocusRect(CRect(m_dragpoint, m_dragsize));

          m_FocusRectangleDrawn = FALSE;

     }

}


OnDragDrop

If the user lets go of an item that is being dragged over ShowString, the item lands in the container and OnDragDrop() is called. The overall structure is in Listing 23.40.


Listing 23.40  ShowStringView.cpp-Structure of OnDrop()

BOOL CShowStringView::OnDrop(COleDataObject* pDataObject, DROPEFFECT dropEffect,

                                                                           CPoint point) 

{

     ASSERT_VALID(this);

     // remove focus rectangle

     // paste in the data object

     // adjust the item dimensions, and make it the current selection

     // update views and set modified flag

     return TRUE;

}


Removing the focus rectangle is simple, as shown in Listing 23.41.


Listing 23.41  ShowStringView.cpp-Removing the Focus Rectangle

     // remove focus rectangle

     CClientDC dc(this);

     if (m_FocusRectangleDrawn)

     {

          dc.DrawFocusRect(CRect(m_dragpoint, m_dragsize));

          m_FocusRectangleDrawn = FALSE;

     }


Next, create a new item to hold the data object, as shown in Listing 23.42.


Listing 23.42  ShowStringView.cpp-Paste in the Data Object

     // paste in the data object

     CShowStringDoc* pDoc = GetDocument();

     CShowStringCntrItem* pNewItem = new CShowStringCntrItem(pDoc);

     ASSERT_VALID(pNewItem);

     if (dropEffect & DROPEFFECT_LINK)

     {

          pNewItem->CreateLinkFromData(pDataObject);

     }

     else

     {

          pNewItem->CreateFromData(pDataObject);

     }

     ASSERT_VALID(pNewItem);


The size of the container item needs to be set, as shown in Listing 23.43.


Listing 23.43  ShowStringView.cpp-Adjust Item Dimensions

     // adjust the item dimensions, and make it the current selection

     CSize size;

     pNewItem->GetExtent(&size, pNewItem->GetDrawAspect());

     dc.HIMETRICtoDP(&size);

     point -= m_dragoffset;

     pNewItem->m_rect = CRect(point,size);

     m_pSelection = pNewItem;


Notice that this code adjusts the place where the user drops the item (point) by m_dragoffset, the coordinates into the item where the user clicked originally.

Finally, make sure the document gets saved on exit, since pasting in a new container item changes it, and redraw the view:


// update views and set modified flag

pDoc->SetModifiedFlag();

pDoc->UpdateAllViews(NULL);

return TRUE;

This function always returns TRUE since there is no error checking at the moment that might require a return of FALSE. Notice, however, that most problems have been prevented-for example, if the data object cannot be used to create a container item, then the DROPEFFECT would have been set to DROPEFFECT_NONE in OnDragEnter() and this code would never have been called. You can be confident this code works.

Testing the Drag Target

All the confidence in the world is no substitute for testing. Build and execute ShowString, and try dragging something into it. To test both the drag source and drop target aspects at once, drag something out and then drag it back in. Now this is starting to become a really useful container. There's only one thing left to do.

Deleting an Object

You can remove an object from your container by dragging it away somewhere, but it makes sense to implement deleting in a more obvious and direct way. The menu item generally used for this is Edit Delete, so you start by adding this item to the IDR_SHOWSTTYPE menu before the Insert New Object item. Don't let Developer Studio set the ID to ID_EDIT_DELETE; instead, change it to ID_EDIT_CLEAR, the traditional resource ID for the command that deletes a contained object. Move to another menu item and then return to Edit, Delete, and you see that the prompt has been filled in for you as Erase the selection\nErase automatically.

It is the view that needs to handle this command, so use the Wizard bar in Showstringview.cpp to catch it. Choose ID_EDIT_CLEAR from the Object IDs drop-down box, and COMMAND from the Messages box, agree to add a function, and then choose UPDATE_COMMAND_UI from the Messages box and add another function. The code for these two handlers is very simple. Since the update handler is simpler, add it first:


void CShowStringView::OnUpdateEditClear(CCmdUI* pCmdUI) 

{

     pCmdUI->Enable(m_pSelection != NULL);

}

If there is a current selection, it can be deleted. If there is not a current selection, the menu item is disabled (grayed). The code to handle the command isn't much longer: it's in Listing 23.44.


Listing 23.44  ShowStringView.cpp-CShowStringView::OnEditClear()

void CShowStringView::OnEditClear() 

{

     if (m_pSelection)

     {

          m_pSelection->Delete();

          m_pSelection = NULL;

          GetDocument()->SetModifiedFlag();

          GetDocument()->UpdateAllViews(NULL);

     }

}


This code checks that there is a selection (even though the menu item is grayed when there is no selection), and then deletes it, sets it to NULL so there is no longer a selection, makes sure the document is marked as modified so the user is prompted to save it when exiting, and gets the view redrawn without the deleted object.

Build and execute ShowString, insert something, and delete it. Now it's an intuitive container that does what you expect a container to do.

From Here...

This chapter developed a powerful container. The boilerplate code generated by AppWizard produced a container that has a number of shortcomings, but the steps presented in this chapter corrected them and built an inuitive interface for the ActiveX container version of ShowString. To learn more about some related topics, check these chapters: