potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C# WinFormsのクリップボード監視コンポーネント

WinFormsのFormへのクリップボード監視機能の追加、コンポーネントNativeWindowで思ったより簡単に実装できました。NativeWindowの理解が浅いので問題が残っているかもしれませんが、とりあえず動きはします。動作確認はC# (.NET 8.0)ですが、名前空間などの書き方を変えれば以前の.NETでも使えると思います。

プロジェクトにClipboardWatcher.cs等の名前で次のコードを追加して一度ビルドします。後はツールボックスからFormに追加できます。

#pragma warning disable SYSLIB1054

using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace Utility.Windows.Forms;

/// <summary>
/// クリップボードの変更を監視します。
/// </summary>
[SupportedOSPlatform("windows")]
[DefaultEvent(nameof(ClipboardUpdated))]
internal sealed class ClipboardWatcher : Component
{
    /// <summary>
    /// クリップボードが変更されました。
    /// </summary>
    public event EventHandler? ClipboardUpdated;

    private sealed class ClipboardWatcherNativeWindow : NativeWindow
    {
        private readonly ClipboardWatcher _owner;
        public ClipboardWatcherNativeWindow(ClipboardWatcher owner)
        {
            _owner = owner;
            CreateHandle(new CreateParams());
        }

        protected override void WndProc(ref Message m)
        {
            const int WM_CLIPBOARDUPDATE = 0x031D;

            switch (m.Msg)
            {
            case WM_CLIPBOARDUPDATE:
                _owner.OnClipboardUpdated(_owner, new());
                break;
            }

            base.WndProc(ref m);
        }

        protected override void OnHandleChange()
        {
            base.OnHandleChange();
            NativeMethods.AddClipboardFormatListener(Handle);
        }

        public override void ReleaseHandle()
        {
            NativeMethods.RemoveClipboardFormatListener(Handle);
            base.ReleaseHandle();
        }

        private static class NativeMethods
        {
            [DllImport("user32.dll")]
            [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public extern static bool AddClipboardFormatListener(nint hwnd);
            [DllImport("user32.dll")]
            [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public extern static bool RemoveClipboardFormatListener(nint hwnd);
        }
    }

    private ClipboardWatcherNativeWindow? _native;

    public ClipboardWatcher()
    {
        if (!DesignMode)
            _native = new(this);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _native?.ReleaseHandle();
            _native = null;
        }

        base.Dispose(disposing);
    }

    /// <summary>
    /// <code>ClipboardUpdated</code>イベントを呼び出すかを取得または設定します。
    /// </summary>
    [DefaultValue(true)]
    [Category("Behavior")]
    public bool Enabled { get; set; } = true;

    private void OnClipboardUpdated(object? sender, EventArgs e)
    {
        if (Enabled)
            ClipboardUpdated?.Invoke(sender, e);
    }
}

作成中に学んだこと。

  • 低レベルのWindowsネイティブウィンドウ操作はNativeWindowクラスの派生クラスを作成する。
  • NativeWindowは自動でウィンドウを作らないので、コンストラクタなどでCreateHandleを呼び出す。
  • CreateHandleの引数でクラス名を指定するとサブクラス化ウィンドウが作成できる。System.Windows.Forms.Button等はこの機能でサブクラス化している。

参考