UI Activation and Deactivation

After we have the in-place–activation state handled, we can add the UI-active state. Cosmo handles the state through CFigure::UIActivate and CFigure::UIDeactivate, which are called from IOleObject::DoVerb(OLEIVERB_UIACTIVATE) and IOleInPlaceObject::UIDeactivate:


HRESULT CFigure::UIActivate(void)
{
if (NULL!=m_pIOleIPSite)
m_pIOleIPSite->OnUIActivate();

SetFocus(m_pHW->Window());

if (NULL!=m_pIOleIPFrame)
{
m_pIOleIPFrame->SetActiveObject(m_pImpIOleIPActiveObject
, PSZ(IDS_INPLACETITLE));
}

if (NULL!=m_pIOleIPUIWindow)
{
m_pIOleIPUIWindow->SetActiveObject(m_pImpIOleIPActiveObject
, PSZ(IDS_INPLACETITLE));
}

InPlaceToolsCreate();
InPlaceMenuCreate();
return NOERROR;
}

void CFigure::UIDeactivate(void)
{
InPlaceToolsDestroy();
InPlaceMenuDestroy();

if (NULL!=m_pIOleIPFrame)
m_pIOleIPFrame->SetActiveObject(NULL, NULL);

if (NULL!=m_pIOleIPUIWindow)
m_pIOleIPUIWindow->SetActiveObject(NULL, NULL);

if (NULL!=m_pIOleIPSite)
m_pIOleIPSite->OnUIDeactivate(FALSE);

return;
}

The most critical parts of these functions are the calls to IOleInPlaceSite::OnUIActivate and OnUIDeactivate, which let the container know when to initialize or uninitialize its own UI-active state. The next step is to pass your IOleInPlaceActiveObject implementation to both IOleInPlaceFrame::SetActiveObject and IOleInPlaceUIWindow::SetActiveObject (if applicable)3 during UI activation, passing NULL pointers to deactivate. In addition, UI activation should include setting the focus to your in-place window so that you can receive keyboard accelerators. The call to SetFocus here gives the hatch window the focus, which in turn gives it to Cosmo's Polyline window.

Assemble and Disassemble the Menu

Cosmo encapsulates menu creation and destruction in CFigure::InPlaceMenuCreate and CFigure::InPlaceMenuDestroy. As described in Chapter 22, creation of the menu involves these steps:

Create a new menu with the Windows API function CreateMenu.

Call IOleInPlaceFrame::InsertMenus to have the container make its contribution, also passing it the OLEMENUGROUPWIDTHS array, in which the container stores the number of items in each of its groups.

Call the Windows API InsertMenu to add your own menu items to the shared menu in the appropriate places and to fill in the remainder of the OLEMENUGROUPWIDTHS array.

Call OleCreateMenuDescriptor with the menu handle and the OLEMENUGROUPWIDTHS array.

Call IOleInPlaceFrame::SetMenu, passing the menu handle, the menu descriptor from step 4, and the handle of the window to receive messages generated from your items on this menu, typically your frame window.

If you have an object that doesn't need a shared menu at all, which is the case for this chapter's version of Polyline, you need to call only IOleInPlaceFrame::SetMenu(NULL, NULL, hWnd), in which hWnd is the object's window. This call tells the container to keep its own menu active.

Cosmo, however, does use a shared menu. It assembles the menu as shown in the following:


