読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

ボタン連打しないでね☆ それはPeekMessageでメッセージループ

プログラミング C# C#2.0 C#3.0

Buttonのクリック処理が動作している最中に他のButtonやコントロール等をクリックした場合でも、
そのクリック操作をすべてシカトしたいなんてことはよくあるお話。


なぜクリック処理中のクリック操作がシカトされずに処理されてしまうのかと言えば、
クリック処理中に他のクリック操作がなされた場合、そのクリック情報はメッセージキューに保留状態となり、
クリック処理が終了次第、Windowsが保留状態にあったメッセージをFIFO*1方式で逐次処理するため。
つまり、メッセージループを用いてクリック直後の保留されている不要なメッセージを削除することで、
ボタンの2度押しを防止することができるってゆー。


2度押しを許容するか否かが選択可能な、拡張Buttonを作る

各ButtonのClickイベントでメッセージを削除することもできるが、そんなマゾはいないと信じたい。
ということで、拡張Buttonを作って、OnClickメソッドをオーバーライドしてメッセージを削除することにします。
例はC#で書きますが、VB.NETでもやることは同じです。

    /// <summary>
    /// 拡張Button
    /// </summary>
    public class ButtonEx : System.Windows.Forms.Button
    {

        #region メンバ
        private bool _AllowDoubleClick;
        #endregion

        #region コンストラクタ
        public ButtonEx()
        {
            this._AllowDoubleClick = false;
        }
        #endregion

        #region プロパティ
        [Category("動作")]
        [DefaultValue(false)]
        [Description("2度押しを許可するか否かを取得または設定します。")]
        public bool AllowDoubleClick
        {
            get { return this._AllowDoubleClick; }
            set { this._AllowDoubleClick = value; }
        }
        #endregion

        #region オーバーライド
        /// <summary>
        /// OnClick
        /// </summary>
        /// <param name="e">イベントデータ</param>
        protected override void OnClick(EventArgs e)
        {
            base.OnClick(e);

            if (!this._AllowDoubleClick)
            {
                //保留されているメッセージを削除し、2度押しを許可しない
                PlatformInvoke.RemovePeekMessage.Invoke();
            }
        }
        #endregion
    }


PlatformInvoke.RemovePeekMessage.Invoke();ってなんなん。
というわけで、その内容は以下のとおりです。

namespace PlatformInvoke {

    /// <summary>
    /// 保留されているウィンドウメッセージを削除
    /// </summary>
    public static class RemovePeekMessage
    {
        #region 定数
        /// <summary>
        /// 該当するウィンドウに再描画が必要とOSが判断したときにポストされるメッセージ
        /// </summary>
        private const int WM_PAINT = 0xF;

        /// <summary>
        ///  ウィンドウのクライアント領域でユーザーがマウスの移動をしたときにポストされるメッセージ
        /// </summary>
        private const int WM_MOUSEMOVE = 0x200;

        /// <summary>
        /// ウィンドウのクライアント領域でユーザーがマウスの左ボタンを押したときにポストされるメッセージ
        /// </summary>
        private const int WM_LBUTTONDOWN = 0x201;

        /// <summary>
        /// ウィンドウのクライアント領域でユーザーがマウスの左ボタンを離したときにポストされるメッセージ
        /// </summary>
        private const int WM_LBUTTONUP = 0x202;

        /// <summary>
        /// マウスポインタが領域外に出た時にポストされるメッセージ
        /// </summary>
        private const int WM_MOUSELEAVE = 0x2A3;

        #endregion

        #region 構造体
        /// <summary>
        /// MSG 構造体型
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct MSG
        {
            /// <summary>
            /// ウィンドウハンドル
            /// </summary>
            public IntPtr hWnd;
            /// <summary>
            /// MessageID
            /// </summary>
            public int msg;
            /// <summary>
            /// WParamフィールド(MessageIDごとに違うよ)
            /// </summary>
            public IntPtr wParam;
            /// <summary>
            /// LParamフィールド(MessageIDごとに違うよ)
            /// </summary>
            public IntPtr lParam;
            /// <summary>
            /// 時間
            /// </summary>
            public int time;
            /// <summary>
            /// カーソル位置(スクリーン座標)
            /// </summary>
            public POINTAPI pt;
        }

