Tuesday, April 12, 2011

FSharpPlot: Copying charts as EMF to the clipboard

F# is a very handy tool when it comes to playing with new algorithms or data analysis. However an analysis is useless if you can’t present your results. Some days ago Don Syme published with FSharpPlot a wrapper for .Net 4.0 charting controls. It is a handy tool when analyzing data with F# Interactive and you can learn some interesting F# techniques from the sources.

FSharpPlot comes with the possibility to save charts as png file or copy them (also as png) to the clipboard (just right click the chart). That is fine, if you want to look at the chart on a computer screen. However if you want to produce printable reports you might want to resort to a vector file format like emf.

Microsoft Chart Controls come with the possibility to produce three kinds of emf-files. Examples how to save a plot can be found in the Samples for Chart Controls. But as it turns out copying an emf as metafile to the clipboard is not straightforward from the .Net Framework as described in KB article 323530.

Following this Knowledge Base article, I introduced a new private module to FSharpChart to be able to access the functionality from the native Windows API.
module private MetafileToClipboard =
  // Following http://support.microsoft.com/kb/323530
  module private Intern = 
    open System
    open System.Runtime.InteropServices

    extern bool OpenClipboard(IntPtr hWndNewOwner);
    extern bool EmptyClipboard();
    extern IntPtr SetClipboardData(uint32 uFormat, IntPtr hMem);
    extern bool CloseClipboard();
    extern IntPtr CopyEnhMetaFile(IntPtr hemfSrc, IntPtr hNULL);
    extern bool DeleteEnhMetaFile(IntPtr hemf);

    open Intern
    open System

    let PutEnhMetafileOnClipboard(hWnd:IntPtr, mf:System.Drawing.Imaging.Metafile) =
      let mutable bResult = false
      let hEMF = mf.GetHenhmetafile()
      if not <| hEMF.Equals(new IntPtr(0)) then
        let hEMF2 = CopyEnhMetaFile(hEMF, new IntPtr(0))
        if not <| hEMF2.Equals(new IntPtr(0)) then
          if OpenClipboard(hWnd) then
            if EmptyClipboard() then
              let hRes = SetClipboardData(uint32 14 , hEMF2)
              let bResult = hRes.Equals(hEMF2)
              CloseClipboard() |> ignore
        DeleteEnhMetaFile(hEMF) |> ignore

Using this module, copying a chart as emf from within the ChartControl class to the clipboard is as easy as
let copyEmfToClipboard (_) =
  use ms = new IO.MemoryStream()
  chart.SaveImage(ms, ChartImageFormat.EmfPlus)
  ms.Seek(0L, IO.SeekOrigin.Begin) |> ignore
  use mf = new Drawing.Imaging.Metafile(ms)
  MetafileToClipboard.PutEnhMetafileOnClipboard(self.Handle, mf) |> ignore

let miCopyEmf = new ToolStripMenuItem("Copy Emf to Clipboard"
ShortcutKeys = (Keys.Control ||| Keys.Shift ||| Keys.C))

I also extended the module ChartExtensions to be able to save the chart directly from a script without opening a ChartForm.
module ChartExtensions =
  type FSharpChart with
    static member SaveImage filename format (width, height) ch =
      let chart = new ChartControl(ch, Size = new Size(width, height))
      chart.SaveImage(filename, format)

Using this tools you can easily create charts and paste them into Word, Powerpoint or many other applications.

The complete extended script can be downloaded here. It is based on FSharpPlot 0.2 from Don Syme, Tomas Petricek and their team.

The zip file also contains a unified diff file created with TortoiseMerge. You can see all changes with respect to version 0.2 published by Don Syme.

In addition to the functionality for copying emf’s to the clipboard there are some other minor changes:

  • I found a subtle bug at line 3670 of the FSharpChart.fsx where it says
    let typesToClone = 
      [ typeof<LabelStyle>; typeof<Axis>; typeof<Grid>; typeof<TickMark>
        typeof<ElementPosition>; typeof<AxisScaleView>; typeof<AxisScrollBar>; ]

    instead of
    let typesToClone = 
      [ typeof<DataVisualization.Charting.LabelStyle>; typeof<Axis>;
        typeof<Grid>; typeof<TickMark>; 
        typeof<ElementPosition>; typeof<AxisScaleView>; typeof<AxisScrollBar>; ]

  • I changed ChartData.Internal.ChartData and ChartTypes.GenericChart.Create from internal to public since the compiler complained when I tried to build the script into a dll.
Original version: FSharpPlot
My extended version: FSharpScript.zip