特定のコントロール上でMouse Eventを排除したい(C# WPF)

C#

IsHitTestVisible云々はここでは忘れてください。
滅多にないと思いますが、クリックはさせたいけど、特定の領域のクリックイベントは握りつぶしたい(別の動作に振り替えたい)、というケースを想定しています。具体的にはListBoxItemのIsSelected=Trueにはさせたくないが、SelectされたItemが欲しいケースです。

前提の確認

MouseLeftButtonDown イベント

まずはGridをネストしたXamlを用意します。

<Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid1">
    <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid2">
        <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid3">
            <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid4">
                <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid5">
                    <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid6">
                        <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid7">

                        </Grid>
                    </Grid>
                </Grid>
            </Grid>
        </Grid>
    </Grid>
</Grid>

不必要なコードは色々端折ってますが、以下のようなコントロールです。

コードビハインドには以下のように書いておきます。

private void Grid_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    Console.WriteLine(((Grid)sender).Name);
}

この状態で中央のグリッドをクリックするとMouseLeftButtonDownイベントは中央で発火し、バブルアップしてGrid1まで到達します。

コンソールではGrid7から順に発火しています。

Console

Grid7
Grid6
Grid5
Grid4
Grid3
Grid2
Grid1

PreviewMouseLeftButtonDown イベント

<Grid PreviewMouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid1">
    <Grid PreviewMouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid2">
        <Grid PreviewMouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid3">
            <Grid PreviewMouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid4">
                <Grid PreviewMouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid5">
                    <Grid PreviewMouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid6">
                        <Grid PreviewMouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid7">

                        </Grid>
                    </Grid>
                </Grid>
            </Grid>
        </Grid>
    </Grid>
</Grid>

MouseLeftButtonDownをPreviewMouseLeftButtonDownに変えただけです。
これで中央のグリッドをクリックするとこうなります。

Console

Grid1
Grid2
Grid3
Grid4
Grid5
Grid6
Grid7

先ほどとは逆向きに流れています。

WPFのイベント制御

WPFのイベント伝播は、親コントロール→子コントロールに向けて流れるトンネルイベントと、
子コントロール→親コントロールに流れるバブルイベントがあります。
トンネルイベントはPreviewというPrefixが使われます。

<Grid>
    <StackPanel>
        <Button/>
    </StackPanel>
</Grid>

 

こんなコントロールがあったとすると、イベントの流れは
Grid -PreviewMouseLeftButtonDown
StackPanel-PreviewMouseLeftButtonDown
Button -PreviewMouseLeftButtonDown
Button -MouseLeftButtonDown
StackPanel-MouseLeftButtonDown
Grid -MouseLeftButtonDown
という順で発火されます。

このイベントの伝播を途中で止めたい場合、EventArgsのHandledプロパティをtrueにすると後ろにイベントが流れません。

先ほどの例で、コードビハインドを以下に書き換えます。

private void Grid_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    Console.WriteLine(((Grid)sender).Name);
    e.Handled = true;
}

こうするとMouseLeftButtonDownイベントを拾っている場合は
Grid1→7にPreviewMouseLeftButtonDownが伝播し、Grid7でMouseLeftButtonDownイベントが来た時点で伝播が終了します。Grid7→Grid1へ向かうバブルアップが抑制されます。

PreviewMouseLeftButtonDownイベントを拾った場合は、Grid1にPreviewMouseLeftButtonDownイベントが来た時点でイベントの伝播が終了するため、Grid2→Grid7→Grid1へのイベント伝播は起こりません。

MVVMでどうやって制御するか?

さてここからが本題です。
コードビハインドを使えば簡単にMouseEventの伝播を止められました。
しかしMVVMではコードビハインドは使えません。

1. EventToReactiveCommandで制御する?

MVVMではEventArgsに触る方法がかなり限られます。
その点、EventToReactiveCommandはコマンドパラメータにEventArgsを直接引けるため、制御できるのでは?

<Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid1">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="PreviewMouseLeftButtonDown">
            <interactivity:EventToReactiveCommand Command="{Binding ClickCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid2">
        <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid3">
            <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid4">
                <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid5">
                    <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid6">
                        <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid7">

                        </Grid>
                    </Grid>
                </Grid>
            </Grid>
        </Grid>
    </Grid>
</Grid>
ViewModel

this.ClickCommand = new ReactiveCommand<MouseButtonEventArgs>().WithSubscribe(x =>
{
    x.Handled = true;
});

こんな感じでGridにはMouseLeftButtonDownイベントを、
EventToReactiveCommandではPreviewMouseLeftButtonDownを購読させます。
そしてコマンドではEventArgs.Handledをtrueにし、イベント伝播を止めます。

目論見としては、 EventToReactiveCommandがPreviewEventを拾うため、GridのMouseLeftButtonDownイベントは発火しないはずでした。

実行結果はこちら。

Console

Grid7
Grid6
Grid5
Grid4
Grid3
Grid2
Grid1

どうやらEventToReactiveCommandは全てのイベントが終わってから実行されるようです。
トンネルイベントを購読してもバブルアップが全て終わってからようやく実行されるため、イベント伝播の制御には使えなさそうです。
ということで個人的には直接EventArgsを触るのは手詰まり。

2. Button等のイベントを制御するコントロールに置き換える

これは発想の転換です。
自力でEventArgsが制御できないのなら、EventArgsを制御するコントロールを使ってしまえ!っていう考えです。

<Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid1">
    <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid2">
        <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid3">
            <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid4">
                <Button MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Button5">
                    <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid6">
                        <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown" Name="Grid7">

                        </Grid>
                    </Grid>
                </Button>
            </Grid>
        </Grid>
    </Grid>
</Grid>

分かりにくいですが、Grid5をGrid→Buttonに変えました。
これを実行するとこうなります。

Console

Grid7
Grid6
Button5

PreviewMouseLeftButtonDownイベントがGrid1→Button5→Grid7までトンネルし、
MouseLeftButtonDownイベントがGrid7→Button5まで行ったところで、EventArgs.Handledがtrueになって以降には流れません。

クリックイベントの制御のためにボタンを使うってのはコードが読みにくくなるので、個人的にはコードビハインドに書いてしまう方が良いと思います。手詰まりになった時にはこんな方法もあるよってことで。

C#
スポンサーリンク
Once and Only

コメント