WPF 可视化树和逻辑树

自开始接触WPF开始一直对可视化树和逻辑树没有清晰地认识和区别,下面进行一次总结。学海无涯,开始吧!!!

 

背景

 

目前SDK文档中关于视觉树和逻辑树的介绍还不是很完全。
事实证明WPF中的元素树相当复杂而且要求WPF类库中很底层的知识来理解这些树。怎样用通用的方式遍历元素树;你觉得在什么地方无法得知元素成分;这些并不像看起来那么简单。不幸的是,WPF没有暴露接口来简化对元素树的遍历。
现在你可能会为什么遍历元素树会这么麻烦。 答案分成几个部分,在下面讨论。

可视化树

可视化树代表你界面上所有的渲染在屏幕上的元素。可视化树用于渲染,事件路由,定位资源(如果该元素没有逻辑父元素)等等等等。向上或者乡下遍历可视化树可以简单的使用VisualTreeHelper和简单的递归方法。

然后,还是有个小别扭让它变得复杂。任何承继自ContentElement的东西都可以在UI上显示,但其实并不在可视化树中。WPF会假定这些元素也在可视化树中,来保持事件路由的一致性,但这只是个幻觉。VisualTreeHelper对ContentElement对象不起作用,因为ContentElement不是继承自Visual或者Visual3D. 下面是Framework中所有承继自ContentElement的类从Reflector中可以看到:


这个文档介绍了为什么ContentElement并不真正存在可视化树中。
内容元素(继承自ContentElement的类)不是可视化树的一部分;他们不是继承自Visual而且没有可视化表示。为了显示在UI上,ContentElement必须寄宿在一个Visual主体上,通常是一个FrameworkElement。你可以认为主体类似于一个可以选择如何展示该ContentElement的浏览器。一旦一个Content被显示主体捕获,这个Content就可以加入到一个特定的和可视化树相关的树处理过程中。一般说来,FrameworkElement类都会包含一段代码用来把ContentElement添加到事件路由中去,通过这个content逻辑树的某个子节点,尽管这个content并不是可视化树的一部分。这很必要,因为content也需要找到路由事件的源头。
这意味着你永远没办法仅仅使用VisualTreeHelper来遍历可视化树。如果你把一个ContentElement传递给VisualTreeHelper的GetParent或者GetChild方法,会抛出一个异常。因为ContentElement不是Visual或者Visual3D的子类,你只能沿着逻辑树查找ContentElement的父元素,直到找到一个Visual对象。这里有个例子遍历到可视化树的根元素。

 

 

 

//遍历可视化树的根元素

DependencyObject FindVisualTreeRoot (DependencyObject initial) { DependencyObject current = initial; DependencyObject result = initial; While(current !=null) { result = current; if(current is Visual || current is Visual3D) { current = VisualTreeHelper.GetParent(current); } else { current = LogicalTreeHelper.GetParent(current); } } return result; }

这段代码在必要的时候沿着逻辑树上溯,如else子句所示。这很有用,假如说用户点击一个在TextBlock中的Run元素你需要从Run元素开始上溯可视化树。由于Run类继承自ContentElement, 所以它不真在可视化树中。所以我们需要走出逻辑树直到我们找到了那个包好Run元素的TextBlock。之后我们就可以回到可视化树上来了,因为TextBlock不是ContentElement的子类。

 

        /// <summary>  
        /// 获得指定元素的所有子元素  
        /// </summary>  
        /// <typeparam name="T"></typeparam>  
        /// <param name="obj"></param>  
        /// <returns></returns>  
        public List<T> GetChildObjects<T>(DependencyObject obj) where T : FrameworkElement
        {
            DependencyObject child = null;
            List<T> childList = new List<T>();

            for (int i = 0; i <= VisualTreeHelper.GetChildrenCount(obj) - 1; i++)
            {
                child = VisualTreeHelper.GetChild(obj, i);

                if (child is T)
                {
                    childList.Add((T)child);
                }
                childList.AddRange(GetChildObjects<T>(child));
            }
            return childList;
        }

该方法获得指定元素的所有子元素 

 

 

逻辑树

