none
TextBox で PreviewMouseDownOutsideCapturedElementEvent を機能させたい RRS feed

  • 質問

  • TextBox を継承したクラスで PreviewMouseDownOutsideCapturedElementEvent を使用したいのですが、自身をクリックした場合もイベントが発生してしまい、正常に動作しません。他のコントロール(Grid等)を継承したクラスでは期待した動作になります。

    TextBox自身をクリックした場合にはイベントを発生させないようにしたいのですがどうしたら良いのでしょうか。よろしくおねがいします。


    環境:
    Windows10 / VisualStudio2015 / .Net 4.5.2

    検証コード:

    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    
    namespace Test
    {
        public class PopupTextBox : TextBox
        {
            public PopupTextBox()
            {
                Loaded += PopupTextBox_Loaded;
            }
    
            private void PopupTextBox_Loaded(object sender, RoutedEventArgs e)
            {
                this.Focus();
    
                this.AddHandler(Mouse.PreviewMouseDownOutsideCapturedElementEvent, new MouseButtonEventHandler(OnMouseDownOutside), true);
                this.CaptureMouse();
            }
    
            private void OnMouseDownOutside(object sender, MouseButtonEventArgs e)
            {
                MessageBox.Show("out click");
            }
        }
    }

    2016年7月8日 3:24

回答

  • TextBox(やRichTextBoxなど)には標準の機能で文字列をマウスで選択するという動作があり、この動作は範囲選択中にコントロール外にマウスが移動してもマウスの動きに追従するようになっています。
    つまり、TextBoxでMouseDownの時にUIElement.CaptureMouseが行われ、MouseUpでUIElement.ReleaseMouseCaptrueが行われていると考えられます。
    そのため別の個所でCaptureMouseを行っていてもTextBox自身が解除してしまうことになります。

    ですから、TextBoxでMouseUpが発生したら再度MouseCaptureを行うか、MouseUpイベントを無効にして解除されないようにすればいいことになります。

    public class PopupTextBox : TextBox
    {
        public PopupTextBox()
        {
            Loaded += PopupTextBox_Loaded;
        }
    
        private void PopupTextBox_Loaded(object sender, RoutedEventArgs e)
        {
            this.Focus();
    
            this.AddHandler(Mouse.PreviewMouseDownOutsideCapturedElementEvent, new MouseButtonEventHandler(OnMouseDownOutside), true);
            this.PreviewMouseUp += PopupTextBox_PreviewMouseUp;
            this.CaptureMouse();
    
            //Mouse.Capture(this, CaptureMode.SubTree);
        }
    
        private void OnMouseDownOutside(object sender, MouseButtonEventArgs e)
        {
            Window w = Window.GetWindow(this);
    
            var pointWindow = e.MouseDevice.GetPosition(w);
            var result = VisualTreeHelper.HitTest(w, pointWindow);
            if (result != null)
            {
                DependencyObject d = result.VisualHit;
                while (d != null)
                {
                    if (d == this)
                    {
                        this.Text = DateTime.Now.ToString("HH:mm:ss.fff") + "\tMouseDown This";
                        return;
                    }
                    d = VisualTreeHelper.GetParent(d);
                }
            }
            this.Text = DateTime.Now.ToString("HH:mm:ss.fff") + "\tMouseDown Out";
                
            //1回だけでいいならここでMouseUpのイベントを消す
            //this.PreviewMouseUp -= PopupTextBox_PreviewMouseUp;
            //this.RemoveHandler(Mouse.PreviewMouseDownOutsideCapturedElementEvent, new MouseButtonEventHandler(OnMouseDownOutside));
        }
    
        void PopupTextBox_PreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            //MouseUp
            e.Handled = true;
        }
    
    }


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク mitoso 2016年7月8日 12:22
    2016年7月8日 9:19

