gw_logo_08.gif (1982 bytes) 
Last edit: 05-03-17 Graham Wideman

Delphi

Delphi ActiveForm Controls: Clipping bug on scrolled pages
...with plausible solution...

Article created: 2000-03-26

Overview

Scenario: An ActiveForm is inserted in a document page in MS Word, MS Excel or Visio.

Bug: When the page is scrolled so that part of the ActiveForm is off the top or left edge of window, then in Run mode the bottom or right of the ActiveForm is clipped instead of the Top or Left.

Figure: In the figure above the left screenshot shows normal appearance, while the right screenshot shows what happens if the sheet is scrolled (scrollbars moved down and right, sheet has moved up and left -- note row/column indexes).

Steps to Reproduce

1. Generate an ActiveForm and stick some sort of recognizable features on it, like a few controls.

2. Compile and register.

3. Open Excel and insert ActiveForm (Toolbox, additional controls).

4. Place Excel in Run mode.  Scroll worksheet and observe clipping behavior of ActiveForm when it's partly off top or left of window.

More Details

In fact, it appears that what is happening is that the ActiveX control is being told by the container (Excel in this case) to resize and reposition so as to stay in the visible part of the page.  The following screen shots demonstrate:

Normal.
Scrolled so as to be "off the top and left". Note how Top and Left are not negative as would be expected. Instead, they have changed to reposition the top left corner of the control at the top-leftmost corner of the page.  Width and Height have been reduced so that the bottom right corner is where it should be. However this creates the wrong effect of clipping the bottom right of the control contents instead of top left.

What's missing is that the control needs to scroll its canvas up and left.

Same thing at bottom right, only here the default behavior looks OK.

To email me about this:  graham@wideman-one.com

Logged as Borland Bug: 433165

Plausible Solution

2001-03-02

A couple of folks emailed me to let me know this was being discussed on the borland newsgroups, and a solution presented as an "official" Borland tip (DocId 10579). Turns out the proposed solution at least got me looking in the right spot... but needed an addition to really work properly.

The area that turns out to be responsible for the misbehavior shown above is in the AxCtrls unit. As shipped it looks like this:

function TActiveXControl.SetObjectRects(const rcPosRect: TRect;  const rcClipRect: TRect): HResult;
var
  WinRect: TRect;
begin
  try
    IntersectRect(WinRect, rcPosRect, rcClipRect);
    FWinControl.BoundsRect := WinRect;
    Result := S_OK;
  the_end:
  except
    Result := HandleException;
  end;
end;

While this creates an ActiveX control area that's the right size and in the right location in the host's window, the control gets repositioned relative to host document space, as we saw above.

The recommended fix was simply:

function TActiveXControl.SetObjectRects(const rcPosRect: TRect;  const rcClipRect: TRect): HResult;
begin
  try
    FWinControl.BoundsRect := rcPosRect; // <--- "fix"
    Result := S_OK;
  except
    Result := HandleException;
  end;
end;

This leaves the ActiveX control in the correct position relative to host document coordinates, but also leaves the size of the control alone. So what happens when the document is scrolled so that the control is hanging over the edge of the window? It gets clipped at the edge of the window's area... so all is well, almost.

Trouble is, some host apps place their own items around the edge of the host window, such as scroll bars or rulers, and therefore the ActiveX control needs to avoid drawing over top of them. That's the whole reason for the rcClipRect parameter. Without attending to that, we get an effect like this:

Here's an ActiveForm control in Visio, with the proposed fix described above. Note how the ActiveX control overlaps the scrollbar and tab area at the bottom, and the ruler area at the left. Not only is this a paint problem, but the ActiveFrom also captures mouse activity from those areas as well.

On browsing the ActiveX code that comes with Visual Studio 6 (10 points for WinGrep!), I came across the ATLCTL.H C++ include file which provides a base implementation for SetObjectRects, and does take into account this situation. Translating to Delphi, I got:

function TActiveXControl.SetObjectRects(const rcPosRect: TRect;  const rcClipRect: TRect): HResult;
var
  WinRect: TRect;
  IxRect: TRect;
  NewWindowRgn: HRGN;
  DoIntersect: Boolean;
Label the_end;
begin
  try
    //(*===========================================
    // GW's fix, follows
    // CComControlBase::IOleInPlaceObject_SetObjectRects
    //-------------------------------------------

    if (@rcPosRect = nil) or (@rcClipRect = nil) then
    Begin
      Result := E_POINTER;
      goto the_end;
    end;

    If FWinControl.HandleAllocated then
    Begin
      DoIntersect := IntersectRect(IxRect, rcPosRect, rcClipRect);
      NewWindowRgn := 0; // default to request no clipping

      //------------------------------
      // check if clipping is needed
      //------------------------------
      if DoIntersect and (not EqualRect(IxRect, rcPosRect)) then
      Begin
        //------------------------------
        // set up clipping region
        //------------------------------
        OffsetRect(IxRect, -rcPosRect.Left, -rcPosRect.Top);
        NewWindowRgn := CreateRectRgnIndirect(IxRect);
      end;

      SetWindowRgn(FWinControl.Handle, NewWindowRgn, True);
      FWinControl.BoundsRect := rcPosRect;
    end;
    //===========================================*)

    Result := S_OK;
  the_end:
  except
    Result := HandleException;
  end;
end;

This is not quite a literal translation, as CComControlBase::IOleInPlaceObject_SetObjectRects does things in a slightly different order, since CComControlBase and TActiveXControl have different fields and need to take care of different things. However, this code does appear in initial testing to have the right behavior.

Note also that neither the ATLCTL.H code nor the Delphi code contains the smarts to render at different scales if the document view percentage is other than 100% (as may easily be set in Visio for example). Instead, the control will simply appear at 100% scale in a smaller or larger region  (according to document zoom factor). In the sample C++ code you can see a comment for where you would handle this situation, and there is more documentation in the MSDN on IOleInPlaceObject::SetObjectRects.

FWIW, I have logged this further report with Borland, and it has reference number: 460160.

ATLCTL.H  C++ Sample

Reproduced below for comparison

inline HRESULT CComControlBase::IOleInPlaceObject_SetObjectRects(LPCRECT prcPos,LPCRECT prcClip)
{
  if (prcPos == NULL || prcClip == NULL)
    return E_POINTER;

  m_rcPos = *prcPos;
  if (m_hWndCD)
  {
    // the container wants us to clip, so figure out if we really
    // need to
    //
    RECT rcIXect;
    BOOL b = IntersectRect(&rcIXect, prcPos, prcClip);
    HRGN tempRgn = NULL;
    if (b && !EqualRect(&rcIXect, prcPos))
    {
      OffsetRect(&rcIXect, -(prcPos->left), -(prcPos->top));
      tempRgn = CreateRectRgnIndirect(&rcIXect);
    }

    SetWindowRgn(m_hWndCD, tempRgn, TRUE);

    // set our control's location, but don't change it's size at all
    // [people for whom zooming is important should set that up here]
    //
    SIZEL size = {prcPos->right - prcPos->left, prcPos->bottom - prcPos->top};
    SetWindowPos(m_hWndCD, NULL, prcPos->left,
    prcPos->top, size.cx, size.cy, SWP_NOZORDER | SWP_NOACTIVATE);
  }
  return S_OK;
}

Go to:  gw_logo_08.gif (1982 bytes)