BOOL CFigure::InPlaceMenuCreate(void)
{
HMENU hMenu, hMenuT;
UINT uTemp=MF_BYPOSITION œ MF_POPUP;
UINT i;
OLEMENUGROUPWIDTHS mgw;

for (i=0; i<6; i++)
mgw.width[i]=0;

//We already have pop-up menu handles in m_pFR->m_phMenu[].

//Create new shared menu and let container do its thing.
hMenu=CreateMenu();
m_pIOleIPFrame->InsertMenus(hMenu, &mgw);

//Add our menus, remembering that container
//has already added its menus.
InsertMenu(hMenu, (WORD)mgw.width[0]
, uTemp, (UINT)m_pFR->m_phMenu[1], PSZ(IDS_MENUEDIT));

//Add Open item to Edit menu.
AppendMenu(m_pFR->m_phMenu[1], MF_SEPARATOR, 0, NULL);
AppendMenu(m_pFR->m_phMenu[1], MF_STRING, IDM_EDITOPEN
, PSZ(IDS_MENUOPEN));

InsertMenu(hMenu, (WORD)mgw.width[0]+1+(WORD)mgw.width[2]
, uTemp, (UINT)m_pFR->m_phMenu[2], PSZ(IDS_MENUCOLOR));

InsertMenu(hMenu, (WORD)mgw.width[0]+1+(WORD)mgw.width[2]+1
, uTemp, (UINT)m_pFR->m_phMenu[3], PSZ(IDS_MENULINE));

//Window menu position changes between MDI and SDI.
#ifdef MDI
hMenuT=m_pFR->m_phMenu[5];
#else
hMenuT=m_pFR->m_phMenu[4];
#endif

InsertMenu(hMenu, (WORD)mgw.width[0]+1+(WORD)mgw.width[2]+2
+(WORD)mgw.width[4], uTemp, (UINT)hMenuT, PSZ(IDS_MENUHELP));

//Tell OLE how many items in each group are ours.
mgw.width[1]=1;
mgw.width[3]=2;
mgw.width[5]=1;

m_hMenuShared=hMenu;
m_hOLEMenu=OleCreateMenuDescriptor(m_hMenuShared, &mgw);

m_pIOleIPFrame->SetMenu(m_hMenuShared, m_hOLEMenu, m_pFR->Window());
return TRUE;
}

You can see how Cosmo uses the container's group widths in OLEMENUGROUPWIDTHS to position our own Edit, Object, and Window groups correctly. Also, we complete this array by filling elements 0, 2, and 4.

Cosmo takes advantage of the capability of Windows to have multiple menus share the same pop-up handles. During an in-place session, Cosmo puts the same Edit, Color, Line, and Help menus on the shared menus that it displays on its own menu bar, using the same pop-up handles that are stored in its CCosmoFrame::m_phMenu array. This saves us the trouble of re-creating each pop-up menu all over again, but it means that we have to be very careful when we disassemble the menu, as we'll see shortly.

Cosmo also makes a small addition to the Edit menu for in-place uses: an Open item, which causes the in-place–active object to deactivate and open in a full window. We'll see how to process this command later, but it is a nice addition for an object to provide.4

The object is responsible for maintaining the menu it creates here, so remember to save the handle with the object, as Cosmo does in CFigure::m_hMenuShared. The object must also save the menu descriptor, a variable of type HOLEMENU (such as CFigure::m_hOLEMenu) that it gets back from OleCreateMenuDescriptor so that it can destroy the descriptor later. But, of course, we still have to send the descriptor to the container through IOleInPlaceFrame::SetMenu, along with the handle of the window to which you want menu messages sent. Generally, this window is your frame window because it's already set up to receive menu messages.

When we deactivate the object's user interface, we'll need to disassemble this shared menu. If you have no menu in the first place, the object (Polyline, for example) has nothing to do. Cosmo, however, performs the following steps to dismantle the menu created in the code above:

Calls IOleInPlaceFrame::SetMenu with NULLs.

Calls OleDestroyMenuDescriptor to free the menu descriptor.

Removes each of its menu items from the shared menu.

Calls IOleInPlaceFrame::RemoveMenus to remove the container menus.

Calls the Windows API function DestroyMenu to free the menu itself.

Here's the code to do it:


BOOL CFigure::InPlaceMenuDestroy(void)
{
int cItems, i, j;
HMENU hMenuT;

//If we don't have shared menu, nothing to do.
if (NULL==m_hMenuShared)
return TRUE;

//Stop container frame from using this menu.
m_pIOleIPFrame->SetMenu(NULL, NULL, NULL);

//Clean up what we got from OleCreateMenuDescriptor.
OleDestroyMenuDescriptor(m_hOLEMenu);
m_hOLEMenu=NULL;

cItems=GetMenuItemCount(m_hMenuShared);

/*
* Walk backward down the menu. For each popup, see whether it
* matches any other popup we know about, and if so, remove it
* from shared menu.
*/
for (i=cItems; i>=0; i--)
{
hMenuT=GetSubMenu(m_hMenuShared, i);

for (j=0; j<=CMENUS; j++)
{
/*
* If submenu matches any we have, remove it; don't
* delete. Because we're walking backward, this
* affects only positions of those menus after us, so
* GetSubMenu call above is not affected.
*/

if (hMenuT==m_pFR->m_phMenu[j])
RemoveMenu(m_hMenuShared, i, MF_BYPOSITION);
}
}

//Remove Open item and separator from Edit menu.
RemoveMenu(m_pFR->m_phMenu[1], 6, MF_BYPOSITION);
RemoveMenu(m_pFR->m_phMenu[1], 5, MF_BYPOSITION);

if (NULL!=m_pIOleIPFrame)
m_pIOleIPFrame->RemoveMenus(m_hMenuShared);

DestroyMenu(m_hMenuShared);
m_hMenuShared=NULL;
return TRUE;
}

