Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top