        /// <summary>
        /// POINTAPI構造体
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct POINTAPI
        {
            /// <summary>
            /// X座標
            /// </summary>
            public int x;
            /// <summary>
            /// Y座標
            /// </summary>
            public int y;
        }
        #endregion

        #region 列挙型
        /// <summary>
        /// wRemoveMsgで利用可能なオプション
        /// </summary>
        enum EPeekMessageOption
        {
            /// <summary>
            /// 処理後メッセージをキューから削除しない
            /// </summary>
            PM_NOREMOVE = 0,
            /// <summary>
            /// 処理後メッセージをキューから削除する
            /// </summary>
            PM_REMOVE
        }
        #endregion

        #region DllImport
        /// <summary>
        /// スレッドのメッセージキューにメッセージがあるかどうかをチェックし
        /// あれば、指定された構造体にそのメッセージを格納します
        /// </summary>
        /// <param name="lpMsg"> メッセージ情報を格納する、MSG 構造体型変数のポインタを指定します</param>
        /// <param name="hWnd">メッセージを取得するウィンドウのハンドルを指定します。全ての場合は NULL</param>
        /// <param name="wMsgFilterMin">メッセージの最小値を指定し、フィルタリングします。しない場合は0</param>
        /// <param name="wMsgFilterMax">メッセージの最大値を指定し、フィルタリングします。しない場合は0</param>
        /// <param name="wRemoveMsg">処理後メッセージをキューを削除するか否か</param>
        /// <returns>メッセージを取得したときは 0 以外、取得しなかったときは 0</returns>
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool PeekMessage(
            out MSG lpMsg,
            int hWnd,
            int wMsgFilterMin,
            int wMsgFilterMax,
            EPeekMessageOption wRemoveMsg
        );

        /// <summary>
        /// 指定された MSG 構造体型変数にメッセージを格納します
        /// </summary>
        /// <param name="lpMsg">メッセージ情報を格納する、MSG 構造体型変数のポインタを指定します</param>
        /// <param name="hWnd">メッセージを取得するウィンドウのハンドルを指定します。全ての場合は NULL</param>
        /// <param name="wMsgFilterMin">メッセージの最小値を指定し、フィルタリングします。しない場合は0</param>
        /// <param name="wMsgFilterMax">メッセージの最大値を指定し、フィルタリングします。しない場合は0</param>
        /// <returns></returns>
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GetMessage(
            out MSG lpMsg, 
            int hWnd,
            int wMsgFilterMin,
            int wMsgFilterMax 
        );

        /// <summary>
        /// 指定されたウィンドウメッセージをウィンドウプロシージャにディスパッチします
        /// </summary>
        /// <param name="lpMsg">ディスパッチするメッセージを格納した MSG 構造体変数のポインタを指定します</param>
        /// <returns>ウィンドウプロシージャの戻り値</returns>
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr DispatchMessage(
             out MSG lpMsg
        );
        #endregion

        #region メソッド
        /// <summary>
        /// 保留されているウィンドウメッセージを削除します。
        /// </summary>
        public static void Invoke()
        {
            MSG wm;
            while (PeekMessage(out wm, 0, 0, 0, EPeekMessageOption.PM_REMOVE))
            {
                switch (wm.msg)
                {
                    case WM_LBUTTONUP:
                    case WM_MOUSELEAVE:
                    case WM_PAINT:
                        DispatchMessage(out wm);
                        break;
                }
            }
        }
        #endregion
    }
}

上記のサンプルではGetMessage()関数は使ってませんが、メッセージループに使うPeekMessage()関数やら
GetMessage()関数なんかは、ゲームプログラミングする人にはお馴染みすね。


余談だが、P/Invokeまわりの処理は、PlatformInvoke名前空間にまとめちゃう場合が多い*2
その理由は個人的な側面もあるが、むしろ公共的な側面によるものが大きい気がする。

*1:First In First Out:FIFO

*2:まぁ、大抵の人は何かしらラップすると思うんだけど