WPF中逻辑树(Logical Tree)与可视化树(Visual Tree)到底是什么

文章目录

在WPF开发中,逻辑树(Logical Tree) 和 可视化树(Visual Tree)是两个非常核心的概念。它们分别从不同的角度描述了用户界面的结构和行为。

视觉树&逻辑树

大家在搞WPF开发的时候,经常会遇到视觉树和逻辑树这俩概念。简单来说,逻辑树就像是一个家族族谱,它记录了控件之间的逻辑结构和关系。比如在一个窗口里,有按钮、文本框这些控件,它们之间的父子关系就通过逻辑树来体现。而视觉树呢,更像是一个建筑的施工蓝图,它描述了控件在界面上是怎么呈现的,包括控件的实际外观和布局。

举个例子,咱们有一个窗口,里面放了一个StackPanel,然后在StackPanel里又放了几个按钮。从逻辑树的角度看,StackPanel是这些按钮的父控件,它们构成了一个逻辑上的层级关系。而从视觉树的角度看,它会考虑到这些按钮在界面上的具体位置、大小、样式等信息。

逻辑树

通俗点来讲,就是不包含控件模板的可视化树,是可视化树的一个子集,它省略了控件模板中的元素。

逻辑树的本质:描述 UI 结构的"业务逻辑"视角。它只关心哪些元素在逻辑上组成了界面,通常对应你在 XAML 中书写的 XML 结构。

LogicalTreeHelper
csharp 复制代码
public void PrintLogicalTree(object parent, int level)
{
	var parentObj = parent as DependencyObject;
	var typeName = parent.GetType().FullName;
	var name = "";
	if (parentObj != null)
	{
		name = (string)(parentObj.GetValue(FrameworkElement.NameProperty) ?? "");
	}
	else
	{
		name = parent.ToString();
	}
	AppendText(GetIndentString().Substring(0, level * 2));
	AppendText($"{typeName}:", true);
	AppendText($" {name}");
	AppendText(Environment.NewLine);
	if (parentObj == null)
	return;
	var children = LogicalTreeHelper.GetChildren(parentObj);
	foreach (object child in children)
	{
		PrintLogicalTree(child, level + 1);
	}
}
名 称 说 明
FindLogicalNode() 根据名称查找特定元素,从指定的元素开始并向下查找逻辑树
BringIntoView() 如果元素在可滚动的容器中,并且当前不可见,就将元素滚动到试图中。FrameworkElement.BringIntoView()方法执行相同的工作
GetPrarent() 获取指定元素的父元素
GetChildren() 获取指定元素的子元素。不同元素支持不同的内容模型。例如,面板支持多个子元素,而内容控件只支持一个子元素。然而,GetChildren()方法抽象了这一区别,并且可使用任何类型的元素进行工作

除了用来执行低级绘图操作的一些方法外,VisualTreeHelper类提供的方法与LogicalTreeHelper类提供的方法类似,也提供了GetChildrenCount()、GetChild()以及GetParent()方法。

VisualTreeHelper类还提供了一种研究应用程序中可视化树的有趣方法。使用GetChild()方法,可以遍历任意窗口的可视化树,并且为了进行分析可以将它们显示出来。这是一种非常好的学习工具,只需要使用一些递归的代码就可以实现。

用途

借助逻辑树,内容模型可以方便地循环访问其可能的子对象,从而实现扩展。

FrameworkElement提供了一个FindName的方法,可以通过名称查找子对象

csharp 复制代码
public object FindName(string name);
// 它内部的实现实际就是借助的逻辑树
internal object FindName(string name, out DependencyObject scopeOwner)
{
	INameScope scope = FindScope(this, out scopeOwner);
	if (scope != null)
	{
		return scope.FindName(name);
	}
	return null;
}
internal static INameScope FindScope(DependencyObject d, out DependencyObject scopeOwner)
{
	while (d != null)
	{
		INameScope scope = NameScope.NameScopeFromObject(d);
		if (scope != null)
		{
			scopeOwner = d;
			return scope;
		}
		DependencyObject parent = LogicalTreeHelper.GetParent(d);
		d = (parent != null) ? parent : Helper.FindMentor(d.InheritanceContext);
	}
	scopeOwner = null;
	return null;
}

可视化树

包含最初指定的大多数元素(在XAML或.cs中)以及控件模板中的元素。

通俗点来讲,就是整个元素的构成树,从最上面的结点到最后一个结点(包括控件模板)

可视化树的本质:描述 UI 结构的"渲染实现"视角。它包含了逻辑树中所有元素被拆解后的具体视觉组件(如 Border, Path, ContentPresenter 等)。

