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).
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:
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).
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: