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構造の違いが原因。
まぁ、簡単なコードとリファレンスの断片しか見ないで書いたので見当違いかもしれませんが、そんな感じで...

