Demo code source : LevelTree3D.zip
Introduction
This article is basically about a control that I have written for WPF, that
allows users to create their own data items, which will be layed out in a 3D Viewport
in layers. You can almost think of supplying a list of
trees to a control that lays these items out in layers in 3D space. The control
presented in this article also supports transparency and allows the
DataTemplating of your custom data items, and allows full 2D interaction with
the DataTemplate
d data items when the corresponding DataTemplate
applied Visual
is created and hosted on a 3D Viewport ViewPort2DVisual3D.Visual
. So you get the
best of both worlds, you get to use powerful techniques such as DataTemplating,
but you also get to see you items layed out in 3D, and you can still interact
with them.
The rest of this article will demonstrate the custom control which I have
called "LevelTree3D"
.
Table Of Contents
Anyway what I am going to cover in this article is as follows:
Demo Video
Since this control is such a visual one, I think the best way to demonstrate
it, is by using a video. The image below links through to another page that
hosts a video of this article code in action.
Have a look
Basic Idea
The basic idea is a simple one, we supply a List<TreeItemBase>
derived
objects to the TreeLevelControl
control supplied with this article. After which
the TreeLevelControl
will recursively traverse the supplied List<TreeItemBase>
derived objects and create a new LevelTree3DContentControl
in the code behind of the
TreeLevelControl which is added to an internal ItemsControl
.
Where since the Content of the LevelTree3DContentControl
is actually the original TreeItemBase
based object, as such any custom
DataTemplate
you provide in your XAML will be applied as a DataTemplate
will be applied.
We also create a new 3D Viewport2DVisual3D
models for each of the newly created
LevelTree3DContentControl
that are added to the ItemsControl
.
The internal ItemsControl
uses a custom Panel
(LevelControl3D.Panel3D
) for its
ItemsPanel
(to create a 3D ViewPort3D
and a new Viewport2DVisual3D
for each LevelTree3DContentControl
, where the Viewport2DVisual3D
models are
positioned correctly in X/Y/Z space to make it appear like trees in 3D space.
The actual ViewPort3D
and Viewport2DVisual3D
s are hosted in another special
control called "SceneControl
" which is hosted in a custom Adorner
control called
"Panel3DAdorner
" which is itself hosted in the AdornerLayer
of the
LevelControl3D.Panel3D
.
That may all sound slighly confusing, but I think this can all be sumarized
quite nicely by the following diagram
Using This Control In Your Own App
To use the TreeLevelControl (which is the main control that this article is
all about) in your own apps, is pretty simple. There are really only a couple of
steps
Step 1 : Use it in your XAML as follows
<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>
Step 2 : Create A Subclass of the LevelTree3D.TreeItemBase class
The next step is to subclass the LevelTree3D.TreeItemBase
and add on any extra
stuff that your 3D tree nodes require. Here is an example from the demo app
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);
}
}
}
Step 3 : Create A DataTemplate For Your Custom Tree Nodes
The next step is to create a custom DataTemplate
for your custom
LevelTree3D.TreeItemBase
sublclasses. Here is an example from the demo app
<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>
Step 4 : Create All The Items For the LevelTree3D Control
The last step is to actually supplied a list of connected TreeItemBase
based
objects as the LevelTree3D.ItemSource
. An example of this may be as follows
(again see the demo app)
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;
}
}
NOTE : This code could just have easily been exposed via a INPC property of a ViewModel, but you get the idea right.
How It Works
The next section will show you how it all works internally, so hopefully by
the time you get to the end of this section you will know what is going down.
TreeItemBase
I order for this control to work correctly with any data you wish to provide your data MUST inherit from a special class called "TreeItemBase
", which is a very simple class that simply allows the construction of a tree like structure. Here is what the TreeItemBase
class looks like in its entirety
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 Control
This is a control where you add a List<TreeItemBase>
derived objects as an ItemsSource
. The main role of this control is to accept an incoming list List<TreeItemBase>
derived objects. For each of these items within the ItemsSource
and new specialized ContentControl
(LevelTree3DContentControl
) is created and hosted in 3d space (thanks to Panel3D
) on a Viewport2DVisual3D
model. The specialized ContentControl
(LevelTree3DContentControl
) have their DataContext
bound to a Wrapper
object which contains extra helper data as well as the original TreeItemBase
derived object.
The ItemsControl
s has a Panel3D
as its ItemsPanel
, and as such will create Viewport2DVisual3D
model for each control that is created for each data item within the ItemsSource of the LevelTreeControl
.
This may all sound a bit nuts, but when you think about it comes down to this
- For every
TreeItemBase
dervived object seen as an item, create a new specialized ContentControl
(LevelTree3DContentControl
), and set its DataContext
to a Wrapper
object, which has additional helper data and also holds a reference to the original TreeItemBase
dervived object.
- Create a new
Viewport2DVisual3D
to host the 2D specialized ContentControl
(LevelTree3DContentControl
) in a 3d world. This is done by Panel3D
where it creates a single Viewport2DVisual3D
for each of the newly created specialized ContentControl
(LevelTree3DContentControl
)
Here is the all the LevelTreeControl
xaml, see not that bad is it
<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">
-->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:Panel3D Loaded="Panel3D_Loaded"
ElementWidth="300"
ElementHeight="300"
/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</UserControl>
And its code behind
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
As we just mentioned the LevelTreeControl
makes use of a little helper class that wraps the original TreeItemBase
dervived object which were provided as the ItemsSource
of the LevelTreeControl
. This Wrapper
literally wraps (decorates if you prefer) the original TreeItemBase
dervived object and supplied more data which is useful within the LevelTreeControl
/Panel3D
/Helper methods when determining the correct layout.
Here is all the code for the Wrapper
object, you can see the idea right, you have things like level etc etc, for helping with layout.
public class Wrapper : DependencyObject
{
public Wrapper(object dataObject, int level)
{
this.DataObject = dataObject;
this.Level = level;
}
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); }
}
}
The Wrapper
s are created by a little helper that is called in the change handler for the LevelTreeControl
ItemsSource DP, which is shown above. Here is that little helper class.
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--;
}
}
}
These Wrappper
s are then use to set as the DataContext
for each newly created specialized ContentControl
(LevelTree3DContentControl
). As such we now have access to level specific data thanks to the Wrappper
, and also the original TreeItemBase
derived object data which is also held inside the Wrappper
object.
Logical Panel
Now WPF is a strange beast, it is all based on XML really, and XML structures
just happen to be a tree like structure. So we can imagine that we have
something like this in our VisualTree
(that the tree of our Visual
elements for
those that did not know that, things like ItemsControl/Border/StackPanel
etc
etc)
ItemsControl
-
ContentControl
-
ContentControl
And then we try and get one of those ItemsControl
owned ContentControl
s onto a 3D ViewPort
Viewport2DVisual3D Visual
, that for this articles code are hosted in the AdornerLayer
of the Panel3D
,
which is the 3D panel that is use for the ItemsControl ItemsPanel
of the
TreeLevelControl
that this article is describing, we will get a problem.
The problem will be of the nature where
WPF tells us we can not add an element to another parent, as it already has a
visual parent. The reason for this is simple, the ContentControl does already
have a visual parent. the ItemsControl
panel (Panel3D
in our case), and by attempting to add it
as a 3D ViewPort Viewport2DVisual3D Visual
we get a problem, and rightly (but
quite annoyingly) so. So what can we do
about that.
Well we could intercept the adding of the original ContentControl
to the
ItemsControl Panel
(which in our case would be the specialized Panel3D
) via the
OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
method that we could override, and at
that point try and re-parent it, which quite frankly end up a bit of a mess, but is none the
less an option.
Is there a better way. Well luckily there is,
Dr WPF, who is the absolute
man, when it comes to WPF, and makes the rest of us look like small infants
playing with the trilobite, published a out of this world work around about this, which
is available right here at codeproject using this link:
ConceptualChildren.aspx
The up shot of Dr WPFs work is that all my Panel3D
code really needs to do is
inherit from Dr WPFs LogicalPanel
(which itself inherits from his own
ConceptualPanel
), and override the following method:
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);
}
}
....
....
....
}
Positioning
Positioning of the Viewport2DVisual3D
Visual
s in 3d space, is actually pretty easy now, since when we added the TreeItemBase
derived objects to the TreeLevelControl
we create some Wrapper
s that hold level information. So all we need to do is position the items relative to their children at the correct level. To do this I use this little helper method.
public static class TreePositioner
{
private static int examiningCurrentLevel = 0;
public static void Layout(IEnumerable<LevelTree3DContentControl> visuals)
{
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;
}
}
This may look complicated but can be summed by the simple 4 steps
- Get items at final level
- For each item in this final level, get items parent
- 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
- Repeat this using the current level + 1 for new iteration
Zooming
Zooming of the 3D ViewPort
is achieved using some simple maths, which is located in the
LevelTree3D.SceneControl
code behind. This is all done in the OnMouseWheel
override
as shown below. What essentially happens is that a new position is
found for the 3D ViewPort PerspectiveCamera
's TranslateTransform3D
, where we are
effectevily repositioning the PerspectiveCamera OffsetZ
position, each time the
MouseWheel
is used.
Here is what the SceneControl
s PerspectiveCamera
setup looks like
<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>
And here is the the MouseWheel override
for the SceneControl
, where the Constants.MAX_Z_DEPTH
is worked out via the positioning that we discussed earlier, and it will be the furthest models OffsetZ
position
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
if (e.Delta > 0)
{
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
{
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;
}
}
Panning
Now that we know how zooming works, we can take a look at panning, which works in much the same way where the
ViewPort PerspectiveCamera
s OffsetX
position is updated. As before this will be done by changing
values of the ViewPort PerspectiveCamera
s TranslateTransform3D
, when the user moves the mouse. This
is done using the following mouse event handler code, and helper methods/data within the LevelTree3D.SceneControl
s
code behind
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 (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;
}
Interacting
Knowing what we now know about how to use this control in your own app, by the use of inheriting from
TreeItemBase
, we can pretty much come up with any addditional properties/commands/methods that we want
in our TreeItemBase
sub class. For example here is one of the demo apps TreeItemBase
sub classes
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);
}
}
}
See how I am inheriting from TreeItemBase
and simply adding on whatever other bits and bobs I want. So how does this help us
interact with the 2D content which is placed on a 3D mesh? Well the answer is quite easy, we just use standard mechanisms that we would
normally use in a 2D based UI, such as ICommand
which we can use via some of the Blend interactivity triggers/action. This is
exactly what the demo app does. Let us examine the DataTemplate
for this specialized TreeItem
.
<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>
Which looks like this when run, and right clicked on
NOTE : I should just point out that the IMessageBoxService
is resolved by a very slim line service resolver that
I just knocked up for this demo. You should probably not use that code, it is throw away code, just to show a
MessageBox
inside
a specialized TreeItem
.
That's It For Now
That is all I wanted to say in this in this article. I hope you liked it. If you did like this article, and would like more, could you spare some time to leave a
comment and a vote. Many thanks.
PS : I know that I am meant to be working on the Task Parallel Library
articles and I am doing that too, I am straight back to that now, well actually I am off for my mates stag doo 1st, so snowboarding here I come. Then TPL I promise.