WPF学习(4)逻辑树和可视树
前面几节说了一些WPF的基础,包括XAML和布局等。在接下来的几节,我们来说说WPF的核心概念,包括逻辑树和可视树、依赖对象和依赖属性、路由事件、命令这几个部分。本节介绍下逻辑树(Logical Tree)和可视树(Visual Tree)。
逻辑树和可视树
在WPF中,用户界面是由XAML来呈现的。粗略地讲,从宏观上看,叶子为布局组件和控件所组成的树既是逻辑树,从微观上看,将逻辑树的叶子再放大可看到其内部是由可视化组件(继承自Visual类)组成的,叶子为这些可视化组件组成的树既是可视树。
逻辑树
举个例子来说明:
<Window x:Class="WpfTreeDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="100" /> <RowDefinition Height="100" /> <RowDefinition Height="100" /> </Grid.RowDefinitions> <TabControl> <TabItem Header="第一页"> <TextBlock Text="This is first page" /> </TabItem> <TabItem Header="第二页"> <TextBox Text="This is second page" /> </TabItem> </TabControl> <Button x:Name="btnOK" Grid.Row="1" Width="80" Height="80" Click="btnOK_Click"> <Button.Content> <Image Source="/Images/photo.png" x:Name="imgPhoto"/> </Button.Content> </Button> <ListView x:Name="lvStudents" Grid.Row="2"> <ListView.View> <GridView> <GridViewColumn Header="Index" DisplayMemberBinding="{Binding Index}" /> <GridViewColumn Header="Username" DisplayMemberBinding="{Binding Username}" /> <GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}"/> </GridView> </ListView.View> </ListView> </Grid> </Window>
来看一下它的逻辑树:
层级感很强,这也正是XAML强大表现力的体现。如何来操作这棵树呢?最简单的方法当然是设置控件的name属性,然后在cs文件中根据name属性值来获取控件。WPF内置了一个LogicalTreeHelper类,我们可以通过它来遍历树,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Collections; using Microsoft.Windows.Themes; using System.Diagnostics; namespace WpfTreeDemo { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); List<Student> students = new List<Student> { new Student{Index=1,Username="Jello",Age=22}, new Student{Index=2,Username="Taffy",Age=21} }; this.lvStudents.ItemsSource = students; PrintLogicalTree(0, this); } private void btnOK_Click(object sender, RoutedEventArgs e) { MessageBox.Show("I am a button"); } public void PrintLogicalTree(int depth, object obj) { Debug.WriteLine(new string(' ', depth) + obj); if (!(obj is DependencyObject)) return; foreach (var child in LogicalTreeHelper.GetChildren(obj as DependencyObject)) { PrintLogicalTree(depth + 1, child); } } } public class Student { public int Index { get; set; } public string Username { get; set; } public int Age { get; set; } } }
Debug运行后可以在Debug输出窗口看到界面的逻辑树。
可视树
想要观察可视树,需要将控件“打碎”来看下,这里我们以TextBox为例,用Blend来“打碎”它,看看这个控件的内部结构:
<LinearGradientBrush x:Key="TextBoxBorder" EndPoint="0,20" MappingMode="Absolute" StartPoint="0,0"> <GradientStop Color="#ABADB3" Offset="0.05"/> <GradientStop Color="#E2E3EA" Offset="0.07"/> <GradientStop Color="#E3E9EF" Offset="1"/> </LinearGradientBrush> <Style x:Key="TextBoxStyle1" BasedOn="{x:Null}" TargetType="{x:Type TextBox}"> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/> <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Padding" Value="1"/> <Setter Property="AllowDrop" Value="true"/> <Setter Property="FocusVisualStyle" Value="{x:Null}"/> <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/> <Setter Property="Stylus.IsFlicksEnabled" Value="False"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TextBox}"> <Microsoft_Windows_Themes:ListBoxChrome x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderFocused="{TemplateBinding IsKeyboardFocusWithin}" SnapsToDevicePixels="true"> <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </Microsoft_Windows_Themes:ListBoxChrome> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
主要是ListBoxChrome及ScrollViewer构成的,这也很好理解,将TextBox的功能拆分来看,ListBoxChrome主要可以用来输入,ScrollViewer用于当内容过多时会有滚动条。这里通过name属性一般是不能直接获取控件的,需要借助VisualTreeHelper类和Visual中的方法来获取。也举个例子:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Collections; using Microsoft.Windows.Themes; using System.Diagnostics; namespace WpfTreeDemo { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); List<Student> students = new List<Student> { new Student{Index=1,Username="Jello",Age=22}, new Student{Index=2,Username="Taffy",Age=21} }; this.lvStudents.ItemsSource = students; PrintLogicalTree(0, this); } protected override void OnContentRendered(EventArgs e) { base.OnContentRendered(e); PrintVisualTree(0, this); } private void btnOK_Click(object sender, RoutedEventArgs e) { MessageBox.Show("I am a button"); } public void PrintLogicalTree(int depth, object obj) { Debug.WriteLine(new string(' ', depth) + obj); if (!(obj is DependencyObject)) return; foreach (var child in LogicalTreeHelper.GetChildren(obj as DependencyObject)) { PrintLogicalTree(depth + 1, child); } } public void PrintVisualTree(int depth, DependencyObject obj) { Debug.WriteLine(new string(' ', depth) + obj); for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) { PrintVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i)); } } } public class Student { public int Index { get; set; } public string Username { get; set; } public int Age { get; set; } } }
之所以在OnContentRendered中调用了一次是因为可视树直到Window完成至少一次布局后才会有节点,否则是空的,而实例化发生在布局完成之前(可以获取逻辑树),
OnContentRendered调用发生在布局完成之后(可以获取可视树)。
这里需要注意几点:
1.并不是所有的元素(无与生俱来的呈现行为)都会出现可视树中,只有继承自Visual类或者Visual3D类的元素才会包含其中。
2.逻辑树是静态的,而可视树是动态的(当用户切换Theme是会改变)。
3.一般情况下,我们不需要考虑可视树,除非要进行控件重塑。