Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF StackPanel PNG Capture not rendering correctly

Tags:

c#

wpf

stackpanel

I am attempting to create a png capture of a StackPanel, however when I save, I am getting a distorted view where all the content is black rectangles, and the size is not correct. The width and height are correct in the image save, however all the content is forced to the top and squished together

<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Views="clr-namespace:POExpress.Views" x:Class="POExpress.MainWindow"
Title="My Window" Height="500" MinWidth="1000" Width="1000">
<Grid>
    <TabControl>
        <TabItem Header="My Epics">
            <Grid Background="#FFE5E5E5">
                <Border Margin="0,52,0,0" BorderThickness="1" BorderBrush="Black">
                    <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
                        <StackPanel x:Name="sp_ports" Orientation="Vertical"/>
                    </ScrollViewer>
                </Border>
                <Button x:Name="btn_capture" Content="Save to png" Margin="0,10,114,0" VerticalAlignment="Top" Height="31" Background="White" HorizontalAlignment="Right" Width="99" Click="Btn_capture_Click"/>
            </Grid>
        </TabItem>
    </TabControl>
</Grid>

public RenderTargetBitmap GetImage()
{
    Size size = new Size(sp_ports.ActualWidth, sp_ports.ActualHeight);
    if (size.IsEmpty)
        return null;

    RenderTargetBitmap result = new RenderTargetBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Pbgra32);

    DrawingVisual drawingvisual = new DrawingVisual();
    using (DrawingContext context = drawingvisual.RenderOpen())
    {
        context.DrawRectangle(new VisualBrush(sp_ports), null, new Rect(new Point(), size));
        context.Close();
    }

    result.Render(drawingvisual);
    return result;
}

public static void SaveAsPng(RenderTargetBitmap src)
{
    Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
    dlg.Filter = "PNG Files | *.png";
    dlg.DefaultExt = "png";
    if (dlg.ShowDialog() == true)
    {
        PngBitmapEncoder encoder = new PngBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(src));
        using (var stream = dlg.OpenFile())
        {
            encoder.Save(stream);
        }
    }
}

private void Btn_capture_Click(object sender, RoutedEventArgs e)
{
    SaveAsPng(GetImage());
}

enter image description here

What it should render as (with some info blacked out) enter image description here

like image 332
JeremyK Avatar asked Jan 23 '19 15:01

JeremyK


1 Answers

All UIElements inherit from Visual, so you can feed your StackPanel to the Render method directly.

public RenderTargetBitmap GetImage()
{
    Size size = sp_ports.DesiredSize;
    if (size.IsEmpty)
        return null;

    RenderTargetBitmap result = new RenderTargetBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Pbgra32);

    result.Render(sp_ports);
    return result;
}

UPDATE

As pointed out by @Clemens, there are some subtle intricacies to using the UIElement directly. His other comment, however, is the million dollar one.

Size size = uiElement.DesiredSize

Gives us the size of the visible portion of uiElement.

Size size = new Size(uiElement.ActualWidth, uiElement.ActualHeight)

Returns the full size of uiElement, also extending in the non-visible range.

Given you've run into this problem, you're after the latter. The main gotcha is you'll need to reevaluate the visual before rendering. Currently, you're projecting the full visual to the desired size (the visible part) of the UIElement.

public RenderTargetBitmap GetImage(FrameworkElement element)
{
    Size size = new Size(element.ActualWidth, element.ActualHeight);
    if (size.IsEmpty)
        return null;
    element.Measure(size);
    element.Arrange(new Rect(size));

    RenderTargetBitmap result = new RenderTargetBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Pbgra32);

    DrawingVisual drawingvisual = new DrawingVisual();
    using (DrawingContext context = drawingvisual.RenderOpen())
    {
        context.DrawRectangle(new VisualBrush(element), null, new Rect(new Point(), size));
    }

    result.Render(drawingvisual);
    return result;
}

