locked
"Save as image" available programatically? RRS feed

  • Question

  • Hi,

    With the rehosted designer it is possible to right click in the design area and choose to save the workflow as an image. This is cool functionality and was wondering if it was possible to call this programatically? This would then allow me to show an image of what the workflow looks like without having to load up the full designer.

    Does anyone know if this is possible or how it can be done.

    Thanks

    Matt
    Wednesday, February 3, 2010 9:27 PM

Answers

  • Alternatively, you can use render the designer view on RenderTargetBitmap. Code similar to the following:

                const double DPI = 96.0;
    
                Rect size = VisualTreeHelper.GetDescendantBounds(view);
                int imageWidth = (int)size.Width;
                int imageHeight = (int)size.Height;
    
                RenderTargetBitmap renderBitmap = new RenderTargetBitmap(imageWidth, imageHeight, DPI, DPI, PixelFormats.Pbgra32);
                renderBitmap.Render(view);
                BitmapFrame bf = BitmapFrame.Create(renderBitmap);
    
                using (FileStream fs = new FileStream(@"c:\test.jpg", FileMode.Create))
                {
                    JpegBitmapEncoder encoder = new JpegBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(bf));
                    encoder.Save(fs);
                    fs.Close();
                }
    



    Best Regards,

    Leo

    This posting is provided "AS IS" and confers no rights or warranties.

    • Proposed as answer by Leo.Chen Wednesday, March 10, 2010 1:38 AM
    • Marked as answer by Scott Mason - MSFT Monday, March 29, 2010 9:51 PM
    Wednesday, March 10, 2010 1:38 AM