The trick here is to remove each menu item that you added before. I emphasize this because you will generally be sharing menu handles between this menu and your normal server's menu, so destroying those pop-up items is not a good idea. Cosmo ensures that it removes all of its items by removing any popup it recognizes in the shared menu (that is, any handle that is also stored in m_phMenu). It also removes the extra Open item we added in the assembly phase. After we've called the IOleInPlaceFrame::RemoveMenus function, we can call DestroyMenu to free the resource. Then we're finished.

It's extremely important that you remove all your menu items properly before calling DestroyMenu because that function also destroys any popups on that menu as well. If you experience weird problems when assembling or disassembling your shared menu, comment out your call to DestroyMenu and see whether the problem still exists. If it does, you are not cleaning up your menu properly. If you missed it earlier, go back and read the sidebar "EXPERIENCE: Menu Destruction—Just Do It (Right)!" in Chapter 22 on page 1046 for a description of what happened when I didn't do it right. I had one major-league hair-puller with this one, and it all turned out to be a bug in my make file, of all things.

Create and Destroy In-Place Tools

To complete full UI activation, we now need to create any in-place tools we want, including toolbars or toolboxes on any side of the container's frame or document window. We can also create floating pop-up windows as needed. Again, be sure that none of these windows send unexpected messages to the container, which will be the parent window in many cases, and that any tools added to the frame or document windows use WS_CLIPSIBLINGS.

Cosmo demonstrates this part of UI activation by using a single toolbar. The toolbar is created in CFigure::InPlaceToolsCreate, which executes these steps:

Negotiate tool space with the container by calling the RequestBorderSpace function in IOleInPlaceFrame for frame-level tools and in IOleInPlaceUIWindow for document windows.

When the container accepts your requests, send those same numbers to the SetBorderSpace members of the appropriate interface.

Create your tools either as child windows of the container's frame or document window or as pop-up windows, and then make them visible. You can use the rectangle from the GetBorder function of the appropriate container interface to set the initial dimensions of the tools if necessary.

All units used here are device units (pixels) because we're dealing with windows whose dimensions are always in device units. Don't use HIMETRIC, which is used almost everywhere else in OLE. Anyway, here's Cosmo's implementation of its in-place tools:


BOOL CFigure::InPlaceToolsCreate(void)
{
BORDERWIDTHS bw;
HWND hWnd;
UINT uState=GIZMO_NORMAL;
UINT utCmd =GIZMOTYPE_BUTTONCOMMAND;
UINT utEx =GIZMOTYPE_BUTTONATTRIBUTEEX;
UINT i;
HBITMAP hBmp;
RECT rc;

//We don't need anything on document, so send zeros.
SetRectEmpty((LPRECT)&bw);

if (NULL!=m_pIOleIPUIWindow)
m_pIOleIPUIWindow->SetBorderSpace(&bw);

if (NULL==m_pIOleIPFrame)
return FALSE;

//Reserve frame space.
if (!InPlaceToolsRenegotiate())
{
//If container doesn't allow us any, don't ask for any.
m_pIOleIPFrame->SetBorderSpace(&bw);
return FALSE;
}

//Create toolbar window.
m_pIOleIPFrame->GetWindow(&hWnd);

//If we already have a toolbar, just show it again.
if (NULL!=m_pTB)
{
ShowWindow(m_pTB->Window(), SW_SHOW);
return TRUE;
}

m_pTB=new CToolBar(m_pFR->m_hInst);

if (NULL==m_pTB)
{
SetRectEmpty((LPRECT)&bw);
m_pIOleIPFrame->SetBorderSpace(&bw);
return FALSE;
}

m_pTB->Init(hWnd, ID_GIZMOBAR, m_cyBar);
g_pInPlaceTB=m_pTB;

//Ensure that tools are initially invisible.
ShowWindow(m_pTB->Window(), SW_HIDE);

//Tell toolbar whom to send messages to.
m_pTB->HwndAssociateSet(m_pFR->m_hWnd);

[Omitted code to create all individual tools]

//Make tools visible.
ShowWindow(m_pTB->Window(), SW_SHOW);

return TRUE;
}