I use FrameworkElement to incorporate ActualWidth and ActualHeight.


UPDATE 2

As soon as I change the size of the stack panel, the screenshot gets hosed again. It seems to remember whatever the longest state was and squishes based on that.

After some fiddling around I was able to reproduce your issue. It occurs when the StackPanel has to extend to fill any remaining space. The solution is to give the uiElement infinite space to calculate its desired size, which relieves us from the dependency on actual sizes.

public RenderTargetBitmap GetImage(FrameworkElement element)
{
    element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
    element.Arrange(new Rect(element.DesiredSize));

    Size size = element.DesiredSize;
    if (size.IsEmpty)
        return null;

    RenderTargetBitmap result = new RenderTargetBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Pbgra32);

    DrawingVisual drawingvisual = new DrawingVisual();
    using (DrawingContext context = drawingvisual.RenderOpen())
    {
        context.DrawRectangle(new VisualBrush(element), null, new Rect(new Point(), size));
    }

    result.Render(drawingvisual);
    return result;
}

I've checked Expander behavior (ref test app) but couldn't find anything funny going on there.


For completeness, here's my test app.

MainWindow.xaml.cs

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace WpfApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        public RenderTargetBitmap GetImage(FrameworkElement element)
        {
            element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
            element.Arrange(new Rect(element.DesiredSize));

            Size size = element.DesiredSize;
            if (size.IsEmpty)
                return null;

            RenderTargetBitmap result = new RenderTargetBitmap((int)size.Width, (int)size.Height, 96, 96, PixelFormats.Pbgra32);

            DrawingVisual drawingvisual = new DrawingVisual();
            using (DrawingContext context = drawingvisual.RenderOpen())
            {
                context.DrawRectangle(new VisualBrush(element), null, new Rect(new Point(), size));
            }

            result.Render(drawingvisual);
            return result;
        }

        public static void SaveAsPng(RenderTargetBitmap src)
        {
            Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
            dlg.Filter = "PNG Files | *.png";
            dlg.DefaultExt = "png";
            if (dlg.ShowDialog() == true)
            {
                PngBitmapEncoder encoder = new PngBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(src));
                using (var stream = dlg.OpenFile())
                {
                    encoder.Save(stream);
                }
            }
        }

        private void Btn_capture_Click(object sender, RoutedEventArgs e)
        {
            SaveAsPng(GetImage(sp_ports));
        }
    }
}

MainWindow.cs

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <DockPanel LastChildFill="True">
        <Button DockPanel.Dock="Top" Click="Btn_capture_Click">Take Pic</Button>
        <StackPanel x:Name="sp_ports">

            <DataGrid>
                <DataGrid.Columns>
                    <DataGridTextColumn Header="H1" Width="40"/>
                    <DataGridTextColumn Header="H2" Width="*"/>
                </DataGrid.Columns>
            </DataGrid>

            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="200" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="400" />
                </Grid.RowDefinitions>
                <StackPanel Background="Red"/>
                <Expander Grid.Row="1" ExpandDirection="Down" IsExpanded="False">
                    <TabControl Height="400">
                        <TabItem Header="Tab 1">
                            <TextBox FontSize="50" TextWrapping="Wrap">Text for Tab 1</TextBox>
                        </TabItem>
                        <TabItem Header="Tab 2">
                            <TextBox FontSize="50" TextWrapping="Wrap">Text for Tab 1</TextBox>
                        </TabItem>
                    </TabControl>
                </Expander>
                <StackPanel Grid.Row="2" Background="Blue"/>
            </Grid>

            <DataGrid>
                <DataGrid.Columns>
                    <DataGridTextColumn Header="H1" Width="40"/>
                    <DataGridTextColumn Header="H2" Width="*"/>
                </DataGrid.Columns>
            </DataGrid>

        </StackPanel>
    </DockPanel>
</Window>
like image 86
Funk Avatar answered Oct 18 '22 05:10

Funk