Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF : A Weird 3d based control

0.00/5 (No votes)
15 Feb 2011 2  
A kind of 3d tree level control

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 DataTemplated 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 Viewport2DVisual3Ds 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 ItemsControls 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

  1. 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.
  2. 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">
        <!-- 
          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>

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;
    }

    /// <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); }
    }

}

The Wrappers 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 Wrapppers 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 ContentControls 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 Visuals in 3d space, is actually pretty easy now, since when we added the TreeItemBase derived objects to the TreeLevelControl we create some Wrappers 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)
    {
        //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;
    }
       

}

This may look complicated but can be summed by the simple 4 steps

  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

 

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 SceneControls 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)
    {
	//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;
    }
}

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 PerspectiveCameras OffsetX position is updated. As before this will be done by changing values of the ViewPort PerspectiveCameras TranslateTransform3D, when the user moves the mouse. This is done using the following mouse event handler code, and helper methods/data within the LevelTree3D.SceneControls 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 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;
}

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here