none
コントロールの画面ハードコピー画像をEMF形式でクリップボードに送りたい RRS feed

  • 質問

  • VS2012のMicrosoft Visual C# 2012を使用しています

    画面に表示されている表(Datagrid)やグラフ(Chart/Oxyplot)、図形(Canvas,Path)をクリップボードに送って

    別のソフトで2次利用したいと思います

    WPFの綺麗な描画を生かしたくベクトルデータのEMF形式で画面を切り出したいと思いましたが、やり方が見つかりません

    ヒント、文献などご紹介頂きたくよろしくお願いします

    以下のような情報が見つかりましたが、何れもWPFには該当しないみたいでした

    (System.Drawing.Imagingが使えない)

    https://support.microsoft.com/en-us/kb/323530

    http://classicalprogrammer.wikidot.com/copy-gdi-drawing-to-clipboard

    https://bytes.com/topic/c-sharp/answers/572657-net-clipboard-metafiles

    http://mitsu.three-atmarks.com/archives/13511

    https://www.devexpress.com/Support/Center/Example/Details/E1156

    http://stackoverflow.com/questions/152729/gdi-c-how-to-save-an-image-as-emf/978346#978346


    2017年1月8日 16:29

回答

  • WPFの綺麗な描画を生かしたくベクトルデータのEMF形式で画面を切り出したいと思いましたが、やり方が見つかりません

    WPFの描画では、EMFの記録できるグラフィックスAPI(GDI,GDI+)を使用していないので、実現は難しいのかなと思います。

    海外のフォーラムにCanvasの要素をEMFに出力するサンプルがありましたが、WPFの個々の図形要素に対して、GDI+(System.Drawing)の描画に対応付ける処理が必要となっていました。

    https://social.msdn.microsoft.com/Forums/en-US/02cf4d5c-a35e-4a07-82c0-010d98b46f6c

    サンプルを少しだけ拡張してクリップボードへEMFをコピーするようにしたものが下記です。

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Canvas Width="200" Height="200" Name="MyCanvas">
                <Ellipse Fill="Blue" Canvas.Left="18" Canvas.Top="54" StrokeThickness="2" Height="40" Stroke="Black" Width="46" />
                <Rectangle Fill="Yellow" Canvas.Left="88" StrokeThickness="2" Canvas.Top="63" Height="31" Stroke="Black" Width="81" />
                <Rectangle Canvas.Left="59.145" Canvas.Top="121" StrokeThickness="2" Fill="Red" Height="31" Stroke="Blue" Width="81" />
            </Canvas>
            <Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Click="Button_Click_1" Margin="10,10,0,0"/>
        </Grid>
    </Window>
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Runtime.InteropServices;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
            public void CopyToClipboard()
            {
                string filePath = @"temp.emf";
    
                int w = Convert.ToInt32(this.MyCanvas.Width);
                int h = Convert.ToInt32(this.MyCanvas.Height);
                System.Drawing.Rectangle rect = new System.Drawing.Rectangle(0, 0, w, h);
                Bitmap bmp = new System.Drawing.Bitmap(w, h);
                Graphics gs = System.Drawing.Graphics.FromImage(bmp);
                Metafile mf = new System.Drawing.Imaging.Metafile(filePath, gs.GetHdc(), rect, System.Drawing.Imaging.MetafileFrameUnit.Pixel);
                Graphics g = Graphics.FromImage(mf);
                WPFPainter painter = new WPFPainter(g, MyCanvas);
                painter.Draw();
                g.Save();
                g.Dispose();
                IntPtr handle = new System.Windows.Interop.WindowInteropHelper(this).Handle;
                ClipboardMetafileHelper.PutEnhMetafileOnClipboard(handle, mf);
                mf.Dispose();
    
                System.IO.File.Delete(filePath);
            }
            private void Button_Click_1(object sender, RoutedEventArgs e)
            {
                CopyToClipboard();
            }
        }
    
        public class WPFPainter
        {
            public System.Drawing.Graphics DrawGraphics;
            public Canvas Container = new Canvas();
            List<Shape> ShapeList = new List<Shape>();
            public WPFPainter(System.Drawing.Graphics _DrawGraphics, Canvas _Container)
            {
                DrawGraphics = _DrawGraphics;
                Container = _Container;
                //Get the Shape elements in the Canvas
                ShapeList = AnalyseShape();
            }
            public List<Shape> AnalyseShape()
            {
                List<Shape> list = new List<Shape>();
                //Container.Children;
                for (int i = 0; i < Container.Children.Count; i++)
                {
                    UIElement element = Container.Children[i];
                    Type t = element.GetType();
                    if (t == typeof(System.Windows.Shapes.Ellipse))
                    {
                        list.Add(element as Shape);
                    }
                    else if (t == typeof(System.Windows.Shapes.Rectangle))
                    {
                        list.Add(element as Shape);
                    }
                    // You can write more code to support more type here
                    //... ...
                }
                return list;
            }
            public void Draw()
            {
                //Convert WPF shape to GDI+ shape and Draw GDI+
                for (int i = 0; i < ShapeList.Count; i++)
                {
                    Shape element = ShapeList[i];
                    Type t = element.GetType();
                    float x = Convert.ToSingle(Canvas.GetLeft(element));
                    float y = Convert.ToSingle(Canvas.GetTop(element));
                    float w = Convert.ToSingle(element.Width);
                    float h = Convert.ToSingle(element.Height);
                    System.Drawing.SolidBrush GDIStroke = ConvertSolidColorBrush(element.Stroke as SolidColorBrush);
                    System.Drawing.SolidBrush GDIFill = ConvertSolidColorBrush(element.Fill as SolidColorBrush);
                    float Thickness = Convert.ToSingle(element.StrokeThickness);
                    System.Drawing.Pen pen = new System.Drawing.Pen(GDIStroke, Thickness);
                    if (t == typeof(System.Windows.Shapes.Ellipse))
                    {
                        DrawEllipse(x, y, w, h, pen, GDIFill);
                    }
                    else if (t == typeof(System.Windows.Shapes.Rectangle))
                    {
                        DrawRectangle(x, y, w, h, pen, GDIFill);
                    }
                }
            }
            public void DrawEllipse(float x, float y, float w, float h, System.Drawing.Pen pen, System.Drawing.SolidBrush GDIFill)
            {
                DrawGraphics.DrawEllipse(pen, x, y, w, h);
                DrawGraphics.FillEllipse(GDIFill, x, y, w, h);
            }
            public void DrawRectangle(float x, float y, float w, float h, System.Drawing.Pen pen, System.Drawing.SolidBrush GDIFill)
            {
                DrawGraphics.DrawRectangle(pen, x, y, w, h);
                DrawGraphics.FillRectangle(GDIFill, x, y, w, h);
            }
            public System.Drawing.SolidBrush ConvertSolidColorBrush(SolidColorBrush scb)
            {
                return new SolidBrush(ConvertColor(scb.Color));
            }
            public System.Drawing.Color ConvertColor(System.Windows.Media.Color WPFColor)
            {
                return System.Drawing.Color.FromArgb(WPFColor.A, WPFColor.R, WPFColor.G, WPFColor.B);
            }
        }
    
        public class ClipboardMetafileHelper
        {
            [DllImport("user32.dll")]
            static extern bool OpenClipboard(IntPtr hWndNewOwner);
            [DllImport("user32.dll")]
            static extern bool EmptyClipboard();
            [DllImport("user32.dll")]
            static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
            [DllImport("user32.dll")]
            static extern bool CloseClipboard();
            [DllImport("gdi32.dll")]
            static extern IntPtr CopyEnhMetaFile(IntPtr hemfSrc, IntPtr hNULL);
            [DllImport("gdi32.dll")]
            static extern bool DeleteEnhMetaFile(IntPtr hemf);
    
            // Metafile mf is set to a state that is not valid inside this function.
            static public bool PutEnhMetafileOnClipboard(IntPtr hWnd, Metafile mf)
            {
                bool bResult = false;
                IntPtr hEMF, hEMF2;
                hEMF = mf.GetHenhmetafile(); // invalidates mf
                if (!hEMF.Equals(new IntPtr(0)))
                {
                    hEMF2 = CopyEnhMetaFile(hEMF, new IntPtr(0));
                    if (!hEMF2.Equals(new IntPtr(0)))
                    {
                        if (OpenClipboard(hWnd))
                        {
                            if (EmptyClipboard())
                            {
                                IntPtr hRes = SetClipboardData(14 /*CF_ENHMETAFILE*/, hEMF2);
                                bResult = hRes.Equals(hEMF2);
                                CloseClipboard();
                            }
                        }
                    }
                    DeleteEnhMetaFile(hEMF);
                }
                return bResult;
            }
        }
    }
    

    ※System.Drawingの参照が必要です。

    全ての図形要素に対して対応付けが行えたとしても、もともと描画エンジンが異なるので完全に一致した表現は難しいのでは?と思います。

    2017年1月8日 19:25
  • XamlToysを使うと完全ではないけれどEMFにできるっぽいです。参考:stackoverflow
    参考先の回答のにあるリンクのhttp://softronix.com/download/WpfToWmfClipboard.zipで試せます。

    ですが.Net4.0からだとそのままでは動かないので、XamlToysのコードをダウンロードしてきて以下のように修正するとDataGridをクリップボードにEMFでコピーできました。
    (XPSファイルにしてXAML化するときにフォントがodttfになってしまうけど、odttfだと正しく読めないらしい)

    /// <summary>Glyphsのフォントをodttfからttfに付け替え</summary>
    /// <param name="d"></param>
    /// <param name="dic">あとで一時TTFファイルは削除してください</param>
    private static void OdttfToTTF(DependencyObject d, Dictionary<Uri, Uri> dic = null)
    {
        if (dic == null)
        {
            dic = new Dictionary<Uri, Uri>();
        }
    
        if (d is System.Windows.Documents.Glyphs)
        {
            System.Windows.Documents.Glyphs glyphs = (System.Windows.Documents.Glyphs)d;
            if (glyphs.FontUri != null)
            {
                var path = glyphs.FontUri.AbsolutePath.ToString();
                if (!System.IO.Path.GetExtension(path).Equals("ttf", StringComparison.InvariantCultureIgnoreCase))
                {
                    Uri uriTTF;
                    if (!dic.TryGetValue(glyphs.FontUri, out uriTTF))
                    {
                        var tempPath=System.IO.Path.GetTempFileName();
                        System.IO.File.Delete(tempPath);
                        tempPath=System.IO.Path.ChangeExtension(tempPath, ".ttf");
                        System.IO.File.Copy(path, tempPath, true);
                        uriTTF = new Uri(tempPath);
                    }
                    glyphs.FontUri = uriTTF;
                }
            }
        }
    
        int count = VisualTreeHelper.GetChildrenCount(d);
        for (int i = 0; i < count; i++)
        {
            OdttfToTTF(VisualTreeHelper.GetChild(d, i), dic);
        }
    }
    
    public static Drawing GetDrawingFromXaml(object xaml)
    {
        var drawing = FindDrawing(xaml);
        if (drawing != null) return drawing;
    
        var fe = xaml as FrameworkElement;
        if (fe != null)
        {
    
            OdttfToTTF(fe);//追加:
    
            RealizeFrameworkElement(fe);
            drawing = WalkVisual(fe);
        }
    
        // Handle FrameworkContentElement
    
        return drawing;
    }
    とりあえずの修正で動きますが、まだまだおかしな挙動があるのであくまで参考です。

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2017年1月9日 6:41

