2009年9月10日 星期四

在WPF中存圖檔的問題(RenderTargetBitmap)

在WPF裡面要把一個視覺化物件給存成圖檔
需要使用到RenderTargetBitmap,可以參考在WPF中存讀圖檔

但是有時候會發現存出來的圖檔被位移或是根本就沒有影像
這是因為RenderTargetBitmap(RTB)在處理視覺化物件的描繪(Render)時
會依照該物件和其父物件(Parent)的相關性做處理
也就是會因為該物件的Margin或其父物件的Padding或BorderThickness
如果完全不做處理,那就會以父物件的左上角為準,多出一個位移
雖然我覺得這應該算WPF的bug
但是依照MicroSoft的解釋RenderTargetBitmap layout offset influence
似乎覺得這是應該發生的,而且不會再改的感覺

基本上目前我知道的解決方法有三種
方法說明
加一層Border簡單,但是會改變視覺物件結構樹
用VisualBrush畫出不改動到原本架構,但需要多做一些繪製的處理
用Measure和Arrange暫時改變相對位置改動後需要手動改回,而且WPF有針對這兩個函式作最佳化處理,所以不容易瞭解要怎麼用

第一個方法最單純,因為RTB只針對父物件作位移
所以就直接在你需要描繪的物件外面直接包一層類似像Border的容器
而且把需要用的margin都移到這個容器
之後就可以直接依照RTB的使用方法直接用了

如果原本的物件是這樣放

<Grid>
<Canvas Margin="20" />
</Grid>

那就改成

<Grid>
<Border Margin="20">
<Canvas />
</Border>
</Grid>

這樣就可以用之前在在WPF中存讀圖檔提到的方法
將物件送進SaveTo()存起來

private void SaveTo(Visual v, string f)
{
/// get bound of the visual
Rect b = VisualTreeHelper.GetDescendantBounds(v);

/// new a RenderTargetBitmap with actual size of c
RenderTargetBitmap r = new RenderTargetBitmap(
(int)b.Width, (int)b.Height,
96, 96, PixelFormats.Pbgra32);

/// render visual
r.Render(v);

/// new a JpegBitmapEncoder and add r into it
JpegBitmapEncoder e = new JpegBitmapEncoder();
e.Frames.Add(BitmapFrame.Create(r));

/// new a FileStream to write the image file
FileStream s = new FileStream(f,
FileMode.OpenOrCreate, FileAccess.Write);
e.Save(s);
s.Close();
}

第二種方法則是先將該視覺物件畫出來
直接將想要繪製的物件傳入修改過的SaveTo()
只是在render之前先將視覺物件作一個轉換

private DrawingVisual ModifyToDrawingVisual(Visual v)
{
/// new a drawing visual and get its context
DrawingVisual dv = new DrawingVisual();
DrawingContext dc = dv.RenderOpen();

/// generate a visual brush by input, and paint
VisualBrush vb = new VisualBrush(v);
dc.DrawRectangle(vb, null, b);
dc.Close();

return dv;
}

這時候要注意,使用context的時候要記得做close()才會真正做動作
如果怕忘記的話可以改用using將使用的區塊包起來
而原本在SaveTo裡面的Render就改寫成

/// render visual
r.Render(ModifyToDrawingVisual(v));


第三種方法是用Measure和Arrange暫時將位置換一下
一樣在render之前將視覺物件做個轉換

private void ModifyPosition(FrameworkElement fe)
{
/// get the size of the visual with margin
Size fs = new Size(
fe.ActualWidth +
fe.Margin.Left + fe.Margin.Right,
fe.ActualHeight +
fe.Margin.Top + fe.Margin.Bottom);

/// measure the visual with new size
fe.Measure(fs);

/// arrange the visual to align parent with (0,0)
fe.Arrange(new Rect(
-fe.Margin.Left, -fe.Margin.Top,
fs.Width, fs.Height));
}

這邊要注意這種方法只能套用在UIElement上面
另外要記得要把位置arrange回來

private void ModifyPositionBack(FrameworkElement fe)
{
/// remeasure a size smaller than need, wpf will
/// rearrange it to the original position

fe.Measure(new Size());
}

因為這時候measure的大小較實際上的大小為小
所以wpf會自動重新做排版,將位置對齊回去
而原本的render則是改寫成

/// render visual
ModifyPosition(v as FrameworkElement);
r.Render(v);
ModifyPositionBack(v as FrameworkElement);

如果對measure和arrange有興趣的可以參考UIElement.Measure 方法

--
參考資料
RenderTargetBitmap layout offset influence
RenderTargetBitmap tips
RenderTargetBitmap and XamChart - Broken, Redux

1 則留言: