Search code examples
wpfxamlpng

Dynamic Height and Width When Rendering XAML Buttons to PNG


I'm putting together a prototype to use XAML to create PNG buttons. The basic idea is that nice, gradient image buttons can be generated from localized strings instead of having them created by any sort of graphics dept.

The most straightforward example I found actually was to build a C# assembly and call it from PHP. I converted it to pure C#. Here's what my basically working prototype looks like.

The XAML file looks like:

<UserControl
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"
mc:Ignorable="d"
Width="640" Height="480">

<Grid x:Name="LayoutRoot">
    <Button HorizontalAlignment="Left" VerticalAlignment="Top" Content="Add To Cart" FontFamily="Arial" FontSize="11" Height="24" FontWeight="Bold" Margin="0,3,0,0">
        <Button.Background>
            <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                <GradientStop Color="#FFF3F3F3" Offset="0"/>
                <GradientStop Color="#FFEBEBEB" Offset="0.5"/>
                <GradientStop Color="#FFDDDDDD" Offset="0.502"/>
                <GradientStop Color="#FFCDCDCD" Offset="1"/>
            </LinearGradientBrush>
        </Button.Background>
    </Button>
</Grid>

The Program.cs looks like:

class Program
{
    static void Main(string[] args)
    {
        FileInfo xamlFile = new FileInfo("Button.xaml");
        var inputXaml = File.ReadAllText(xamlFile.FullName);
        Thread pngCreationThread = new Thread((ThreadStart) delegate()
            {
                GenImageFromXaml(inputXaml);
            });
        pngCreationThread.IsBackground = true;
        pngCreationThread.SetApartmentState(ApartmentState.STA);
        pngCreationThread.Start();
        pngCreationThread.Join();
    }
    public static void GenImageFromXaml(string xaml)
    {
        var element = (FrameworkElement)XamlReader.Parse(xaml);
        var pngBytes = GetPngImage(element);
        
        using(BinaryWriter binWriter =
        new BinaryWriter(File.Open(@"output.png", FileMode.Create)))
        {
           binWriter.Write(pngBytes);
        }
    }
    private static byte[] GetPngImage(FrameworkElement element)
    {
        var size = new Size(element.Width, element.Height);
        element.Measure(size);
        element.Arrange(new Rect(size));
        var renderTarget =
          new RenderTargetBitmap((int)element.RenderSize.Width,
                                 (int)element.RenderSize.Height,
                                 96, 96,
                                 PixelFormats.Pbgra32);
        var sourceBrush = new VisualBrush(element);
        var drawingVisual = new DrawingVisual();
        using (DrawingContext drawingContext = drawingVisual.RenderOpen())
        {
            drawingContext.DrawRectangle(
                sourceBrush, null, new Rect(
                                       new Point(0, 0),
                                       new Point(element.RenderSize.Width,
                                       element.RenderSize.Height)));
        }
        renderTarget.Render(drawingVisual);
        var pngEncoder = new PngBitmapEncoder();
        pngEncoder.Frames.Add(BitmapFrame.Create(renderTarget));
        using (var outputStream = new MemoryStream())
        {
            pngEncoder.Save(outputStream);
            return outputStream.ToArray();
        }
    }
}

What I want is XAML files that can be used as templates and the actual string in the button swapped out. My prototype works on a basic level, rendering out an image from the input XAML file.

However, the XAML file specifies the height and width explicitly. That's going to be a problem to have the string on the button itself supplied dynamically since the width won't be known in advance and any font modifications would mess with it too.

What I'm looking for is a way to have the height and width either calculated at runtime or a straightforward way to pre-calculate them based on the text string. Any ideas?


Solution

  • You could try the follwing

    1 Do not set the width and height in xaml

    2 Replace you measure/arrange calls by this:

    var size = new Size(double.PositiveInfinity , double.PositiveInfinity );
    element.Measure(size);
    element.Arrange(new Rect(element.DesiredSize));
    

    Measure sets the DesiredSize property based on the size that you pass it, this is probably the size you want it to be.

    3 (not sure if this is needed) Set your Grid (LayoutRoot) to Top/Left alignment, because Grid likes to take all the available size by default, you dont want that.