すべての返信

  • WPFの綺麗な描画を生かしたくベクトルデータのEMF形式で画面を切り出したいと思いましたが、やり方が見つかりません

    WPFの描画では、EMFの記録できるグラフィックスAPI(GDI,GDI+)を使用していないので、実現は難しいのかなと思います。

    海外のフォーラムにCanvasの要素をEMFに出力するサンプルがありましたが、WPFの個々の図形要素に対して、GDI+(System.Drawing)の描画に対応付ける処理が必要となっていました。

    https://social.msdn.microsoft.com/Forums/en-US/02cf4d5c-a35e-4a07-82c0-010d98b46f6c

    サンプルを少しだけ拡張してクリップボードへEMFをコピーするようにしたものが下記です。

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Canvas Width="200" Height="200" Name="MyCanvas">
                <Ellipse Fill="Blue" Canvas.Left="18" Canvas.Top="54" StrokeThickness="2" Height="40" Stroke="Black" Width="46" />
                <Rectangle Fill="Yellow" Canvas.Left="88" StrokeThickness="2" Canvas.Top="63" Height="31" Stroke="Black" Width="81" />
                <Rectangle Canvas.Left="59.145" Canvas.Top="121" StrokeThickness="2" Fill="Red" Height="31" Stroke="Blue" Width="81" />
            </Canvas>
            <Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Click="Button_Click_1" Margin="10,10,0,0"/>
        </Grid>
    </Window>
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Runtime.InteropServices;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
            public void CopyToClipboard()
            {
                string filePath = @"temp.emf";
    
                int w = Convert.ToInt32(this.MyCanvas.Width);
                int h = Convert.ToInt32(this.MyCanvas.Height);
                System.Drawing.Rectangle rect = new System.Drawing.Rectangle(0, 0, w, h);
                Bitmap bmp = new System.Drawing.Bitmap(w, h);
                Graphics gs = System.Drawing.Graphics.FromImage(bmp);
                Metafile mf = new System.Drawing.Imaging.Metafile(filePath, gs.GetHdc(), rect, System.Drawing.Imaging.MetafileFrameUnit.Pixel);
                Graphics g = Graphics.FromImage(mf);
                WPFPainter painter = new WPFPainter(g, MyCanvas);
                painter.Draw();
                g.Save();
                g.Dispose();
                IntPtr handle = new System.Windows.Interop.WindowInteropHelper(this).Handle;
                ClipboardMetafileHelper.PutEnhMetafileOnClipboard(handle, mf);
                mf.Dispose();
    
                System.IO.File.Delete(filePath);
            }
            private void Button_Click_1(object sender, RoutedEventArgs e)
            {
                CopyToClipboard();
            }
        }
    
        public class WPFPainter
        {
            public System.Drawing.Graphics DrawGraphics;
            public Canvas Container = new Canvas();
            List<Shape> ShapeList = new List<Shape>();
            public WPFPainter(System.Drawing.Graphics _DrawGraphics, Canvas _Container)
            {
                DrawGraphics = _DrawGraphics;
                Container = _Container;
                //Get the Shape elements in the Canvas
                ShapeList = AnalyseShape();
            }
            public List<Shape> AnalyseShape()
            {
                List<Shape> list = new List<Shape>();
                //Container.Children;
                for (int i = 0; i < Container.Children.Count; i++)
                {
                    UIElement element = Container.Children[i];
                    Type t = element.GetType();
                    if (t == typeof(System.Windows.Shapes.Ellipse))
                    {
                        list.Add(element as Shape);
                    }
                    else if (t == typeof(System.Windows.Shapes.Rectangle))
                    {
                        list.Add(element as Shape);
                    }
                    // You can write more code to support more type here
                    //... ...
                }
                return list;
            }
            public void Draw()
            {
                //Convert WPF shape to GDI+ shape and Draw GDI+
                for (int i = 0; i < ShapeList.Count; i++)
                {
                    Shape element = ShapeList[i];
                    Type t = element.GetType();
                    float x = Convert.ToSingle(Canvas.GetLeft(element));
                    float y = Convert.ToSingle(Canvas.GetTop(element));
                    float w = Convert.ToSingle(element.Width);
                    float h = Convert.ToSingle(element.Height);
                    System.Drawing.SolidBrush GDIStroke = ConvertSolidColorBrush(element.Stroke as SolidColorBrush);
                    System.Drawing.SolidBrush GDIFill = ConvertSolidColorBrush(element.Fill as SolidColorBrush);
                    float Thickness = Convert.ToSingle(element.StrokeThickness);
                    System.Drawing.Pen pen = new System.Drawing.Pen(GDIStroke, Thickness);
                    if (t == typeof(System.Windows.Shapes.Ellipse))
                    {
                        DrawEllipse(x, y, w, h, pen, GDIFill);
                    }
                    else if (t == typeof(System.Windows.Shapes.Rectangle))
                    {
                        DrawRectangle(x, y, w, h, pen, GDIFill);
                    }
                }
            }
            public void DrawEllipse(float x, float y, float w, float h, System.Drawing.Pen pen, System.Drawing.SolidBrush GDIFill)
            {
                DrawGraphics.DrawEllipse(pen, x, y, w, h);
                DrawGraphics.FillEllipse(GDIFill, x, y, w, h);
            }
            public void DrawRectangle(float x, float y, float w, float h, System.Drawing.Pen pen, System.Drawing.SolidBrush GDIFill)
            {
                DrawGraphics.DrawRectangle(pen, x, y, w, h);
                DrawGraphics.FillRectangle(GDIFill, x, y, w, h);
            }
            public System.Drawing.SolidBrush ConvertSolidColorBrush(SolidColorBrush scb)
            {
                return new SolidBrush(ConvertColor(scb.Color));
            }
            public System.Drawing.Color ConvertColor(System.Windows.Media.Color WPFColor)
            {
                return System.Drawing.Color.FromArgb(WPFColor.A, WPFColor.R, WPFColor.G, WPFColor.B);
            }
        }
    
        public class ClipboardMetafileHelper
        {
            [DllImport("user32.dll")]
            static extern bool OpenClipboard(IntPtr hWndNewOwner);
            [DllImport("user32.dll")]
            static extern bool EmptyClipboard();
            [DllImport("user32.dll")]
            static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
            [DllImport("user32.dll")]
            static extern bool CloseClipboard();
            [DllImport("gdi32.dll")]
            static extern IntPtr CopyEnhMetaFile(IntPtr hemfSrc, IntPtr hNULL);
            [DllImport("gdi32.dll")]
            static extern bool DeleteEnhMetaFile(IntPtr hemf);
    
            // Metafile mf is set to a state that is not valid inside this function.
            static public bool PutEnhMetafileOnClipboard(IntPtr hWnd, Metafile mf)
            {
                bool bResult = false;
                IntPtr hEMF, hEMF2;
                hEMF = mf.GetHenhmetafile(); // invalidates mf
                if (!hEMF.Equals(new IntPtr(0)))
                {
                    hEMF2 = CopyEnhMetaFile(hEMF, new IntPtr(0));
                    if (!hEMF2.Equals(new IntPtr(0)))
                    {
                        if (OpenClipboard(hWnd))
                        {
                            if (EmptyClipboard())
                            {
                                IntPtr hRes = SetClipboardData(14 /*CF_ENHMETAFILE*/, hEMF2);
                                bResult = hRes.Equals(hEMF2);
                                CloseClipboard();
                            }
                        }
                    }
                    DeleteEnhMetaFile(hEMF);
                }
                return bResult;
            }
        }
    }
    

    ※System.Drawingの参照が必要です。

    全ての図形要素に対して対応付けが行えたとしても、もともと描画エンジンが異なるので完全に一致した表現は難しいのでは?と思います。

    2017年1月8日 19:25
  • XamlToysを使うと完全ではないけれどEMFにできるっぽいです。参考:stackoverflow
    参考先の回答のにあるリンクのhttp://softronix.com/download/WpfToWmfClipboard.zipで試せます。

    ですが.Net4.0からだとそのままでは動かないので、XamlToysのコードをダウンロードしてきて以下のように修正するとDataGridをクリップボードにEMFでコピーできました。
    (XPSファイルにしてXAML化するときにフォントがodttfになってしまうけど、odttfだと正しく読めないらしい)

    /// <summary>Glyphsのフォントをodttfからttfに付け替え</summary>
    /// <param name="d"></param>
    /// <param name="dic">あとで一時TTFファイルは削除してください</param>
    private static void OdttfToTTF(DependencyObject d, Dictionary<Uri, Uri> dic = null)
    {
        if (dic == null)
        {
            dic = new Dictionary<Uri, Uri>();
        }
    
        if (d is System.Windows.Documents.Glyphs)
        {
            System.Windows.Documents.Glyphs glyphs = (System.Windows.Documents.Glyphs)d;
            if (glyphs.FontUri != null)
            {
                var path = glyphs.FontUri.AbsolutePath.ToString();
                if (!System.IO.Path.GetExtension(path).Equals("ttf", StringComparison.InvariantCultureIgnoreCase))
                {
                    Uri uriTTF;
                    if (!dic.TryGetValue(glyphs.FontUri, out uriTTF))
                    {
                        var tempPath=System.IO.Path.GetTempFileName();
                        System.IO.File.Delete(tempPath);
                        tempPath=System.IO.Path.ChangeExtension(tempPath, ".ttf");
                        System.IO.File.Copy(path, tempPath, true);
                        uriTTF = new Uri(tempPath);
                    }
                    glyphs.FontUri = uriTTF;
                }
            }
        }
    
        int count = VisualTreeHelper.GetChildrenCount(d);
        for (int i = 0; i < count; i++)
        {
            OdttfToTTF(VisualTreeHelper.GetChild(d, i), dic);
        }
    }
    
    public static Drawing GetDrawingFromXaml(object xaml)
    {
        var drawing = FindDrawing(xaml);
        if (drawing != null) return drawing;
    
        var fe = xaml as FrameworkElement;
        if (fe != null)
        {
    
            OdttfToTTF(fe);//追加:
    
            RealizeFrameworkElement(fe);
            drawing = WalkVisual(fe);
        }
    
        // Handle FrameworkContentElement
    
        return drawing;
    }
    とりあえずの修正で動きますが、まだまだおかしな挙動があるのであくまで参考です。

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2017年1月9日 6:41
  • 早速情報を頂きありがとうございます

    一般的な方法がないようで残念です

    Graphics gの取得が難関と言うことが理解できました

    コントロールを指定してその描画情報をgにコピーしたり、指定範囲の描画情報をgに転送できれば良いのですが、

    これができないと言うことですね

    WPFの描画データをベクトル形式でクリップボードに記録できれば良いわけですが、EMF以外の書式を使った方が

    得策なんでしょうか

    先ずは情報を元に研究してみたいと思います

    なお、System.Drawing.Imagingが使えないというのはSystem.Drawingの参照が未設定のためでした

    失礼しました

    2017年1月9日 15:30