Continued from Part 2.
So now for the second scenario. Hosting WPF controls in legacy containers. Well let’s start with hosting a WPF control in a Windows Form. Then we’ll have a bash at hosting a WPF control in an unmanaged application (hee, hee).
Here is the source code for the three projects described in this post.
Hosting WPF Controls in a Windows Form
Hosting WPF Controls in a Windows Forms application is quite straightforward. The key is the ElementHost control in the Systems.Windows.Forms.Integration namespace which is a Windows Forms Control and can reference any WPF UIElement as its Child property.
You will find a walkthrough in MSDN here and there are a number of good posts on the subject. But just for the sake of completion, I created 3 projects here which demonstrate: implementation of the PolygonControl as a WPF UserControl, hosting of the control in a WPF application and then hosting it in a Windows Forms Application.
Let’s start with the PolygonControl in WPF (project WpfControlLibrary in the source code).
Here is my XAML:
<UserControl x:Class="WpfControlLibrary.PolygonControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300"
Width="300"
Loaded="UserControl_Loaded">
<Canvas x:Name="canvas" MouseDown="canvas_MouseDown">
<Rectangle Name="rectangle"
Width="{Binding ElementName=canvas, Path=ActualWidth}"
Height="{Binding ElementName=canvas, Path=ActualHeight}"
Fill="White"></Rectangle>
<Polygon x:Name="polygon"
Fill="Yellow">
</Polygon>
</Canvas>
</UserControl>
And the code behind:
public partial class PolygonControl : UserControl
{
public PolygonControl()
{
InitializeComponent();
}
public event EventHandler ClickIn;
public event EventHandler ClickOut;
int m_nSides;
public int Sides
{
get { return polygon.Points.Count; }
set
{
if (value >= 3 && value <= 100)
{
m_nSides = value;
CalculatePoints(canvas.ActualWidth, canvas.ActualHeight);
}
}
}
private void CalculatePoints(double width, double height)
{
Point ptCenter = new Point();
double dblRadiusx = width / 2;
double dblRadiusy = height / 2;
double dblAngle = 3 * Math.PI / 2; // Start at the top
double dblDiff = 2 * Math.PI / m_nSides; // Angle each side will make
ptCenter.X = width / 2;
ptCenter.Y = height / 2;
PointCollection points = new PointCollection(m_nSides);
// Calculate the points for each side
for (int i = 0; i < m_nSides; i++)
{
points.Add(new Point(
dblRadiusx * Math.Cos(dblAngle) + ptCenter.X,
dblRadiusy * Math.Sin(dblAngle) + ptCenter.Y));
dblAngle += dblDiff;
}
polygon.Points = points;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
Sides = 3;
}
private void canvas_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.Source == polygon)
{
polygon.Fill = Brushes.Red;
if (ClickIn != null)
ClickIn(this, EventArgs.Empty);
}
else
{
polygon.Fill = Brushes.Yellow;
if (ClickOut != null)
ClickOut(this, EventArgs.Empty);
}
}
}
Wow, that was easy. We’ve come a long way since ATL, wouldn’t you say?
Worthy note in this code are the following:
- In order to be able to determine where the mouse was “mouse downed” I used good old routed events.
- A Rectangle occupies the whole Canvas, and the Polygon lies over it.
- MouseDown is a bubbling event so we can catch it firing at the Canvas. In the handler of the Canvas we can use e.Source to determine whether the click is happening in the polygon or in the rectangle.
- In order to get the rectangle to stretch over the Canvas I used binding to bind its dimensions to those of the Canvas.
- Here again we see the beauty of a retainable graphics system. As soon as I set the PointCollection on the Polygon, WPF updates the screen (no need to manually call FireViewChange like in ATL).
As for the hosting code in WPF and in Windows Forms, they are almost identical (once you add the ElementHost in the Windows Form to host your control). See the WpfUseControl and WfUseControl projects respectively in the source code for this post.
And now, for the real challenge:
Hosting WPF Controls in an Unmanaged Application
Unfortunately, to the best of my knowledge there is no easy way to host a WPF UserControl in an unmanaged application. Yes, its true that you can expose the WPF UserControl as a COM object (you know the routine, define an interface, derive the control from the interface, add the Guid attribute and the ComVisible attribute, sign the application and check the “Register for COM interop” on the Build tab of the project properties).
This enables you to create instances of the control and call its methods from C++, but doesnt give you any GUI. Even if you make your control a Window instead of a UserControl, you wont see anything (I assume because you dont have an Application object) but most important, your new COM component is just NOT an ActiveX.
An ActiveX is a special sort of COM that supports ‘OLE’ - object linking and embedding into a container through a set of unmanaged COM interfaces. The only interfaces your COM component supports are those you explicitly made visible.
I say “there is no easy way” because I assume it is possible to write a wrapper COM component that implements the OLE interfaces and provides the glue between the OLE container and a UIElement, but to the best of my knowledge it hasn’t been written yet.