原文地址:https://www.cnblogs.com/younShieh/p/11279420.html
项目中遇到一个难题,需要将上百个没有显示出来的Canvas存储为图片保存在本地。
操作步骤应该是将Canvas转换成位图,然后将位图转换成本地图片。所以一步一步的来吧。
1 . 查阅资料后(百度一下)后得知把位图转换为本地图片进行保存可以通过BitmapSource来进行转换,通过PngBitmapEncoder() 来实现。具体代码如下:
//path为保存路径
using (FileStream outStream = new FileStream(path, FileMode.Create))
{
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
encoder.Save(outStream);
}
2 . 如何将位图转换成图片的方法知道了,接下来就是要把Canvas转换成Bitmap了。RenderTargetBitmap() 方法就可以实现Visual对象到位图的转换。查看Canvas是继承于Visual的,所以理论上应该没问题。尝试了一下,确实如此。代码如下:
RenderTargetBitmap bitmap = new RenderTargetBitmap((int)canvas.ActualWidth, (int)gd.ActualHeight, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(canvas);
3 . 理论上到这里就已经完了,但是我的Canvas并没有显示出来,导致ActualWidth和ActualHeight两个属性值都为0,且不能通过设置大小改变其实际尺寸,这就有点抠脑壳了。那就继续查资料吧。对于ActualWidth和ActualHeight这两个属性,MSDN如是说:
此属性是基于其他宽度输入和布局系统的计算的值。
值由布局系统本身基于实际呈现的传递,设置,因此可能稍微小于属性的设置值如Width是作为输入更改的基础。
因为ActualWidth是计算后的值,应注意可能有多次或递增的报告更改为它作为各种操作结果由布局系统。布局系统可能会计算子元素所需的测量空间、父元素的约束等。
尽管您不能设置此属性从XAML,您可以基于Trigger样式中其值。
MSDN的机翻虽然前言不搭后语,但是大致能明白是什么意思。也就是说ActualWidth和ActualHeight是不能设置的,是通过实际呈现来自动计算出来的。但是我的Canvas没有呈现出来啊,就不能被动计算出来了,这可怎么办呢。。。不可以被动计算,就只有主动去设置,主动计算这条路可以走了。。。又是查阅资料后,还是发现了有路可走的。
通过学习大佬们的经验得知,没有显示的界面也是可以转换成位图的,主要是需要去测量Measure() 和定位Arrange() 来设置Canvas的位置和大小,通过这两个方法可以形成递归布局更新。也就是说通过这两个方法,设置了父元素为子元素计算的最终大小,也就是实际的尺寸。
canvas.Measure(new Size(300, 300));
canvas.Arrange(new Rect(new Size(300,300)));
4 . 理论上到这里就已经完了,但是我的Cancas们还有一个先决条件,就是他们有很多。(问题确实有点多。。。)我需要保存的Canvas数量太多,所以不负众望,我的内存爆了(要爆啦~)。但是我已经做了自动回收,没理由啊。 我尝试查找了很多地方的的可能会出现的问题,毫不吝啬的使用 GC.Collect(); Dispose(); using(),异步,并行,多线程 等等等方法,但是却没什么卵用。。。扣破脑壳,一段代码一段代码的屏蔽,分布排查到底是哪里的问题。我以为是位图转换导致的内存泄露,但是分步调试后发现其实并不怎么消耗内存,而且都做了合理的回收。慢慢调试后我发现原来是我的测量Measure() 和定位Arrange() 这两个方法占用的大量的内存,而且应该是没有回收到。
5 . 首先我想的是我能不能不用这两个方法。因为我的界面不能显示,所以我要把他放到内存里面去渲染,如果不渲染的话,直接设置RenderTargetBitmap() 的 尺寸得到的是空白的图(试过了。。)。查了很久的资料后,确实是没有找到合适的办法,能做到对不显示的图片不用Arrange() 方法就能转换成位图。所以这两个方法我必须要用,所以也不能投机取巧了,只能硬着头皮上了。
6 . baidu、google轮番上阵,MSDN、*各路齐飞,,,都没有找到合适的办法解决。或许没有遇到我这么特殊情况的人吧,也有可能是我的搜索方式有问题。不过,正在我扣破脑壳之际,我想到会不会Canvas 也有对应的Dispose() 方法呢?(原谅我已经晕了,Dispose()只能用于继承于IDisposable类”的知识点早已飞到九霄云外)不过,Canvas确实没有的Dispose() 方法,,,伤心,绝望。那会不会有Arrange() 对应的 Dispose() 方法呢?尝试在Canvas后输入Arrage,得到了一个 InvalidateArrange() 方法:
使元素排列状态(布局)无效。 排列状态失效后,该元素将更新其布局,更新将以异步方式发生,除非随后由 System.Windows.UIElement.UpdateLayout强制执行。
使元素排列状态(布局)无效,不就是释放布局占用的内存资源了?尝试了一下,果然如此。内存占用情况再也不是“一行白鹭上青天”了。。。唉,内存爆了的问题就此得以解决,妈妈再也不用担心我的软件崩溃啦~~~
附代码如下:
canvas.Measure(new System.Windows.Size(1920, 1080));
canvas.Arrange(new Rect(0, 0, 1920, 1080));
renderBitmap.Render(canvas);
canvas.InvalidateArrange();
canvas.InvalidateMeasure();
canvas.UpdateLayout();
由此得到的教训是,代码还是得自己敲。。。不然掉到坑你都不知道怎么爬出来。。。
打完收工。