All replies

  • you can use following code to call saveasimage

    can you try following code?
    new DesignerMetadata().Register();
                WorkflowDesigner designer = new WorkflowDesigner();
                designer.Load(new Sequence());

    Window win = new Window();

     

    win.Show();
                ((RoutedCommand)DesignerView.SaveAsImageCommand).Execute(null, designer.Context.Services.GetService<DesignerView>());

    win.Content = designer.View;

    • Proposed as answer by Ye Yu - MSFT Wednesday, February 3, 2010 10:10 PM
    • Edited by Ye Yu - MSFT Friday, February 5, 2010 12:36 AM make the code more complete
    Wednesday, February 3, 2010 10:08 PM
  • Thanks for the reply, it's not quite what I am after.

    What I want to do is to be able to generate an image of the workflow and then display that on the screen. So I guess more of a copy rather than a save to file. I want this to be able to be displayed in my application without having to create a Workflow Designer.

    I tried using the DesignerView.CopyAsImageCommand and then calling Clipboard.GetAsImage immediatly afterwards but that just returned null.
    Thursday, February 4, 2010 7:06 AM
  • I think I misunderstood what you want before. I had thought you just don't want to launch the whole rehost app.
    however, it seems you don't want to launch the workflowdesigner totally.

    it's impossible to copyasimage or saveasimage without launch the workflowdesigner.
    at least, you need to add following code before your copyasimage code

     

     

    Window win = new Window();

    win.Content = designer.View;

    win.Show();


    because without these code, the designer's height and width are zero.
    the screenshot can't be taken.
    Friday, February 5, 2010 12:34 AM
  • That makes sense - thanks for that.

    Friday, February 5, 2010 9:58 PM
  • when you add above code, you can get the image from clipboard.
    Friday, February 5, 2010 10:39 PM
  • For anyone else who didn't quite understand these answers (probably because I've never done any WPF or anything similar before) Here is the code listing that I came up with:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Activities.Presentation;
    using System.Activities.Statements;
    using System.Activities.Presentation.View;
    using System.Activities.Core.Presentation;
    using System.Windows;
    using System.Windows.Input;
    using System.Threading;
    using System.Windows.Media.Imaging;
    using System.IO;
    
    namespace ConsoleApplication1
    {
    
        class Program
        {
            
            static void Main(string[] args)
            {
                Thread thread = new Thread(new ThreadStart(DrawPicture));
                thread.SetApartmentState(ApartmentState.STA);
                thread.Start();
            }
    
            static void DrawPicture()
            {
                new DesignerMetadata().Register();
                WorkflowDesigner designer = new WorkflowDesigner();
                designer.Load(new Sequence());
                Window win = new Window();
                win.Show();
                ((RoutedCommand)DesignerView.CopyAsImageCommand).Execute(null, designer.Context.Services.GetService<DesignerView>());
    
                win.Content = (object)designer.View;
                win.Show();
    
                System.Windows.Interop.InteropBitmap m = (System.Windows.Interop.InteropBitmap)Clipboard.GetImage();
    
                JpegBitmapEncoder encoder = new JpegBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(m));
                using (FileStream fs = new FileStream(@"C:\out.jpg", FileMode.OpenOrCreate))
                {
                    encoder.Save(fs);
                }
            }
        }
    }
    
    George
    • Proposed as answer by Andrew_Zhu Wednesday, November 24, 2010 7:14 AM
    Tuesday, March 9, 2010 2:18 PM
  • For anyone else who didn't quite understand these answers (probably because I've never done any WPF or anything similar before) Here is the code listing that I came up with:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Activities.Presentation;
    using System.Activities.Statements;
    using System.Activities.Presentation.View;
    using System.Activities.Core.Presentation;
    using System.Windows;
    using System.Windows.Input;
    using System.Threading;
    using System.Windows.Media.Imaging;
    using System.IO;
    
    namespace ConsoleApplication1
    {
    
        class Program
        {
            
            static void Main(string[] args)
            {
                Thread thread = new Thread(new ThreadStart(DrawPicture));
                thread.SetApartmentState(ApartmentState.STA);
                thread.Start();
            }
    
            static void DrawPicture()
            {
                new DesignerMetadata().Register();
                WorkflowDesigner designer = new WorkflowDesigner();
                designer.Load(new Sequence());
                Window win = new Window();
                win.Show();
                ((RoutedCommand)DesignerView.CopyAsImageCommand).Execute(null, designer.Context.Services.GetService<DesignerView>());
    
                win.Content = (object)designer.View;
                win.Show();
    
                System.Windows.Interop.InteropBitmap m = (System.Windows.Interop.InteropBitmap)Clipboard.GetImage();
    
                JpegBitmapEncoder encoder = new JpegBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(m));
                using (FileStream fs = new FileStream(@"C:\out.jpg", FileMode.OpenOrCreate))
                {
                    encoder.Save(fs);
                }
            }
        }
    }
    
    George
    Tuesday, March 9, 2010 2:19 PM
  • Alternatively, you can use render the designer view on RenderTargetBitmap. Code similar to the following:

                const double DPI = 96.0;
    
                Rect size = VisualTreeHelper.GetDescendantBounds(view);
                int imageWidth = (int)size.Width;
                int imageHeight = (int)size.Height;
    
                RenderTargetBitmap renderBitmap = new RenderTargetBitmap(imageWidth, imageHeight, DPI, DPI, PixelFormats.Pbgra32);
                renderBitmap.Render(view);
                BitmapFrame bf = BitmapFrame.Create(renderBitmap);
    
                using (FileStream fs = new FileStream(@"c:\test.jpg", FileMode.Create))
                {
                    JpegBitmapEncoder encoder = new JpegBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(bf));
                    encoder.Save(fs);
                    fs.Close();
                }
    



    Best Regards,

    Leo

    This posting is provided "AS IS" and confers no rights or warranties.

    • Proposed as answer by Leo.Chen Wednesday, March 10, 2010 1:38 AM
    • Marked as answer by Scott Mason - MSFT Monday, March 29, 2010 9:51 PM
    Wednesday, March 10, 2010 1:38 AM
  • Wow, that's a nice one Leo. :-)
    Tim

    Wednesday, March 10, 2010 5:16 AM
  • Hello,

    I can't get this method to work correctly when trying to load in a workflow using the load from path method. I simply get a blank image. Interestingly if I call showdialog I get the correct image being displayed and when I close the screen I get the correct image being displayed and written to file. But I unfortunately cant use show dialog as I want to automate this process.

    I alternatively tried create the workflow instance using the ActivityXamlServices and load this. Using this technique I ran in to a null reference exception here: System.Activities.Presentation.View.ImportDesigner.OnContextChanged(), which I tried to resolve adding  AssemblyContextControlItem where I referenced the assembly that the simple workflow (which contained a sequence activity and that is all) resided in but to no avail.

    I expect I missing something quite simple, but I'm tearing my hair out with this.

     

    Any help, insight would be gratefully recieved.

     

    Regards

    Darren


    Darren
    Tuesday, May 18, 2010 12:52 PM
  • Alternatively, you can use render the designer view on RenderTargetBitmap. Code similar to the following:

          const double DPI = 96.0;
    
    
    
          Rect size = VisualTreeHelper.GetDescendantBounds(view);
    
          int imageWidth = (int)size.Width;
    
          int imageHeight = (int)size.Height;
    
    
    
          RenderTargetBitmap renderBitmap = new RenderTargetBitmap(imageWidth, imageHeight, DPI, DPI, PixelFormats.Pbgra32);
    
          renderBitmap.Render(view);
    
          BitmapFrame bf = BitmapFrame.Create(renderBitmap);
    
    
    
          using (FileStream fs = new FileStream(@"c:\test.jpg", FileMode.Create))
    
          {
    
            JpegBitmapEncoder encoder = new JpegBitmapEncoder();
    
            encoder.Frames.Add(BitmapFrame.Create(bf));
    
            encoder.Save(fs);
    
            fs.Close();
    
          }
    
    
    
    



    Best Regards,

    Leo

    This posting is provided "AS IS" and confers no rights or warranties.


    Dear Leo,

    Would you please provide completed code for what you have posted here?

    I am a littel new to this concept and I didn't find where the 'VIEW' come from in line provided below

    VisualTreeHelper.GetDescendantBounds(view);

    Thanks

    Saturday, May 22, 2010 5:36 AM
  • I'm new to both WPF and Workflow and am trying to implement a "Print" command for the current full Workflow diagram in a rehosted WF4 Designer. 

    When I try your (Leo's) RenderTargetBitmap technique here to get an image of the current WorkflowDesigner's view diagram, I only get the part of the image that is visible within the current WorkflowDesigner.View viewport.   If the diagram is too large to show completely within the GUI, all of the workflow diagram that is not currently visible on the actual GUI is truncated to match what is currently visible.

    There is no truncation to what is currently visible in the view when I use the SaveAsImageCommand approach.   The resulting image from that approach is complete and is exactly what I need to print.   But I need to capture that image in memory so I can print it or display it.   The current MSDN documentation on that SaveAsImageCommand is not very helpful in terms of shedding any light on how it works or where it is implemented.    

    I was hoping to use Reflector to see how SaveAsImage was implemented to see if I could write the resulting image to a memory stream or anything other than a physical file.    If not, at least I wanted to see if I could at directly specify my own hidden temporary directory in code instead of having the default SaveAsImage implementation pop a FileOpenDialog directly to the end user.    

    But Reflector's disassembly window shows nothing for the SaveAsImageCommand interface if I directly examine System.Activities.Presentation.View.DesignerView since it is just an interface rather than an implementation.   Reflector says it used only by the DesignerView's constructor, but disassembly of the constructor just shows RoutedCommand delegate being assigned to a property and so still doesn't give any idea where the actual implementation behind that interface lives.

    I did try the CopyAsImageCommand route but also found that I get null when I try to access the clipboard for the resulting image; even though the diagram is up and showing in the current WorkflowDesigner.View.

    Does anyone have any kind of workaround to get the full, non-truncated current workflow diagram image to print without using the default SaveAsImageCommand implementation and without popping any other intermediate screen constructs (dialogs, extraneous Windows, etc.) visible to the end user?   If at all possible, we need to do this without writing an intermediate physical file to the end-user's local file system.   But if there's no other way, I can do that as long as the whole process is invisible to the user.

    Does anyone know where the actual SaveAsImageCommand implementation lives within the Frameworks assemblies so I can use Reflector to see how the full image is being sucessfully obtained?

    Thanks!

    Wednesday, September 8, 2010 3:24 PM
  • Hi Bob,
    There is a method 'OnSaveAsImageCommandExecuted' on DesignerView. DesignerView is a class, not an interface, make sure you are looking at the real .net assemblies under WINDOWS\Microsoft.net, and not the metadata reference assemblies under Program Files.
    Tim

     

    Wednesday, September 8, 2010 4:39 PM
  • Thank you.   When I said "Interface", I was referring to the ICommand return typing of the "SaveAsImageCommand" ; not the DesignerView itself.   I had not seen that "OnSaveAsImageCommandExecuted" was assigned to that command.   In the DesignView constructor, I had seen:

     SaveAsImageCommand = new RoutedCommand("SaveAsImageCommand", typeof(DesignerView));

    and had expected to easily find a "SaveAsImageCommand" implementation.   Your reply pointed me in the right direction.  

    But I've still hit a snag.   Some of the methods being traversed in OnSaveAsImageExecuted rely on private methods and variables that I do not know how to emulate given only an instance.  Since they are private vs. protected, it seems that subclassing would not help gain access to these resources.

    The call chain I saw from "OnSaveAsImageCommandExecuted" would be reproducible down to the GetScreenShotVisual() call.   But in that call, the DesignerView code directly accesses "this.designerPresenter" and apparently uses that as a Visual to produce the full size, non-truncated diagram visual representation.   It's also used to get the actual sizes via this call:

    Rect descendantBounds = VisualTreeHelper.GetDescendantBounds(this.designerPresenter);

    However "designerPresenter" isflagged by Reflector as "internal" and is not accessible from my code.  The examples in this thread instead use:

    Rect size = VisualTreeHelper.GetDescendantBounds(view);

    From what I've seen under the debugger, GetDescendantBounds(view) returns an already truncated view port size.   So it is not clear to me how getting the bounds of "view" could be used to get the full size, non-truncated diagram visual.  

    (I also tried to get the "_workflowDesigner.View.DesiredSize".  But that size was also truncated to view port of the grid column in which the view is mounted in my rehosted designer).   I also tried artificially setting up the size of the RenderTargetBitmap to very high values and still got the truncated view; just with a much larger unused border area).

    I'm new to WPF, though so I'm sure there are more ways to get to untruncated image.   If the "designerPresenter" is really just the default ContentPresenter (from which it descends), maybe there is a way to get it in code so I can try a similar sequence as is found in OnSaveAsImageCommandExecuted.

    Thank you again for your help.

    Wednesday, September 8, 2010 7:07 PM
  • Okay, I seem to be missing some assembly references. In the code George has written, there is a

    Window win = new Window();

    where is that class defined? I cant seem to find it in any assembly. even the Quick Add Reference tool cant find this.

    In Leo's code, he refers to a "view". Again, what is that view? where is the rest of the code.

    Does anyone else know where the Window() class is defined and what the 'view' Leo refers to is?

    Thanks

    Benjy

    Saturday, October 9, 2010 8:05 PM
  • One problem solved. I had to add references to "PresentationFramework", "System.Xaml" and "WindowsBase" to get the Window() reference to resolve. This seems to work and shows an empty sequence in a JPEG. It doesnt show anything else even if i add activities to the sequence but i guess thats some other issue.

    Would still like to know where the "view" comes from that Leo was referring to

    Thanks
    benjy

    Saturday, October 9, 2010 8:42 PM
  • Hi Benjy,
    Please check out G Banfill's post in the thread, to see how he gets designer.View.
    Tim
    Tuesday, October 12, 2010 4:40 PM
  • I just stumbled across this thread and thought I would post a utility class that I use "Nikon".  It takes any frameworkelement (even the workflowdesigner) and renders it as a bitmapsource.  There are convenience methods to convert it to stream (for cross domain applications) and to put it on the clipboard.  In addition it supports one off functionality via static members and instance members for a constructed design.

        class Nikon
        {
            private readonly FrameworkElement _element;
    
            public string Format { get; set; }
            public Nikon(FrameworkElement element)
            {
                _element = element;
                Format = "png";
            }
    
            /// <summary>
            /// Renders the wrapped frameworkelement to the clipboard
            /// </summary>
            public void SnapshotToClipboard()
            {
                ElementToClipboard(_element,Format);
            }
            /// <summary>
            /// Renders the wrapped frameworkelement to a bitmap
            /// </summary>
            /// <returns>The rendered bitmap</returns>
            public Bitmap SnapshotToBitmap()
            {
                using (var str = SnapshotToStream())
                {
                    return (str == null) ? null : new Bitmap(str);
                }
            }
    
            /// <summary>
            /// Renders the wrapped frameworkelement to a bitmap stored in a memory stream
            /// </summary>
            /// <returns>Memorystream</returns>
            public Stream SnapshotToStream()
            {
                return ElementStream(_element, Format);
            }
    
            /// <summary>
            /// Renders a frameworkelement to the clipboard as a image
            /// </summary>
            /// <param name="element">the element to render</param>
            /// <param name="encoding">Bitmap format, values are "gif","png","jpg"</param>
            /// <returns></returns>
            public static void ElementToClipboard(FrameworkElement element, string encoding)
            {
                var bms = ElementSnapShot(element, encoding);
                Clipboard.SetImage(bms);
            }
            /// <summary>
            /// Renders a frameworkelement to a bitmap in a memorystream
            /// </summary>
            /// <param name="element">the element to render</param>
            /// <param name="encoding">Bitmap format, values are "gif","png","jpg"</param>
            /// <returns></returns>
            public static Stream ElementStream(FrameworkElement element,string encoding)
            {
                var rtb = RenderTarget(element);
                var encoder = EncoderFor(encoding);
                encoder.Frames.Add(BitmapFrame.Create(rtb));
                var ms = new MemoryStream();
                encoder.Save(ms);
                ms.Position = 0;
                return ms;
            }
            /// <summary>
            /// Renders a frameworkelement to a bitmapsource
            /// </summary>
            /// <param name="element">the element to render</param>
            /// <param name="encoding">Bitmap format, values are "gif","png","jpg"</param>
            /// <returns></returns>
            public static BitmapSource ElementSnapShot(FrameworkElement element,string encoding)
            {
                var rtb = RenderTarget(element);
                var encoder = EncoderFor(encoding);
                encoder.Frames.Add(BitmapFrame.Create(rtb));
                return rtb;
            }
            private static BitmapEncoder EncoderFor(string format)
            {
                BitmapEncoder encoder;
                switch (format)
                {
                    case "gif":
                    case "GIF":
                        encoder = new GifBitmapEncoder();
                        break;
                    case "png":
                    case "PNG":
                        encoder = new PngBitmapEncoder();
                        break;
                    case "jpg":
                    case "JPG":
                        encoder = new JpegBitmapEncoder();
                        break;
                    default:
                        encoder = new PngBitmapEncoder();
                        break;
                }
                return encoder;
            }
            private static RenderTargetBitmap RenderTarget(FrameworkElement element)
            {
                var rtb=new RenderTargetBitmap(Convert.ToInt32(element.RenderSize.Width), Convert.ToInt32(element.RenderSize.Height), 96, 96, PixelFormats.Pbgra32);
                var brush = new VisualBrush(element);
                var visual = new DrawingVisual();
                var context = visual.RenderOpen();
                context.DrawRectangle(brush, null, new Rect(new Point(0, 0), new Point(element.RenderSize.Width, element.RenderSize.Height)));
                context.Close();
                rtb.Render(visual);
                return rtb;
            }
        }
    
    


    Monday, October 24, 2011 6:46 PM
  • Hi Bob,

    I hope you have already solved your issue because your post is from a long time ago. Anyway, I am going to post how I solved the scenario you described here. Maybe it can help you or someone else who come across the same problem.

    As you mentioned you get "an already truncated view port size". In my opinion this is because what you get is a virtualized result, that means not the complete view rendered.

    what I have seen using a reflector tool is:

    void OnCopyAsImageCommandExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                VirtualizedContainerService virtualizingContainerService = this.Context.Services.GetService<virtualizedcontainerservice>();
                virtualizingContainerService.BeginPopulateAll((Action)(() =>

                {

    //Here goes the logic that save the image.

         }

    So, what I have done also based on  reflectors output is:

     VirtualizedContainerService virtualizingContainerService = workflowDesigner.Context.Services.GetService<VirtualizedContainerService>();
                    Type typeVir = typeof(VirtualizedContainerService);
     
                    object[] actions = new object[] { new Action(() => { workflowDesigner.SaveWorkflowAsImage(fileName);
                                                                       }) };
     
                    var methodeName = typeVir.GetMethod("BeginPopulateAll"BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);
     
                    if (methodeName != null)
                    {
                        methodeName.Invoke(virtualizingContainerService, actions);
                    }

    This solve the issue of having a truncated view. I am a new in WorkFlow foundation so I don't have a sound understanding of it and all of it services .

    Regards,

    Sebastian

    Friday, June 8, 2012 11:13 AM
  • I have been able to make it work with this code:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Activities.Presentation;
    using System.Activities.Statements;
    using System.Activities.Presentation.View;
    using System.Activities.Core.Presentation;
    using System.Windows;
    using System.Windows.Input;
    using System.Threading;
    using System.Windows.Media.Imaging;
    using System.IO;
    using System.ServiceModel.Activities;
    using System.Xaml;
    using System.Activities;
    using System.Windows.Media;
    using System.Windows.Interop;
    
    namespace ConsoleApplication1
    {
        public static class Program
        {
            static volatile Window window;
            static volatile WorkflowDesigner designer;
            static string xamlPath;
            static string targetFile;
    
    
            [MTAThread]
            static void Main(string[] args)
            {
                xamlPath = args[0];
                targetFile = args[1];
                CreateAndShowDesigner();
                SaveAsImage();
                window.Dispatcher.Invoke(() => window.Close());
            }
    
            public static void SaveAsImage()
            {
                window.Dispatcher.Invoke( () => 
                {
                    Clipboard.Clear();
                    var command = (RoutedCommand)DesignerView.CopyAsImageCommand;
                    var designerView = designer.Context.Services.GetService<DesignerView>();
                    command.Execute(null, designerView);
                });
    
                InteropBitmap bm = null;
                while (bm == null)
                {
                    window.Dispatcher.Invoke(() => 
                    { 
                        bm = (InteropBitmap)Clipboard.GetImage();
                        if (bm != null)
                        {
                            var encoder = new PngBitmapEncoder();
                            encoder.Frames.Add(BitmapFrame.Create(bm));
                            using (FileStream fs = new FileStream(targetFile, FileMode.Create))
                            {
                                encoder.Save(fs);
                            }
                        }
                    });
                    if (bm == null) Thread.Sleep(1);
                }
            }
    
            static void CreateAndShowDesigner()
            {
                using (var ev = new AutoResetEvent(false))
                {
                    Thread thread = new Thread(() =>
                    {
                        window = new Window();
                        window.ContentRendered += (sender, e) => ev.Set();
                        new DesignerMetadata().Register();
                        designer = new WorkflowDesigner();
                        designer.Load(xamlPath);
                        window.Content = (object)designer.View;
                        window.ShowDialog();
                    });
                    thread.SetApartmentState(ApartmentState.STA);
                    thread.Start();
                    ev.WaitOne();
                }
            }
        }
    }
    

    Tuesday, September 24, 2013 11:07 AM
  • Hi, Jesús López

    Thanks a lot for your code.

    I got the statemachine image using your code.

    But there is a problem:

    I can get the statemachine as an image normally at the first time,

    But , when I click the button second time , the image can not be created completely.

    Only some state are showed in the picture, almost all the transitions are lost, 

    even some state names can't be displayed completely.

    Could you help me ?

    Monday, December 16, 2013 10:00 AM
  • Hi, I have the same problem, some transitions are lost when I cliked the button second time. Dis you resolve this problem? help me please
    Thursday, July 31, 2014 4:24 PM