在Windows上开发客户端程序的时候,有时候我们希望能将其他进程的窗口嵌入到我们自己的程序窗口中,从视觉效果上看就像是其他进程的窗口时我们自己的程序窗口的一部分。具体的思路是,调用Windows API的SetParent方法,设置外部进程主窗口的父容器设置为我们自己的程序容器句柄。
在Winforms程序中,很容易实现此功能。但是在WPF中会稍微麻烦一点,因为WPF的容器控件是没有自己的独立的句柄的。因此解决思路为先在WPF中嵌入一个Winform的Panel控件(Winform中的Panel控件有自己独立的句柄),然后再将Panel控件的句柄设置为外部程序主窗口的父容器。
为了便于复用,我将相关的功能整理后封装成了一个WPF自定义控件。
一 代码结构
如上图,整个控件的代码结构分为三部分:一是控件的默认模板AppContainer.xaml,二是控件的逻辑控制代码,包括一些对外接口方法的类AppContainer.cs,三是c#调用Win32Api的接口类Win32Api.cs。
二 默认模板
AppContainer的默认模板非常的简单,模板中只有一个WindowsFormsHost控件,此控件用来存放Winform的Panel控件。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wfi ="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"
xmlns:local="clr-namespace:AppContainers">
<Style TargetType="{x:Type local:AppContainer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:AppContainer}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<wfi:WindowsFormsHost x:Name="PART_Host"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
三 Win32Api
主要用到了Win32Api的SetParent方法来设置被嵌入程序的父容器句柄以及MoveWindow来设置被嵌入程序在容器中的位置。
[DllImport("user32.dll", SetLastError = true)]
public static extern int SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool MoveWindow(IntPtr hwnd, int x, int y, int cx, int cy, bool repaint);
1 控件的初始化
如代码所以,在复写控件的OnApplyTemplate方法的时候,通过GetTemplateChild方法找到模板中的WindowsFormHost控件,当其不为空的时候,实例化Winform的Panel控件,并将其添加到WindowsFormHost中去。
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_winFormHost = GetTemplateChild("PART_Host") as WindowsFormsHost;
if(_winFormHost != null)
{
_hostPanel = new System.Windows.Forms.Panel();
_winFormHost.Child = _hostPanel;
}
}
2 外部窗口的嵌入
外部窗口的嵌入方法有两个:一个是给定程序路径,让控件启动并嵌入程序;一个是当被嵌入程序已经启动时,直接传入已经启动的被嵌程序的进程,然后调用嵌入进程的接口嵌入程序。
启动并嵌入外部进程的方法:
public bool StartAndEmbedProcess(string processPath)
{
bool isStartAndEmbedSuccess = false;
_eventDone.Reset();
//启动进程
_process = StartApp(processPath);
if (_process == null)
{
return false;
}
//确保可获取到句柄
Thread thread = new Thread(new ThreadStart(() =>
{
while (true)
{
if (_process.MainWindowHandle != (IntPtr)0)
{
_eventDone.Set();
break;
}
Thread.Sleep(10);
}
}));
thread.Start();
//嵌入进程
if (_eventDone.WaitOne(10000))
{
isStartAndEmbedSuccess = EmbedApp(_process);
if (!isStartAndEmbedSuccess)
{
CloseApp(_process);
}
}
return isStartAndEmbedSuccess;
}
直接嵌入外部进程的方法:
public bool EmbedExistProcess(Process process)
{
_process = process;
return EmbedApp(process);
}
/// <summary>
/// 将外进程嵌入到当前程序
/// </summary>
/// <param name="process"></param>
private bool EmbedApp(Process process)
{
//是否嵌入成功标志,用作返回值
bool isEmbedSuccess = false;
//外进程句柄
IntPtr processHwnd = process.MainWindowHandle;
//容器句柄
IntPtr panelHwnd = _hostPanel.Handle;
if (processHwnd != (IntPtr)0 && panelHwnd != (IntPtr)0)
{
//把本窗口句柄与目标窗口句柄关联起来
int setTime = 0;
while (!isEmbedSuccess && setTime < 10)
{
isEmbedSuccess = (Win32Api.SetParent(processHwnd, panelHwnd) != 0);
Thread.Sleep(100);
setTime++;
}
//设置初始尺寸和位置
Win32Api.MoveWindow(_process.MainWindowHandle, 0, 0, (int)ActualWidth, (int)ActualHeight, true);
}
if(isEmbedSuccess)
{
_embededWindowHandle = _process.MainWindowHandle;
}
return isEmbedSuccess;
}
3 当外部程序放大缩小时,被嵌入程序窗口界面要能跟着改变,所以要复写OnRender方法,在方法中调用MoveWindow方法来设置被嵌程序的初始位置和大小
protected override void OnRender(DrawingContext drawingContext)
{
if (_process != null)
{
Win32Api.MoveWindow(_process.MainWindowHandle, 0, 0, (int)ActualWidth, (int)ActualHeight, true);
}
base.OnRender(drawingContext);
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
this.InvalidateVisual();
base.OnRenderSizeChanged(sizeInfo);
}
4 当外部程序关闭时,要能同时关闭被嵌入进程。
/// <summary>
/// 关闭进程
/// </summary>
/// <param name="process"></param>
private void CloseApp(Process process)
{
if (process != null && !process.HasExited)
{
process.Kill();
}
}
public void CloseProcess()
{
CloseApp(_process);
}
五 控件的应用
<Window x:Class="WpfAppContainerTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfAppContainerTest"
xmlns:container="clr-namespace:AppContainers;assembly=AppContainers"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<container:AppContainer x:Name="ctnTest" Margin="20"/>
</Grid>
</Window>
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
if (!_isLoadSuccess)
{
_isLoadSuccess = ctnTest.StartAndEmbedProcess(@"C:\Windows\system32\mspaint.exe");
}
}