2012年5月2日水曜日

wpf : TabItemのヘッダにマウスイベントを登録

TabItemのヘッダにマウスイベントを登録する場合について考えます。 xamlで全部のTabItemにイベントを書く場合には特に考える必要はありません。 普通にこんなふうに書きます。

<TabItem
        Header="ささ"
        PreviewMouseLeftButtonDown="OnPreviewMouseLeftButtonDown"
>
    パンダ
</TabItem>

でも、全部のTabItemに同じイベントを登録するときはイチイチ書くのは面倒ですよね? そして、ItemsSourceを使う場合はイベントを直書きすることもできません。 こういう場合はItemContainerStyleでイベントを登録します。

このItemContainerStyleの「ItemContainer」というのはTabItemのことです。 「TabControlに登録された各項目(Item)をTabControl上で表示するためのコンテナ」のことを表しています。 TabControlに限らず、ItemsControl系のコントロールにはこの様なItemContainerが用意されています。 ListBoxならListBoxItem、TreeViewにはTreeViewItemといった具合です。

TabItemに話を戻して、挙動確認用にこんなスタイルを書いてみました。

<Style x:Key="style1" TargetType="TabItem">
    <EventSetter
            Event="MouseLeftButtonDown"
            Handler="OnTabItemMouseLeftButtonDown1"
    />
    <EventSetter
            Event="PreviewMouseLeftButtonDown"
            Handler="OnTabItemPreviewMouseLeftButtonDown1"
    />
</Style>

これを次のTabControlのItemContainerStyleに適用してみます。

<TabControl Name="tab" Grid.Row="3" Margin="4" VerticalAlignment="Stretch">
    <TabItem Header="ささ">パンダ</TabItem>
    <TabItem Header="鮭">熊</TabItem>
    <TabItem Header="ユーカリ">コアラ</TabItem>
    <TabItem Header="猫">
        <Label>ネズミL</Label>
    </TabItem>
</TabControl>

最後のTabItemにだけLabelが付いているのは挙動の確認用です。 こういうイベントハンドラで受けてみました。

private void OnTabItemMouseLeftButtonDown1(object sender, MouseButtonEventArgs evt)
{
    if (evt.Source is TabItem)
    {
        TabItem item = (TabItem)evt.Source;
        PrintLog("タブでMouseDown : " + item.Header.ToString());
    }
    else
    {
        string contentText = tab.SelectedContent.ToString();
        PrintLog("コンテンツでMouseDown : " + contentText);

    }
}

private void OnTabItemPreviewMouseLeftButtonDown1(object sender, MouseButtonEventArgs evt)
{
    if (evt.Source is TabItem)
    {
        TabItem item = (TabItem)evt.Source;
        PrintLog("[P]タブでMouseDown : " + item.Header.ToString());
    }
    else
    {
        string contentText = tab.SelectedContent.ToString();
        PrintLog("[P]コンテンツでMouseDown : " + contentText);
    }
}

evt.SourceがTabItem型かどうかで「Headerでイベントが起きたのか? Contentでイベントが起きたのか?」を判断しています。 PrintLogメソッドはデバッグ出力するためのコードです。 サンプルコードを動かして、Headerをマウスクリックしてこのコードの出力を見ると次のようになります。

[P]タブでMouseDown : ユーカリ

PreviewMouseLeftButtonDownのイベントハンドラが動いているのが確認できました。 それに対して、MouseLeftButtonDownの方は確認できません。 なぜでしょう? 確かめるために次のようなメソッドを作ってみました。

private void PrintVisualTree(DependencyObject control, int indent = 1)
{
    PrintLog(new string(' ', indent * 4) + control.GetType().Name);
    for(int i = 0; i < VisualTreeHelper.GetChildrenCount(control); i++)
        PrintVisualTree(VisualTreeHelper.GetChild(control, i), indent + 1);
}

このメソッドにPreviewMouseLeftButtonDownで受け取ったTabItemを渡すと、次のような出力になります。

[P]タブでMouseDown : 鮭
    TabItem
        Grid
            Border
                ContentPresenter
                    TextBlock

Headerの文字列「鮭」を表示するためにこれだけのコントロールが使われているんですね。 バブルイベントのMouseLeftButtonDownは途中で取られてTabItemまで届かないのでしょう。 TabItemのヘッダで受け取れるイベントはPreviewが付くトンネルイベントだけのようです。

とりあえずここまでのまとめ。

  • TabItemのヘッダにマウスイベントを登録するには、ItemContainerStyleにEventSetterを書く。
  • 使えるイベントはPreviewが付くトンネルイベントだけ。

