I have the tabbed application with a separate canvas (with different UserControls on them) on every page like this. Now I need to save all pages (canvases) to images. The code is like this:
public static System.Drawing.Bitmap ExportToImage(Canvas canvas)
{
// Save old background
Brush background = canvas.Background;
// Clear background to make images free of it
canvas.Background = null;
//canvas.UpdateLayout();
//canvas.InvalidateVisual();
// Create a render bitmap and push the surface to it
RenderTargetBitmap renderBitmap =
new RenderTargetBitmap(
(int)canvas.Width,
(int)canvas.Height,
96d,
96d,
PixelFormats.Pbgra32);
renderBitmap.Render(canvas);
MemoryStream picStream = new MemoryStream();
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
encoder.Save(picStream);
canvas.Background = background;
return new System.Drawing.Bitmap(picStream);
}
I make additional transformations, such as changing of margins and size, but it's not significant.
For canvas that is on active page (currently on the screen) I get the normal rendered image: no background, arranged position and size etc).
But for canvas on inactive pages I get images with the original canvas look (with background and not arranged). How can I force canvas to apply my modifications and render with them? I try to use UpdateLayout and InvalidateVisual on canvas, but I got no effect.
WPF unloads non-visible objects, and this means that when you navigate away from a Tab, it will unload the UI controls on it and and load a new set of controls.
To avoid this behavior, I typically use an extended version of the TabControl that stores the ContentPresenter of each TabItem when you navigate away from it, and when you go back to that tab it reloads the ContentPresenter instead of redrawing everything. It takes up a bit more memory, however I find it better on performance since the TabItem no longer has to re-create all the controls that were on it whenever you switch tabs.
You should be able to use this and base your images off the ContentPresenter stored for each TabItem
The original code is from here, although the site's been down for a few months now and I don't know where it moved to. I've altered it a bit because I needed to allow for dragging/dropping tab items to rearrange them without redrawing them, but that shouldn't affect anything.
// Extended TabControl which saves the displayed item so you don't get the performance hit of
// unloading and reloading the VisualTree when switching tabs
// Obtained from http://www.pluralsight-training.net/community/blogs/eburke/archive/2009/04/30/keeping-the-wpf-tab-control-from-destroying-its-children.aspx
// and made a some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations
[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : System.Windows.Controls.TabControl
{
// Holds all items, but only marks the current tab's item as visible
private Panel _itemsHolder = null;
// Temporaily holds deleted item in case this was a drag/drop operation
private object _deletedObject = null;
public TabControlEx()
: base()
{
// this is necessary so that we get the initial databound selected item
this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
/// <summary>
/// if containers are done, generate the selected item
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
UpdateSelectedItem();
}
}
/// <summary>
/// get the ItemsHolder and generate any children
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
UpdateSelectedItem();
}
/// <summary>
/// when the items change we remove any generated panel children and add any new ones as necessary
/// </summary>
/// <param name="e"></param>
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (_itemsHolder == null)
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Reset:
_itemsHolder.Children.Clear();
if (base.Items.Count > 0)
{
base.SelectedItem = base.Items[0];
UpdateSelectedItem();
}
break;
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
// Search for recently deleted items caused by a Drag/Drop operation
if (e.NewItems != null && _deletedObject != null)
{
foreach (var item in e.NewItems)
{
if (_deletedObject == item)
{
// If the new item is the same as the recently deleted one (i.e. a drag/drop event)
// then cancel the deletion and reuse the ContentPresenter so it doesn't have to be
// redrawn. We do need to link the presenter to the new item though (using the Tag)
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
int index = _itemsHolder.Children.IndexOf(cp);
(_itemsHolder.Children[index] as ContentPresenter).Tag =
(item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
}
_deletedObject = null;
}
}
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
_deletedObject = item;
// We want to run this at a slightly later priority in case this
// is a drag/drop operation so that we can reuse the template
this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
new Action(delegate()
{
if (_deletedObject != null)
{
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
this._itemsHolder.Children.Remove(cp);
}
}
}
));
}
}
UpdateSelectedItem();
break;
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException("Replace not implemented yet");
}
}
/// <summary>
/// update the visible child in the ItemsHolder
/// </summary>
/// <param name="e"></param>
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
void UpdateSelectedItem()
{
if (_itemsHolder == null)
{
return;
}
// generate a ContentPresenter if necessary
TabItem item = GetSelectedTabItem();
if (item != null)
{
CreateChildContentPresenter(item);
}
// show the right child
foreach (ContentPresenter child in _itemsHolder.Children)
{
child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
}
}
/// <summary>
/// create the child ContentPresenter for the given item (could be data or a TabItem)
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
ContentPresenter CreateChildContentPresenter(object item)
{
if (item == null)
{
return null;
}
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
return cp;
}
// the actual child to be added. cp.Tag is a reference to the TabItem
cp = new ContentPresenter();
cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
cp.ContentTemplate = this.SelectedContentTemplate;
cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
cp.ContentStringFormat = this.SelectedContentStringFormat;
cp.Visibility = Visibility.Collapsed;
cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
_itemsHolder.Children.Add(cp);
return cp;
}
/// <summary>
/// Find the CP for the given object. data could be a TabItem or a piece of data
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
ContentPresenter FindChildContentPresenter(object data)
{
if (data is TabItem)
{
data = (data as TabItem).Content;
}
if (data == null)
{
return null;
}
if (_itemsHolder == null)
{
return null;
}
foreach (ContentPresenter cp in _itemsHolder.Children)
{
if (cp.Content == data)
{
return cp;
}
}
return null;
}
/// <summary>
/// copied from TabControl; wish it were protected in that class instead of private
/// </summary>
/// <returns></returns>
protected TabItem GetSelectedTabItem()
{
object selectedItem = base.SelectedItem;
if (selectedItem == null)
{
return null;
}
if (_deletedObject == selectedItem)
{
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
}
return item;
}
}
Edit per comments below
If this doesn't work for you, you can try rendering your Canvas like normal, then printing it at a lower DispatcherPriority than DispatcherPriority.Render so it prints after all the rendering has occurred.
Something like this:
public static void ExportToImage(Canvas canvas, System.Drawing.Bitmap bmp)
{
// Save old background
Brush background = canvas.Background;
// Clear background to make images free of it
canvas.Background = null;
// Create a render bitmap and push the surface to it
RenderTargetBitmap renderBitmap =
new RenderTargetBitmap(
(int)canvas.Width,
(int)canvas.Height,
96d,
96d,
PixelFormats.Pbgra32);
renderBitmap.Render(canvas);
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
new Action(delegate()
{
MemoryStream picStream = new MemoryStream();
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
encoder.Save(picStream);
canvas.Background = background;
// I don't think you can simply return your value here,
// so you'll probably need to setup something else to
// return your bitmap to your calling code
bmp = new System.Drawing.Bitmap(picStream);
}));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With