在ControlTemplate中使用VisualTreeHelper和LogicalTreeHelper

在TabControl中给TabItem定义如下的Template:

1
2
3
4
5
6
7
8
9
10
11
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid x:Name="templateRoot" SnapsToDevicePixels="true">
<Border x:Name="mainBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1,1,1,0" Background="{TemplateBinding Background}" Margin="0">
<Border x:Name="innerBorder" BorderBrush="{StaticResource TabItem.Selected.Border}" BorderThickness="1,1,1,0" Background="{StaticResource TabItem.Selected.Background}" Margin="-1" Opacity="0"/>
</Border>
<DockPanel Margin="2,0">
<Button Click="Button_Click" DockPanel.Dock="Right" Content="X" Background="{x:Null}" BorderBrush="{x:Null}" Foreground="Blue"/>
<ContentPresenter x:Name="contentPresenter" ContentSource="Header" Focusable="False" HorizontalAlignment="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
</DockPanel>
</Grid>
</ControlTemplate>

实现的效果是给每一个TabItem添加关闭按钮:

现在需要实现关闭按钮的功能,那么则需要在后台Button_Click事件中通过sender找到类型为TabItem的parent。

然而却发现通过 FrameWorkElement.Parent 方法怎么也没法找到TabItem,当找到x:Name="templateRoot"的Grid时,再往上找,就是null.

问题的原因就在于FrameWorkElement.Parent是获取此元素的逻辑父级元素。也就是通过LogicalTreeHelper查找。在MSDN中备注了:

对于模板,模板的 Parent 最终将为 null。 若要忽略这一点并扩展到实际应用模板的逻辑树,请使用 TemplatedParent。

所以就简单来说,后台代码可以是:

1
2
3
4
5
6
private void Button_Click(object sender, RoutedEventArgs e)
{
Button btn = sender as Button;
TabItem item = btn.TemplatedParent as TabItem;
//....
}

那么如果一定要使用一层一层查找Parent的方法,就需要用VisualTreeHelper.我们在下面的代码执行完Button btn = sender as Button;的地方加入一个断点。

1
2
3
4
5
private void Button_Click(object sender, RoutedEventArgs e)
{
Button btn = sender as Button;
//....
}

触发断点时将鼠标光标移到btn上,点击”放大镜”,就可以打开WPF可视化工具。

在这里可以看到视觉树中可以看到TabItem.

那么使用VisualTree重写后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void Button_Click(object sender, RoutedEventArgs e)
{
Button btn = sender as Button;
DependencyObject parent = VisualTreeHelper.GetParent(btn);
while (parent != null)
{
if (parent is TabItem)
{
break;
}
else
{
parent = VisualTreeHelper.GetParent(parent);
}
}
if (parent != null)
{
TabItem item = parent as TabItem;
TabControl tc = item.Parent as TabControl;

tc.Items.Remove(item);
}
else
{
MessageBox.Show("没有找到TabItem");
}
}

这样就能正确的找到TabItem了,如果替换VisualTreeHelper为LogicTreeHelper,则会弹出提示”没有找到TabItem”.

其实VisualTreeHelper和LogicTreeHelper在ControlTemplate上有个交点,LogicTreeHelper不会跨过ControlTemplate,因为一个Control是一个逻辑元件,所以LogicTreeHelper只工作在在一个Control的ControlTemplate内部,当LogicTreeHelper获取控件的Parent到达ControlTemplate边界,则返回null。视觉树VisualTreeHelper则不同,它的层级关系是贯通的。不会受到ControlTemplate影响。如果要在ControlTemplate中使用相关操作,需要注意以上几点。

在ControlTemplate中使用VisualTreeHelper和LogicalTreeHelper

https://wurang.net/logicaltreehelper_visualtreehelper/

作者

Wu Rang

发布于

2014-07-11

更新于

2021-12-06

许可协议

评论