Clipping legacy content hosted inside a WPF scrolling region


Recently, I sat down at my computer in my home office tasked with making a better looking experience around Team Foundation Server work items. To date, we have a really cool Windows Forms version of the work item viewer inside Visual Studio but I wanted a more light-weight and portable means to view my work items. So, I chose Windows Presentation Foundation as the UI composition engine for my new work item viewer and began the task of writing a rendering engine in WPF to plug into the current Work Item store implementation. After getting neck-deep in the implementation, I encountered a nasty interop issue around the problem of Airspace.

Basically, my Airspace problem is one of Z-order: when you nest Windows Forms controls inside a WPF visual container (i.e. Window, Page, Panel, etc.) the Windows Forms control actually sits on top of the WPF window and maintains its own HWND. This, in and of itself, is not a problem because the fine developers of WPF took this into consideration when they wrote the interop control WindowsFormsHost. The WindowsFormsHost control acts as a placeholder for Windows Forms content so that the WPF layout engine can treat it as a native control.

You may be thinking: "Ok, so they figured it out…what’s the big deal?" There is a nasty little behavior in the WindowsFormsHost that involves putting a Windows Forms control inside a WPF scrolling region (ScrollViewer).

Initial Repro 

The sample application above has a Windows Forms WebBrowser control inside a WPF scrolling region (along with several other controls) . This application simulates one of the conditions where lack of clipping on the hosted Windows Forms control is demonstrable. Below is the same application only with the scrolling region scrolled down until the web browser control "bleeds" through to the tab control and form above it:

repro_scroll_past_bounds

What we need here is the ability to either build our own custom forms host to support clipping of the hosted content, or use some GDI APIs to clip the hosted control at the HWND level. There are pros and cons of both approaches–the custom forms host has to support a large number of state changes in the hosted control and WPF visual tree while the GDI solution is more difficult to "tighten the screws." I’m going to talk about the second of these approaches in this blog (and will save the first for another entry).

GDI Approach

In order to use this approach we will derive from the WindowsFormsHost control–used by the WPF layout engine to provide a hosting platform for Win32 content. Once we have derived from this type we need to know when each of the scroll regions that we are contained within have scrolled. This can be accomplished using a class handler for the ScrollChanged event:

EventManager.RegisterClassHandler(typeof(ScrollViewer), ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(ScrollHandler));

Once we know when each scroll region we are contained within has scrolled we need to calculate how much of the control to keep and how much to clip. This is done by first getting the viewport of the most constrained scroll viewer and translating it to the containing windows’ scale:

GeneralTransform transform = scrollViewer.TransformToAncestor(RootElement);
Point size = new Point(scrollViewer.ViewportWidth, scrollViewer.ViewportHeight);
Point location = new Point(0, 0);

return (transform.TransformBounds(new Rect(location.X, location.Y, size.X, size.Y)));

We will also need to grab the controls’ extents and transform them to the containing windows’ scale:

Point controlSize = new Point(RenderSize.Width, RenderSize.Height);
Point controlLocation = new Point(Padding.Left, Padding.Right);
GeneralTransform controlTransform = TransformToAncestor(RootElement);
Rect controlTransformRect = controlTransform.TransformBounds(new Rect(controlLocation.X, controlLocation.Y, controlSize.X, controlSize.Y));

Now that we have both sets of coordinates in the same scale, we can calculate the region that is the intersection between the two:

Rect intersectRect = Rect.Intersect(scrollRect, controlTransformRect);

This gives us the portion of the control that is visibly rendered within the viewport of the scroll region. Now that we have the visible region needed we can use the SetWindowRgn GDI function to perform the clipping for us:

[DllImport("User32.dll", SetLastError = true)]
private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

It is important to note that you will need to have the HWND of the control that the WPF interop control uses to synchronize the placeholder WPF element with your Windows Forms control.  Using reflection you can easily query the interop control for this handle  If you plan to support more than one font size (i.e. DPI) you will need to adjust your measurements accordingly:

CompositionTarget ct = source.CompositionTarget;
Matrix m = ct.TransformToDevice;
Point transformed = m.Transform(p);

Once you have the clipping code in place the result will be a Windows Forms control that clips correctly inside a WPF scrolling region:

fixed_initial fixed_scroll


Comments (16)

  1. tanjaWeb_2007 says:

    Hi Ryan,

    could you send me the code.

    Somehow it is not working. I don’t know what’s wrong.

    Bye, Tanja.

  2. koenj says:

    Great article,

    Some code would be nice,

    some variables/types are not that clear..

    Thanks,

    Koen

  3. You know the drill: Raw/unedited WPF discussions from inside the mothership. Subject: Accessibility tree,

  4. strommen says:

    Ryan,

      This approach works great with Aero glass disabled.  However, with Aero glass enabled, the WPF scrollbars flicker like crazy.  Do you have any suggestions for this issue?

    Thanks

    Joe Strommen

  5. grellsworth says:

    When you get a chance, I’d like to see some example code, please?

    We are using multiple WebBrowser controls within nested ScrollViewers and this ‘bleedover’ problem is very ugly.  Using the new WPF WebBrowser control is even worse, it doesn’t seem to repaint after scrolling, AND it still has the ‘bleedover’ issue.

    Any help would be appreciated.

  6. captain says:

    Could be nice to have a full code sample instead of just bits of it.

    I believe that this is a common problem among many users who use a Windows Forms control inside WPF.

  7. captain says:

    Could be nice to have a full code sample instead of just bits of it.

    I believe that this is a common problem among many users who use a Windows Forms control inside WPF.

    Other than that, very nice post, glad to see that there's a some workaround to this problem.

    Thanks

  8. captain says:

    Could be nice to have a full code sample instead of just bits of it.

    I believe that this is a common problem among many users who use a Windows Forms control inside WPF.

    Other than that, very nice post, glad to see that there's a workaround to this problem.

    Thanks

  9. Ashwin says:

    I am really in need of it.

    Need to clip the WebBrowser in the scroller.

    I couldn't solve the problem with the given code.

    Coould you please help me out in solving the issue.

    can mail me at "ashwin893@gmail.com"

    Thanks

  10. dIeGoLi says:

    based on this blog i wrote once a solution:

    http://www.mycsharp.de/…/thread.php

    it is not the most actual version, i can share it there next week. the only change i made is, that i replaced the scrollviewer register list, because my solution does not work in nested scrollviewers. instead of registering multiple scrollviewers you can just pass in one via constructor for example…

    if you have any further suggestions plz let me know.

    and thank you ryan! your blog was really very helpful

  11. kbevins says:

    @dIeGoLi:  Did you come up with a solution that works with nested scrollviewers?  I am trying to solve that right now, and would love to see what you did if you got it to work…

    Thanks for the greate blog entry!

  12. Hi,

    i gone through that solution but i still didn't get the problem solve..

    So if any one got the solution of that problem please mail me that solution

    My mail Id is avisatna@gmail.com

  13. Jitesh says:

    Is it possible to make it work with Windowsformshost controls hosted inside a datagrid? I tried the solution, but my hosted control does not draw.

  14. Ian says:

    This works well, but if your monitor dpi is not 96 some adjustments are needed because windows forms works with physical pixels not device independent ones.

  15. avisatna says:

    Check solution for Scroll Viewer issue here,

    stackoverflow.com/…/14099937