locked
Question about rendering WPF Canvas containing multiple Images RRS feed

  • Question

  • Hello Devels,

    I want to save the Content of a WPF Canvas (which is in a ScrollViewer, so not all Content is visible) to a png. With the following code I get it working.

    void SaveUsingEncoder(Canvas myCanvas, string fileName, int dpi)
            {
                PngBitmapEncoder encoder = new PngBitmapEncoder();
                RenderTargetBitmap bitmap = new RenderTargetBitmap(
                    (int)myCanvas.ActualWidth*(int)(dpi/96.0),
                    (int)myCanvas.ActualHeight*(int)(dpi/96.0),
                    dpi,
                    dpi,
                    PixelFormats.Pbgra32);
    
                Rect bounds = VisualTreeHelper.GetDescendantBounds(myCanvas);
                Console.WriteLine(bounds.X + "|" + bounds.Y + "  " + bounds.Width + "|" + bounds.Height);
                DrawingVisual dv = new DrawingVisual();
                using (DrawingContext ctx = dv.RenderOpen())
                {
                    VisualBrush vb = new VisualBrush(myCanvas);
                    ctx.DrawRectangle(vb, null, new Rect(bounds.Location, bounds.Size));
                }
                bitmap.Render(dv);
                BitmapFrame frame = BitmapFrame.Create(bitmap);
                encoder.Frames.Add(frame);
                using (var stream = File.Create(fileName))
                {
                    encoder.Save(stream);
                }
            }

    But there is the Issue of Quality: I want to put pictures inside of the Canvas Element, but saving the Canvas is more like making a screenshot with a specific resulution. Are there any better ways to do this?

    At the end my application should be able to act like a little paint clone, you can drop pictures into the canvas where you can zoom in/out and then save it to png.

    Friday, April 26, 2013 10:24 AM

Answers

  • Hi,

    I think I finally understood your problem...

    So what about simply rendering the (entire) Canvas to a RTB and saving that? Without a VisualBrush.

                //c1 is the name of the canvas
                //FileName is the path of the file on disk
                RenderTargetBitmap rtb = new RenderTargetBitmap((int)c1.RenderSize.Width, (int)c1.RenderSize.Height, 96, 96, PixelFormats.Default);
                rtb.Render(c1);
    
                //image1.Source = rtb;
    
                using (System.IO.FileStream fileStream = new System.IO.FileStream(FileName, System.IO.FileMode.OpenOrCreate))
                {
                    PngBitmapEncoder encoder = new PngBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(rtb));
                    encoder.Save(fileStream);
                }

    #######################################

    this xaml:

        <ScrollViewer>
            <Canvas x:Name="c1" Width="1400" Height="1000">
                <Button Canvas.Left="1164" Canvas.Top="118" Content="Button" Height="23" Name="button1" Width="75" />
                <Button Canvas.Left="28" Canvas.Top="22" Content="Button" Height="23" Name="button2" Width="75" Click="button2_Click" />
                <Image Canvas.Left="144" Canvas.Top="115" Height="150" Name="image1" Stretch="Fill" Width="200" />
            </Canvas>
        </ScrollViewer>

    with this code:

            private void button2_Click(object sender, RoutedEventArgs e)
            {
                int j = 0;
                string FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "jadflk.png");
                while (System.IO.File.Exists(FileName))
                {
                    FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "jadflk" + j.ToString() + ".png");
                    j++;
                }
                //c1 is the name of the canvas
                RenderTargetBitmap rtb = new RenderTargetBitmap((int)c1.RenderSize.Width, (int)c1.RenderSize.Height, 96, 96, PixelFormats.Default);
                rtb.Render(c1);
    
                //image1.Source = rtb;
    
                using (System.IO.FileStream fileStream = new System.IO.FileStream(FileName, System.IO.FileMode.OpenOrCreate))
                {
                    PngBitmapEncoder encoder = new PngBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(rtb));
                    encoder.Save(fileStream);
                }
            }

    will render the canvas in orig size to the bitmap.

    Regards,

      Thorsten




    • Edited by Thorsten Gudera Wednesday, May 8, 2013 9:44 AM
    • Marked as answer by Duffkess Wednesday, May 8, 2013 10:39 AM
    Wednesday, May 8, 2013 9:37 AM

