C#からClipboardを操作する

最近 C# から Clipboard をゴニョゴニョするプログラムを作る必要があったため,いろいろと調べてみました(VS2005sp1 + .NET Framework 2.0).


.NET Framework では,Clipboard クラス (System.Windows.Forms) として Clipboard に対する操作がまとめられており,簡単にデータの設定/取得を行うことができます.
Clipboard クラスを使用したサンプルは,DOBON.NET: VB.NET, C#, 無料ソフトウェア... にて紹介されています.
例:クリップボードにファイルをコピーまたは切り取りをする、クリップボードからファイルを取得する、貼り付ける: .NET Tips: C#, VB.NET


しかし,Clipboard に対するイベントは Clipboard クラスに定義されていないため,Clipboard の監視を行いたい場合は,API を使って自分で何とかするしかありません.調べてみると,なんと Clipboardの監視はできない? − Insider.NET − @IT にコードが載っています.ありがたや(^_^;

また,英語ですが Using the Clipboard の "Creating a Clipboard Viewer Window" という項目でも,API ベースではありますが Clipboard Viewer のサンプルコードが紹介されており,参考になります(MSDN ライブラリの方では日本語資料があるのかな?).

今回は,テキストデータや画像データだけではなく,Officeアプリケーション上でコピーした内容も扱えるようにしようと考えました.


string[] IDataObject.GetFormats(); で,変換可能な形式を取得してみると...
Power Point の描画オブジェクトやクリップアートをコピーすると,

Office Drawing Shape Format
PNG+Office Art
JFIF+Office Art
GIF+Office Art
Object Descriptor
PNG
JFIF
GIF
MetaFilePict
EnhancedMetafile
PowerPoint 8.0 Internal Slides
ActiveClipboard
System.Drawing.Bitmap
Bitmap

Power Point のスライドをコピーすると,

Embedded Object
Object Descriptor
EnhancedMetafile
MetaFilePict
PNG
JFIF
GIF
Link Source
Link Source Descriptor
PowerPoint 8.0 Internal Slides
ActiveClipboard

Excelのセルをコピーすると,

1回目:Clipboardを空にしてくる
2回目:
EnhancedMetafile
MetaFilePict
System.Drawing.Bitmap
Bitmap
Biff8
Biff5
BIFF4
Biff3
Biff
SymbolicLink
Wk1
DataInterchangeFormat
XML Spreadsheet
HTML Format
System.String
UnicodeText
Text
Csv
Rich Text Format
Embed Source
Object Descriptor
Link Source
Link Source Descriptor
Link
Format129

Excelのグラフをコピーすると,

EnhancedMetafile
Embed Source
Object Descriptor
Link Source
Link Source Descriptor
Link
(上記の内容で,2回更新される)

(なんだか,見慣れない名前のフォーマットがいくつかあります)


例えば,Clipboardの内容をOffice描画オブジェクトとして扱いたいのであれば,

IDataObject data = Clipboard.GetDataObject();
if (data.GetDataPresent("Office Drawing Shape Format"))
{
    MemoryStream memory = (MemoryStream)data.GetData("Office Drawing Shape Format");    // 取得

    Clipboard.SetText("ダミーテキスト");

    Clipboard.SetData("Office Drawing Shape Format", memory);    // 設定
}

のように,MemoryStreamとして取り扱うことができます(キャストする型はGetTypeで調べてください).


さて,これらをどう扱っていこうかなぁ...( ´Д`)

ただ,問題がありまして,DataFormats.EnhancedMetafile を指定して GetData すると null が返ってきます(Microsoft Power Point のオブジェクトをコピーしたとき等).場合によっては,例外が発生します.

IDataObject data = Clipboard.GetDataObject();
if (data.GetDataPresent(DataFormats.EnhancedMetafile))
{
    object obj = data.GetData(DataFormats.EnhancedMetafile);    // nullが返ってくる
}


MSDN 曰く,

クリップボードでメタファイル形式を使用する場合には、特別な配慮が必要な場合があります。DataObject クラスの現在の実装における制限により、.NET Framework で使用されるメタファイル形式は、旧メタファイル形式を使用するアプリケーションでは認識されない場合があります。この場合は、Win32 クリップボード アプリケーション プログラミング インターフェイス (API: Application Programming Interface) で相互運用する必要があります。詳細については、http://support.microsoft.com にある Microsoft サポート技術情報の文書 323530 (「Metafiles on Clipboard Are Not Visible to All Applications」) を参照してください。

だそうな.

この件については,先ほどの DOBON.NET: VB.NET, C#, 無料ソフトウェア... でも紹介されています.


ならば,ということで,上記2つのページを参考に,Clipboard への Metafile の設定/取得メソッドを実装してみたところ,"取得"のほうは(おそらく)うまく行きましたが(試しに Metafile.GetThumbnailImage を用いてサムネイルを表示させると,正しく表示される,ぐらいしか確認はしていませんが),"設定"のほうはうまく行きませんでした.


まず,PRB: Metafiles on Clipboard Are Not Visible to All Applications に載っているサンプル通りに実装すると,

    hEMF2 = CopyEnhMetaFile(hEMF, new IntPtr(0));

が,"hEMF2 == IntPtr.Zero" を返してきます.
また,"hEMF" をダイレクトに(hEMF2 = hEMF; みたいにして)SetClipboardData 関数へ渡すと,今度は SetClipboardData 関数が NULL を返してきます.


さらに,Clipboard クラスの SetData メソッドを使用しても,Officeアプリケーション側からは"認識できないファイル",または"Windows メタファイル"とか名前が出ても実際は貼り付けられない等の状態になってしまいます.

Metafile.Saveメソッドを用いて,MemoryStreamに格納しようとしても,encoder が null だよと例外が飛んでくる.

ぬぬぬ...(`皿´)

WndProc を Hook する

前述のように,Clipboard が更新されたことを知るためには,対象となるフォームのウィンドウプロシージャを override する必要があります.でも,何となく Clipboard のために override した WndProc を,Form のコードに書きたくないなぁ,とか思ったので,ウィンドウプロシージャを Hook することにしました.調べてみると,NativeWindow クラス (System.Windows.Forms) という,便利なクラスがあります.


これを使ってみることにしました.

public class ClipboardHelper
{
    #region イベント定義

    /// <summary>
    /// クリップボードの内容が更新されたときに発生するイベント
    /// </summary>
    public event EventHandler DrawClipboard = null;

    protected void OnDrawClipboard()
    {
        if (this.DrawClipboard != null)
        {
            this.DrawClipboard(this, new EventArgs());
        }
    }

    #endregion

    /// <summary>
    /// ウィンドウプロシージャをHookするクラス
    /// </summary>
    private class Hook : NativeWindow
    {
        private ClipboardHelper _helper = null;

        private IntPtr nextHandle = IntPtr.Zero;
        private const int WM_DRAWCLIPBOARD = 0x0308;
        private const int WM_CHANGECBCHAIN = 0x030D;

        [DllImport("user32.dll")]
        private static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer);
        [DllImport("user32.dll")]
        private static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private extern static int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);


        public Hook(Form target, ClipboardHelper helper)
        {
            target.Load += new EventHandler(target_Load);
            target.FormClosed += new FormClosedEventHandler(target_FormClosed);
            target.HandleCreated += new EventHandler(target_HandleCreated);
            target.HandleDestroyed += new EventHandler(target_HandleDestroyed);

            this._helper = helper;
        }

        void target_Load(object sender, EventArgs e)
        {
            this.nextHandle = SetClipboardViewer(((Form)sender).Handle);
        }

        void target_FormClosed(object sender, FormClosedEventArgs e)
        {
            ChangeClipboardChain(((Form)sender).Handle, this.nextHandle);
        }

        void target_HandleCreated(object sender, EventArgs e)
        {
            AssignHandle(((Form)sender).Handle);
        }

        void target_HandleDestroyed(object sender, EventArgs e)
        {
            ReleaseHandle();
        }


        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case WM_DRAWCLIPBOARD:
                    
                    this._helper.OnDrawClipboard();

                    if (this.nextHandle != IntPtr.Zero)
                    {
                        SendMessage(this.nextHandle, m.Msg, m.WParam, m.LParam);
                    }
                    break;

                case WM_CHANGECBCHAIN:
                    if ((IntPtr)m.WParam == this.nextHandle)
                    {
                        this.nextHandle = (IntPtr)m.LParam;
                    }
                    else if (this.nextHandle != IntPtr.Zero)
                    {
                        SendMessage(this.nextHandle, m.Msg, m.WParam, m.LParam);
                    }
                    break;
            }
            base.WndProc(ref m);
        }
    }
    private Hook hook = null;

    public ClipboardHelper(Form wnd)
    {
        this.hook = new Hook(wnd, this);
    }
}