VisualTreeHelper
csharp 复制代码
public void PrintVisualTree(DependencyObject parent, int level)
{
	string typeName = parent.GetType().FullName ?? parent.GetType().Name;
	string name = (string)(parent.GetValue(FrameworkElement.NameProperty) ?? "");
	AppendText("           ".Substring(0, level * 2));
	AppendText($"{typeName}:",true);
	AppendText($" {name}");
	AppendText(Environment.NewLine);
	for (int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i)
	{
	DependencyObject child = VisualTreeHelper.GetChild(parent, i);
	PrintVisualTree(child, level + 1);
	}
}
用途

如果我们想查找这个控件,并为之添加事件处理程序或设置一些属性,都可以通过可视化树来实现。

csharp 复制代码
public Visual EnumVisual(Visual myVisual,string controlName)
{
	for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++)
	{
		Visual childVisual = (Visual)VisualTreeHelper.GetChild(myVisual, i);
		var nameObj = childVisual.GetValue(NameProperty);
		if (nameObj != null && nameObj.ToString() == controlName)
		return childVisual;
		EnumVisual(childVisual,controlName);
	}
	return null;
}
//查找控件模板中的名称为Border的控件
var border = EnumVisual(this.btn1, "Border");
if (border != null)
{
	var borderObj = border as Border;
	if (borderObj != null)
	{
		borderObj.CornerRadius = new CornerRadius(10);
	}
}

// 实际上我们还可以通过FrameworkTemplate.FindName方法进行查找,使用方法如下:
object findObj = this.btn1.Template.FindName("Border",this.btn1);

视觉树&逻辑树的区别

1. 结构与用途不同

逻辑树主要关注的是控件之间的逻辑关系,它是用来组织和管理控件的。比如在一个复杂的界面里,我们可以通过逻辑树来快速找到某个控件的父控件或者子控件。而视觉树则更侧重于控件的实际渲染和显示。它会考虑到控件的布局、样式、动画等因素,确保控件能够正确地显示在界面上。

2. 节点组成不同

逻辑树的节点通常是由控件本身构成的,比如Button、TextBox等。而视觉树的节点除了控件本身,还包括一些用于渲染的辅助元素,比如边框、背景等。这些辅助元素在逻辑树中是不存在的,但在视觉树中却起着重要的作用。

3. 遍历方式不同

遍历逻辑树相对比较简单,我们可以通过控件的Parent和Children属性来访问父控件和子控件。而遍历视觉树则需要使用专门的方法,比如VisualTreeHelper类中的方法。

csharp 复制代码
// 找到窗口中的StackPanel控件
StackPanel stackPanel = FindVisualChild<StackPanel>(this);

// 遍历StackPanel中的所有按钮
foreach (Button button in stackPanel.Children.OfType<Button>())
{
	// 处理按钮
	button.Click += Button_Click;
}

// 查找视觉子元素的方法
private T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
	for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
	{
		DependencyObject child = VisualTreeHelper.GetChild(parent, i);
		if (child is T typedChild)
		{
			return typedChild;
		}
		else
		{
			T foundChild = FindVisualChild<T>(child);
			if (foundChild != null)
			{
				return foundChild;
			}
		}
	}
	return null;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
	// 处理按钮点击事件
}

在这个示例中,我们首先通过FindVisualChild方法找到窗口中的StackPanel控件,然后遍历StackPanel中的所有按钮,并为每个按钮添加了点击事件处理程序。

特性 (Property) 逻辑树 (Logical Tree) 可视化树 (Visual Tree)
定义层级 (Definition Level) FrameworkElement / FrameworkContentElement Visual / Visual3D
用途 (Usage) 描述 UI 结构语义(布局、资源、绑定等) 描述最终渲染结构(绘图、命中测试、动画等)
包含内容 (Content) 不包含控件模板 (Control Template) 内的元素 包含控件模板及其内部视觉结构
遍历方法 (Traversal) 使用 LogicalTreeHelper 使用 VisualTreeHelper
是否可见 (Visibility) 抽象概念,不可见 实际参与界面渲染
关注点 层次结构和父子关系。 具体的绘图实现和渲染细节。
元素密度 节点较少,仅包含定义的控件。 节点极多,包含控件内部的所有零件。
主要用途 属性继承 (Property Inheritance)、资源查找 (Resource Lookup)。 点击测试 (Hit Testing)、事件路由 (Event Routing)、渲染变换。
核心类 LogicalTreeHelper VisualTreeHelper

通过视觉树优化界面渲染性能

1. 减少视觉树的深度

