65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF:一个奇特的 3D 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (52投票s)

2011 年 2 月 12 日

CPOL

10分钟阅读

viewsIcon

131901

downloadIcon

1277

一种 3D 树状控件。

演示代码源: LevelTree3D.zip

引言

本文主要介绍我为 WPF 编写的一个控件,该控件允许用户创建自己的数据项,这些数据项将在 3D Viewport 中分层布局。您可以将它想象成向一个控件提供一个树形列表,该控件会将这些项分层布局在 3D 空间中。本文介绍的控件还支持透明度,允许您自定义数据项的 DataTemplate,并允许在 3D ViewportViewport2DVisual3D.Visual 上创建和托管对应的 DataTemplate 应用的 Visual 时,与 DataTemplate 化后的数据项进行完整的 2D 交互。这样您就可以两全其美,既可以使用 DataTemplating 等强大技术,又能将您的项以 3D 形式呈现,并且仍然可以与它们进行交互。

本文的其余部分将演示我称为“LevelTree3D”的自定义控件。

 

目录 

总之,本文将介绍以下内容:

演示视频

由于这是一个非常注重视觉的控件,我认为演示它的最佳方式是使用视频。下面的图片链接到一个托管了本文代码演示视频的页面。

看看

基本思想

基本思路很简单,我们将一个继承自 TreeItemBaseList<TreeItemBase> 对象提供给本文提供的 TreeLevelControl 控件。之后,TreeLevelControl 将递归遍历提供的继承自 TreeItemBaseList<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 空间中的树。实际的 ViewPort3DViewport2DVisual3D 被托管在另一个称为“SceneControl”的特殊控件中,该控件被托管在称为“Panel3DAdorner”的自定义 Adorner 控件中,而该控件本身被托管在 LevelControl3D.Panel3DAdornerLayer 中。

这一切听起来可能有点令人困惑,但我认为可以用下图很好地总结:

 

在您自己的应用程序中使用此控件

要在您自己的应用程序中使用 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 控件

这是一个控件,您将继承自 TreeItemBaseList<TreeItemBase> 对象作为 ItemsSource 添加。此控件的主要作用是接受传入的继承自 TreeItemBaseList<TreeItemBase> 对象。对于 ItemsSource 中的每个项,都会创建一个新的专用 ContentControl (LevelTree3DContentControl),并在 3D 空间(感谢 Panel3D)中将其放置在 Viewport2DVisual3D 模型上。专用 ContentControl (LevelTree3DContentControl) 的 DataContext 绑定到一个 Wrapper 对象,该对象包含额外的辅助数据以及原始的 TreeItemBase 派生对象。

ItemsControlPanel3D 作为其 ItemsPanel,因此它将为 LevelTreeControl 的 ItemsSource 中的每个数据项创建的每个控件创建一个 Viewport2DVisual3D 模型。

这一切听起来可能有点疯狂,但仔细想想,它归结为:

  1. 对于在项中看到的每个继承自 TreeItemBase 的对象,创建一个新的专用 ContentControl (LevelTree3DContentControl),并将其 DataContext 设置为一个 Wrapper 对象,该对象具有额外的辅助数据,并且还保存对原始继承自 TreeItemBase 的对象的引用。
  2. 创建一个新的 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 的对象,这些对象被提供为 LevelTreeControlItemsSource。这个 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 ViewPortViewport2DVisual3D Visual 上,对于本文的代码来说,它们被托管在 Panel3DAdornerLayer 中,Panel3D 是本文所描述的 TreeLevelControlItemsControl ItemsPanel 所使用的 3D 面板,我们会遇到问题。

问题将是 WPF 告诉我们不能将一个元素添加到另一个父级,因为它已经有一个视觉父级。原因很简单,ContentControl 确实已经有一个视觉父级,即 ItemsControl 面板(在我们的例子中是 Panel3D),当我们尝试将其添加为 3D ViewPortViewport2DVisual3D Visual 时,我们会遇到问题,而且是正确的(但非常恼人)。那么我们能做什么呢?

我们可以通过重写 OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved) 方法来拦截原始 ContentControl 添加到 ItemsControl Panel(在我们的情况下就是专用的 Panel3D)的过程,并在那个时候尝试重新父化它,老实说这会变得有点混乱,但仍然是一种选择。

有没有更好的方法?幸运的是,有!WPF 方面的绝对专家 **Dr WPF** (他让其他人看起来像玩三叶虫的幼儿)发布了一个关于此问题的绝妙的解决方案,可以通过此链接在 codeproject 上找到:

ConceptualChildren.aspx

**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 Visuals 实际上非常容易,因为当我们向 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 个步骤:

  1. 获取最终层级的项
  2. 对于此最终层级中的每个项,获取其父项
  3. 如果父项 != null,查看父项有多少个子项,然后计算第一个子项的位置和最后一个子项的位置,并将父项放置在两者之间
  4. 使用当前层级 + 1 对新迭代重复此操作

 

缩放

3D ViewPort 的缩放是通过一些简单的数学实现的,这些数学位于 LevelTree3D.SceneControl 的代码隐藏中。这一切都在 OnMouseWheel override 中完成,如下所示。本质上发生的是为 3D ViewPortPerspectiveCameraTranslateTransform3D 找到了一个新的位置,我们有效地重新定位了 PerspectiveCameraOffsetZ 位置,每次使用 MouseWheel 时。

以下是 SceneControlPerspectiveCamera 设置的外观:

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

这是 SceneControlMouseWheel 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;
    }
}

平移

现在我们知道了缩放是如何工作的,我们可以看看平移,它以类似的方式工作,其中 ViewPortPerspectiveCameraOffsetX 位置会被更新。同样,这可以通过在用户移动鼠标时更改 ViewPortPerspectiveCameraTranslateTransform3D 的值来完成。这是使用以下鼠标事件处理程序代码以及 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 的交互触发器/操作来实现。这正是演示应用程序所做的。让我们来看看这个专用 TreeItemDataTemplate

<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。

© . All rights reserved.