C#でClipboard監視(Windows API)

# 追記 - より良い方法
記事を書いたものの、Vista以降は別の方法(`AddClipboardFormatListener`)のほうが安定する（チェイン管理をOSがやってくれる）ようである。
[クリップボードの監視](https://wiki.clockahead.com/index.php?Coding%2F.NET%2F%A5%AF%A5%EA%A5%C3%A5%D7%A5%DC%A1%BC%A5%C9%A4%CE%B4%C6%BB%EB)
→  <b> https://gist.github.com/unarist/6342758 </b> が参考になりそう。

---

# 概略
Windows APIである`SetClipboardViewer`と`ChangeClipboardChain`を使ってクリップボード監視チェインへの登録・削除をし、
Windowメッセージ`WM_DRAWCLIPBOARD`と`WM_CHANGECBCHAIN`を処理して、クリップボードの変更通知受け取りと、チェインの維持を行う。
キーボードフックとかに近い構造のようだが、チェインの維持をしないといけない点が異なる。

<b>※チェインに登録されているアプリが強制終了とかで後処理せずに終了すると不安定になると思われるため、本記事の冒頭で言及しているAddClipboardFormatListenerを使う方法をおすすめします。</b>

# サンプルコード
参考サイトの1つ目をベースに作成。[^1] 
文字列をクリップボードにコピーするたびに、コンソールにその文字列を出力します。

```csharp

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Security.Permissions;

public class ClipboardEventArgs : EventArgs
{
    private string text;
    public string Text { get { return this.text; } }
    public ClipboardEventArgs(string str) { this.text = str;}
}

public delegate void cbEventHandler(object sender, ClipboardEventArgs ev);


[PermissionSet(SecurityAction.Demand,Name = "FullTrust")]
internal class MyClipboardListener : NativeWindow
{
    class NativeMethods
    {
        [DllImport("user32")]
        public static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer);

        [DllImport("user32")]
        public static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);

        [DllImport("user32")]
        public extern static int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
        // 2019.12.27追記: SendMessageの戻り値の型LRESULTはintではなくIntPtrにすべき。気が向いたら直します。。
    }
    private const int WM_DRAWCLIPBOARD = 0x0308;
    private const int WM_CHANGECBCHAIN = 0x030D;

    private IntPtr nextHandle;
    private IntPtr nwHandle;
    //private Form parent;
    public event cbEventHandler ClipboardHandler;

    public MyClipboardListener(Form f)
    {
        f.HandleCreated   += OnHandleCreated;
        f.HandleDestroyed += OnHandleDestroyed;
        //this.parent = f;
    }

    internal void OnHandleCreated(object sender, EventArgs e)
    {
        AssignHandle(((Form)sender).Handle); // NativeWindowクラスへのForm登録(メッセージフック開始)
        nwHandle = this.Handle;
        nextHandle = NativeMethods.SetClipboardViewer(this.Handle); // クリップボードチェインに登録
    }

    internal void OnHandleDestroyed(object sender, EventArgs e)
    {
        NativeMethods.ChangeClipboardChain(this.Handle, nextHandle); // クリップボードチェインから削除
        ReleaseHandle(); // NativeWindowクラスの後始末(Formに対してのメッセージフック解除)
    }

    // ハンドル変更されるシーンがあるか不明。
    //override void OnHandleChange()
    //{
    //    ChangeClipboardChain(nwHandle, nextHandle); // クリップボードチェインから削除
    //    nwHandle = this.Handle;
    //    nextHandle = SetClipboardViewer(this.Handle); // クリップボードチェインに登録
    //}

    protected override void WndProc(ref Message msg)
    {
        switch (msg.Msg) {
            case WM_DRAWCLIPBOARD:
                if (Clipboard.ContainsText()) { // Note: ここを変更すれば、テキスト以外も通知可能
                    // クリップボードの内容がテキストの場合のみ
                    if (ClipboardHandler != null) {
                        // クリップボードの内容を取得してハンドラを呼び出す
                        ClipboardHandler(this, new ClipboardEventArgs(Clipboard.GetText()));
                    }
                }
                if (nextHandle != IntPtr.Zero) {
                    NativeMethods.SendMessage(nextHandle, msg.Msg, msg.WParam, msg.LParam);
                }
                break;

            // クリップボード・ビューア・チェーンが更新された
            case WM_CHANGECBCHAIN:
                if (msg.WParam == nextHandle) {
                    nextHandle = msg.LParam;
                }
                else if(nextHandle != IntPtr.Zero) {
                    NativeMethods.SendMessage(nextHandle, msg.Msg, msg.WParam, msg.LParam);
                }
                break;
        }
        base.WndProc(ref msg);
    }
}

public class ClipboardMonitorTest : Form
{
    MyClipboardListener viewer;

    public ClipboardMonitorTest()
    {
        viewer = new MyClipboardListener(this);
        viewer.ClipboardHandler += this.OnClipBoardChanged;        // イベントハンドラを登録
    }

    // クリップボードにテキストがコピーされると呼び出される
    private void OnClipBoardChanged(object sender, ClipboardEventArgs args)
    {
        Console.WriteLine(args.Text);
    }

    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new ClipboardMonitorTest());
    }
}
```
# 注意点
試してみたところでは、`SetClipboardViewer`に登録すると、ユーザーがクリップボードにコピーする操作をしていなくても、クリップボードの更新通知(`WM_DRAWCLIPBOARD`)が来るようである。


# 参考サイト
1. [.NET TIPS
クリップボードの内容をリアルタイムに取得するには？［C#、VB］ - ＠IT](https://www.atmarkit.co.jp/fdotnet/dotnettips/848cbviewer/cbviewer.html)
2. [クリップボードビューアを作る - Wisdom Soft](http://wisdom.sakura.ne.jp/system/winapi/win32/win92.html) ・・・WindowsAPIの解説
3. [NativeWindowクラス - Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.nativewindow?view=netframework-4.8)

[^1]: そのままだと64bit環境でハマる可能性あり。`IntPtr`を`int`にキャストして`0`と比較しているとこは、`int`キャストをやめて`IntPtr.Zero`との比較に修正した。