WPF的逻辑树与视觉树


基本概念

摘要

逻辑树与视觉树属于 WPF 的基本概念,学过 WPF 或 Silverlight 的朋友一定会对其有所耳闻。这篇文章将探讨逻辑树与视觉树的特质以及两者的区别。

本节提纲

  1. WPF Inspector 工具介绍
  2. 观察逻辑树和视觉树
  3. 与 ASP.NET 服务器控件比较(控件为逻辑树,HTML 为视觉树)
  4. 与 JavaScript 客户端控件比较(一个根逻辑树,HTML 为视觉树)
  5. 组装控件
  6. 小结

WPF Inspector 工具介绍

观察逻辑树与视觉树

  • WPF 启动程序的根元素均为 Application
  • 逻辑树与 XAML 的布局结构是相同的
  • 视觉树是根据控件的模板来程序的,我们很难猜测视觉树的结构,因为控件还可以自定义模板

与 ASP.NET 服务器控件比较(控件为逻辑树,HTML 为视觉树)

与 JavaScript 客户端控件比较(一个根逻辑树,HTML 为视觉树)

组装控件

为 WPF 创建一个新的控件是非常简单的,一般有以下两种方式

  1. 采用用户控件

    <UserControl x:Class="WpfApplication1.LoginView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
        <Grid>
            <Label Content="用?户§名?:" Height="28" HorizontalAlignment="Left" Margin="56,89,0,0" Name="label1" VerticalAlignment="Top" />
            <Label Content="密ü码?:" Height="28" HorizontalAlignment="Left" Margin="56,134,0,0" Name="label2" VerticalAlignment="Top" />
            <TextBox Height="23" HorizontalAlignment="Left" Margin="111,94,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" />
            <PasswordBox Height="23" HorizontalAlignment="Left" VerticalAlignment="Top" Width="120"  Margin="111,134,69,146"></PasswordBox>
            <Button Content="确·定¨" Height="23" HorizontalAlignment="Left" Margin="156,179,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
        </Grid>
    </UserControl>
    
  2. 采用模板

    <ContentControl>
        <ContentControl.ContentTemplate>
            <DataTemplate>
                <Grid>
                    <Label Content="UserName:" Height="28" HorizontalAlignment="Left" Margin="56,89,0,0" Name="label1" VerticalAlignment="Top" />
                    <Label Content="Password:" Height="28" HorizontalAlignment="Left" Margin="56,134,0,0" Name="label2" VerticalAlignment="Top" />
                    <TextBox Height="23" HorizontalAlignment="Left" Margin="111,94,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" />
                    <PasswordBox Height="23" HorizontalAlignment="Left" VerticalAlignment="Top" Width="120"  Margin="111,134,69,146"></PasswordBox>
                    <Button Content="Summit" Height="23" HorizontalAlignment="Left" Margin="156,179,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
                </Grid>
            </DataTemplate>
        </ContentControl.ContentTemplate>
    </ContentControl>
    

    采用模板的时候,逻辑树将变得更少,视觉树将保持不变

    不要将模板内容控件纳入逻辑树范围内,否则你会很失望的无法找到模板内部的元素

总结

简单的介绍了 WPF 视觉树与逻辑树的概念。

Visual 容器

摘要

虽然我们平时几乎不会从该类派生,但要想了解视觉树就必须要了解 Visual,Visual 是一个基本抽象类,继承自 DependencyObject,其是所有控件的基类,并提供了视觉树操作的基本方法。

提纲

  1. 视觉树是一棵树
  2. 遍历视觉树
  3. 内置 Visual 集合容器 ContainerVisual
  4. 小结

视觉树是一棵树

Visual 提供的一些基本的成员

  • Properties
    • VisualParent
  • Methods
    • AddVisualChild
    • OnVisualChildrenChanged
    • OnVisualParentChanged
    • RemoveVisualChild

>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;

namespace WpfApplication3
{
    public class CustomerVisual : Visual
    {
        public string Key { get; set; }

        protected override void OnVisualChildrenChanged(System.Windows.DependencyObject visualAdded, System.Windows.DependencyObject visualRemoved)
        {
            Console.WriteLine(this.Key + " ChildrenChanged");
            if(visualAdded!=null)
                Console.WriteLine((visualAdded as CustomerVisual).Key+ " Added");
            if (visualRemoved != null)
                Console.WriteLine((visualRemoved as CustomerVisual).Key + " Removed");
            base.OnVisualChildrenChanged(visualAdded, visualRemoved);
        }

        protected override void OnVisualParentChanged(System.Windows.DependencyObject oldParent)
        {
            Console.WriteLine(this.Key + " ParentChanged");
            if (oldParent != null)
                Console.WriteLine((oldParent as CustomerVisual).Key);
            base.OnVisualParentChanged(oldParent);
        }

        public static void Test()
        {
            CustomerVisual test1 = new CustomerVisual();
            test1.Key = "test1";
            CustomerVisual test2 = new CustomerVisual();
            test2.Key = "test2";
            test1.AddVisualChild(test2);
            CustomerVisual test3 = new CustomerVisual();
            test3.Key = "test3";
            test2.AddVisualChild(test3);
            CustomerVisual test4 = new CustomerVisual();
            test4.Key = "test4";
            test1.AddVisualChild(test4);
            test1.RemoveVisualChild(test4);
        }

    }
}

遍历视觉树

在调用 AddVisualChild 的时候,将会为两个 Visual 之间建立父子关系,子级知道父级,但是父级却不知道有几个子级,所以很难遍历全部节点,需要把子节点保存下来。Visual 提供了两个成员用于视觉树的遍历,只要实现这两个成员就可以使用 VisualTreeHelper 进行遍历了。

