慢吞云雾缓吐愁 发表于 2024-7-26 20:01:05

.NET 控件转图片

Windows应用开发有很多场景需要动态获取控件表现的图像,即控件转图片,用于别的界面的表现、传输图片数据流、生存为本舆图片等用途。
下面分别介绍下一些实现方式以及主要使用场景
RenderTargetBitmap

控件转图片BitmapImage/BitmapSource,在WPF中可以使用RenderTargetBitmap获取捕获控件的图像。
RenderTargetBitmap是用于将任何 Visual 元素内容渲染为位图的主要工具下面我们展示下简单快速的获取控件图片:
1   private void CaptureButton_OnClick(object sender, RoutedEventArgs e)
2   {
3         var dpi = GetAppStartDpi();
4         var bitmapSource = ToImageSource(Grid1, Grid1.RenderSize, dpi.X, dpi.Y);
5         CaptureImage.Source = bitmapSource;
6   }
7   /// <summary>
8   /// Visual转图片
9   /// </summary>
10   public static BitmapSource ToImageSource(Visual visual, Size size, double dpiX, double dpiY)
11   {
12         var validSize = size.Width > 0 && size.Height > 0;
13         if (!validSize) throw new ArgumentException($"{nameof(size)}值无效:${size.Width},${size.Height}");
14         if (Math.Abs(size.Width) > 0.0001 && Math.Abs(size.Height) > 0.0001)
15         {
16             RenderTargetBitmap bitmap = new RenderTargetBitmap((int)(size.Width * dpiX), (int)(size.Height * dpiY), dpiX * 96, dpiY * 96, PixelFormats.Pbgra32);
17             bitmap.Render(visual);
18             return bitmap;
19         }
20         return new BitmapImage();
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>
4   private static BitmapImage ToBitmapImage(BitmapSource bitmap,Size size,double dpiX,double dpiY)
5   {
6         MemoryStream memoryStream = new MemoryStream();
7         BitmapEncoder encoder = new PngBitmapEncoder();
8         encoder.Frames.Add(BitmapFrame.Create(bitmap));
9         encoder.Save(memoryStream);
10         memoryStream.Seek(0L, SeekOrigin.Begin);
11
12         BitmapImage bitmapImage = new BitmapImage();
13         bitmapImage.BeginInit();
14         bitmapImage.DecodePixelWidth = (int)(size.Width * dpiX);
15         bitmapImage.DecodePixelHeight = (int)(size.Height * dpiY);
16         bitmapImage.StreamSource = memoryStream;
17         bitmapImage.EndInit();
18         bitmapImage.Freeze();
19         return bitmapImage;
20   }这里选择了Png编码器,先将bitmapSource转换成图片流,然后再解码为BitmapImage。
图片编码器有很多种用途,上面是将流转成内存流,也可以转成文件流生存本地文件:
1   var encoder = new PngBitmapEncoder();
2   encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
3   using Stream stream = File.Create(imagePath);
4   encoder.Save(stream);回到控件图片捕获,上方操纵是在界面控件渲染后的场景。如果控件未加载,需要更新布局下:
1   //未加载到视觉树的,按指定大小布局
2   //按size显示,如果设计宽高大于size则按sie裁剪,如果设计宽度小于size则按size放大显示。
3   element.Measure(size);
4   element.Arrange(new Rect(size));另外也存在场景:控件不确定它的详细尺寸,只是想单纯捕获图像,那代码整理后如下:
1   public BitmapSource ToImageSource(Visual visual, Size size = default)
2   {
3         if (!(visual is FrameworkElement element))
4         {
5             return null;
6         }
7         if (!element.IsLoaded)
8         {
9             if (size == default)
10             {
11               //计算元素的渲染尺寸
12               element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
13               element.Arrange(new Rect(new Point(), element.DesiredSize));
14               size = element.DesiredSize;
15             }
16             else
17             {
18               //未加载到视觉树的,按指定大小布局
19               //按size显示,如果设计宽高大于size则按sie裁剪,如果设计宽度小于size则按size放大显示。
20               element.Measure(size);
21               element.Arrange(new Rect(size));
22             }
23         }
24         else if (size == default)
25         {
26             Rect rect = VisualTreeHelper.GetDescendantBounds(visual);
27             if (rect.Equals(Rect.Empty))
28             {
29               return null;
30             }
31             size = rect.Size;
32         }
33
34         var dpi = GetAppStartDpi();
35         return 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"
2         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6         xmlns:local="clr-namespace:VisualBrushDemo"
7         mc: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"
16                        Width="100" Height="30" Background="Red" TextAlignment="Center" LineHeight="30" Padding="0 6 0 0"
17                        MouseDown="TestTextBlock_OnMouseDown"
18                        MouseMove="TestTextBlock_OnMouseMove"
19                        MouseUp="TestTextBlock_OnMouseUp"/>
20         </Canvas>
21         <Grid x:Name="Grid2" Grid.Column="2">
22             <Grid.Background>
23               <VisualBrush Stretch="UniformToFill"
24                              AlignmentX="Center" AlignmentY="Center"
25                              Visual="{Binding ElementName=Grid1}"/>
26             </Grid.Background>
27         </Grid>
28   </Grid>
29 </Window>CS代码:
1   private bool _isDown;
2   private Point _relativeToBlockPosition;
3   private void TestTextBlock_OnMouseDown(object sender, MouseButtonEventArgs e)
4   {
5         _isDown = true;
6         _relativeToBlockPosition = e.MouseDevice.GetPosition(TestTextBlock);
7         TestTextBlock.CaptureMouse();
8   }
9
10   private void TestTextBlock_OnMouseMove(object sender, MouseEventArgs e)
11   {
12         if (_isDown)
13         {
14             var position = e.MouseDevice.GetPosition(Grid1);
15             Canvas.SetTop(TestTextBlock, position.Y - _relativeToBlockPosition.Y);
16             Canvas.SetLeft(TestTextBlock, position.X - _relativeToBlockPosition.X);
17         }
18   }
19
20   private void TestTextBlock_OnMouseUp(object sender, MouseButtonEventArgs e)
21   {
22         TestTextBlock.ReleaseMouseCapture();
23         _isDown = false;
24   }https://img2024.cnblogs.com/blog/685541/202407/685541-20240726164202506-969784593.gif
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 the
3   // main tree and LayoutUpdated is fired, then we do layout for the
4   // VisualBrush tree.
5   // However, this can fail in the case where the main tree is composed
6   // of just Visuals and never does layout nor fires LayoutUpdated. So
7   // 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 look
11   // at Loaded callbacks, then render, and then finally the Dispather will
12   // fire us for layout. So during loaded callbacks we would not have done
13   // layout on the VisualBrush tree.
14   //
15   // Depending upon which of the two layout passes comes first, we cancel
16   // the other layout pass.
17   element.LayoutUpdated += OnLayoutUpdated;
18   _DispatcherLayoutResult = Dispatcher.BeginInvoke(
19         DispatcherPriority.Normal,
20         new DispatcherOperationCallback(LayoutCallback),
21         element);
22   _pendingLayout = true;而表现绑定元素,VisualBrush内部是通过元素Visual.Render方法将图像给到渲染上下文:
1   RenderContext rc = new RenderContext();
2   rc.Initialize(channel, DUCE.ResourceHandle.Null);
3   vVisual.Render(rc, 0);其内部是将Visual的快照拿来表现输出。VisualBrush基于这种渲染快照的机制,不会影响原始视觉元素在原来视觉树的位置,所以并不会导致差别视觉树之间的冲突。
此类VisualBrush方案,适合制作预览表现,比如打印预览、PPT页面预览列表等。
下面是我们团队开发的会议白板-页面列表预览效果:
https://img2024.cnblogs.com/blog/685541/202407/685541-20240726173221237-1973522108.png
 
 关键字:RenderTargetBitmap、VisualBrush、控件转图片/控件截图
出处:http://www.cnblogs.com/kybs0/本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文毗连,否则生存追究法律责任的权利。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: .NET 控件转图片