逻辑树表示UI的核心结构。和XAML文件中定义的元素近乎相等,排除掉内部生成的那些用来帮助渲染的可视化元素。WPF用逻辑树来决定依赖属性,值继承,资源解决方案等。
逻辑树用起来不像可视化树那么简单。对于新手来说,逻辑树可以包含类型对象,这一点和可视化树不同,可视化树只包含Dependancy子类的实例。遍历逻辑树时,要记住逻辑树的叶子可以是任何类型。由于LogicTreeHelper只对DependencyObject有效,遍历逻辑树时需要非常小心,最好做类型检查。看个例子:

void WalkDownLogicalTree(object current)
{
	DoSomethingWithObjectInLogicalTree(current);


	DependencyObject depObj = current as DependencyObject;


	if(depObj != null)
	{
		foreach(object logicalChild in LogicalTreeHelper.GetChildren(depObj))
			WalkDownLogicalTree(logicalChild);
	}
}

个给定的Window/Page/Control会有一棵视觉树,但是可以有几个逻辑树。这些逻辑树互相不相连。可以仅仅使用LogicalTreeHelper来在几棵逻辑树之间遍历。在这篇文章中,我会把顶层控件的逻辑树称作主逻辑树,在他里面的其他逻辑树称作逻辑岛。逻辑岛实际上就是普通的逻辑树但是“岛”可以帮助说明它们和主逻辑树并不相连。
这种无关性可以归结于一个概念:模板。
控件和数据对象本身并没有可见的外观。相反,它们依赖模板来决定怎样进行渲染。一个模板就像一个“拼图块”可以扩展开来以便展示正真的用来渲染的可视元素。这些元素是可扩展模板的一部分,称之为“模板元素”。这些元素有自己的逻辑树,和生成这些元素的对象所拥有的逻辑树不相连。这些小的逻辑树就是我说的逻辑岛。
你只能写额外的代码来在不同的逻辑树或者逻辑岛之间进行切换。遍历逻辑树时,为了连接这些岛,需要使用类似FrameworkElement.TemplateParent,FrameworkContentElement.TemplateParent这些属性来返回持有这些模板的元素,这样就把逻辑岛包含进来了。以下是找到任意元素
的TemplateParent的一个方法:

DependencyObject GetTemplatedParen(DependencyObject depObj)
{
	FrameworkElement fe = depObj as FrameworkElement;
	FrameworkElementElement fce = depObj as FrameworkContentElement;


	DependencyObject result;


	if(fe != null)
		result = fe.TemplatedParent;


	else if(fce != null)
		result = fce.TemplateParent;
	
	else
		return null;


	return result;
}

demo

效果图:

 

 

<Window x:Class="WpfApplication1.Window3"  
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Title="Window3" Height="553.847" Width="774.231">  
    <DockPanel LastChildFill="True">  
        <Border Height="50" DockPanel.Dock="Top" BorderBrush="Blue">  
            <StackPanel Orientation="Horizontal">  
                <Button x:Name="btnShowLogicalTree" Content="编程方式查看 逻辑树"  
                        Margin="4" BorderBrush="Blue" Height="40" Click="btnShowLogicalTree_Click"/>  
                <Button x:Name="btnShowVisualTree" Content="编程方式查看 可视化树"  
                        BorderBrush="Blue" Height="40" Click="btnShowVisualTree_Click"/>  
            </StackPanel>  
        </Border>  
  
        <TextBox x:Name="txtDisplayArea" Margin="10" Background="AliceBlue" IsReadOnly="True" BorderBrush="Red"  
                 HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Width="400"  
                 />  
  
        <Border DockPanel.Dock="Right" Margin="10" BorderBrush="DarkGreen" BorderThickness="4" >  
            <StackPanel>  
                <Label Content="Enter Full Name of WPF Control(查看控件默认模板)" Width="340" FontWeight="demiBold"/>  
                <TextBox x:Name="txtFullName" Width="340" BorderBrush="Green" Background="BlanchedAlmond" Height="22"  
                         Text="System.Windows.Controls.Button"/>  
                <Button x:Name="btnTemplate" Content="See Template" BorderBrush="Green" Height="40" Width="100" Margin="5"  
                        Click="btnTemplate_Click" HorizontalAlignment="Left"/>  
                <Border BorderBrush="DarkGreen" BorderThickness="2" Height="260" Width="301" Margin="10" Background="LightGreen">  
                    <StackPanel x:Name="stackTemplatePanel"/>  
  
                </Border>  
  
            </StackPanel>  
        </Border>  
  
    </DockPanel>  
