WPF:一个奇特的 3D 控件






4.92/5 (52投票s)
一种 3D 树状控件。
演示代码源: LevelTree3D.zip
引言
本文主要介绍我为 WPF 编写的一个控件,该控件允许用户创建自己的数据项,这些数据项将在 3D Viewport
中分层布局。您可以将它想象成向一个控件提供一个树形列表,该控件会将这些项分层布局在 3D 空间中。本文介绍的控件还支持透明度,允许您自定义数据项的 DataTemplate
,并允许在 3D Viewport
的 Viewport2DVisual3D.Visual
上创建和托管对应的 DataTemplate
应用的 Visual
时,与 DataTemplate
化后的数据项进行完整的 2D 交互。这样您就可以两全其美,既可以使用 DataTemplating 等强大技术,又能将您的项以 3D 形式呈现,并且仍然可以与它们进行交互。
本文的其余部分将演示我称为“LevelTree3D
”的自定义控件。
目录
总之,本文将介绍以下内容:
演示视频
由于这是一个非常注重视觉的控件,我认为演示它的最佳方式是使用视频。下面的图片链接到一个托管了本文代码演示视频的页面。
看看
基本思想
基本思路很简单,我们将一个继承自 TreeItemBase
的 List<TreeItemBase>
对象提供给本文提供的 TreeLevelControl
控件。之后,TreeLevelControl
将递归遍历提供的继承自 TreeItemBase
的 List<TreeItemBase>
对象,并在 TreeLevelControl
的代码隐藏中创建一个新的 LevelTree3DContentControl
,该控件将被添加到内部的 ItemsControl
中。由于 LevelTree3DContentControl
的 Content 实际上是原始的 TreeItemBase
对象,因此您在 XAML 中提供的任何自定义 DataTemplate
都将作为 DataTemplate
应用。
我们还将为添加到 ItemsControl
中的每个新创建的 LevelTree3DContentControl
创建一个新的 3D Viewport2DVisual3D
模型。
内部的 ItemsControl
使用自定义的 Panel
(LevelControl3D.Panel3D
) 作为其 ItemsPanel
(为每个 LevelTree3DContentControl
创建一个 3D ViewPort3D
和一个新的 Viewport2DVisual3D
),其中 Viewport2DVisual3D
模型在 X/Y/Z 空间中被正确放置,使其看起来像是 3D 空间中的树。实际的 ViewPort3D
和 Viewport2DVisual3D
被托管在另一个称为“SceneControl
”的特殊控件中,该控件被托管在称为“Panel3DAdorner
”的自定义 Adorner
控件中,而该控件本身被托管在 LevelControl3D.Panel3D
的 AdornerLayer
中。
这一切听起来可能有点令人困惑,但我认为可以用下图很好地总结:
在您自己的应用程序中使用此控件
要在您自己的应用程序中使用 TreeLevelControl(本文的重点控件)非常简单。实际上只有几个步骤:
步骤 1:在 XAML 中这样使用它:
<Window x:Class="LevelTree3D.DemoApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:levelTree="clr-namespace:LevelTree3D;assembly=LevelTree3D"
Title="Level Tree 3D" Height="auto" Width="auto"
WindowStartupLocation="CenterScreen">
<levelTree:TreeLevelControl x:Name="treeLevelControl" />
</Window>
步骤 2:创建一个 LevelTree3D.TreeItemBase 类的子类
下一步是继承 LevelTree3D.TreeItemBase
类,并添加您的 3D 树节点所需的任何额外内容。以下是演示应用程序中的一个示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;
namespace LevelTree3D.DemoApp
{
public class TreeItem : TreeItemBase
{
private string title;
private string bodyText;
private bool titleIsTop = false;
private List<TreeItemBase> children;
private IMessageBoxService messageBoxService;
public TreeItem(string title, string bodyText,
bool titleIsTop, List<TreeItemBase> children)
{
this.Parent = null;
this.Title = title;
this.BodyText = bodyText;
this.titleIsTop = titleIsTop;
this.Children = children;
foreach (TreeItemBase child in children)
{
child.Parent = this;
}
messageBoxService =
ServiceResolver.GetService<MessageBoxService>(typeof(IMessageBoxService));
ShowSelectedTextCommand =
new SimpleCommand<object, object>(ExecuteShowSelectedTextCommand);
}
public ICommand ShowSelectedTextCommand { get; private set; }
public bool TitleIsTop
{
get { return titleIsTop; }
}
public string Title
{
get { return title; }
set
{
title = value;
Notify("Title");
}
}
public string BodyText
{
get { return bodyText; }
set
{
bodyText = value;
Notify("BodyText");
}
}
#if DEBUG
public override string DebugText
{
get { return string.Format("Title {0}",Title); }
}
#endif
public override List<TreeItemBase> Children
{
get { return children; }
set
{
children = value;
Notify("Children");
}
}
private void ExecuteShowSelectedTextCommand(object parameter)
{
messageBoxService.ShowMessage(DebugText);
}
}
}
步骤 3:为您的自定义树节点创建 DataTemplate
下一步是为您的自定义 LevelTree3D.TreeItemBase
子类创建自定义 DataTemplate
。以下是演示应用程序中的一个示例:
<DataTemplate DataType="{x:Type local:TreeItem}">
<Grid Background="Transparent" Width="300" Height="300"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseRightButtonDown" >
<i:InvokeCommandAction Command="{Binding ShowSelectedTextCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="396"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="row0" Height="40"/>
<RowDefinition x:Name="row1" Height="260"/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="0" Grid.RowSpan="2"
Fill="White" VerticalAlignment="Stretch"/>
<Border x:Name="titleText" Grid.Row="0" Grid.Column="1" Grid.RowSpan="1"
Background="White" Height="40" Width="292" HorizontalAlignment="Left">
<Label Content="{Binding Title}" Foreground="Black" FontSize="20"
Margin="0" Padding="0" VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Border>
<TextBlock x:Name="bodyText" Grid.Row="1"
Grid.Column="1" Grid.RowSpan="1"
TextWrapping="Wrap" Text="{Binding BodyText}"
Foreground="White" FontSize="12" Margin="0" Padding="10"
Height="260" LineStackingStrategy="BlockLineHeight"
VerticalAlignment="Center" HorizontalAlignment="Left" Width="292" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding TitleIsTop}" Value="false">
<Setter TargetName="row0" Property="Height" Value="260"/>
<Setter TargetName="row1" Property="Height" Value="40"/>
<Setter TargetName="titleText" Property="Grid.Row" Value="1"/>
<Setter TargetName="bodyText" Property="Grid.Row" Value="0"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
步骤 4:创建 LevelTree3D 控件的所有项
最后一步是实际提供一个连接的 TreeItemBase
对象列表作为 LevelTree3D.ItemSource
。例如,这可能如下所示(同样请参阅演示应用程序):
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
treeLevelControl.ItemsSource = this.CreateItems();
}
.....
.....
private List<TreeItemBase> CreateItems()
{
List<TreeItemBase> items = new List<TreeItemBase>();
TreeItem a111 = new TreeItem("a111",GetJabberwockedBodyText(),
rand.NextDouble() > 0.5, new List<TreeItemBase>());
TreeItem a112 = new TreeItem("a112", GetJabberwockedBodyText(),
rand.NextDouble() > 0.5, new List<TreeItemBase>());
TreeItem b111 = new TreeItem("b111", GetJabberwockedBodyText(),
rand.NextDouble() > 0.5, new List<TreeItemBase>());
TreeItem b112 = new TreeItem("b112", GetJabberwockedBodyText(),
rand.NextDouble() > 0.5, new List<TreeItemBase>());
TreeItem a11 = new TreeItem("a11", GetJabberwockedBodyText(),
rand.NextDouble() > 0.5, new List<TreeItemBase>() { a111, a112 });
TreeItem a12 = new TreeItem("a12", GetJabberwockedBodyText(),
rand.NextDouble() > 0.5, new List<TreeItemBase>());
TreeItem b11 = new TreeItem("b11", GetJabberwockedBodyText(),
rand.NextDouble() > 0.5, new List<TreeItemBase>() { b111, b112 });
TreeItem b12 = new TreeItem("b12", GetJabberwockedBodyText(),
rand.NextDouble() > 0.5, new List<TreeItemBase>());
TreeItemL2 a1 = new TreeItemL2("A1", GetJabberwockedBodyText(),
new List<TreeItemBase>() { a11, a12 });
TreeItemL2 a2 = new TreeItemL2("A2", GetJabberwockedBodyText(),
new List<TreeItemBase>());
TreeItemL2 a3 = new TreeItemL2("A3", GetJabberwockedBodyText(),
new List<TreeItemBase>());
TreeItemL2 b1 = new TreeItemL2("B1", GetJabberwockedBodyText(),
new List<TreeItemBase>() { b11, b12 });
TreeItemL2 b2 = new TreeItemL2("B2", GetJabberwockedBodyText(),
new List<TreeItemBase>());
TreeItemL2 c1 = new TreeItemL2("B3", GetJabberwockedBodyText(),
new List<TreeItemBase>());
TreeItem a = new TreeItem("a", GetJabberwockedBodyText(), true,
new List<TreeItemBase>() { a1, a2, a3 });
TreeItem b = new TreeItem("b", GetJabberwockedBodyText(), true,
new List<TreeItemBase>() { b1, b2 });
TreeItem c = new TreeItem("c", GetJabberwockedBodyText(), true,
new List<TreeItemBase>());
items.Add(a);
items.Add(b);
items.Add(c);
return items;
}
}
注意:此代码也可以通过 ViewModel 的 INPC 属性公开,但您明白这个意思,对吧。
工作原理
下一部分将展示它是如何工作的,因此希望到本节结束时您就能明白其中的原理。
TreeItemBase
为了使此控件能够正确地与您提供的任何数据配合使用,您的数据**必须**继承自一个名为“TreeItemBase
”的特殊类,这是一个非常简单的类,它允许构建树状结构。以下是 TreeItemBase
类的完整内容:
public abstract class TreeItemBase : INotifyPropertyChanged
{
#region Public Properties
public TreeItemBase Parent { get; set; }
public abstract List<TreeItemBase> Children { get; set; }
#if DEBUG
public abstract string DebugText { get; }
#endif
#endregion
#region INotifyPropertyChanged Members
public void Notify(params string[] propertyNames)
{
if (PropertyChanged != null)
{
foreach (string propertyName in propertyNames)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
Level Tree 控件
这是一个控件,您将继承自 TreeItemBase
的 List<TreeItemBase>
对象作为 ItemsSource
添加。此控件的主要作用是接受传入的继承自 TreeItemBase
的 List<TreeItemBase>
对象。对于 ItemsSource
中的每个项,都会创建一个新的专用 ContentControl
(LevelTree3DContentControl
),并在 3D 空间(感谢 Panel3D
)中将其放置在 Viewport2DVisual3D
模型上。专用 ContentControl
(LevelTree3DContentControl
) 的 DataContext
绑定到一个 Wrapper
对象,该对象包含额外的辅助数据以及原始的 TreeItemBase
派生对象。
ItemsControl
将 Panel3D
作为其 ItemsPanel
,因此它将为 LevelTreeControl
的 ItemsSource 中的每个数据项创建的每个控件创建一个 Viewport2DVisual3D
模型。
这一切听起来可能有点疯狂,但仔细想想,它归结为:
- 对于在项中看到的每个继承自
TreeItemBase
的对象,创建一个新的专用ContentControl
(LevelTree3DContentControl
),并将其DataContext
设置为一个Wrapper
对象,该对象具有额外的辅助数据,并且还保存对原始继承自TreeItemBase
的对象的引用。 - 创建一个新的
Viewport2DVisual3D
来在 3D 世界中托管 2D 专用ContentControl
(LevelTree3DContentControl
)。这由Panel3D
完成,它为每个新创建的专用ContentControl
(LevelTree3DContentControl
) 创建一个单独的Viewport2DVisual3D
。
这是 LevelTreeControl
的所有 XAML,看看,是不是不算太糟糕。
<UserControl x:Class="LevelTree3D.TreeLevelControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LevelTree3D"
Height="Auto" Width="Auto"
x:Name="theView">
<ItemsControl x:Name="itemsControl">
<!--
Tell the ItemsControl to use our custom
3D layout panel to arrage its items.
-->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:Panel3D Loaded="Panel3D_Loaded"
ElementWidth="300"
ElementHeight="300"
/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</UserControl>
以及它的代码隐藏。
public partial class TreeLevelControl : UserControl
{
private Panel3D panel3D;
private List<Wrapper> wrappers;
public TreeLevelControl()
{
InitializeComponent();
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(List<TreeItemBase>),
typeof(TreeLevelControl),
new FrameworkPropertyMetadata((List<TreeItemBase>)null,
new PropertyChangedCallback(OnItemsSourceChanged)));
public List<TreeItemBase> ItemsSource
{
get { return (List<TreeItemBase>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
private static void OnItemsSourceChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
((TreeLevelControl)d).OnItemsSourceChanged(e);
}
protected virtual void OnItemsSourceChanged(DependencyPropertyChangedEventArgs e)
{
wrappers = TreeToWrapperConverter.GetWrappersFromTree((List<TreeItemBase>)e.NewValue);
itemsControl.Items.Clear();
foreach (var wrap in (from x in wrappers orderby x.Level ascending select x))
{
itemsControl.Items.Add(CreateControl(wrap));
}
}
private LevelTree3DContentControl CreateControl(Wrapper wrapper)
{
LevelTree3DContentControl cont = new LevelTree3DContentControl(wrapper);
cont.VerticalAlignment = System.Windows.VerticalAlignment.Center;
cont.HorizontalAlignment = System.Windows.HorizontalAlignment.Center;
return cont;
}
private void Panel3D_Loaded(object sender, RoutedEventArgs e)
{
panel3D = (Panel3D)sender;
panel3D.FinishAddingItems();
}
}
Wrapper
正如我们刚才提到的,LevelTreeControl
使用一个小的辅助类来包装原始的继承自 TreeItemBase
的对象,这些对象被提供为 LevelTreeControl
的 ItemsSource
。这个 Wrapper
字面意思是包装(如果您愿意,也可以称为装饰)原始的继承自 TreeItemBase
的对象,并提供更多在 LevelTreeControl
/Panel3D
/Helper 方法中确定正确布局时有用的数据。
这是 Wrapper
对象的所有代码,您可以看到其中的想法,例如层级等,用于辅助布局。
public class Wrapper : DependencyObject
{
public Wrapper(object dataObject, int level)
{
this.DataObject = dataObject;
this.Level = level;
}
/// <summary>
/// Represents dataobject for the item. This will be a TreeItemBase object
/// </summary>
public static readonly DependencyProperty DataObjectProperty =
DependencyProperty.Register("DataObject", typeof(object), typeof(Wrapper),
new FrameworkPropertyMetadata((object)null));
public object DataObject
{
get { return (object)GetValue(DataObjectProperty); }
set { SetValue(DataObjectProperty, value); }
}
public static readonly DependencyProperty LevelProperty =
DependencyProperty.Register("Level", typeof(int), typeof(Wrapper),
new FrameworkPropertyMetadata((int)1));
public int Level
{
get { return (int)GetValue(LevelProperty); }
set { SetValue(LevelProperty, value); }
}
}
Wrapper
由一个小的辅助程序创建,该程序在上面所示的 LevelTreeControl
ItemsSource DP 的更改处理程序中调用。这是那个小辅助类。
public static class TreeToWrapperConverter
{
private static List<Wrapper> wrappers = new List<Wrapper>();
public static List<Wrapper> GetWrappersFromTree(List<TreeItemBase> treeItems)
{
int level = 1;
foreach (TreeItemBase item in treeItems)
{
Wrapper wrap = new Wrapper(item,level);
wrappers.Add(wrap);
RecurseTree(item, ++level);
level = 1;
}
return wrappers;
}
private static void RecurseTree(TreeItemBase parent, int level)
{
foreach (TreeItemBase item in parent.Children)
{
Wrapper wrap = new Wrapper(item, level);
wrappers.Add(wrap);
RecurseTree(item, ++level);
level--;
}
}
}
然后,这些 Wrapper
对象被用作每个新创建的专用 ContentControl
(LevelTree3DContentControl
) 的 DataContext
。因此,由于 Wrapper
,我们现在可以访问层级特定的数据,并且还可以访问 Wrapper
对象中保存的原始继承自 TreeItemBase
的对象数据。
Logical Panel
WPF 是一个奇怪的东西,它基本上是基于 XML 的,而 XML 结构恰好是树状结构。所以我们可以想象我们的 VisualTree
(对于不知道的人来说,这是我们 Visual
元素的树,例如 ItemsControl/Border/StackPanel
等等)中有类似这样的东西:
ItemsControl
-
ContentControl
-
ContentControl
然后我们尝试将一个 ItemsControl
拥有的 ContentControl
放到一个 3D ViewPort
的 Viewport2DVisual3D Visual
上,对于本文的代码来说,它们被托管在 Panel3D
的 AdornerLayer
中,Panel3D
是本文所描述的 TreeLevelControl
的 ItemsControl ItemsPanel
所使用的 3D 面板,我们会遇到问题。
问题将是 WPF 告诉我们不能将一个元素添加到另一个父级,因为它已经有一个视觉父级。原因很简单,ContentControl 确实已经有一个视觉父级,即 ItemsControl
面板(在我们的例子中是 Panel3D
),当我们尝试将其添加为 3D ViewPort
的 Viewport2DVisual3D Visual
时,我们会遇到问题,而且是正确的(但非常恼人)。那么我们能做什么呢?
我们可以通过重写 OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
方法来拦截原始 ContentControl
添加到 ItemsControl Panel
(在我们的情况下就是专用的 Panel3D
)的过程,并在那个时候尝试重新父化它,老实说这会变得有点混乱,但仍然是一种选择。
有没有更好的方法?幸运的是,有!WPF 方面的绝对专家 **Dr WPF** (他让其他人看起来像玩三叶虫的幼儿)发布了一个关于此问题的绝妙的解决方案,可以通过此链接在 codeproject 上找到:
**Dr WPF** 的工作的要点是,我的所有 Panel3D
代码只需要继承自 **Dr WPF** 的 LogicalPanel
(它本身继承自他自己的 ConceptualPanel
),并重写以下方法:
public class Panel3D : LogicalPanel
{
....
....
....
....
protected override void OnLogicalChildrenChanged(UIElement elementAdded, UIElement elementRemoved)
{
LevelTree3DContentControl levelTree3DContentControlAdded =
(LevelTree3DContentControl)elementAdded;
LevelTree3DContentControl levelTree3DContentControlRemoved =
(LevelTree3DContentControl)elementAdded;
bool add = elementAdded != null &&
!_visualTo3DModelMap.ContainsKey(levelTree3DContentControlAdded);
if (add)
{
var model = Build3DModel(levelTree3DContentControlAdded);
_visualTo3DModelMap.Add(levelTree3DContentControlAdded, model);
_viewport.Children.Insert(1, model);
}
bool remove = elementRemoved != null &&
_visualTo3DModelMap.ContainsKey(levelTree3DContentControlRemoved);
if (remove)
{
var model = _visualTo3DModelMap[levelTree3DContentControlRemoved];
_viewport.Children.Remove(model);
_visualTo3DModelMap.Remove(levelTree3DContentControlRemoved);
}
}
....
....
....
}
定位
在 3D 空间中定位 Viewport2DVisual3D
Visual
s 实际上非常容易,因为当我们向 TreeLevelControl
添加继承自 TreeItemBase
的对象时,我们创建了一些 Wrapper
来保存层级信息。所以我们只需要根据它们正确的层级相对于子级来定位项。我使用这个小辅助方法来完成此操作。
public static class TreePositioner
{
private static int examiningCurrentLevel = 0;
public static void Layout(IEnumerable<LevelTree3DContentControl> visuals)
{
//1. Get items at final level
//2. For each item in this final level, get items parent
//3. If parent != null see how many children parent Has,
// then work out where 1st child Position is
// And where last is, and position parent in centre of both
//4. Repeat this using the current level + 1 for new iteration
int maxLevel = (from x in visuals select x.Level).Max();
examiningCurrentLevel = maxLevel;
IEnumerable<LevelTree3DContentControl> visualsAtLevel =
GetVisualsAtLevel(visuals,examiningCurrentLevel);
do
{
foreach (LevelTree3DContentControl visual in visualsAtLevel)
{
Reposition(visuals,visual);
}
examiningCurrentLevel--;
visualsAtLevel = GetVisualsAtLevel(visuals, examiningCurrentLevel);
}
while (examiningCurrentLevel > 1);
}
private static IEnumerable<LevelTree3DContentControl> GetVisualsAtLevel(
IEnumerable<LevelTree3DContentControl> visuals,
int level)
{
return (from x in visuals where x.Level == level select x);
}
private static LevelTree3DContentControl GetVisualForTree(
IEnumerable<LevelTree3DContentControl> visuals, TreeItemBase treeItem)
{
return (from x in visuals where x.OriginalTreeItem == treeItem select x).Single();
}
private static TranslateTransform3D GetMidPointForParent(
IEnumerable<LevelTree3DContentControl> visuals, TreeItemBase parentTreeItem,
TranslateTransform3D parentCurrentPosition)
{
IEnumerable<LevelTree3DContentControl> allChildrenForParent =
(from x in visuals where x.OriginalTreeItem.Parent == parentTreeItem select x);
TranslateTransform3D firstChildPosition =
allChildrenForParent.First().TranslateTransform3D;
TranslateTransform3D lastChildPosition =
allChildrenForParent.Last().TranslateTransform3D;
double parentOffSetX = (double)((lastChildPosition.OffsetX -
firstChildPosition.OffsetX) / 2);
parentOffSetX += firstChildPosition.OffsetX;
return new TranslateTransform3D(parentOffSetX,parentCurrentPosition.OffsetY,
parentCurrentPosition.OffsetZ);
}
private static void Reposition(
IEnumerable<LevelTree3DContentControl> visuals,
LevelTree3DContentControl currentChildVisual)
{
LevelTree3DContentControl parent = GetVisualForTree(visuals,
currentChildVisual.Parent);
if (parent.HasBeenRepositionedByLayoutAlgorithm)
return;
TranslateTransform3D newParentPosition =
GetMidPointForParent(visuals, parent.OriginalTreeItem,
parent.TranslateTransform3D);
parent.HasBeenRepositionedByLayoutAlgorithm = true;
parent.ModelVisual3D.Transform = newParentPosition;
}
}
这看起来可能很复杂,但可以归结为简单的 4 个步骤:
- 获取最终层级的项
- 对于此最终层级中的每个项,获取其父项
- 如果父项 != null,查看父项有多少个子项,然后计算第一个子项的位置和最后一个子项的位置,并将父项放置在两者之间
- 使用当前层级 + 1 对新迭代重复此操作
缩放
3D ViewPort
的缩放是通过一些简单的数学实现的,这些数学位于 LevelTree3D.SceneControl
的代码隐藏中。这一切都在 OnMouseWheel override
中完成,如下所示。本质上发生的是为 3D ViewPort
的 PerspectiveCamera
的 TranslateTransform3D
找到了一个新的位置,我们有效地重新定位了 PerspectiveCamera
的 OffsetZ
位置,每次使用 MouseWheel
时。
以下是 SceneControl
的 PerspectiveCamera
设置的外观:
<Viewport3D x:Name="viewPort">
<Viewport3D.Camera>
<PerspectiveCamera
LookDirection="0,0,-10"
Position="0,1,0"
UpDirection="0,1,0"
FieldOfView="40"
FarPlaneDistance="80">
<PerspectiveCamera.Transform>
<Transform3DGroup>
<TranslateTransform3D x:Name="contTrans" OffsetX="0"
OffsetY="0" OffsetZ="0"/>
<ScaleTransform3D ScaleX="1" ScaleY="1" ScaleZ="1"/>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="contAngle"
Angle="0" Axis="0,1,0"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</Transform3DGroup>
</PerspectiveCamera.Transform>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
这是 SceneControl
的 MouseWheel override
,其中 Constants.MAX_Z_DEPTH
是通过我们之前讨论的定位计算出来的,它将是最后一个模型的 OffsetZ
位置。
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
if (e.Delta > 0)
{
//divide the value by 10 so that it is more smooth
double value = Math.Max(0, e.Delta / 20);
value = Math.Max(-value, Constants.MAX_Z_DEPTH);
if (contTrans.OffsetZ < (Constants.MAX_Z_DEPTH + zoomlimit))
{
contTrans.OffsetZ = Constants.MAX_Z_DEPTH + 7;
}
else
{
contTrans.OffsetZ += value;
}
}
else
{
//divide the value by 10 so that it is more smooth
double value = Math.Max(0, -e.Delta / 20);
value = Math.Max(value, Constants.MAX_Z_DEPTH);
contTrans.OffsetZ += value;
if (contTrans.OffsetZ > -zoomlimit)
contTrans.OffsetZ = 0;
}
}
平移
现在我们知道了缩放是如何工作的,我们可以看看平移,它以类似的方式工作,其中 ViewPort
的 PerspectiveCamera
的 OffsetX
位置会被更新。同样,这可以通过在用户移动鼠标时更改 ViewPort
的 PerspectiveCamera
的 TranslateTransform3D
的值来完成。这是使用以下鼠标事件处理程序代码以及 LevelTree3D.SceneControl
代码隐藏中的辅助方法/数据完成的。
Point startPoint;
bool IsDragging;
private double min = 0;
private double zoomlimit = 10;
private double currX = 0;
public SceneControl()
{
InitializeComponent();
dockPanel.MouseLeftButtonDown += OnMouseLeftButtonDown;
dockPanel.MouseMove += OnMouseMove;
dockPanel.MouseLeftButtonUp += OnMouseLeftButtonUp;
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ReleaseAll();
e.Handled = false;
}
private void ReleaseAll()
{
IsDragging = false;
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
//if dragging, get the delta and add it to selected
//element origin
if (IsDragging)
{
Point currentPosition = e.GetPosition(dockPanel);
double xpos = -Math.Sign(currentPosition.X - startPoint.X) ;
currX += xpos;
currX = Math.Max(min, Math.Min(Constants.MAX_X_COLUMNS, currX));
contTrans.OffsetX = currX;
#if DEBUG
Debug.WriteLine(string.Format("CurrentX {0}", currX));
#endif
}
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (!IsDragging)
{
startPoint = e.GetPosition(dockPanel);
IsDragging = true;
}
e.Handled = false;
}
交互
根据我们现在对如何在您自己的应用程序中使用此控件的了解,通过继承 TreeItemBase
,我们可以随心所欲地创建任何额外的属性/命令/方法。例如,这是演示应用程序中的一个 TreeItemBase
子类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;
namespace LevelTree3D.DemoApp
{
public class TreeItem : TreeItemBase
{
private string title;
private string bodyText;
private bool titleIsTop = false;
private List<TreeItemBase> children;
private IMessageBoxService messageBoxService;
public TreeItem(string title, string bodyText,
bool titleIsTop, List<TreeItemBase> children)
{
this.Parent = null;
this.Title = title;
this.BodyText = bodyText;
this.titleIsTop = titleIsTop;
this.Children = children;
foreach (TreeItemBase child in children)
{
child.Parent = this;
}
messageBoxService =
ServiceResolver.GetService<MessageBoxService>(typeof(IMessageBoxService));
ShowSelectedTextCommand =
new SimpleCommand<object, object>(ExecuteShowSelectedTextCommand);
}
public ICommand ShowSelectedTextCommand { get; private set; }
public bool TitleIsTop
{
get { return titleIsTop; }
}
public string Title
{
get { return title; }
set
{
title = value;
Notify("Title");
}
}
public string BodyText
{
get { return bodyText; }
set
{
bodyText = value;
Notify("BodyText");
}
}
#if DEBUG
public override string DebugText
{
get { return string.Format("Title {0}",Title); }
}
#endif
public override List<TreeItemBase> Children
{
get { return children; }
set
{
children = value;
Notify("Children");
}
}
private void ExecuteShowSelectedTextCommand(object parameter)
{
messageBoxService.ShowMessage(DebugText);
}
}
}
看看我是如何继承 TreeItemBase
并简单地添加任何我想要的额外内容。那么这如何帮助我们与放置在 3D 网格上的 2D 内容进行交互呢?答案很简单,我们只需使用通常在 2D UI 中使用的标准机制,例如 ICommand
,可以通过 Blend 的交互触发器/操作来实现。这正是演示应用程序所做的。让我们来看看这个专用 TreeItem
的 DataTemplate
。
<DataTemplate DataType="{x:Type local:TreeItem}">
<Grid Background="Transparent" Width="300" Height="300"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseRightButtonDown" >
<i:InvokeCommandAction
Command="{Binding ShowSelectedTextCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="396"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="row0" Height="40"/>
<RowDefinition x:Name="row1" Height="260"/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="0" Grid.RowSpan="2"
Fill="White" VerticalAlignment="Stretch"/>
<Border x:Name="titleText" Grid.Row="0" Grid.Column="1" Grid.RowSpan="1"
Background="White" Height="40" Width="292" HorizontalAlignment="Left">
<Label Content="{Binding Title}" Foreground="Black" FontSize="20"
Margin="0" Padding="0" VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Border>
<TextBlock x:Name="bodyText" Grid.Row="1"
Grid.Column="1" Grid.RowSpan="1"
TextWrapping="Wrap" Text="{Binding BodyText}"
Foreground="White" FontSize="12" Margin="0" Padding="10"
Height="260" LineStackingStrategy="BlockLineHeight"
VerticalAlignment="Center" HorizontalAlignment="Left" Width="292" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding TitleIsTop}" Value="false">
<Setter TargetName="row0" Property="Height" Value="260"/>
<Setter TargetName="row1" Property="Height" Value="40"/>
<Setter TargetName="titleText" Property="Grid.Row" Value="1"/>
<Setter TargetName="bodyText" Property="Grid.Row" Value="0"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
运行时右键单击后,它看起来像这样:
注意:我应该指出,IMessageBoxService
是通过我为演示而创建的一个非常简单的服务解析器解决的。您可能不应该使用该代码,它是可丢弃的代码,只是为了在专用 TreeItem
中显示一个 MessageBox
。
暂时就这些
这就是我在这篇文章中想说的全部内容。希望您喜欢。如果您喜欢这篇文章,并希望阅读更多,能否请您花点时间留下评论和投票。非常感谢。
附注:我知道我应该继续写关于 Task Parallel Library 的文章,我也会这么做。我现在就回到那个去了,嗯,实际上我先要去参加我朋友的单身派对,所以我要去滑雪了。然后我保证会写 TPL。