WPF ContentControl与ContentPresenter 逻辑树和可视化树
ContentControl与ContentPresenter
Answer one
ContentControl is a base class for controls that contain other elements and have a Content-property (for example, Button).
ContentControl是包含其他元素并具有Content-property(例如Button)的控件的基类。
ContentPresenter is used inside control templates to display content.
ContentPresenter用于控件模板中显示内容。
ContentControl, when used directly (it’s supposed to be used as a base class), has a control template that uses ContentPresenter to display it’s content.
当直接使用ContentControl时(它应该被用作一个基类),它有一个控件模板,使用ContentPresenter来显示它的内容。
My rules of thumb (not applicable in every case, use your judgment):
我的经验法则(并非适用于所有情况,你自己判断):
Inside ControlTemplate use ContentPresenter 在ControlTemplate内部使用ContentPresenter
Outside of ControlTemplate (including DataTemplate and outside templates) try not to use any of them, if you need to, you must prefer ContentPresenter
在ControlTemplate之外(包括DataTemplate和外部模板)尽量不要使用它们中的任何一个,如果你需要,你必须更喜欢ContentPresenter
Subclass ContentControl if you are creating a custom “lookless” control that host content and you can’t get the same result by changing an existing control’s template (that should be extremely rare).
如果你正在创建一个自定义的“无lookless”控件来托管内容,并且你不能通过更改现有控件的模板来获得相同的结果(这应该是非常罕见的)。
Answer Two
ContentPresenter is usually used in a ControlTemplate, as a placeholder to say “put the actual content here”.
ContentPresenter通常用在ControlTemplate中,作为一个占位符来表示“把实际的内容放在这里”。
A ContentControl can be used anywhere, not necessarily in a template. It will pick up any DataTemplate defined for the type of content assigned to it
ContentControl可以在任何地方使用,不一定要在模板中使用。它将提取为分配给它的内容类型定义的任何DataTemplate
总结
ContentControl类的目的是为了将Content的内容显示在wpf窗体。那没有了ConentControl类,我们就不能将内容显示在wpf窗体上了吗?或者问ContentControl类的Content是如何显示在wpf窗体上的呢?
这里我们就需要知道ContentPresenter类的作用是将ContentControl类的Content显示在wpf窗体,ContentPresenter的作用就是显示内容(Content)。
ContentPresenter 是一个内容呈现模板,
特点1 :做模板时,如果对于内容(Content)属性可以不用绑定。
将ContentPresenter放在模板中即可,也不需要做任何的额外的绑定(难道不需要做吗?只不过ContentPresenter内部帮我们做了默认的绑定如下:
一般元素做模板,需要绑定“Content属性”ContentPresenter模板,不需要
ItemsPresenter
有时候控件并非维护本身逻辑,而是依赖于父子元素的,如了上诉的ContentPresenter,我们还有一个非常常用的ListBox控件,因为继承自ItemsControl,所以有一个ItemsPanel属性作为集合元素承载容器,但集合控件本身却不负责呈现控件,那么这个任务就留给了子元素ItemsPresenter,其实用也很简单,只要把ItemsPresenter放在内部模板中,那么ItemsPresenter则会去检测父元素是否为集合控件,然后将ItemsPanel添加到其内部视觉树当中
下图显示继承关系:
ContentControl:Control (在Control类并没有Content属性, 所以在这之上再写了一个ContentControl, 使控件有Content属性可以显示内容)
ContentPresenter:FrameworkElement (ContentPresenter一般用在CT里负责把Control指定的Content显示出来)
Control:FrameworkElement
ItemsControl:Control
ItemsPresenter:FrameworkElement


接著来我们看一下实例:
使用ContentPresenter
输出结果: YangMark
正确显示Content!!不使用ContentPresenter 输出结果:
无法显示出Content!!
结论1:ContentPresenter通常出现在ControlTemplate内,且若不使用ContentPresenter则Content属性就无法正常显示。实例2:ContentPresenter中的ContentSource属性
为什么只为了显示出Content属性要大费周张弄出ContentPresenter呢??
我们可以先比较以下两种代码不同之类, 输出结果:Hello!! YangMark 输出结果:YangMark
仅出现Content属性的内容!!
结论2: 与 意义上是相同的。
写ContentSource它们同时绑定了Content, ContentStringFormat, ContentTemplate和ContentTemplateSelector等内容
若仅用Content="{TemplateBinding Content}"代表只绑定Content属性而已,还要手动绑定其他ContentStringFormat, ContentTemplate和ContentTemplateSelector等。实例3:ContentSource的应用以HeaderContentControl为例,使用ContentPresenter绑定内容属性。 输出结果:
I'm Header
I’m Content结论3:ContentSource若指定对象为Content是可以省略的,若不为Content(如:Header)则不能省略。
总结:
Content, ContentStringFormat, ContentTemplate和ContentTemplateSelector等属性, 我将它们称为内容属性.
1. ContentPresenter的作用就是用来显示内容属性
2.ContentSource若指定对象为Content,则等同于
ContentControl
我们先来看MSDN对其的介绍
Displays the content of a ContentControl 似乎其是为ContentControl定身量做的.
为了理解这一点,首先我们要对WPF内容模型有所了解,上面这篇文章有提到过ContentControl继承自Control,多了Content属性,继承自ContentControl的均可以称之为内容模型的控件.如下
这里似乎看不到ContentPresenter的影子.下面来举一些例子

一个ContentPresenter的例子

ContentPresenter可以直接在xaml中输出,其视觉树中包含一个TextBlock,在默认的WPF样式定义中找不到任何关于ContentPresenter的样式,说明了ContentPresenter并非是真正代码逻辑与样式分离的,而是在内部代码中提供了一个默认的模板即呈现了TextBlock,如果在内部创建模板的话,一般均会采用FrameworkElementFactory创建根元素,但这种方式太过于复杂,适用于一些简单默认操作,就如ContentPresenter内部的TextBlock.
那么问题出来了,为什么不直接用TextBlock呢,还要包装一个ContentPresenter?
ContentPresenter与TextBlock:
如要回答上面的问题,那么就犹如来讨论两者的区别,我们来看下
TextBlock

只不过默认是呈现文字而已,但两者概念完全不同,Content属性是object类型,而非string,可以自己以Content为数据源而重新定义模板(ContentTemplate),如下示例

这样的话ContentPresenter将不再局限于文字的呈现.
ContentControl与ContentPresenter:
先看一个被重新定义的的Button样式

上面可以看到,使用ContentPresenter非常的方便,只要将ContentPresenter放在模板中即可,也不需要做任何的额外的绑定(难道不需要做吗?只不过ContentPresenter内部帮我们做了默认的绑定),但如果使用TextBlock呢?如下还是需要做绑定的
如上看来我们还不如说ContentControl是ContentPresenter的一个特例.ContentPresenter则是ContentControl的基础.
为了适配ContentPresenter,ContentControl提供了内容模型的相关属性,本质上ContentPresenter并非仅仅只是用到ContentControl而已,ContentPresenter可以通过指定ContentSource来绑定指定的源属性

记住内容模型不仅仅只是呈现文字而已,如果只是为了呈现文字的话,是不需要ContentPresenter的.
父子元素之间的关系(ItemsPresenter):
有时候控件并非维护本身逻辑,而是依赖于父子元素的,如了上诉的ContentPresenter,我们还有一个非常常用的ListBox控件,因为继承自ItemsControl,所以有一个ItemsPanel属性作为集合元素承载容器,但集合控件本身却不负责呈现控件,那么这个任务就留给了子元素ItemsPresenter,其实用也很简单,只要把ItemsPresenter放在内部模板中,那么ItemsPresenter则会去检测父元素是否为集合控件,然后将ItemsPanel添加到其内部视觉树当中
如下视觉树,StackPanel作为ItemsControl的默认容器

逻辑树和可视化树
WPF中有两中“树”:一种叫逻辑树(Logical Tree);一种叫可视化元素树(Visual Tree)。
Logical Tree 最显著的特点就是它完全由布局组件和控件构成(包括列表类控件中的条目元素),换句话说就是它的每个节点不是布局组件就是控件。那什么是 Visual Tree 呢?我们知道,如果把一片树叶放在放大镜下观察,你会发现这片叶子也像一棵树一样——有自己的基部并向上生长出多级分叉。
在WPF的 Logical Tree 上,充当叶子的一般都是控件,在放大镜下观察,会发现每个WPF控件本身也是一棵由更细微级别的组件组成的树。如果把 Logical Tree 延申至 Template 组件级别,我们得到的就是 Visual Tree。
在 WPF 中,存在两种重要的树状结构:逻辑树(Logical Tree)和可视化树(Visual Tree)。
-
逻辑树(Logical Tree):
- 逻辑树是由 WPF 元素(Element)构成的树状结构。
- 逻辑树用于表示 UI 元素之间的逻辑关系和组织结构。
- 在逻辑树中,每个元素都可以有一个父元素和多个子元素。
- 逻辑树中的元素通常继承自
System.Windows.DependencyObject,并具有逻辑上的层次结构。
-
可视化树(Visual Tree):
- 可视化树是由视觉元素(Visual)构成的树状结构。
- 可视化树用于表示 UI 元素的可视层次结构。
- 在可视化树中,每个视觉元素都可以有一个父元素和多个子元素。
- 可视化树中的元素通常继承自
System.Windows.Media.Visual,并用于绘制和渲染 UI。
逻辑树和可视化树之间有一定的对应关系:
- 每个逻辑元素(Logical Element)对应一个或多个可视元素(Visual Element)。
- 逻辑元素通过
System.Windows.FrameworkElement类的VisualTreeHelper方法访问其对应的可视元素。
逻辑树和可视化树的区别在于它们的用途和表示方式。逻辑树主要用于组织和管理 UI 元素的逻辑关系,而可视化树则用于描述 UI 元素的可视化层次结构。
通过理解和操作逻辑树和可视化树,开发者可以更好地管理和操作 WPF 应用程序中的 UI 元素,并实现各种功能和效果。
逻辑树(Logical Tree)
逻辑树是由每个控件的节点组成,本质上就是XAML文件中的UI元素,我们可以通过LogicalTreeHelper类提供的静态方法获取逻辑树的对象,我们通过一个例子演示获取整个页面逻辑树。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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 WpfApp1
{/// /// RepresentLogicalTree.xaml 的交互逻辑/// public partial class RepresentLogicalTree : Window{public RepresentLogicalTree(){InitializeComponent();}private void btnClick_Click(object sender, RoutedEventArgs e){logicalTree.Items.Add(GetLogicTree(this));}public static TreeViewItem GetLogicTree(DependencyObject obj){if (obj == null){return null;}TreeViewItem treeNode = new TreeViewItem { Header = obj.GetType().FullName, IsExpanded = true };foreach (var child in LogicalTreeHelper.GetChildren(obj)){var item = GetLogicTree(child as DependencyObject);if (item != null){treeNode.Items.Add(item);}}return treeNode;}}
}

我们可以很清晰的看到整个逻辑树节点对应到XAML页面中的元素
可视化树(Visual Tree)
可视化树是逻辑树的一种扩展,逻辑树的每个结点都被分解为核心视觉组件,逻辑树节点对我们来说是个黑箱,而视觉树暴露了视觉的实现细节,我们可以通过VisualTreeHelper类提供的静态方法获取可视化树节点,可视化树要比逻辑树呈现的粒度更细,XAML结构和上面结构相同,我们来呈现可视化树结构:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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 WpfApp1
{/// /// RepresentVisualTree.xaml 的交互逻辑/// public partial class RepresentVisualTree : Window{public RepresentVisualTree(){InitializeComponent();}private void btnVisual_Click(object sender, RoutedEventArgs e){VisualTree.Items.Add(GetVisualTree(this));}public static TreeViewItem GetVisualTree(DependencyObject obj){if (obj == null){return null;}TreeViewItem treeNode = new TreeViewItem { Header = obj.GetType().FullName, IsExpanded = true };for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++){var child = VisualTreeHelper.GetChild(obj, i);var item = GetVisualTree(child);if (item != null){treeNode.Items.Add(item);}}return treeNode;}}
}
从上面的例子我们可以看到更细粒度的控件组成部件,下图展示了整个视觉图的组成,绿色表示可视化树,红色表示逻辑树,可视化树范围包含逻辑树
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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;namespace WpfApp1
{/// /// MainWindow.xaml 的交互逻辑/// public partial class MainWindow : Window{public MainWindow(){InitializeComponent();string tree = getTree(this);string tree2 = getTree(this.grid);}private void Window_Loaded(object sender, RoutedEventArgs e){string Loadedtree = getTree(this);}string getTree(FrameworkElement container){StringBuilder sb = new StringBuilder();sb.AppendLine("********Logical Tree********");getLogicalChildren(container, sb, 0);sb.AppendLine();sb.AppendLine("********Visual Tree********");getVisualChildren(container, sb, 0);sb.AppendLine();return sb.ToString();}void appendLine(FrameworkElement frameworkElement, StringBuilder sb, int num){sb.Append("".PadLeft(num));string name = frameworkElement.Name;if (string.IsNullOrWhiteSpace(name)){name = $"({frameworkElement.GetType().Name})";}sb.AppendLine($"{num}.{name}");}void getLogicalChildren(FrameworkElement container, StringBuilder sb, int num){appendLine(container, sb, num);foreach (var child in LogicalTreeHelper.GetChildren(container)){FrameworkElement frameworkElement = child as FrameworkElement;if (frameworkElement != null){getLogicalChildren(frameworkElement, sb, num + 1);}}}void getVisualChildren(FrameworkElement container, StringBuilder sb, int num){appendLine(container, sb, num);int count = VisualTreeHelper.GetChildrenCount(container);for (int i = 0; i < count; i++){FrameworkElement frameworkElement = VisualTreeHelper.GetChild(container, i) as FrameworkElement;if (frameworkElement != null){getVisualChildren(frameworkElement, sb, num + 1);}}}}
}
1. 构造函数里遍历两个树(Window)
public MainWindow()
{InitializeComponent();string tree = getTree(this);
}tree结果为:********Logical Tree********
0.(MainWindow)1.grid2.textBox2.stackPanel3.button4.checkBox2.dockPanel3.toggleButton4.textBlock2.border3.repeatButton********Visual Tree********
0.(MainWindow)2. 构造函数里遍历两个树(Grid)public MainWindow(){InitializeComponent();string tree = getTree(this.grid);}tree结果为:********Logical Tree********
0.grid1.textBox1.stackPanel2.button3.checkBox1.dockPanel2.toggleButton3.textBlock1.border2.repeatButton********Visual Tree********
0.grid1.textBox1.stackPanel2.button1.dockPanel2.toggleButton1.border2.repeatButton3. Loaded完成后遍历两个树 private void Window_Loaded(object sender, RoutedEventArgs e){string tree = getTree(this);}tree结果为:********Logical Tree********
0.(MainWindow)1.grid2.textBox2.stackPanel3.button4.checkBox2.dockPanel3.toggleButton4.textBlock2.border3.repeatButton********Visual Tree********
0.(MainWindow)1.(Border)2.(AdornerDecorator)3.(ContentPresenter)4.grid5.textBox6.rectangle5.stackPanel6.button7.border8.contentPresenter9.checkBox10.templateRoot11.checkBoxBorder12.markGrid13.optionMark13.indeterminateMark11.contentPresenter5.dockPanel6.toggleButton7.border8.contentPresenter9.textBlock5.border6.repeatButton7.border8.contentPresenter3.(AdornerLayer)
在前面章节中,花费大量时间分析了窗口的内容模型——换句话说,研究了如何在其他元素中嵌套元素,进而构建完整的窗口。
例如,考虑下图中显示的一个非常简单的窗口,该窗口包含两个按钮。为创建该按钮,在窗口中嵌套了一个StackPanel控件。在StackPanel控件中,放置了两个Button控件,并且在每个按钮中可以添加所选择的内容。

下面是该窗口的标记:
添加的元素分类称为逻辑树,下图中显示了逻辑树。WPF编程人员需要耗费大部分时间构建逻辑树,然后使用事件处理代码支持他们。实际上,到目前为止介绍的所有WPF特性(如属性值继承、事件路由以及样式)都是通过逻辑树进行工作的。

然而,如果希望自定义元素,逻辑树起不到多大帮助作用。显然,可使用另一个元素替换整个元素(例如,可使用自定义的FacyButton类替换当前的Button类),但这需要更多工作,并且可能扰乱应用程序的用户界面或代码。因此,WPF通过可视化树进入更深层次。
可视化树是逻辑树的扩展版本。它将元素分为更小的部分,换句话说,它并不查看被精心封装到一起的黑色方框,如按钮,而是查看按钮的可视化元素——使按钮具有阴影背景特性的边框(由ButtonChrome类表示)、内部的容器(ContentPresenter对象)以及存储按钮文本的块(由大家熟悉的TextBlock表示)。下图显示上面示例的可视化树。

所有这些细节本身都是元素——换句话说,控件(如按钮)中的每个单独的细节都是由FrameworkElement类的派生类表示的。
到目前为止介绍的内容似乎并没有什么值得注意的,只是介绍了所有WPF元素可被分解成更小的部分。但这对于WPF开发人员有什么用处呢?通过可视化树可以完成以下两项非常有用的工作:
- 可视化样式改变可视化树中的元素。可使用Style.TargetType熟悉选择希望修改的特定元素。甚至当控件属性发生变化时,可使用触发器自动完成更改。不过,某些特定的细节很难甚至无法修改。
- 可为控件创建新模板。对于这种情况,控件模板将被用于按期望的方式构建可视化树。
非常有趣的是,WPF提供了用于浏览逻辑树和可视化树的两个类:System.Windows.LogicalTreeHelper和System.Windows.Media.VisualTreeHelper。
LogicalTreeHelper类允许通过动态加载XAML文档在WPF应用程序中关联事件处理程序。LogicalTreeHelper类提供了较少的方法,下表列出了这些方法。尽管这些方法偶尔很有用,但大多数情况下回改用特定的FrameworkElement类中的方法。
表 LogicalTreeHelper类的方法
| 名 称 | 说 明 |
| FindLogicalNode() | 根据名称查找特定元素,从指定的元素开始并向下查找逻辑树 |
| BringIntoView() | 如果元素在可滚动的容器中,并且当前不可见,就将元素滚动到试图中。FrameworkElement.BringIntoView()方法执行相同的工作 |
| GetPrarent() | 获取指定元素的父元素 |
| GetChildren() | 获取指定元素的子元素。不同元素支持不同的内容模型。例如,面板支持多个子元素,而内容控件只支持一个子元素。然而,GetChildren()方法抽象了这一区别,并且可使用任何类型的元素进行工作 |
除了抓们用来执行低级绘图操作的一些方法外,VisualTreeHelper类提供的方法与LogicalTreeHelper类提供的方法类似,也提供了GetChildrenCount()、GetChild()以及GetParent()方法。
VisualTreeHelper类还提供了一种研究应用程序中可视化树的有趣方法。使用GetChild()方法,可以遍历任意窗口的可视化树,并且为了进行分析可以将它们显示出来。这是一种非常好的学习工具,只需要使用一些递归的代码就可以实现。
下图显示了一种可能的实现。该例在一个单独的窗口中显示了一颗完整的可视化树,该可视化树从提供的任意对象开始。

在该例中,名为MainWindow的窗口包含一个Border元素,这个Border元素包含一个AdornerDecorator元素(AdornerDecorator类在装饰层中添加对绘制内容的支持,装饰层是特殊的不可见区域,该区域覆盖在元素内容之上。WPF使用装饰层绘制一些细节,如焦点提示以及拖放指示器)。AdornerDecorator元素内是一个ContentPresenter元素,该元素承载了窗口内容。窗口内容包含的StackPanel面板具有两个Button控件,每个Button控件包含一个ButtonChrome元素(该元素绘制按钮的标准化可视外观)和一个ContentPresenter元素(该元素包含了按钮的内容)。最后,在每个按钮的ContentPresenter元素中是TextBlock元素,TextBlock元素封装了在窗口中可见的文本。
下面是VisualTreeDisplay窗口的完整代码:
public partial class VisualTreeDisplay : Window{public VisualTreeDisplay(){InitializeComponent();}public void ShowVisualTree(DependencyObject element){// Clear the tree.treeElements.Items.Clear();// Start processing elements, begin at the root.ProcessElement(element, null);}private void ProcessElement(DependencyObject element, TreeViewItem previousItem){// Create a TreeViewItem for the current element.TreeViewItem item = new TreeViewItem();item.Header = element.GetType().Name;item.IsExpanded = true;// Check whether this item should be added to the root of the tree//(if it's the first item), or nested under another item.if (previousItem == null){treeElements.Items.Add(item);}else{previousItem.Items.Add(item);}// Check if this element contains other elements.for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++){// Process each contained element recursively.ProcessElement(VisualTreeHelper.GetChild(element, i), item);}} }
一旦为项目添加这棵树,就可以使用其他任何窗口的代码显示其可视化树:
VisualTreeDisplay treeDisplay = new VisualTreeDisplay();
treeDisplay.ShowVisualTree(this);
treeDisplay.Show();
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