次に、さっきサラッと書いた「Headerでイベントが起きたのか? Contentでイベントが起きたのか? の確認」について説明します。

↑の確認コードを動かしてTabItemのContentをクリックすると、Labelが挟まっている4番目のみ「コンテンツでMouseDown」が表示されます。

[P]コンテンツでMouseDown : System.Windows.Controls.Label: ネズミL
コンテンツでMouseDown : System.Windows.Controls.Label: ネズミL

どうやらContentに登録された内容によってHeaderからのイベントのみ発行されるのか、Contentのイベントも発行されるのかが異なるようです。 どちらがクリックされて発行されたイベントかを確認するには、マウスイベントのイベントソースがTabItem型かどうかをチェックします。 それがさっきのイベントハンドラのコードです。

private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs evt)
{
 if (evt.Source is TabItem)
 {
  ヘッダーがクリックされたときの処理
 }
 else
 {
  コンテンツがクリックされたときの処理
 }
}

ではなぜイベントソースを見るとどちらで発行されたイベントかわかるのでしょうか?

まず、概念的にはTabControlはこんな構成になっています。

TabControlの直下に凸型のTabItemがあって、その子にTabItem.HeaderとTabItem.Contentがあるという構成です。 HeaderもContentもTabItemの1部なので、どちらからでもイベントがします。

これはLogicalTreeHelperを使ってTabItemの親をたどればなんとなくわかります。 こんなメソッドを用意して、

private void PrintControlParent(DependencyObject control, bool printVisualTree = true)
{
    do
    {
        PrintLog("    " + control.GetType().Name);
        control = (printVisualTree ?
                VisualTreeHelper.GetParent(control) :
                LogicalTreeHelper.GetParent(control)
        );
    }
    while (control != tab);
    PrintLog("    TabControl //");
}

引数printVisualTree=falseでマウスイベントのイベントソールを渡すと、 タブのHeaderをクリックしたときは、

[P]タブでMouseDown : 猫
    TabItem
    TabControl //

タブのContentをクリックしたときは、

[P]コンテンツでMouseDown : System.Windows.Controls.Label: ネズミL
    Label
    TabItem
    TabControl //
コンテンツでMouseDown : System.Windows.Controls.Label: ネズミL

どちらも親にTabItemが含まれています。

では、VisualTreeHelperで親をたどったときはどうなるでしょう? タブのHeaderをクリックしたときは、

[P]タブでMouseDown : 猫
    TabItem
    TabPanel
    Grid
    TabControl //

もちろんTabItemがあります。

ではタブのContentをクリックしたときは?

[P]コンテンツでMouseDown : System.Windows.Controls.Label: ネズミL
    Label
    ContentPresenter
    Border
    Grid
    TabControl //
コンテンツでMouseDown : System.Windows.Controls.Label: ネズミL

TabItemがありませんね。 つまり、Visualの構造はこうなっているのです。

テンプレートによって間にボーダーとかパネルとかが入るかもしれませんが、大まかな構造はこんな感じです。 TabControlの子には上側にTabPanel、下側にContentPresenterが別々にあります。 TabPanelはTabItemを並べるだけのコントロールです。 ここでいうTabItemの部分にはTabItemの本体、つまりTabItem.Headerだけが表示されます。 ContentPresenterにはTabPanel.SelectedContentが関連付けられていて、選ばれたTabItemに応じてそのContentが表示されます。

これを考えるのには、こちらのControlTemplateの例も参考にしました。

ちなみに、TabItemの本体はHeader部分というのはちょっと変な考え方かもしれませんが、TabItem.Templateを編集してもHeader部分にしか影響が無いことを考えるとそうなんんじゃないかなぁと思います。

Visualの構造で考えるとTabItemのHeaderとContentは別物です。 HeaderでMouseDownされたら、それは「TabItemそのものでMouseDownされた」ということなので、当然マウスイベントのソースはTabItemになります。 ContentでMouseDownされた場合は、Visual構造的には無関係なのでイベントソースはContentに応じて設定されます。 ですがLogical構造的にはTabItemの子なのでイベントは発行しなければなりません。 イベントソースはそのままにMouseDownが発行されることになります。

というわけでまとめ。

  • Headerでイベントが起きたのか? Contentでイベントが起きたのか? を見分けるにはマウスイベントのソースの型がTabItemかをチェックする。
  • 多分、見分けられる理由はLogical構造とVisual構造の違いが原因。

まぁ、簡単なコードとリファレンスの断片しか見ないで書いたので見当違いかもしれませんが、そんな感じで...