views:

422

answers:

2

I have a program wherein there are multiple tabs added dynamically to a TabControl object programmatically. What I want to do is render the Content value of each of these tabs to a PNG. I am using a script I picked up elsewhere on StackOverflow or perhaps Google (lost the source). My code looks like this:

if (tabPanel.Items.Count > 0)
{
    SaveFileDialog fileDialog = new SaveFileDialog();
    fileDialog.Filter = "PNG|*.png";
    fileDialog.Title = "Save Tabs";
    fileDialog.ShowDialog();

    if (fileDialog.FileName.Trim().Length > 0)
    {
        try
        {
            string filePrefix = fileDialog.FileName.Replace(".png", "");
            int tabNo = 1;
            foreach (TabItem tabItem in tabPanel.Items)
            {
                string filename = filePrefix + "_" + tabNo + ".png";

                TabContentControl content = tabItem.Content as TabContentControl;
                Rect rect = new Rect(content.RenderSize);
                RenderTargetBitmap rtb = new RenderTargetBitmap((int)rect.Right, (int)rect.Bottom, 96d, 96d, System.Windows.Media.PixelFormats.Default);
                rtb.Render(content);

                BitmapEncoder pngEncoder = new PngBitmapEncoder();
                pngEncoder.Frames.Add(BitmapFrame.Create(rtb));

                System.IO.MemoryStream ms = new System.IO.MemoryStream();
                pngEncoder.Save(ms);
                System.IO.File.WriteAllBytes(filename, ms.ToArray());
                ms.Close();

                tabNo++;
            }
        }
        catch (Exception ex)
        {
            // log exception
        }
    }
}

This code works as desired if I have gone through and viewed all the tabs that must be rendered before invoking this code. It goes ahead and creates filePrefix_1.png, filePrefix_2.png, etc. with the correct content rendered from TabContentControl. However, if I invoke the handler that uses this code before having viewed all the tabs, my code throws an exception at new RenderTargetBitmap(...) because content.RenderSize is {0.0, 0.0}. When I try to force the render size of an unviewed tab to one of the viewed once, my outputted PNG is of the correct dimensions but completely empty.

So I guess I need some way to force rendering of TabContentControl. Seems like the Render event is only run, as it should be, when the UIElement needs to be rendered. Is their any trickery that I can perform to get around this?

I have also tried to "trick" WPF into painting a tab content by adding the following code when the tabs are created, in a Page_Loaded event handler:

void Page_Loaded(object sender, RoutedEventArgs e)
{
    // irrelevant code
    foreach (// iterate over content that is added to each tab)
    {
        TabItem tabItem = new TabItem();
        // load content
        tabPanel.Items.Add(tabItem);
        tabItem.IsSelected = true;
    }
    // tabPanel.SelectedIndex = 0;
}

When the last line in the Page_Loaded handler is commented out, the last tab is in focus and has the RenderSize property defined for its content. When the last line is not commented out, the first tab is in focus, with the same behavior. The other tabs do not have any rendering information.

+1  A: 

This is not really a ideal solution, but as you create your tabs and add them to the tab control, you can just store the tab that is currently open, then switch to the tab you have just created, and then switch back. To the user it would just be a slight flicker but in reality you have just tricked winforms (or wpf accordingly) into drawing the object.

Tommy
I tried doing something similar but for the life of me couldn't figure out how to change the visible tab programmatically. Tried setting `TabControl.SelectedItem` to a `TabItem` as well as incrementing the `TabControl.SelectedIndex` property.
sohum
Actually..... I just realized that what I was doing was trying to change the tabs after the save button had been clicked, from within the handler... so I guess there was no opportunity for the tab to be repainted? I guess if I do this when adding the tabs it won't be an issue.
sohum
Just tried it, it did not work. WPF seems to optimize and only paint the last tab opened.
sohum
In that case, you could always go through your tabs one by one, saving each as you go
Tommy
I'm pretty sure I've tried that already, but I'll give it another swing.
sohum
Yup. The same issue remains. Unless I somehow force the render to happen after setting `tab.IsSelected = true`, the UI thread only gets a chance to update after the save has failed.
sohum
A: 

Finally figured it out, thanks to this blog post. The solution involved creating an extension method for UIElement with a Refresh method that invoked an empty delegate with render priority. Apparently scheduling something with Render priority caused all other more important items to be executed, thus changing the tab.

Code replicated here in case blog is deleted:

public static class ExtensionMethods
{
   private static Action EmptyDelegate = delegate() { };

   public static void Refresh(this UIElement uiElement)
   {
      uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
   }
}

void Page_Loaded(object sender, RoutedEventArgs e)
{
    // irrelevant code
    foreach (// iterate over content that is added to each tab)
    {
        TabItem tabItem = new TabItem();
        // load content
        tabPanel.Items.Add(tabItem);
        tabItem.IsSelected = true;
        tabItem.Refresh();
    }
    // tabPanel.SelectedIndex = 0;
}

To use it, just include the extension namespace in the code file that you need to use this functionality and it will appear in the list of methods.

sohum