Visual 容器

Visual 本身具备一些功能,同时也可以充当容器。

在实际情况下,容器分为两种,单容器和集合容器,比如 Border 就是一个单容器,其内部只可以放一个元素,Panel 是一个集合容器,可以放置多个元素。

单容器实现

public class SigletonVisual : DefaultVisual
{
    public Visual _child;
    public Visual Child
    {
        get
        {
            return _child;
        }
        set
        {
            this.RemoveVisualChild(_child);
            this.AddVisualChild(value);
            _child = value;
        }
    }

    protected override Visual GetVisualChild(int index)
    {
        return _child;
    }

    protected override int VisualChildrenCount
    {
        get
        {
            if (this._child != null)
            {
                return 1;
            }
            return 0;
        }
    }
}

集合容器实现

public class PanelVisual : DefaultVisual
{
    public List<Visual> Visuals { get; set; }

    public PanelVisual()
    {
        Visuals = new List<Visual>(5);
    }

    public void Add(Visual visual)
    {
        Visuals.Add(visual);
        this.AddVisualChild(visual);
    }

    protected override Visual GetVisualChild(int index)
    {
        return Visuals[index];
    }

    protected override int VisualChildrenCount
    {
        get
        {
            return Visuals.Count;
        }
    }
}

内置 Visual 集合容器 ContainerVisual

其实我们不用那么复杂,WPF 内置类 ContainerVisual 已经默认实现了 Visual 集合容器,ContainerVisual 内部采用 VisualCollection 集合来维护视觉树,所以当我们添加 Visual 的时候,不需要调用 AddVisualChild 方法,而是应该调用 VisualCollection 的 Add 和 Remove 等方法。

如果不从 ContainerVisual 继承又想简单的维护 Visual 的话,可以使用 VisualCollection 来维护。

小结

Visual 本身具备服自己关系的功能,但默认没有容器,需要我们自己实现,内置的 ContainerVisual 使用 VisualCollection 实现了一个 Visual 容器功能,有了容器才能遍历整个视觉树,对 Visual 进行一些交互。

Visual 呈现

绘图方式有两种

  1. 继承 UIElement,重写 OnRender 方法。

    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;
    
    namespace WpfApplication3
    {
        /// <summary>
        /// MyWindow.xaml 的交互逻辑
        /// </summary>
        public partial class MyWindow : Window
        {
            public MyWindow()
            {
                InitializeComponent();
                this.Content = new RectangleElement();
            }
        }
    
        public class RectangleElement:UIElement
        {
            protected override void OnRender(DrawingContext drawingContext)
            {
                drawingContext.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 20));
                base.OnRender(drawingContext);
            }
        }
    
    }
    
  2. DrawingVisual 轻量级绘图,只提供显示和测试点击功能,DrawingVisual 继承自 ContainerVisual,所以它也是 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.Shapes;
    
    namespace WpfApplication3
    {
        /// <summary>
        /// MyWindow.xaml 的交互逻辑
        /// </summary>
        public partial class MyWindow : Window
        {
            public MyWindow()
            {
                InitializeComponent();
                this.Content = new RectangleElement();
            }
        }
    
        public class RectangleElement:UIElement
        {
            DrawingVisual dv;
    
            public RectangleElement()
            {
                dv = new DrawingVisual();
                var drawingContext = dv.RenderOpen();
                drawingContext.DrawRectangle(Brushes.Gray, null, new Rect(100, 20, 100, 20));
                drawingContext.Close();
                this.AddVisualChild(dv);
            }
    
            protected override Visual GetVisualChild(int index)
            {
                return dv;
            }
    
            protected override int VisualChildrenCount
            {
                get
                {
                    return 1;
                }
            }
    
            protected override void OnRender(DrawingContext drawingContext)
            {
                drawingContext.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 20));
                base.OnRender(drawingContext);
            }
        }
    
    }
    

DrawingVisual 无法单独存在,必须放在一个容器中(需要有布局系统)呈现,我们看到每次添加一个 Visual 的时候,总还是难免要实现 GetVisualChild 和 VisualChildrenCount 这两个成员。除了 ContainerVisual 这些轻量级的都系,Panel 会帮我买做掉上面那些工作,但基类却变成了 UIElement。事实上当添加 Visual 以后,同时还要计算布局的尺寸,所以有必要的话,可以对 UIElement 或者 FrameworkElement 重写以上两个成员。因为有时候我们只需要一次布局和添加多个 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.Shapes;

namespace WpfApplication3
{
    /// <summary>
    /// CustomerWindow.xaml 的交互逻辑
    /// </summary>
    public partial class CustomerWindow : Window
    {
        DrawingVisual dv;

        public CustomerWindow()
        {
            InitializeComponent();
            dv = new DrawingVisual();
        }

        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            var context = dv.RenderOpen();
            context.DrawRectangle(Brushes.Navy, null, new Rect(0, 0, this.ActualWidth, this.ActualHeight));
            context.Close();
            base.OnRenderSizeChanged(sizeInfo);
        }

        protected override Visual GetVisualChild(int index)
        {
            return dv;
        }

    }
}

知识共享许可协议
《WPF的逻辑树与视觉树》 常伟华 创作。
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议 | 3.0 中国大陆许可协议进行许可。

站内公告

A PHP Error was encountered

Severity: Core Warning

Message: PHP Startup: zip: Unable to initialize module Module compiled with module API=20060613 PHP compiled with module API=20090626 These options need to match

Filename: Unknown

Line Number: 0

Backtrace: