Negotiate Tool Space

Those pesky little objects are now going to make demands on the container for their own in-place tools. But being the good citizens that we are, we'll negotiate with the objects by implementing the IOleInPlaceUIWindow::RequestBorderSpace function for both our frame and our document windows. Negotiating objects will fill a BORDERWIDTHS structure and pass it to this function for your consideration. If you accept, you'll get a call to IOleInPlaceUIWindow::SetBorderSpace to actually allocate the space. Some objects might simply call SetBorderSpace first, and you have to handle that case. Anyway, if SetBorderSpace succeeds, the object will most likely then call IOleInPlaceUIWindow::GetBorder to ask for the size of the window in question. These functions all work on device units (pixels) and not HIMETRIC units, as many other parts of OLE do. Everything we do here is oriented toward the screen, so we don't need any fancy units.

Within SetBorderSpace, a container needs to reposition its various windows when asked to make space for tools. A container should try its best to keep the object in the same absolute position on the screen. After we look at the three member functions, we'll see what this means for our container.

GetBorder

This function is relatively easy to implement. Simply return the client-area rectangle for the appropriate window if you have no restrictions about where an object can place tools. The rectangle represents the space (the amount can be negotiated) from which the object can make requests for the space it needs and also provides the object with the dimensions it should use when creating tools. Any space you do not include in the rectangle is strictly off-limits to the object. Patron itself makes no restrictions for its document window, but it excludes the status line area for the frame window, as we can see in the following code:


//From IIPUIWIN.CPP
STDMETHODIMP CImpIOleInPlaceUIWindow::GetBorder(LPRECT prcBorder)
{
if (NULL==prcBorder)
return ResultFromScode(E_INVALIDARG);

GetClientRect(m_pDoc->m_hWnd, prcBorder);
return NOERROR;
}

//From PATRON.CPP
STDMETHODIMP CPatronFrame::GetBorder(LPRECT prcBorder)
{
if (NULL==prcBorder)
return ResultFromScode(E_INVALIDARG);

GetClientRect(m_hWnd, prcBorder);
prcBorder->bottom-=CYSTATSTRIP; //Height of status line

return NOERROR;
}

RequestBorderSpace

Implementing this function can range from quite trivial to extravagantly complicated, depending on how picky you want to be about giving away border space. If you are not picky at all, simply return NOERROR from this function, as Patron does.

If you have some restrictions, you need to check each of the values in the BORDERWIDTHS structure passed to this function. BORDERWIDTHS has the fields left, top, right, and bottom, which specify how much space the object wants on each side of the window in question (frame or document). If you can grant the space, return NOERROR; otherwise, return INPLACE_E_NOTOOLSPACE.

SetBorderSpace

Two important things happen inside SetBorderSpace: if the container can grant the request, it should move its windows around to make room for the object's allocation and then remember those allocations so that it can resize its client-area windows properly when resizing the frame or document window.

Objects are allowed to call SetBorderSpace directly without first calling RequestBorderSpace—so don't assume a particular calling sequence. It's easy to have your SetBorderSpace call your own RequestBorderSpace to validate the object's request, returning INPLACE_E_NOTOOLSPACE to deny the request. The object must resort to negotiation at that point.

At the same time that it holds the object's request, the BORDERWIDTHS pointer passed from the object can be NULL or the structure to which it points can contain zeros. In the former case, the object is telling us that it doesn't need any tools itself and that we can leave all of our tools visible. Most OLE Controls do exactly this (as does the Polyline sample from Chapter 23). If we're given a BORDERWIDTHS full of zeros, the object doesn't want anyone's tools to appear, so we have to remove all of ours (excluding the status line) even though the object isn't going to create its own.

When you know what border space to allocate on all four sides of the window in question, you need to remember those allocations in case the user resizes the window. Patron's CFrame::SetBorderSpace saves these values, which are used later in the WM_SIZE case of CPatronFrame::FMessageHook to resize the client-area window (managed in the CPatronClient class) so that it doesn't overlap any of the object's tools. This is pretty basic code.

The trickiest aspect of SetBorderSpace is the initial resizing and repositioning of the client-area window, which is generally necessary to accommodate the object's space request. The trick is that you should, if at all possible, keep the object's window in the same place on the screen regardless of what other windows you need to move around. The reason for this is that the user will be staring at the object in the document. The less you have to move that object, the less disconcerting the user interface changes will be to the user. This means that new object tools might overlap the object itself, but that is better than always shifting that embedded object around on the screen and giving the end user a case of the jitters.

Inside CPatronFrame::SetBorderSpace, Patron calculates the current position of the site (and therefore the object) and calculates the number of pixels, in both horizontal and vertical directions, that we'll have to scroll everything in the document to counter any change in the position of the client-area window. In other words, if we have to move the client-area window down by 10 pixels and right by 18, we need to move everything in the document up by 10 pixels and left by 18, because as children of the client-area window, the document's elements would otherwise move with the parent.

Unless the new border space requests require no repositioning of the client-area window, Patron's SetBorderSpace function calls CPatronClient::MoveWithoutFamily (in CLIENT.CPP), passing the new rectangle that the client window should occupy along with the differences between the old window position and the new one. MoveWithoutFamily moves the client window first and then moves all its children in the opposite direction. Truly the parent is leaving its family behind, as you can see in the following code:


void CPatronClient::MoveWithoutFamily(LPRECT prc, int dx, int dy)
{
RECT rc;
HWND hWndFrame;
HWND hWnd;
POINT pt;

hWndFrame=GetParent(m_hWnd);
SendMessage(hWndFrame, WM_SETREDRAW, FALSE, 0L);

ShowWindow(m_hWnd, SW_HIDE);
SetWindowPos(m_hWnd, NULL, prc->left, prc->top
, prc->right-prc->left, prc->bottom-prc->top
, SWP_NOZORDER œ SWP_NOACTIVATE);

//Move all children of client.
hWnd=GetWindow(m_hWnd, GW_CHILD);

while (NULL!=hWnd)
{
GetWindowRect(hWnd, &rc);
SETPOINT(pt, rc.left, rc.top);
ScreenToClient(m_hWnd, &pt);

if (pt.x!=dx && pt.y!=dy && !IsZoomed(hWnd))
{
//Move window in opposite direction of client.
SetWindowPos(hWnd, NULL, pt.x-dx, pt.y-dy
, rc.right-rc.left, rc.bottom-rc.top
, SWP_NOZORDER œ SWP_NOACTIVATE œ SWP_NOSIZE);
}

hWnd=GetWindow(hWnd, GW_HWNDNEXT);
}

SendMessage(hWndFrame, WM_SETREDRAW, TRUE, 0L);
ShowWindow(m_hWnd, SW_SHOW);

return;
}

The use of WM_SETREDRAW on the frame prevents the user from seeing some serious jitters. Without it, the user would see all the windows first move with the client and then move back one by one. To avoid this, we disable repaints and hide the client before moving anything. After moving everything, we reenable repaints and force an update by showing the client again.


EXPERIENCE: The Jitters and DeferWindowPos

BeginDeferWindowPos, DeferWindowPos, and EndDeferWindowPos will not move a collection of container windows together, as we need to do here. These functions turn into no-ops when the windows you send to DeferWindowPos are not all siblings. Thus, you cannot use these functions to move a parent window and its children at the same time. EndDeferWindowPos indicates that the process worked (returns TRUE), but nothing actually happens. Hence the solution used in Patron.



EXPERIENCE: Take Care with IOleClientSite::ShowObject

In Chapter 17, Patron first implemented ShowObject to scroll an object into view if less than a quarter of it was visible. Such an action can easily ruin all the effort you put into keeping that object steady when activating in place, so if you've seen a call to IOleInPlaceSite::OnInPlaceActivate, don't scroll your document if any part of the object is visible—scroll only if the entire object is completely out of view.


Repaint Optimizations

Switching the user interface around can cause considerable flickering and flashing as tools appear and disappear. Consider, for example, the case in which the container has an in-place object fully activated—user interface and all—so that the object's shared menu and tools are displayed. Now the end user immediately jumps over and double-clicks on another in-place object. Without any optimizations, the first object's tools disappear, the container's tools reappear, and then the second object is activated, causing the container's tools to disappear again and the new tools to appear. Blech! We'd like to prevent any excess work in making these changes, which applies to switching menus around as well.

The OLE Programmer's Reference documents some techniques for minimizing repaints that affect IOleInPlaceSite::OnUIActivate and OnUIDeactivate. It also suggests the creation of frame-level functions to add or remove specific user interface elements. In Patron, these are implemented as CPatronFrame::ShowUIAndTools and CPatronFrame::ReinstateUI. (The variable m_fOurToolsShowing determines whether CPatronFrame::FMessageHook will take the UI-active object's border space requests into consideration when processing WM_SIZE, as described earlier.)


void CPatronFrame::ShowUIAndTools(BOOL fShow, BOOL fMenu)
{
HWND hWndTB;

//This is the only menu case....Restore our original menu.
if (fMenu && fShow)
SetMenu(NULL, NULL, NULL);

hWndTB=m_pTB->Window();
ShowWindow(hWndTB, fShow ? SW_SHOW : SW_HIDE);

if (fShow)
{
InvalidateRect(hWndTB, NULL, TRUE);
UpdateWindow(hWndTB);
}

m_fOurToolsShowing=fShow;
return;
}


void CPatronFrame::ReinstateUI(void)
{
BORDERWIDTHS bw;

ShowUIAndTools(TRUE, TRUE);
SetRect((LPRECT)&bw, 0, m_cyBar, 0, 0,);
SetBorderSpace(&bw);
return;
}

These functions are then used within IOleInPlaceSite::OnUIActivate and IOleInPlaceSite::OnUIDeactivate, which themselves involve a number of Patron's functions. The code is rather lengthy, so here's a description of the process instead:

I admit that this isn't perfect. For one thing, it's single-threaded because we make use of a global variable. Second, the check for a double click doesn't include a check for a single click on an inside-out object. You would still see some flicker in that circumstance. Finally, this code uses functions and variables in almost every internal object inside Patron: CPatronFrame, CPatronDoc, CPage, CPages, and CTenant. Not the cleanest model because such optimizations were an afterthought and had to deal with the existing structures and the permissions each had to access the other. Certainly this could stand improvement, but I hope it will give you a good idea of what is needed to support these optimizations.

Testing It All

It's a good idea to stop here and test everything you've done by using a few different in-place–capable objects. Check that all the menus, toolbars, and other user interface elements show up correctly and that you can keep those objects fixed when shifting around the tools. A good test to see whether you've repositioned windows correctly is to drag a document window (assuming an MDI container) around and watch where the mouse is allowed to go. If you resized your client-area window to not overlap any of the object's tools, the mouse will be restricted to the visible client area. Otherwise, you'll see that you can move the mouse into the area occupied by tools and leave the document window there, perhaps with its caption bar completely hidden!

Also try resizing the frame window to be sure that the client is still resized properly by using the same test. You will notice after resizing the frame (or the document) that the object's tools are not resized to match. That's some of the polish we'll add in the section "Round the Corners: Other Miscellany" later in this chapter. But first let's handle accelerators.