</Window>  
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.Shapes;  
  
using System.Reflection;  
using System.Xml;  
using System.Windows.Markup;  
namespace WpfApplication1  
{  
    /// <summary>  
    /// Window3.xaml 的交互逻辑  
    /// </summary>  
    public partial class Window3 : Window  
    {  
        private string dataToShow = string.Empty;  
        private Control ctrlToExamine = null;  
        public Window3()  
        {  
            InitializeComponent();  
        }  
  
        private void btnShowLogicalTree_Click(object sender, RoutedEventArgs e)  
        {  
            dataToShow = "";  
            BuildLogicalTree(0, this);  
            this.txtDisplayArea.Text = dataToShow;  
  
        }  
        //编程方式查看 逻辑树  
        void BuildLogicalTree(int depth,object obj)  
        {  
            dataToShow += new string(' ', depth) + obj.GetType().Name + "\n";  
            if (!(obj is DependencyObject))  
                return;  
            //LogicalTreeHelper.GetChildren 获取逻辑树子对象   
            //obj as DependencyObject  将obj转换成 依赖对象  
            foreach(object child in LogicalTreeHelper.GetChildren(obj as DependencyObject))  
                BuildLogicalTree(depth+5,child);  
        }  
  
          
        private void btnShowVisualTree_Click(object sender, RoutedEventArgs e)  
        {  
            dataToShow = "";  
            BuildVisualTree(0, this);  
            this.txtDisplayArea.Text = dataToShow;  
        }  
  
        //编程方式查看 可视化树  
        void BuildVisualTree(int depth, DependencyObject obj)  
        {  
            dataToShow += new string(' ', depth) + obj.GetType().Name + "\n";  
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj);i++ )  
            {  
                BuildVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i));  
            }  
        }  
  
        private void btnTemplate_Click(object sender, RoutedEventArgs e)  
        {  
            dataToShow = "";  
            ShowTemplate();  
            this.txtDisplayArea.Text = dataToShow;  
        }  
  
        //控件模板  
        private void ShowTemplate()  
        {  
            if (ctrlToExamine != null)  
                stackTemplatePanel.Children.Remove(ctrlToExamine);  
            try {  
                Assembly asm = Assembly.Load("PresentationFramework,version=4.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35");  
                ctrlToExamine = (Control)asm.CreateInstance(txtFullName.Text);  
                ctrlToExamine.Height = 200;  
                ctrlToExamine.Width = 200;  
                ctrlToExamine.Margin = new Thickness(5);  
                stackTemplatePanel.Children.Add(ctrlToExamine);  
  
                //定义xml一些设置,以保持缩进  
                XmlWriterSettings xmlSetting = new XmlWriterSettings();  
                xmlSetting.Indent = true;  
  
                //创建 StringBuilder 来保存xml  
                StringBuilder strBuilder = new StringBuilder();  
  
                //创建基于设置的 XmlWriter  
                XmlWriter xWriter = XmlWriter.Create(strBuilder,xmlSetting);  
  
                //将基于ControlTemplate的XAML保存到XmlWriter对象  
                XamlWriter.Save(ctrlToExamine.Template,xWriter);  
  
                //文本框显示XAML  
                dataToShow = strBuilder.ToString();  
            }  
            catch(Exception ex) {  
                dataToShow = ex.Message;  
            }  
        }  
    }  
}  

 


逻辑树现在变的非常不一样。它比之前的那棵树要小很多,根是ButtonChrome而不是Button, 叶子是一个ContentPresenter而不是一个string.之所以不同,是因为我们在看的是一个逻辑岛。这个逻辑岛是Button内容的模板元素所用的逻辑树。

值得注意的是,可视化树包括了所有的可视化元素,包括刚刚测试过的按钮,它不关心这些元素是否来自于模板。所有显示的元素都可以在可视化树中找到。 这样就更好理解了。

 

结论

第一次了解元素树的时候,可能会觉得很好理解。深入研究会发现其实它们也没那么简单。对于大多数WPF编程,知道这些细节都没有什么害处,而对于一些高级场景,这些知识就变得很必须。希望这篇文章对于理解这些晦涩的细节能有所裨益。

 


 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值