WPF Hwnd窗口互操作系列
第一章 嵌入Hwnd窗口
第二章 嵌入WinForm控件
第三章 嵌入WPF控件
第四章 底部嵌入HwndHost(本章)
文章目录
前言
前面三章内容是笔者基于本章内容研发过程中的附带产物,但是意外的发现第三章可嵌入wpf控件后,本章的意义就变得不那么大了。本章讲述如何从底部嵌入hwnd窗口,以此来做到嵌入窗口不覆盖wpf控件的效果,这种实现思路参考了flutter的一个插件flutter_native_view,其内部实现就用了这种方式。对于wpf实现会复杂一些,因为提供自绘没有BlendMode之类的东西,无法直接消除底部画面,能够使用的方式是Clip,这种方式限制比较多。不过最终还是实现了功能。
一、如何实现?
1、底部创建窗口
(1)、创建透明窗口
csharp
_backgroundWindow = new Window() { WindowStyle = WindowStyle.None, ResizeMode = ResizeMode.NoResize, Focusable = false, Width = 0, Height = 0, ShowInTaskbar = false, ShowActivated = false, Background = Brushes.Transparent, Content = new Grid(), AllowsTransparency = true, };
_backgroundHandle = (new WindowInteropHelper(_backgroundWindow)).EnsureHandle();
(2)、监控顶部窗口事件
添加hook监控上层窗口的事件
csharp
HwndSource.FromHwnd(_forgroundHandle).AddHook(new HwndSourceHook(WndProc));
(3)、窗口保持在底部
监控相关事件保证窗口始终贴在下面。
csharp
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case WM_ACTIVATE:
case WM_MOVE:
case WM_SIZE:
RECT rect;
GetWindowRect(_forgroundHandle, out rect);
SetWindowPos(_backgroundHandle, _forgroundHandle, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE);
break;
}
return IntPtr.Zero;
}
2、HwndHost加入底部窗口
需要嵌入的hwnd窗口,直接嵌入到底部窗口。下列代码中Content是HwndHost控件,在xaml中设置。在cs代码中直接将其添加到底部窗口中。
csharp
[ContentProperty("Content")]
public class HwndHostBottomEmbbeder : FrameworkElement
{
public HwndHost Content
{
get { return (HwndHost)GetValue(HostProperty); }
set { SetValue(HostProperty, value); }
}
}
csharp
var grid = _backgroundWindow.Content as Grid;
grid.Children.Add(Content);
3、Clip穿透顶部窗口
和其他嵌入方式一样,需要先继承HwndHost。clip原理参考《C# wpf利用Clip属性实现截屏框》
csharp
//顶部窗口句柄
IntPtr _forgroundHandle = IntPtr.Zero;
//底部窗口句柄
IntPtr _backgroundHandle = IntPtr.Zero;
//底部窗口句柄
Window _backgroundWindow = null;
//顶部窗口
Window _foregroundWindow = null;
//顶部窗口的Content
FrameworkElement? _foreroundContent;
//嵌入窗口的区域
RectangleGeometry? _clipRect;
csharp
private void NativeHost_LayoutUpdated(object? sender, EventArgs e)
{
if (_foreroundContent == null) _foreroundContent = _foregroundWindow.Content as FrameworkElement;
//计算控件在底部窗口的位置
var pos = TranslatePoint(new Point(0, 0), _foregroundWindow);
var dp = pos - Content.TranslatePoint(new Point(0, 0), _backgroundWindow);
Content.Margin = new Thickness(Content.Margin.Left + dp.X, Content.Margin.Top + dp.Y, Content.Margin.Right - dp.X, Content.Margin.Bottom - dp.Y);
Content.SetSize(ActualWidth, ActualHeight);
//创建clip
var gg = _foreroundContent.Clip as GeometryGroup;
if (gg == null)
{
_foreroundContent.Clip = gg = new GeometryGroup();
gg.FillRule = FillRule.EvenOdd;
var rg = new RectangleGeometry();
rg.Rect = new Rect(0, 0, _foreroundContent.ActualWidth, _foreroundContent.ActualHeight);
gg.Children.Add(rg);
gg.Children.Add(new CombinedGeometry() { Geometry1 = new RectangleGeometry(), Geometry2 = new GeometryGroup() { FillRule = FillRule.Nonzero } });
}
//底下的rect必须保持和容器大小一致
var backRg = gg.Children.First() as RectangleGeometry;
//上层形状即为穿透区域,CombinedGeometry类型用于支持任意个区域穿透
var foreRg = (gg.Children[1] as CombinedGeometry).Geometry2 as GeometryGroup;
if (_clipRect == null)
{
_clipRect = new RectangleGeometry();
//添加当前控件的穿透区域
foreRg.Children.Add(_clipRect);
}
var newRect = new Rect(0, 0, _foreroundContent.ActualWidth, _foreroundContent.ActualHeight);
//判断窗口大小是否改变
if (backRg.Rect != newRect)
{
backRg.Rect = newRect;
}
pos = TranslatePoint(new Point(0, 0), _foreroundContent);
newRect = new Rect(pos.X, pos.Y, ActualWidth, ActualHeight);
//判断控件区域是否变化
if (_clipRect.Rect != newRect)
{
_clipRect.Rect = newRect;
}
}
4、WPF控件放在装饰层
由于clip会截其范围内的所有控件,所有有clip的控件上面放置其他控件是看不到的,但是clip不会截取装饰层的控件,所有我们可将控件放到装饰层。
添加一个装饰器属性,在xaml中使用
csharp
public class HwndHostBottomEmbbeder : FrameworkElement
{
public UIElement Adorner
{
get { return (UIElement)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
}
定义一个装饰器对象
csharp
class NormalAdorner : Adorner
{
UIElement _child;
/// <summary>
/// 构造方法
/// </summary>
/// <param name="adornedElement">被添加装饰器的元素</param>
/// <param name="child">放到装饰器中的元素</param>
public NormalAdorner(UIElement adornedElement, UIElement child) : base(adornedElement)
{
_child = child;
AddVisualChild(child);
}
public UIElement Child => _child;
protected override Visual GetVisualChild(int index) => _child;
protected override int VisualChildrenCount => 1;
protected override System.Windows.Size ArrangeOverride(Size finalSize)
{
_child.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
}
在初始化代码中将xaml中设置的Adorner属性的控件加入到装饰层。要注意添加一个容器用于获取鼠标事件,将事件透传到原本的控件(clip后无法响应鼠标事件)。
csharp
var layer = AdornerLayer.GetAdornerLayer(c);
if (layer == null)
throw new Exception("获取控件装饰层失败,控件可能没有装饰层!");
var grid = new Grid();
grid.Background = Brushes.Transparent;
grid.DataContext = c;
grid.SetBinding(Grid.VisibilityProperty, new Binding("Visibility") { Mode = BindingMode.OneWay });
//事件透传
c.MouseByPassFrom(grid);
if (adronerContent != null)
{
grid.Children.Add(adronerContent);
}
layer.Add(new NormalAdorner((UIElement)c, grid));
二、完整代码
1、对象定义
csharp
/************************************************************************
* @Project: HwndHostBottomEmbbeder
* @Decription: 底部Hwnd嵌入器,是一个控件,可包含HwndHost控件将其变成底部嵌入,wpf控件显示在装饰层上。
* 当前版本使用限制:
* 1、窗口需要设置WindowChrome,
* 2、会占用Window.Content的Clip属性,
* 3、不能点击穿透到Hwnd窗口。
* 4、不支持半透明或全透明背景色窗口,因为嵌入hwnd窗口拖动会有残影。
* 当前版本适用于显示视频之类的不需要交互的hwnd控件。
* @Verision: v1.0.0
* @Author: Xin Nie
* @Create: 2024/03/29 17:33:00
* @LastUpdate: 2024/03/29 17:33:00
************************************************************************
* Copyright @ 2024. All rights reserved.
************************************************************************/
[ContentProperty("Content")]
public class HwndHostBottomEmbbeder : FrameworkElement
{
/// <summary>
/// HwndHost控件
/// </summary>
public HwndHost Content { get; set; }
/// <summary>
/// 装饰层控件
/// </summary>
public UIElement Adorner { get; set; }
}
2、完整代码
之后上传
三、使用示例
1、嵌入Winform控件
MainWindow.xaml
xml
<Window x:Class="WpfHwndElement.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:WpfHwndElement"
xmlns:ac="clr-namespace:AC"
mc:Ignorable="d"
Background="Transparent"
WindowStyle="None"
ResizeMode="NoResize"
Title="MainWindow" Height="360" Width="640"
xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
>
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" CaptionHeight="0" />
</WindowChrome.WindowChrome >
<Grid Margin="0" Background="#ffffffff" >
<local:HwndHostBottomEmbbeder Height="200" Width="200" >
<!--将WindowsFormsHost转换到底部-->
<WindowsFormsHost>
<wf:TextBox Text="WinForm Text Box" BackColor="255, 77,78,141" />
</WindowsFormsHost>
<!--在装饰层放wpf控件-->
<local:HwndHostBottomEmbbeder.Adorner>
<ToggleButton Margin="5" Width="25" Height="30" Cursor="Hand" >
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Grid Background="Transparent">
<Polygon x:Name="pol" Points="0,0 25,15 0,30" Fill="Gray" Visibility="Visible"></Polygon>
<Rectangle x:Name="rec1" HorizontalAlignment="Left" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
<Rectangle x:Name="rec2" HorizontalAlignment="Right" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
</Grid>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</local:HwndHostBottomEmbbeder.Adorner>
</local:HwndHostBottomEmbbeder>
</Grid>
</Window>
效果预览
2、显示视频
示例代码依赖了《播放器》
MainWindow.xaml
xml
<Window x:Class="WpfHwndElement.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:WpfHwndElement"
xmlns:ac="clr-namespace:AC"
mc:Ignorable="d"
Background="Transparent"
WindowStyle="None"
ResizeMode="NoResize"
Title="MainWindow" Height="360" Width="640"
xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
>
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" CaptionHeight="0" />
</WindowChrome.WindowChrome >
<Grid Margin="0" Background="#ffffffff" >
<local:HwndHostBottomEmbbeder Height="200" Width="200" >
<!--将WindowsFormsHost转换到底部-->
<WindowsFormsHost>
<wf:TextBox x:Name="tb_video" Text="WinForm Text Box" BackColor="255,77,78,141" />
</WindowsFormsHost>
<!--在装饰层放wpf控件-->
<local:HwndHostBottomEmbbeder.Adorner>
<Border Width="150" Height="150" >
<ToggleButton Margin="5" Width="25" Height="30" Cursor="Hand" Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Grid Background="Transparent">
<Polygon x:Name="pol" Points="0,0 25,15 0,30" Fill="Gray" Visibility="Visible"></Polygon>
<Rectangle x:Name="rec1" HorizontalAlignment="Left" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
<Rectangle x:Name="rec2" HorizontalAlignment="Right" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="pol" Property="Visibility" Value="Hidden"></Setter>
<Setter TargetName="rec1" Property="Visibility" Value="Visible"></Setter>
<Setter TargetName="rec2" Property="Visibility" Value="Visible"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Border>
</local:HwndHostBottomEmbbeder.Adorner>
</local:HwndHostBottomEmbbeder>
</Grid>
</Window>
MainWindow.xaml.cs
csharp
using AC;
using System.Windows;
namespace WpfHwndElement
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
Play _play;
public MainWindow()
{
InitializeComponent();
}
private void ToggleButton_Checked(object sender, RoutedEventArgs e)
{
if (_play == null)
{
_play = new Play();
_play.IsLoop = true;
//获取tb_video的句柄,通过句柄渲染
_play.Window = tb_video.Handle;
_play.HardwareAccelerateType = HardwareAccelerateType.Dxva2;
//开始播放,播放器会在传入的句柄窗口中渲染视频。
_play.Start(@"D:\测试\Sony_4K_Camp_Main.mp4");
}
else
{
_play.IsPause = false;
}
}
private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
{
_play.IsPause = true;
}
}
}
效果预览
3、圆角矩形视频
示例代码依赖了《播放器》
MainWindow.xaml
xml
<Window x:Class="WpfHwndElement.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:WpfHwndElement"
xmlns:ac="clr-namespace:AC"
mc:Ignorable="d"
Background="Transparent"
WindowStyle="None"
ResizeMode="NoResize"
Title="MainWindow" Height="360" Width="640"
ac:Resize.IsResizeable="True"
ac:Resize.IsWindowDragSmooth="True"
ac:Move.IsDragMoveable="True"
xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
>
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" CaptionHeight="0" />
</WindowChrome.WindowChrome >
<Grid Margin="0" Background="#ffffffff" >
<local:HwndHostBottomEmbbeder Height="200" Width="200" >
<!--裁剪圆角矩形-->
<local:HwndHostBottomEmbbeder.Clip>
<RectangleGeometry Rect="0 0 200 200" RadiusX="10" RadiusY="10"></RectangleGeometry>
</local:HwndHostBottomEmbbeder.Clip>
<!--将WindowsFormsHost转换到底部-->
<WindowsFormsHost>
<wf:TextBox x:Name="tb_video" Text="WinForm Text Box" BorderStyle="None" BackColor="255,77,78,141" />
</WindowsFormsHost>
<!--在装饰层放wpf控件-->
<local:HwndHostBottomEmbbeder.Adorner>
<Border Width="150" Height="150" >
<ToggleButton Margin="5" Width="25" Height="30" Cursor="Hand" Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Grid Background="Transparent">
<Polygon x:Name="pol" Points="0,0 25,15 0,30" Fill="Gray" Visibility="Visible"></Polygon>
<Rectangle x:Name="rec1" HorizontalAlignment="Left" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
<Rectangle x:Name="rec2" HorizontalAlignment="Right" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="pol" Property="Visibility" Value="Hidden"></Setter>
<Setter TargetName="rec1" Property="Visibility" Value="Visible"></Setter>
<Setter TargetName="rec2" Property="Visibility" Value="Visible"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Border>
</local:HwndHostBottomEmbbeder.Adorner>
</local:HwndHostBottomEmbbeder>
</Grid>
</Window>
MainWindow.xaml.cs
cs代码同上略。
效果预览
4、拖动位置大小
MainWindow.xaml
xml
<Window x:Class="WpfHwndElement.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:WpfHwndElement"
xmlns:ac="clr-namespace:AC"
mc:Ignorable="d"
Background="Transparent"
WindowStyle="None"
ResizeMode="NoResize"
Title="MainWindow" Height="360" Width="640"
ac:Resize.IsResizeable="True"
ac:Move.IsDragMoveable="True"
xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
>
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" CaptionHeight="0" />
</WindowChrome.WindowChrome >
<Grid Margin="0" Background="#ffffffff" >
<local:HwndHostBottomEmbbeder Height="200" Width="200" ac:Resize.IsResizeable="True" ac:Move.IsDragMoveable="True" >
<!--将WindowsFormsHost转换到底部-->
<WindowsFormsHost>
<!--使用窗体设置全透明-->
<wf:Form x:Name="tb_video" TopLevel="False" FormBorderStyle="None" />
</WindowsFormsHost>
<!--在装饰层放wpf控件-->
<local:HwndHostBottomEmbbeder.Adorner>
<!--非播放时wpf层填充颜色,避免直接透到桌面-->
<Border x:Name="bd_mask" Background="RoyalBlue">
<ToggleButton Margin="5" Width="25" Height="30" Cursor="Hand" Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Grid Background="Transparent">
<Polygon x:Name="pol" Points="0,0 25,15 0,30" Fill="Gray" Visibility="Visible"></Polygon>
<Rectangle x:Name="rec1" HorizontalAlignment="Left" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
<Rectangle x:Name="rec2" HorizontalAlignment="Right" Width="8" Fill="Gray" Visibility="Hidden"></Rectangle>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="pol" Property="Visibility" Value="Hidden"></Setter>
<Setter TargetName="rec1" Property="Visibility" Value="Visible"></Setter>
<Setter TargetName="rec2" Property="Visibility" Value="Visible"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Border>
</local:HwndHostBottomEmbbeder.Adorner>
</local:HwndHostBottomEmbbeder>
</Grid>
</Window>
MainWindow.xaml.cs
csharp
using AC;
using System.Reflection;
using System.Windows;
namespace WpfHwndElement
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
Play _play;
public MainWindow()
{
InitializeComponent();
}
private void ToggleButton_Checked(object sender, RoutedEventArgs e)
{
if (_play == null)
{
_play = new Play();
_play.IsLoop = true;
//获取tb_video的句柄,通过句柄渲染
_play.Window = tb_video.Handle;
_play.HardwareAccelerateType = HardwareAccelerateType.Dxva2;
//开始播放,播放器会在传入的句柄窗口中渲染视频。
_play.Start(@"D:\Sony_4K_Camp.mp4");
//实现透明窗口,避免拖动时与视频绘制冲突,出现界面闪烁
tb_video.GetType().GetMethod("SetStyle", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(tb_video, new object[] { ControlStyles.Opaque, true });
}
else
{
_play.IsPause = false;
}
bd_mask.Background =System.Windows.Media. Brushes.Transparent;
}
private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
{
_play.IsPause = true;
bd_mask.Background = System.Windows.Media.Brushes.RoyalBlue;
}
}
}
总结
以上就是今天要讲的内容,本章的实现是有一定难度的,其灵感来源有flutter,也是刚好发现wpf的Clip属性能做到穿透,才有了实现本章的基础,但是限制也不小,当然作为初步探索的版本,以后可以继续优化。本章实现的嵌入方式虽然有限制,但还是能够用来做视频渲染的,尤其是需要做圆角边框以及拖动的场景,就能做到渲染性能和界面效果的完美结合。