Windows应用开发有很多场景需要动态获取控件显示的图像,即控件转图片,用于其它界面的显示、传输图片数据流、保存为本地图片等用途。
下面分别介绍下一些实现方式以及主要使用场景
RenderTargetBitmap
控件转图片BitmapImage/BitmapSource,在WPF中可以使用RenderTargetBitmap获取捕获控件的图像。
下面我们展示下简单快速的获取控件图片:
1privatevoidCaptureButton_OnClick(object sender, RoutedEventArgs e)2{3var dpi =GetAppStartDpi();4var bitmapSource = ToImageSource(Grid1, Grid1.RenderSize, dpi.X, dpi.Y);5 CaptureImage.Source =bitmapSource;6}7///<summary>8///Visual转图片9///</summary>10publicstatic BitmapSource ToImageSource(Visual visual, Size size, doubledpiX,doubledpiY)11{12var validSize = size.Width > 0 && size.Height > 0;13if(!validSize)thrownewArgumentException($"{nameof(size)}值无效:${size.Width},${size.Height}");14if (Math.Abs(size.Width) > 0.0001 && Math.Abs(size.Height) > 0.0001)15{16 RenderTargetBitmap bitmap = newRenderTargetBitmap((int)(size.Width * dpiX), (int)(size.Height * dpiY), dpiX * 96, dpiY * 96, PixelFormats.Pbgra32);17bitmap.Render(visual);18returnbitmap;19}20returnnewBitmapImage();21}
获取当前窗口所在屏幕DPI,使用控件已经渲染的尺寸,就可以捕获到指定控件的渲染图片。捕获到图片BitmapSource,即可以将位图分配给Image的Source属性来显示。
DPI获取可以参考 C# 获取当前屏幕DPI - 唐宋元明清2188 - 博客园 (cnblogs.com)
上面方法获取的是BitmapSource,BitmapSource是WPF位图的的抽象基类,继承自ImageSource,因此可以直接用作WPF控件如Image的图像源。RenderTargetBitmap以及BitmapImage均是BitmapSource的派生实现类
RenderTargetBitmap此处用于渲染Visual对象生成位图,RenderTargetBitmap它可以用于拼接、合并(上下层叠加)、缩放图像等。BitmapImage主要用于从文件、URL及流中加载位图。
而捕获返回的基类BitmapSource可以用于通用位图的一些操作(如渲染、转成流数据、保存),BitmapSource如果需要转成可以支持支持更高层次图像加载功能和延迟加载机制的BitmapImage,可以按如下操作:
1///<summary>2///WPF位图转换3///</summary>4privatestatic BitmapImage ToBitmapImage(BitmapSource bitmap,Size size,doubledpiX,doubledpiY)5{6 MemoryStream memoryStream = newMemoryStream();7 BitmapEncoder encoder = newPngBitmapEncoder();8encoder.Frames.Add(BitmapFrame.Create(bitmap));9encoder.Save(memoryStream);10memoryStream.Seek(0L, SeekOrigin.Begin);1112 BitmapImage bitmapImage = newBitmapImage();13bitmapImage.BeginInit();14 bitmapImage.DecodePixelWidth = (int)(size.Width *dpiX);15 bitmapImage.DecodePixelHeight = (int)(size.Height *dpiY);16 bitmapImage.StreamSource =memoryStream;17bitmapImage.EndInit();18bitmapImage.Freeze();19returnbitmapImage;20}
这里选择了Png编码器,先将bitmapSource转换成图片流,然后再解码为BitmapImage。
图片编码器有很多种用途,上面是将流转成内存流,也可以转成文件流保存本地文件:
1var encoder = newPngBitmapEncoder();2encoder.Frames.Add(BitmapFrame.Create(bitmapSource));3using Stream stream =File.Create(imagePath);4encoder.Save(stream);
回到控件图片捕获,上方操作是在界面控件渲染后的场景。如果控件未加载,需要更新布局下:
1//未加载到视觉树的,按指定大小布局2//按size显示,如果设计宽高大于size则按sie裁剪,如果设计宽度小于size则按size放大显示。3element.Measure(size);4element.Arrange(newRect(size));
另外也存在场景:控件不确定它的具体尺寸,只是想单纯捕获图像,那代码整理后如下:
1public BitmapSource ToImageSource(Visual visual, Size size = default)2{3if(!(visualis FrameworkElement element))4{5returnnull;6}7if(!element.IsLoaded)8{9if (size == default)10{11//计算元素的渲染尺寸12element.Measure(newSize(double.PositiveInfinity,double.PositiveInfinity));13element.Arrange(newRect(new Point(), element.DesiredSize));14 size =element.DesiredSize;15}16else17{18//未加载到视觉树的,按指定大小布局19//按size显示,如果设计宽高大于size则按sie裁剪,如果设计宽度小于size则按size放大显示。20element.Measure(size);21element.Arrange(newRect(size));22}23}24elseif (size == default)25{26 Rect rect =VisualTreeHelper.GetDescendantBounds(visual);27if(rect.Equals(Rect.Empty))28{29returnnull;30}31 size =rect.Size;32}3334var dpi =GetAppStartDpi();35return ToImageSource(visual, size, dpi.X, dpi.Y);36}
控件未加载时,可以使用DesiredSize来临时替代操作,这类方案获取的图片宽高比例可能不太准确。已加载完的控件,可以通过VisualTreeHelper.GetDescendantBounds获取视觉树子元素集的坐标矩形区域Bounds。
kybs00/VisualImageDemo: RenderTargetBitmap获取控件图片 (github.com)
所以控件转BitmapSource、保存等,可以使用RenderTargetBitmap来实现
VisualBrush
如果只是程序内其它界面同步展示此控件,就不需要RenderTargetBitmap了,可以直接使用VisualBrush
VisualBrush是非常强大的类,允许使用另一个Visual对象(界面显示控件最底层的UI元素基类)作为画刷的内容,并将其绘制在其它UI元素上(当然,不是直接挂到其它视觉树上,WPF也不支持元素同时存在于俩个视觉树的设计)
具体的可以看下官网VisualBrush 类 (System.Windows.Media) | Microsoft Learn,这里做一个简单的DEMO:
1 <Window x:Class="VisualBrushDemo.MainWindow"2xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"3xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"4xmlns:d="http://schemas.microsoft.com/expression/blend/2008"5xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"6xmlns:local="clr-namespace:VisualBrushDemo"7mc:Ignorable="d"Title="MainWindow"Height="450"Width="800">8<Grid>9<Grid.ColumnDefinitions>10 <ColumnDefinition Width="*"/>11 <ColumnDefinition Width="10"/>12<ColumnDefinition/>13</Grid.ColumnDefinitions>14 <Canvas x:Name="Grid1"Background="BlueViolet">15 <TextBlock x:Name="TestTextBlock"Text="截图测试"VerticalAlignment="Center"HorizontalAlignment="Center"16Width="100"Height="30"Background="Red"TextAlignment="Center"LineHeight="30"Padding="0 6 0 0"17MouseDown="TestTextBlock_OnMouseDown"18MouseMove="TestTextBlock_OnMouseMove"19MouseUp="TestTextBlock_OnMouseUp"/>20</Canvas>21 <Grid x:Name="Grid2"Grid.Column="2">22<Grid.Background>23 <VisualBrush Stretch="UniformToFill"24AlignmentX="Center"AlignmentY="Center"25Visual="{Binding ElementName=Grid1}"/>26</Grid.Background>27</Grid>28</Grid>29</Window>
CS代码:
1privatebool_isDown;2private Point _relativeToBlockPosition;3privatevoidTestTextBlock_OnMouseDown(object sender, MouseButtonEventArgs e)4{5 _isDown = true;6 _relativeToBlockPosition =e.MouseDevice.GetPosition(TestTextBlock);7TestTextBlock.CaptureMouse();8}910privatevoidTestTextBlock_OnMouseMove(object sender, MouseEventArgs e)11{12if(_isDown)13{14var position =e.MouseDevice.GetPosition(Grid1);15 Canvas.SetTop(TestTextBlock, position.Y -_relativeToBlockPosition.Y);16 Canvas.SetLeft(TestTextBlock, position.X -_relativeToBlockPosition.X);17}18}1920privatevoidTestTextBlock_OnMouseUp(object sender, MouseButtonEventArgs e)21{22TestTextBlock.ReleaseMouseCapture();23 _isDown = false;24}
kybs00/VisualImageDemo: RenderTargetBitmap获取控件图片 (github.com)
左侧操作一个控件移动,右侧区域动态同步显示左侧视觉。VisualBrush.Visual可以直接绑定指定控件,一次绑定、后续同步界面变更,延时超低
同步界面变更是如何操作的?下面是部分代码,我们看到,VisualBrush内有监听元素的内容变更,内容变更后VisualBrush也会自动同步DoLayout(element)一次:
1// We need 2 ways of initiating layout on the VisualBrush root.2// 1. We add a handler such that when the layout is done for the3// main tree and LayoutUpdated is fired, then we do layout for the4// VisualBrush tree.5// However, this can fail in the case where the main tree is composed6// of just Visuals and never does layout nor fires LayoutUpdated. So7// we also need the following approach.8// 2. We do a BeginInvoke to start layout on the Visual. This approach 9// alone, also falls short in the scenario where if we are already in 10// MediaContext.DoWork() then we will do layout (for main tree), then look11// at Loaded callbacks, then render, and then finally the Dispather will 12// fire us for layout. So during loaded callbacks we would not have done13// layout on the VisualBrush tree.14//15// Depending upon which of the two layout passes comes first, we cancel16// the other layout pass.17 element.LayoutUpdated +=OnLayoutUpdated;18 _DispatcherLayoutResult =Dispatcher.BeginInvoke(19DispatcherPriority.Normal,20newDispatcherOperationCallback(LayoutCallback),21element);22 _pendingLayout = true;
而显示绑定元素,VisualBrush内部是通过元素Visual.Render方法将图像给到渲染上下文:
1 RenderContext rc = newRenderContext();2 rc.Initialize(channel, DUCE.ResourceHandle.Null);3vVisual.Render(rc,0);
其内部是将Visual的快照拿来显示输出。VisualBrush基于这种渲染快照的机制,不会影响原始视觉元素在原来视觉树的位置,所以并不会导致不同视觉树之间的冲突。
此类VisualBrush方案,适合制作预览显示,比如打印预览、PPT页面预览列表等。
下面是我们团队开发的会议白板-页面列表预览效果:
关键字:RenderTargetBitmap、VisualBrush、控件转图片/控件截图