すべての返信

  • 期待している動作になるかわからないのですが、

    this.CaptureMouse();

    の部分を

    Mouse.Capture(this, CaptureMode.SubTree);


    とすると、TextBox自信をクリックしたときにeventは発生しませんでした。

    参考サイト: http://stackoverflow.com/questions/30460953/how-to-capture-and-handle-previewmousedownoutsidecapturedelementevent

    2016年7月8日 4:17
  • Mouse.Capture(this, CaptureMode.SubTree); を使用した場合、一度でもTextBox自身をクリックしてしまうと以後イベントが発生しなくなってしまいます。

    期待する動作は、TextBoxをクリックしたときにはイベントが発生せず、TextBox以外がクリックされたときにはイベントが発生することです。

    2016年7月8日 5:09
  • PreviewMouseDownOutsideCapturedElementEvent は名前の通りマウスがキャプチャーされている状態のときに発生するイベントです。

    質問時にご提示されたコードでも、TextBoxの外側または内側のクリック時にマウスのCaptureが外れてしまうので、PreviewMouseDownOutsideCapturedElementEvent は1度しか発生しないようになっておりました。

    このイベントを連続で発生しようとした場合、常にCapture状態にしなければならないと思います。ただ、そうなると他のコントロール(例えばボタン)などをマウスで操作できないようになってしまいますが、それは構わなかったのでしょうか?

    どのような挙動をさせるために PreviewMouseDownOutsideCapturedElementEvent を使用されようとしているのでしょうか?目的を達成するために別の方法があるかもしれません。

    2016年7月8日 8:03
  • > このイベントを連続で発生しようとした場合、常にCapture状態にしなければならないと思います。ただ、そうなると他のコントロール(例えばボタン)などをマウスで操作できないようになってしまいますが、それは構わなかったのでしょうか?

    PreviewMouseDownOutsideCapturedElementEvent の発生は1度だけで問題ありません。このコントロール自体を終了させるために使用します。
    TextBoxをクリックしたときに無効になってしまうため困っております。

    > どのような挙動をさせるために PreviewMouseDownOutsideCapturedElementEvent を使用されようとしているのでしょうか?目的を達成するために別の方法があるかもしれません。

    自前のListBoxでのアイテムの名前変更を行いたいと思っており、具体的にはExplorerでのファイル名変更と同じような挙動を実装させようと思っております。名前変更時にその項目の場所にテキスト入力エリアを表示し、入力エリア外をクリックしたら確定、といった動作です。

    最初は PopupコントロールにTextBoxを配置して実装しようとしたのですが、日本語変換のポップアップがカーソルとは別の位置に表示されてしまい、この実装方法は断念しております。
    Popupと似たような挙動ができそうな PreviewMouseDownOutsideCapturedElementEvent を見つけ、今に至っております。
    2016年7月8日 8:38
  • WPFは専門外ですが、エクスプローラのファイル名称の変更などと同じ動作を実装するなら

    (1)編集中(キーボードフォーカスを持つ)のEditがキーボードフォーカスを失ったときに編集完了とする。

    が、その動作となります。
    マウスクリックの事象自体は、キーボードフォーカスを失うきっかけとなっているだけで、
    それを編集確定のタイミングと考えるのは誤りではないでしょうか。
    例えば、当該リストを編集中に、裏で実行中の他のアプリケーションがメッセージボックスを表示して自身にフォーカスを要求したり、ユーザーがAlt+TABを入力したりなどといったケースも想定してしかるべきだと考えられます。

    2016年7月8日 9:08
  • TextBox(やRichTextBoxなど)には標準の機能で文字列をマウスで選択するという動作があり、この動作は範囲選択中にコントロール外にマウスが移動してもマウスの動きに追従するようになっています。
    つまり、TextBoxでMouseDownの時にUIElement.CaptureMouseが行われ、MouseUpでUIElement.ReleaseMouseCaptrueが行われていると考えられます。
    そのため別の個所でCaptureMouseを行っていてもTextBox自身が解除してしまうことになります。

    ですから、TextBoxでMouseUpが発生したら再度MouseCaptureを行うか、MouseUpイベントを無効にして解除されないようにすればいいことになります。

    public class PopupTextBox : TextBox
    {
        public PopupTextBox()
        {
            Loaded += PopupTextBox_Loaded;
        }
    
        private void PopupTextBox_Loaded(object sender, RoutedEventArgs e)
        {
            this.Focus();
    
            this.AddHandler(Mouse.PreviewMouseDownOutsideCapturedElementEvent, new MouseButtonEventHandler(OnMouseDownOutside), true);
            this.PreviewMouseUp += PopupTextBox_PreviewMouseUp;
            this.CaptureMouse();
    
            //Mouse.Capture(this, CaptureMode.SubTree);
        }
    
        private void OnMouseDownOutside(object sender, MouseButtonEventArgs e)
        {
            Window w = Window.GetWindow(this);
    
            var pointWindow = e.MouseDevice.GetPosition(w);
            var result = VisualTreeHelper.HitTest(w, pointWindow);
            if (result != null)
            {
                DependencyObject d = result.VisualHit;
                while (d != null)
                {
                    if (d == this)
                    {
                        this.Text = DateTime.Now.ToString("HH:mm:ss.fff") + "\tMouseDown This";
                        return;
                    }
                    d = VisualTreeHelper.GetParent(d);
                }
            }
            this.Text = DateTime.Now.ToString("HH:mm:ss.fff") + "\tMouseDown Out";
                
            //1回だけでいいならここでMouseUpのイベントを消す
            //this.PreviewMouseUp -= PopupTextBox_PreviewMouseUp;
            //this.RemoveHandler(Mouse.PreviewMouseDownOutsideCapturedElementEvent, new MouseButtonEventHandler(OnMouseDownOutside));
        }
    
        void PopupTextBox_PreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            //MouseUp
            e.Handled = true;
        }
    
    }


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク mitoso 2016年7月8日 12:22
    2016年7月8日 9:19
  • マウスクリックの事象自体は、キーボードフォーカスを失うきっかけとなっているだけで、

    TextBoxが子ウィンドウとして配置されていた場合、TextBoxの外側の何もないところをクリックしてもキーボードフォーカスは失わないと思います。pop upウィンドウにすれば話は違うかもしれませんが、、

    2016年7月8日 9:22
  • 自前のListBoxでのアイテムの名前変更を行いたいと思っており、具体的にはExplorerでのファイル名変更と同じような挙動を実装させようと思っております。名前変更時にその項目の場所にテキスト入力エリアを表示し、入力エリア外をクリックしたら確定、といった動作です。

    最初は PopupコントロールにTextBoxを配置して実装しようとしたのですが、日本語変換のポップアップがカーソルとは別の位置に表示されてしまい、この実装方法は断念しております。

    日本語の変換(IME)が絡むとややこしくなりそうですね。IMEの変換候補をマウスで選ぶと、PreviewMouseDownOutsideCapturedElementEventが呼ばれて範囲外となってしまうことがあるので、、

    2016年7月8日 9:28
  • >TextBoxが子ウィンドウとして配置されていた場合、TextBoxの外側の何もないところをクリックしてもキーボードフォーカスは失わないと思います。pop upウィンドウにすれば話は違うかもしれませんが、、

    そうなる場合と、ならない場合を知っています。
    エクスプローラの場合子ウインドウであるEditはマウスをキャプチャしないため、Edit外をクリックするとフォーカスを失います。
    ダイアログ等では、もし意味のないクリックを検知した場合は「ユーザーが誤ってクリックした可能性が高い」と判定し、
    編集は続行されます。 Editはこの場合キーボードフォーカスを失いません。
    この動作が一般的ユーザーが期待する(又は予測する)標準的な動作となり、質問者の設計とやや対立すると考えられるわけです。

    リストボックス内の他の項目をクリックした場合は「その項目の編集を開始したい」というユーザーの意志を感じます。
    この場合「編集の開始」を指示しているのであって、それを行うためには結果的に直前の編集を終了させなければならないということになり、この場合はマウスクリックの事象は編集完了の遠因となると思います。

    もし、クリックにより他の項目の編集を開始しないのであれば、編集を完了させるための、他のアクションをユーザーに要求すべきかもしれません。
    エクスプローラはリターンキーやESCキーの押下アクションを要求するようです。

    2016年7月8日 10:10
  • IMEのポップアップやっかいですね...。
    PreviewMouseDownOutsideCapturedElementEvent では素直な実装は難しそうということで、別の実装として、ウィンドウいっぱいに広がる当たり判定のみのコントロールを下に置いてそのクリックイベントで代用できないかを検討することにしました。

    IMEの問題はありますが、質問の回答として最もふさわしいgekka様の回答を回答としてマークさせていただきました。
    皆様、ご回答ありがとうございました。

    2016年7月8日 12:32