2013年11月23日土曜日

ボタンクリックでメニューを表示してみる

Widowsフォーム アプリケーションでボタンをクリックした時にメニューを表示してみる。
といっても、ただメニューを表示するだけなら簡単!!
    public partial class CustomButton : Button
    {
        // ボタンクリックで表示するメニュー
        public ContextMenuStrip DropDownMenu { get; set; }

        // コンストラクタ
        public CustomButton()
        {
            InitializeComponent();
        }

        protected override void OnClick(EventArgs e)
        {
            // メニューを表示可能か?
            if (DropDownMenu != null)
            {
                // メニューを開く
                DropDownMenu.Show(this, new Point(0, Height));
            else
            {
                // クリックイベントを発生させる
                base.OnClick(e);
            }
        }
    }
namespaceは省略してます。
で、これだけでとりあえずDropDownMenuプロパティに入れたメニューがボタンクリックで開くようになる。こんな感じ。
まあ、これだけでも概ねOKだけどメニューが開いている状態でもう一度ボタンをクリックすると、メニューが一瞬閉じてからすぐにまた開いてしまう。
メニューを開いている状態でボタンクリックしたら閉じて欲しいんだけど。。
ボタンクリックの前にメニューが閉じてしまうから「メニューが閉じられていたら開く」ってこともできない。
        protected override void OnClick(EventArgs e)
        {
            // メニューを表示可能か?
            if (DropDownMenu != null && !DropDownMenu.Visible) // ←すでにメニューは閉じているのでダメ!
            {
                // メニューを開く
                DropDownMenu.Show(this, new Point(0, Height));
            }
            else
            {
                // クリックイベントを発生させる
                base.OnClick(e);
            }
        }
しょうがないのでWndProcで何とかならないかと調べてみると、ボタンクリック時にこんなメッセージが飛んできているのを発見!

「WM_MOUSEACTIVATE = 0x0021(マウスクリックによりアクティブウインドウが移った)」

つまり、メニューが閉じられてアクティブなウィンドウがメニューからフォームに移ったって意味らしい。(ちなみにメニューが開く時はフォームからメニューにアクティブウィンドウが移ったって意味になる)

しかもこのメッセージ、メニューが閉じる前に飛んできてるやん!!

というわけで、こんな感じに実装してみた。
    public partial class CustomButton : Button
    {
        // WndProcのWM_MOUSEACTIVATE(マウスクリックによりアクティブウインドウが移った)のメッセージID
        private const int WM_MOUSEACTIVATE = 0x0021;

        // メニューのClosedイベント
        private ToolStripDropDownClosedEventHandler closedHandler;

        // Click時にメニューを表示可能であることを示すフラグ
        private bool allowShowMenu = true;

        // メニューがボタンクリックによって閉じられることを示すフラグ
        private bool menuCloseByClick = false;

        // ボタンクリックで表示するメニュー
        public ContextMenuStrip DropDownMenu { get; set; }

        // コンストラクタ
        public CustomButton()
        {
            InitializeComponent();
            // メニューのClosedイベントハンドラ生成
            closedHandler = new ToolStripDropDownClosedEventHandler(MenuClosed);
        }

        protected override void OnClick(EventArgs e)
        {
            // メニューを表示可能か?
            if (DropDownMenu != null && allowShowMenu)
            {
                // イベントハンドラ登録
                DropDownMenu.Closed += closedHandler;
                // メニューを開く
                DropDownMenu.Show(this, new Point(0, Height));
            }
            else
            {
                // クリックイベントを発生させる
                base.OnClick(e);
            }
            // 次回クリックに備えてメニュー表示可能フラグを立てる
            allowShowMenu = true;
            // 次回クリックに備えてメニューがボタンクリックによって閉じられたことを示すフラグをたたむ
            menuCloseByClick = false;
        }

        // メニューが閉じられた時の処理
        private void MenuClosed(object sender, ToolStripDropDownClosedEventArgs e)
        {
            // イベントハンドラを破棄
            DropDownMenu.Closed -= closedHandler;
            if (e.CloseReason == ToolStripDropDownCloseReason.AppClicked && menuCloseByClick)
            {
                // ボタンクリックによってメニューが閉じられた場合
                // この後に走るOnClickでメニューを開かないようにフラグを立てる
                allowShowMenu = false;
            }
            else
            {
                // ボタンクリック以外でメニューが閉じられた場合
                // 次回のボタンクリックでメニューが開くようにフラグをたたむ
                allowShowMenu = true;
            }
            // ボタンクリック以外でメニューが閉じられた場合に備えて
            // ここでボタンクリックによってメニューが閉じられたことを示すフラグをたたむ
            menuCloseByClick = false;
        }

        // Windowメッセージを処理します
        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_MOUSEACTIVATE)
            {
                // ボタンクリックによってメニューが閉じられてアクティブウィンドウが移った場合フラグを立てる
                // (正確にはメニューが表示される時にもフラグが立つがMenuClosedは走らないので問題ない)
                menuCloseByClick = true;
            }
            base.WndProc(ref m);
        }
    }
