WPF Hwnd窗口互操作系列
第一章 嵌入Hwnd窗口
第二章 嵌入WinForm控件
第三章 嵌入WPF控件
第四章 底部嵌入HwndHost(本章)
文章目录
- WPF Hwnd窗口互操作系列
- 前言
- 一、如何实现?
- 1、底部创建窗口
- (1)、创建透明窗口
- (2)、监控顶部窗口事件
- (3)、窗口保持在底部
- 2、HwndHost加入底部窗口
- 3、Clip穿透顶部窗口
- 4、WPF控件放在装饰层
- 二、完整代码
- 1、对象定义
- 2、完整代码
- 三、使用示例
- 1、嵌入Winform控件
- 2、显示视频
- 3、圆角矩形视频
- 4、拖动位置大小
- 总结
前言
前面三章内容是笔者基于本章内容研发过程中的附带产物,但是意外的发现第三章可嵌入wpf控件后,本章的意义就变得不那么大了。本章讲述如何从底部嵌入hwnd窗口,以此来做到嵌入窗口不覆盖wpf控件的效果,这种实现思路参考了flutter的一个插件flutter_native_view,其内部实现就用了这种方式。对于wpf实现会复杂一些,因为提供自绘没有BlendMode之类的东西,无法直接消除底部画面,能够使用的方式是Clip,这种方式限制比较多。不过最终还是实现了功能。
一、如何实现?
1、底部创建窗口
(1)、创建透明窗口
_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监控上层窗口的事件
HwndSource.FromHwnd(_forgroundHandle).AddHook(new HwndSourceHook(WndProc));
(3)、窗口保持在底部
监控相关事件保证窗口始终贴在下面。
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代码中直接将其添加到底部窗口中。
[ContentProperty("Content")]
public class HwndHostBottomEmbbeder : FrameworkElement
{public HwndHost Content{get { return (HwndHost)GetValue(HostProperty); }set { SetValue(HostProperty, value); }}
}
var grid = _backgroundWindow.Content as Grid;
grid.Children.Add(Content);
3、Clip穿透顶部窗口
和其他嵌入方式一样,需要先继承HwndHost。clip原理参考《C# wpf利用Clip属性实现截屏框》
//顶部窗口句柄
IntPtr _forgroundHandle = IntPtr.Zero;
//底部窗口句柄
IntPtr _backgroundHandle = IntPtr.Zero;
//底部窗口句柄
Window _backgroundWindow = null;
//顶部窗口
Window _foregroundWindow = null;
//顶部窗口的Content
FrameworkElement? _foreroundContent;
//嵌入窗口的区域
RectangleGeometry? _clipRect;
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);//创建clipvar 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中使用
public class HwndHostBottomEmbbeder : FrameworkElement{public UIElement Adorner{get { return (UIElement)GetValue(ContentProperty); }set { SetValue(ContentProperty, value); }}
}
定义一个装饰器对象
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后无法响应鼠标事件)。
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、对象定义
/************************************************************************* @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
<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
<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
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
<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
<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
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属性能做到穿透,才有了实现本章的基础,但是限制也不小,当然作为初步探索的版本,以后可以继续优化。本章实现的嵌入方式虽然有限制,但还是能够用来做视频渲染的,尤其是需要做圆角边框以及拖动的场景,就能做到渲染性能和界面效果的完美结合。