视觉树的深度越深,渲染的性能就越低。因为渲染引擎需要逐层遍历视觉树来确定每个控件的位置和样式。所以,我们要尽量减少视觉树的深度。比如,避免使用过多的嵌套控件。

举个例子,我们原本有这样的代码:

xml 复制代码
<Grid>
	<StackPanel>
		<Button Content="Button 1"/>
		<Button Content="Button 2"/>
	</StackPanel>
</Grid>

// 如果我们发现StackPanel在这里并不是必需的,就可以直接改成:
<Grid>
	<Button Content="Button 1"/>
	<Button Content="Button 2"/>
</Grid>

这样就减少了视觉树的一层深度,从而提高了渲染性能。

2. 合理使用虚拟化

在处理大量数据时,我们可以使用虚拟化技术来减少视觉树中的节点数量。比如,在ListBox或ListView中,我们可以设置VirtualizingStackPanel.IsVirtualizing属性为True,这样只有当前可见的项目才会被渲染,从而大大提高了性能。

以下是一个简单的示例:

xml 复制代码
<ListBox VirtualizingStackPanel.IsVirtualizing="True">
	<ListBoxItem Content="Item 1"/>
	<ListBoxItem Content="Item 2"/>
<!-- 更多项目 -->
</ListBox>

3. 避免不必要的重绘

当控件的属性发生变化时,可能会触发重绘操作。我们要尽量避免不必要的属性变化,或者使用缓存来减少重绘的次数。比如,当我们需要更新控件的文本内容时,可以先判断文本是否真的发生了变化,再进行更新。

csharp 复制代码
private string _text;
public string Text
{
	get { return _text; }
	set
	{
		if (_text != value)
		{
			_text = value;
			// 更新界面显示
			UpdateTextDisplay();
		}
	}
}

private void UpdateTextDisplay()
{
	// 更新文本显示的逻辑
}

应用场景

1. 逻辑树的应用场景

逻辑树主要用于控件的管理和事件处理。比如,当我们需要在一个复杂的界面中找到某个特定的控件时,就可以通过逻辑树来进行查找。另外,在处理控件的事件时,逻辑树也可以帮助我们确定事件的来源和传递路径。

2. 视觉树的应用场景

视觉树主要用于界面的渲染和布局。比如,当我们需要对控件进行动画效果处理时,就需要通过视觉树来获取控件的实际位置和大小。另外,在进行自定义控件开发时,视觉树也可以帮助我们实现控件的自定义渲染。

技术优缺点

1. 逻辑树的优缺点

优点:逻辑树结构清晰,易于理解和管理。通过逻辑树,我们可以方便地找到控件的父控件和子控件,进行控件的组织和管理。 缺点:逻辑树只关注控件的逻辑关系,不考虑控件的实际渲染和显示。在某些情况下,可能无法满足复杂的界面需求。

2. 视觉树的优缺点

优点:视觉树能够准确地描述控件的实际渲染和显示情况,对于实现复杂的界面效果非常有帮助。 缺点:视觉树的结构相对复杂,遍历和操作起来比较困难。而且,视觉树的深度过深会影响渲染性能。

注意事项

1. 遍历视觉树时的性能问题

在遍历视觉树时,要注意性能问题。因为视觉树的节点数量可能会很多,遍历过程可能会比较耗时。所以,在遍历视觉树时,要尽量减少不必要的操作,避免频繁地访问视觉树。

2. 虚拟化技术的使用限制

虽然虚拟化技术可以提高性能,但并不是所有的场景都适用。比如,在需要对所有项目进行操作的场景下,虚拟化技术可能就不适用了。所以,在使用虚拟化技术时,要根据具体的需求来选择是否使用。

相关推荐
周杰伦fans2 小时前
深入 C# 匿名类型:从 `new { Ask = ask }` 说起
开发语言·c#
软泡芙2 小时前
【.NET】创建一个ai聊天应用
人工智能·flask·.net
不会编程的懒洋洋3 小时前
C# IDisposable 和 using
开发语言·笔记·机器学习·c#·.net·visual studio·c#基础
家有娇妻张兔兔3 小时前
Apache POI 导出 Word 踩坑实录:Word 分栏为什么做不好左右平铺
c#·word·apache·poi·分栏
炸炸鱼.3 小时前
ELK 企业级日志分析系统完整部署手册
elk·wpf
唐青枫3 小时前
C#.NET MediatR 深入解析:进程内消息分发、CQRS、通知事件与管道行为实战
c#·.net
njsgcs13 小时前
拆分多实体到装配体 solidworks c#
c#
何以解忧唯有撸码14 小时前
C# 视频录制监控系统
c#·winform