All replies

  • Hi,

    you simply could save the image(s) that are in the canvas. [or render and save all canvas children in a loop]

    Regards,

      Thorsten


    Friday, April 26, 2013 1:51 PM
  • but how do i do that? is there some kind of save(string filename) function in the canvas class?
    Friday, May 3, 2013 11:18 AM
  • Hi,

    if you know that the children of the canvas are images, simply open a filestream, use an image-encoder, add the frame, save and close the filestream.

            private void button1_Click(object sender, RoutedEventArgs e)
            {
                for (int i = 0; i < canvas1.Children.Count; i++)
                {
                    if (canvas1.Children[i] is Image)
                    {
                        Image img = this.canvas1.Children[i] as Image;
    
                        string FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "TestImg" + i.ToString() + ".png");
    
                        int j = 1;
                        while (System.IO.File.Exists(FileName))
                        {
                            FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "TestImg" + (i + j).ToString() + ".png");
                            j++;
                        }
    
                        using (System.IO.FileStream fileStream = new System.IO.FileStream(FileName, System.IO.FileMode.OpenOrCreate))
                        {
                            PngBitmapEncoder encoder = new PngBitmapEncoder();
                            encoder.Frames.Add(BitmapFrame.Create((BitmapSource)img.Source));
                            encoder.Save(fileStream);
                        }
                    }
                }
            }

    Regards,

      Thorsten



    Friday, May 3, 2013 12:49 PM
  • Thank you very much for your fast replys Thorsten!

    This is working perfectly for saving ONLY the Images of the Canvas, of course. Thank you for that.

    but the problem is that in the end it needs to be one image containing multiple images and text (Labels). Here is an example UI:

    https://www.dropbox.com/sh/k6b4wnkk0gjk176/JnfrKGLUcb#f:example.jpg

    when clicking the button, the page (the root-canvas), should be rendered to one png.

    Is that possible by simple add more Frames to one PngBitmaEncoder object? Could I position them anyhow?

    Thanks again!

    best Regards,

    Duffkess

    === Edit: ===

    For easier implementation of features like highlighting the active image (the user is editing), I wont add Image objects to the root Canvas, I want to add Child-Canvas with a background which has the Image as source, which will look nearly the same.

    • Edited by Duffkess Wednesday, May 8, 2013 8:21 AM
    Wednesday, May 8, 2013 6:46 AM
  • Hi,

    maybe the easiest would be to create one DrawingVisual and draw each Control / Image to it and finally render this drawingVisual into a RenderTargetBitmap...

    [We actually had some - solved - issues with that for very large sizes, see:

    http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/00501aa9-dd54-466f-bd57-3b8ce72e88d0 ]

    If you like I'll do an example this evening... or maybe someone else is faster...

    Regards,

      Thorsten


    Wednesday, May 8, 2013 9:01 AM
  • Hi,

    I think I finally understood your problem...

    So what about simply rendering the (entire) Canvas to a RTB and saving that? Without a VisualBrush.

                //c1 is the name of the canvas
                //FileName is the path of the file on disk
                RenderTargetBitmap rtb = new RenderTargetBitmap((int)c1.RenderSize.Width, (int)c1.RenderSize.Height, 96, 96, PixelFormats.Default);
                rtb.Render(c1);
    
                //image1.Source = rtb;
    
                using (System.IO.FileStream fileStream = new System.IO.FileStream(FileName, System.IO.FileMode.OpenOrCreate))
                {
                    PngBitmapEncoder encoder = new PngBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(rtb));
                    encoder.Save(fileStream);
                }

    #######################################

    this xaml:

        <ScrollViewer>
            <Canvas x:Name="c1" Width="1400" Height="1000">
                <Button Canvas.Left="1164" Canvas.Top="118" Content="Button" Height="23" Name="button1" Width="75" />
                <Button Canvas.Left="28" Canvas.Top="22" Content="Button" Height="23" Name="button2" Width="75" Click="button2_Click" />
                <Image Canvas.Left="144" Canvas.Top="115" Height="150" Name="image1" Stretch="Fill" Width="200" />
            </Canvas>
        </ScrollViewer>

    with this code:

            private void button2_Click(object sender, RoutedEventArgs e)
            {
                int j = 0;
                string FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "jadflk.png");
                while (System.IO.File.Exists(FileName))
                {
                    FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "jadflk" + j.ToString() + ".png");
                    j++;
                }
                //c1 is the name of the canvas
                RenderTargetBitmap rtb = new RenderTargetBitmap((int)c1.RenderSize.Width, (int)c1.RenderSize.Height, 96, 96, PixelFormats.Default);
                rtb.Render(c1);
    
                //image1.Source = rtb;
    
                using (System.IO.FileStream fileStream = new System.IO.FileStream(FileName, System.IO.FileMode.OpenOrCreate))
                {
                    PngBitmapEncoder encoder = new PngBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(rtb));
                    encoder.Save(fileStream);
                }
            }

    will render the canvas in orig size to the bitmap.

    Regards,

      Thorsten




    • Edited by Thorsten Gudera Wednesday, May 8, 2013 9:44 AM
    • Marked as answer by Duffkess Wednesday, May 8, 2013 10:39 AM
    Wednesday, May 8, 2013 9:37 AM
  • Thank you again,

    I adjusted the size/quality a little bit:

    int dpi = 300;
    int dpiDefault = 96;
    RenderTargetBitmap rtb = new RenderTargetBitmap(
         (int)canvas.RenderSize.Width * (int)(dpi / dpiDefault),
         (int)canvas.RenderSize.Height * (int)(dpi / dpiDefault),
         dpi, 
         dpi, 
         PixelFormats.Default
         );
                

    but one more question, must the canvas be visible to be rendered? because later there will be a canvas array which is like "drawing pages" you can go forward and backwards and then I have to render all pages to different files.

    I think I will set always one Element of the Canvas Array as the Content/Child of the ScrollViewer, so the user is able to switch beetween them.

    Thank you for your big help!

    Regards,

    Duffkess

    Wednesday, May 8, 2013 10:39 AM
  • Hi,

    the visibility should be "Visible", else you'll get a black result or an error (when collapsed the size is 0,0). But it could be in the background like this xaml plus the code above will give you the correct result:

        <Grid>
      
            <ScrollViewer>
                <Canvas x:Name="c1" Width="1400" Height="1000" Visibility="Visible">
                    <Button Canvas.Left="1164" Canvas.Top="118" Content="Button" Height="23" Name="button1" Width="75" />
                    <Button Canvas.Left="28" Canvas.Top="22" Content="Button" Height="23" Name="button2" Width="75" Click="button2_Click" />
                    <Image Canvas.Left="144" Canvas.Top="115" Height="150" Name="image1" Stretch="Fill" Width="200" />
                </Canvas>
            </ScrollViewer>      
            <Button x:Name="btn" Click="button2_Click" Content="lkfdjl"></Button>
        </Grid>

    Regards,

      Thorsten


    Wednesday, May 8, 2013 11:03 AM