ちゃんとボタンクリックでメニューが開いて、もう一回クリックで閉じてる。いい感じかな(^_^)
やっていることは簡単!
1.WndProcでmenuCloseByClickフラグを立てる
2.メニューClosedイベントでmenuCloseByClickが立っていたらallowShowMenuフラグを立てる
3.ボタンクリックでallowShowMenuが立っていたらメニューを開いて、フラグを戻しておく
特に問題なさそうだし、これでOKかな。。。

あれ?ボタンを右クリックしてメニューを閉じたらどうなるんだろう???
・・・ボタンを2回クリックしないとメニューが開かなくなった。。○| ̄|_
ボタンをダブルクリックしてメニューを開くとメニューClosedイベントが2回走るし。。。

なんか、もう面倒臭くなってきたなぁ( ̄◇ ̄)
    public partial class CustomButton : Button
    {
        // WndProcのWM_MOUSEACTIVATE(マウスクリックによりアクティブウインドウが移った)のメッセージID
        private const int WM_MOUSEACTIVATE = 0x0021;

        // メニューのClosedイベント
        private ToolStripDropDownClosedEventHandler closedHandler;

        // Click時にメニューを表示可能であることを示すフラグ
        private bool allowShowMenu = true;

        // メニューがボタンクリックによって閉じられることを示すフラグ
        private bool menuCloseByClick = false;

        // ボタンクリックで表示するメニュー
        public ContextMenuStrip DropDownMenu { get; set; }

        // コンストラクタ
        public CustomButton()
        {
            InitializeComponent();
            // メニューのClosedイベントハンドラ生成
            closedHandler = new ToolStripDropDownClosedEventHandler(MenuClosed);
        }

        // マウス押下時の処理
        protected override void OnMouseDown(MouseEventArgs mevent)
        {
            base.OnMouseDown(mevent);
            // ボタン上で左クリック以外の場合、次回の左クリックで
            // メニューが開くようにフラグを立てる
            if (mevent.Button != MouseButtons.Left)
            {
                allowShowMenu = true;
            }
        }

        // ボタンクリック時の処理
        protected override void OnClick(EventArgs e)
        {
            // メニューを表示可能か?
            if (DropDownMenu != null && allowShowMenu)
            {
                if (!DropDownMenu.Visible)  // メニューが既に開いている場合は何もしない(ダブルクリック対策)
                {
                    // イベントハンドラ登録
                    DropDownMenu.Closed += closedHandler;
                    // メニューを開く
                    DropDownMenu.Show(this, new Point(0, Height));
                }
            }
            else
            {
                // クリックイベントを発生させる
                base.OnClick(e);
            }
            // 次回クリックに備えてメニュー表示可能フラグを立てる
            allowShowMenu = true;
            // 次回クリックに備えてメニューがボタンクリックによって閉じられたことを示すフラグをたたむ
            menuCloseByClick = false;
        }

        // メニューが閉じられた時の処理
        private void MenuClosed(object sender, ToolStripDropDownClosedEventArgs e)
        {
            // イベントハンドラを破棄
            DropDownMenu.Closed -= closedHandler;
            if (e.CloseReason == ToolStripDropDownCloseReason.AppClicked && menuCloseByClick)
            {
                // ボタンクリックによってメニューが閉じられた場合
                // この後に走るOnClickでメニューを開かないようにフラグを立てる
                allowShowMenu = false;
            }
            else
            {
                // ボタンクリック以外でメニューが閉じられた場合
                // 次回のボタンクリックでメニューが開くようにフラグをたたむ
                allowShowMenu = true;
            }
            // ボタンクリック以外でメニューが閉じられた場合に備えて
            // ここでボタンクリックによってメニューが閉じられたことを示すフラグをたたむ
            menuCloseByClick = false;
        }

        // Windowメッセージを処理します
        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_MOUSEACTIVATE)
            {
                // ボタンクリックによってメニューが閉じられてアクティブウィンドウが移った場合フラグを立てる
                // (正確にはメニューが表示される時にもフラグが立つがMenuClosedは走らないので問題ない)
                menuCloseByClick = true;
            }
            base.WndProc(ref m);
        }

        // ボタン描画時の処理
        protected override void OnPaint(PaintEventArgs pevent)
        {
            base.OnPaint(pevent);
            // 三角形を描画
            using (var brush = new SolidBrush(ForeColor))
            {
                float width = 7;
                float height = 5;
                float x = pevent.ClipRectangle.Width - width - 5;
                float y = (pevent.ClipRectangle.Height - height) / 2;
                var triangle = new PointF[] { new PointF(x, y)
                                            , new PointF(x + width, y)
                                            , new PointF(x + (width / 2), y + height) };
                pevent.Graphics.FillPolygon(brush, triangle);
            }
        }
    }
とりあえず、これで右クリックとダブルクリックでも問題ないはず。
一応、ボタンの右端に▼も描画してみたw

とりあえず今日はこんなところで終わり。

0 件のコメント:

コメントを投稿