The
cleanest way to integrate WPF and Windows Forms content is to place
each in a separate window. That way your application consists of
well-encapsulated window classes, each of which deals with just a
single technology. Any interoperability details are handled in the glue code—the logic that creates and shows your windows.
1. Adding Forms to a WPF Application
The easiest approach to
mixing windows and forms is to add one or more forms (from the Windows
Forms toolkit) to an otherwise ordinary WPF application. Visual Studio
makes this easy—just right-click the project name in the Solution
Explorer, and choose Add → New Item. Then, select the Windows Forms
category on the left side, and choose the Windows Form template.
Lastly, give your form a file name, and click Add. The first time you
add a form, Visual Studio adds references to all the required Windows
Forms assemblies, including System.Windows.Forms.dll and
System.Drawing.dll.
You can design a form in a WPF
project in the same way that you design it in a Windows Forms project.
When you open a form, Visual Studio loads the normal Windows Forms
designer and fills the Toolbox with Windows Forms controls. When you
open the XAML file for a WPF window, you get the familiar WPF design
surface instead.
For better separation between
WPF and Windows Forms content, you might choose to place the "foreign"
content in a separate class library assembly. For example, a Windows
Forms application might use the WPF windows defined in a separate
assembly. This approach makes especially good sense if you plan to
reuse some of these windows in both Windows Forms and WPF applications.
|
|
2. Adding WPF Windows to a Windows Forms Application
The reverse trick is a
bit more awkward. Visual Studio doesn't directly allow you to create a
new WPF window in a Windows Forms application. (In other words, you
won't see it as one of the available templates when you right-click
your project and choose Add → New Item.) However, you can add the
existing .cs and .xaml files that define a WPF window from another WPF
project. To do so, right-click your project in the Solution Explorer,
choose Add → Existing Item, and find both these files. You'll also need
to add references to the core WPF assemblies (PresentationCore.dll,
PresentationFramework.dll, and WindowsBase.dll).
There's a shortcut to adding the WPF references you need. You can add a WPF user control (which Visual Studio does
support), which causes Visual Studio to add these references
automatically. You can then delete the user control from your project.
To add a WPF user control, right-click the project, choose Add → New
Item, pick the WPF category, and select the User Control (WPF) template.
|
|
Once you add a WPF window
to a Windows Forms application, it's treated correctly. When you open
it, you'll be able to use the WPF designer to modify it. When you build
the project, the XAML will be compiled, and the automatically generated
code will be merged with your code-behind class, just as it is in a
full-fledged WPF application.
Creating a project that
uses forms and windows isn't too difficult. However, there are a few
extra considerations when you show these forms and windows at runtime.
If you need to show a window or form modally (as you would with a
dialog box), the task is straightforward, and your code is essentially
unchanged. But if you want to show a window modelessly, you need a bit
of extra code to ensure proper keyboard support, as you'll see in the
following sections.
3. Showing Modal Windows and Forms
Showing a modal form from a
WPF application is effortless. You use exactly the same code you'd use
in a Windows Forms project. For example, if you have a form class named
Form1, you'd use code like this to show it modally:
Form1 frm = new Form1();
if (frm.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
MessageBox.Show("You clicked OK in a Windows Forms form.");
}
You'll notice that the
Form.ShowDialog() method works in a slightly different way than WPF's
Window.ShowDialog() method. While Window.ShowDialog() returns true,
false, or null, Form.ShowDialog() returns a value from the DialogResult
enumeration.
The reverse trick—showing a
WPF window from a form—is just as easy. Once again, you simply interact
with the public interface of your Window class, and WPF takes care of
the rest:
Window1 win = new Window1();
if (win.ShowDialog() == true)
{
MessageBox.Show("You clicked OK in a WPF window.");
}
4. Showing Modeless Windows and Forms
It's not quite as
straightforward if you want to show windows or forms modelessly. The
challenge is that keyboard input is received by the root application
and needs to be delivered to the appropriate window. In order for this
to work between WPF and Windows Forms content, you need a way to
forward these messages along to the right window or form.
If you want to show a WPF
window modelessly from inside a Windows Forms application, you must use
the static ElementHost.EnableModelessKeyboardInterop() method. You'll
also need a reference to the WindowsFormsIntegration.dll assembly,
which defines the ElementHost class in the
System.Windows.Forms.Integration namespace. (You'll learn more about
the ElementHost class later in this chapter.)
You call the
EnableModelessKeyboardInterop() method after you create the window but
before you show it. When you call it, you pass in a reference to the
new WPF window, as shown here:
Window1 win = new Window1();
ElementHost.EnableModelessKeyboardInterop(win);
win.Show();
When you call
EnableModelessKeyboardInterop(), the ElementHost adds a message filter
to the Windows Forms application. This message filter intercepts
keyboard messages when your WPF window is active and forwards them to
your window. Without this detail, your WPF controls won't receive any
keyboard input.
If you need to show a modeless
Windows Forms application inside a WPF application, you use the similar
WindowsFormsHost.EnableWindowsFormsInterop() method. However, you don't
need to pass in a reference to the form you plan to show. Instead, you
simply need to call this method once before you show any form. (One
good choice is to call this method at application startup.)
WindowsFormsHost.EnableWindowsFormsInterop();
Now you can show your form modelessly without a hitch:
Form1 frm = new Form1();
frm.Show();
Without the call to
EnableWindowsFormsInterop(), your form will still appear, but it won't
recognize all keyboard input. For example, you won't be able to use the
Tab key to move from one control to the next.
You can extend this process to
multiple levels. For example, you could create a WPF window that shows
a form (modally or modelessly), and that form could then show a WPF
window. Although you won't need to do this very often, it's more
powerful than the element-based interoperability support you'll learn
about later. This support allows you to integrate different types of
content in the same window but doesn't allow you to nest more than one
layer deep (for example, creating a WPF window that contains a Windows
Forms control that, in turn, hosts a WPF control).
5. Visual Styles for Windows Forms Controls
When you show a form in a WPF
application, that form uses the old fashioned (pre–Windows XP) styles
for buttons and other common controls. That's because support for the
newer styles must be explicitly enabled by calling the
Application.EnableVisualStyles() method. Ordinarily, Visual Studio adds
this line of code to the Main() method of every new Windows Forms
application. However, when you create a WPF application, this detail
isn't included.
To resolve this issue, just
call the EnableVisualStyles() method once before showing any Windows
Forms content. A good place to do this is when the application is first
started, as shown here:
public partial class App : System.Windows.Application
{
protected override void OnStartup(StartupEventArgs e)
{
// Raises the Startup event.
base.OnStartup(e);
System.Windows.Forms.Application.EnableVisualStyles();
}
}
Notice that the EnableVisualStyles() method is defined in the System.Windows.Forms.Application class, not the System.Windows.Application class that forms the core of your WPF application.
6. Windows Forms Classes That Don't Need Interoperability
As you know,
Windows Forms controls have a different inheritance hierarchy than WPF
elements. These controls can't be used in a WPF window without
interoperability. However, there are some Windows Forms components
that don't have this limitation. Provided you have a reference to the
necessary assembly (usually System.Windows.Forms.dll), you can use
these types without any special considerations.
For example, you can use the
dialog classes (such as ColorDialog, FontDialog, PageSetupDialog, and
so on) directly. In practice, this isn't terribly useful because these
dialog boxes are slightly outdated and because they wrap structures
that are part of Windows Forms, not WPF. For example, if you use the
ColorDialog, you'll get a System.Drawing.Color object rather than the
System.Windows.Media.Color object you really want. The same is true
when you use the FontDialog, PageSetupDialog, and PrintPreviewDialog
that are designed to work with the older Windows Forms printing model.
In fact, the only Windows Forms dialog box that's of any use and that
doesn't have a WPF equivalent in the Microsoft.Win32 namespace is
FolderBrowserDialog, which lets the user pick a folder.
More useful Windows
Forms components include the SoundPlayer, which you can use as a
lightweight equivalent to WPF's MediaPlayer and MediaElement; the
BackgroundWorker ,
which you can use to manage an asynchronous task safely; and the
NotifyIcon (described next), which allows you to show a system tray
icon.
The only disadvantage to
using the NotifyIcon in a WPF window is that there's no design-time
support. It's up to you to create the NotifyIcon by hand, attach event
handlers, and so on. Once you supply an icon using the Icon property
and set Visible to true, your icon will appear in the system tray
(shown in Figure 1). When your application ends, you should call Dispose() on the NotifyIcon to remove it from the system tray immediately.