Because we don't want any document tools, we tell this to the container by calling IOleInPlaceUIWindow::SetBorderSpace with a BORDERWIDTHS structure full of zeros. (Because the structure is the same as a RECT, we can use SetRectEmpty here.) This call means, "We want no space and no tools showing." If we call SetBorderSpace with a NULL pointer, however, that tells the container, "We don't want any space, and you can leave your tools showing." An object that does nothing for the UI-active state should call SetBorderSpace with NULLs for the container's document and frame window as necessary.

Cosmo wants a toolbar in the frame window, so it negotiates for space in CFigure::InPlaceToolsRenegotiate. This separate function exists because we'll need it later in IOleInPlaceActiveObject::ResizeBorder:


BOOL CFigure::InPlaceToolsRenegotiate(void)
{
HRESULT hr;
BORDERWIDTHS bw;

SetRect((LPRECT)&bw, 0, m_pFR->m_cyBar, 0, 0);

hr=m_pIOleIPFrame->RequestBorderSpace(&bw);

if (NOERROR!=hr)
return FALSE;

//Safety net: RequestBorderSpace can modify values in bw.
SetRect((LPRECT)&bw, 0, m_pFR->m_cyBar, 0, 0);

m_pIOleIPFrame->SetBorderSpace(&bw);
return TRUE;
}

If this function returns FALSE, the container will not allow us space for our toolbar. In this case, Cosmo merely lives without the tools because everything is still available on the shared menu. Some other objects might not want to activate in place in such a situation and should deactivate everything done to this point (including the shared menu) and open as a normal embedded object. Still other objects can elect to place those same tools in a floating pop-up window, which the container cannot restrict.

If the container does allow us space, we create our toolbar using the window from IOleInPlaceFrame::GetWindow as the parent and the dimensions from IOleInPlaceFrame::GetBorder.5 Cosmo's toolbar stretches across the container's frame window, so we use the horizontal extent from GetBorder for this dimension with the vertical extent that we negotiated for previously. In general, tools on the top or bottom of a window should be created with the horizontal extent from GetBorder and the negotiated height. Tools on the side of the window should use their negotiated width and the vertical dimension from GetBorder.

After executing this code, the toolbar should be visible in the container. To remove it, we need only to destroy the toolbar because the container will reinstate its own when we call IOleInPlaceSite::OnUIDeactivate. Cosmo's CFigure::InPlaceToolsDestroy is straightforward:


BOOL CFigure::InPlaceToolsDestroy(void)
{
//Nothing to do if we never created anything.
if (NULL==m_pTB)
return TRUE;

if (NULL!=m_pTB)
{
delete m_pTB;
m_pTB=NULL;
g_pInPlaceTB=NULL;
}

return TRUE;
}

It is perfectly reasonable to simply hide your tools at this point in case the object becomes UI active again, but remember that you must always negotiate for space whenever the object becomes UI active. You can't assume that the container will allow the same tool space for each activation.

If you compile and run your object at this point, you'll see the user interface created and destroyed as necessary. Pretty cool! Now resize the frame or document windows in which these tools appear. Ugly, huh? Your tools didn't resize with the container windows. We have to implement the IOleInPlaceActiveObject interface to handle this. But before we do this, let's finish up the last major modification—accelerator support.

3 Cosmo includes a title string for itself to remain backward compatible with older containers that still use the string for changing the caption bar. All in-place–capable objects should do this.

4 If your application is an MDI multiple-use server, you should not modify a pop-up menu that is used from two top-level menus (that is, the shared menu and the server's normal menu) because you'll be modifying the menu anywhere it's used. Either abstain from such modifications or create a separate pop-up menu for the shared menu and make your modifications to it alone. Remember also that you need to save this menu handle so that you can enable or disable items in it while processing WM_INITMENUPOPUP.

5 Cosmo saves a copy of the in-place toolbar in g_pInPlaceTB in such a way that when the user selects a line style from the menu, the CCosmoFrame::CheckLineSelection function can activate the appropriate button on the in-place toolbar as it does on the normal toolbar. You will see extra code in COSMO.CPP to handle this.