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

WPF 的蜘蛛类型控件树

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (66投票s)

2008年9月21日

CPOL

5分钟阅读

viewsIcon

268892

downloadIcon

5272

WPF 蜘蛛类型控件树。

引言

我最近开始了一份新工作,作为一名 WPF 开发人员。到达后,同事们给了我一个任务,要制作一个酷炫的应用程序,他们非常喜欢 Vertigo 的 FamilyShow 示例的风格。我也很喜欢它,还有 Infragistics 的 Tangerine,它们是我最喜欢的 WPF 演示。

我喜欢这两者的流畅的动画,尤其是 FamilyShow 示例中使用的图表方法。我刚开始工作的那里的同事问我,创建类似 FamilyShow 示例中看到的图表组件有多难。所以,我立即联系了我最喜欢的、在奇怪 WPF 项目方面合作的伙伴——Fredrik Bornander 先生,我喜欢和他一起进行这些更奇特的想法。我们似乎一起做得还不错,至少我是这么认为的。

本文将描述一个我们称之为“SpiderControl”的类似树状的图表控件。

这里有一张截图,仅供参考

本文的其余部分将描述我们是如何构建这个小控件的

它有什么作用

以下是该控件实际功能的列表

  • 使用专门的 ScrollViewer,它允许用户使用鼠标进行启用摩擦的拖动操作(这真的很酷)
  • 最多只显示树的三层,以保持整洁
  • 当前选定的节点会在可用区域内居中显示
  • 节点折叠/展开按钮会根据当前节点的子节点数量自动启用

它是如何制作的

现在,我们进入正题,这部分可能才是您最想看的。

所以,首先,让我们快速看一下基本结构。

我将把它分成两个图表,没有其他原因,就是因为我想不出办法让 Word 的 SmartArt 在其标准 SmartArt 图表中添加更多层级,诅咒技术。

从上面的图表可以看出,HostWindow 持有一个 DragViewer 实例。所以窗口的代码很简单,如下所示:

<Window x:Class="SpiderTreeControl.HostWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:diagram="clr-namespace:SpiderTreeControl.Diagram"  
      WindowStartupLocation="CenterScreen"
      Title="HostWindow" Height="400" Width="400">
    <Grid>
        <diagram:DragViewer x:Name="dragViewer" 
                            Width="auto" Height="auto" 
                            Margin="0"/>
    </Grid>
</Window>

然后,我们关注实际的 DragViewer,我们将 DiagramViewer 包装在 FrictionScrollViewer 中。

FrictionScrollViewer 是一个特殊的 ScrollViewer,它通过摩擦来创建流畅的拖动操作。我曾在我以前的一篇博客文章中对此进行了更详细的介绍,您可以在这里阅读:http://sachabarber.net/?p=225

<UserControl x:Class="SpiderTreeControl.Diagram.DragViewer"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:diagram="clr-namespace:SpiderTreeControl.Diagram;assembly="                
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Height="auto" Width="auto">
    <diagram:FrictionScrollViewer x:Name="sv" 
            Style="{StaticResource ScrollViewerStyle}">
        <diagram:DiagramViewer x:Name="diagramViewer" 
            Margin="0" Width="2000" Height="2000"/>
    </diagram:FrictionScrollViewer>
</UserControl>

滚动条的 Style 是使用位于 AppStyles.xaml ResourceDictionary 中的一些样式实现的。这使得 ScrollViewer 具有如下所示的外观:

但所有这些都是简单的视觉效果,我们需要关注核心内容。

DragViewer 类

回到 DragViewer,它持有一个 DiagramViewer 实例。在代码隐藏中,DragViewer 负责设置其嵌入的 DiagramViewer 使用的节点集合。

这在代码中如下实现:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace SpiderTreeControl.Diagram
{
    /// <summary>
    /// Interaction logic for DragViewer.xaml
    /// </summary>
    public partial class DragViewer : UserControl
    {

        public DragViewer()
        {
            InitializeComponent();
            this.Loaded+=delegate
            {
                LoadDiagramNodes();
            };
        }

        public void LoadDiagramNodes()
        {

            DiagramNode root = new DiagramNode("Root", null, 
              "../Images/DiagramRootNode.png", "Dummy1View", 
              "this is the root node");

            DiagramNode a = new DiagramNode("A", root, 
              "../Images/DiagramNode.png", "Dummy1View", 
              "this is node A");
            DiagramNode b = new DiagramNode("B", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node B");
            DiagramNode c = new DiagramNode("C", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node C");
            DiagramNode d = new DiagramNode("D", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node D");
            DiagramNode e = new DiagramNode("E", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node E");
            DiagramNode f = new DiagramNode("F", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node F");

            diagramViewer.RootNode = root;
            diagramViewer.FrictionScrollViewer = this.sv;
        }
    }
}

其中创建了单独的 DiagramNode 对象,它们之间的关系是通过将相关的 DiagramNode 作为构造函数参数传递给另一个 DiagramNode 来建立的。显然,对于根 DiagramNode,此值为 null。最后要做的是设置嵌入的 DiagramViewerRootNode 属性为根 DiagramNode。还需要设置嵌入的 DiagramViewerFrictionScrollViewer 属性,以便 DiagramViewer 的布局算法可以响应 ScrollViewer 位置的新变化,如果用户移动 ScrollViewer 或拖动图表。

DiagramViewer 类

在这里发生所有包含的 DiagramNode 的布局。这是一个简单的用户控件,包含一个单独的 TreeCanvas,它包含实际的 DiagramNode 集合,并绘制它们之间的线条,下面将对此进行讨论。DiagramViewer 还监听来自 DiagramNode 的事件,例如 Selected/Collapsed/Expanded,届时它将根据节点选择执行布局。

DiagramViewer 使用一个径向算法来布局子节点集合围绕父节点。这是使用标准的三角函数实现的。基本思想是为每个 DiagramNode 分配一个边界圆,以确保所有节点的大小均匀,然后计算节点之间的角度。

此过程在 NodeExpanded() 中完成,如下所示:

private void NodeExpanded(DiagramNode sender, RoutedEventArgs eventArguments)
{
    rootNode.Location = new Point(
        (double)GetValue(Canvas.ActualWidthProperty) / 2.0,
        (double)GetValue(Canvas.ActualHeightProperty) / 2.0);


    MakeChildrenVisible(sender);

    if (sender.DiagramParent != null)
    {
        sender.DiagramParent.Visibility = Visibility.Visible;
        foreach (DiagramNode sibling in sender.DiagramParent.DiagramChildren)
        {
            if (sibling != sender)
                sibling.Visibility = Visibility.Collapsed;
        }
        if (sender.DiagramParent.DiagramParent != null)
            sender.DiagramParent.DiagramParent.Visibility = Visibility.Collapsed;
    }

    if (sender.DiagramChildren.Count > 0)
    {
        double startAngle = CalculateStartAngle(sender);
        double angleBetweenChildren = (sender == rootNode ? Math.PI * 2.0 : Math.PI) / 
                    ((double)sender.DiagramChildren.Count - 0);

        double legDistance = CalculateLegDistance(sender, angleBetweenChildren);

        for (int i = 0; i < sender.DiagramChildren.Count; ++i)
        {
            DiagramNode child = sender.DiagramChildren[i];
            child.Selected += new NodeStateChangedHandler(NodeSelected);
            child.Expanded += new NodeStateChangedHandler(NodeExpanded);
            child.Collapsed += new NodeStateChangedHandler(NodeCollapsed);

            Point parentLocation = sender.Location;

            child.Location = new Point(
                parentLocation.X + Math.Cos(startAngle + 
                   angleBetweenChildren * (double)i) * legDistance,
                parentLocation.Y + Math.Sin(startAngle + 
                  angleBetweenChildren * (double)i) * legDistance);

            foreach (DiagramNode childsChild in child.DiagramChildren)
            {
                childsChild.Visibility = Visibility.Collapsed;
            }
        }
    }

    BaseCanvas.InvalidateArrange();
    BaseCanvas.UpdateLayout();
    BaseCanvas.InvalidateVisual();
}

上述过程还依赖于另一个过程,即确定节点之间连线长度(从一个节点到其子节点的线条长度)的过程。

如下所示:

private static double CalculateLegDistance(DiagramNode sender, double angleBetweenChildren)
{
    double legDistance = 1.0;
    double childToChildMinDistance = 1.0;
    foreach (DiagramNode child in sender.DiagramChildren)
    {
        legDistance = Math.Max(legDistance, 
            sender.BoundingCircle + child.BoundingCircle);
        foreach (DiagramNode otherChild in sender.DiagramChildren)
        {
            if (otherChild != child)
            {
                childToChildMinDistance = 
                Math.Max(childToChildMinDistance, 
                child.BoundingCircle + otherChild.BoundingCircle);
            }
        }
    }

    legDistance = Math.Max(
        legDistance,
        (childToChildMinDistance / 2.0) / Math.Sin(angleBetweenChildren / 2.0));
    return legDistance;
}

TreeCanvas 类

这是一个特殊的 Canvas 控件,它简单地绘制所有 DiagramNode 之间的线条,这些节点当前显示在 DiagramViewer 中。这是通过覆盖 Canvas 控件的 OnRender() 方法来实现的。如下所示:

protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    base.OnRender(dc);
    foreach (UIElement uiElement in Children)
    {
        if (uiElement is DiagramNode)
        {
            DiagramNode node = (DiagramNode)uiElement;

            if (node.Visibility == Visibility.Visible)
            {
                if (node.DiagramParent != null && 
                    node.DiagramParent.Visibility == Visibility.Visible)
                {
                    dc.DrawLine(new Pen(Brushes.Black, 2.0), 
                        node.Location, node.DiagramParent.Location);
                }
            }
        }
    }
}

DiagramNode 类

这是一个相当标准的 WPF UserControl,代表 DiagramViewer 中的单个 DiagramNode。这真的很标准,几个用于展开/折叠和选中状态的按钮。让我们看看它的 XAML,好吗?

<UserControl x:Class="SpiderTreeControl.Diagram."
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:diagram="clr-namespace:SpiderTreeControl.Diagram"  
        Height="80" Width="90" 
        Background="Transparent"
        BorderBrush="Transparent">

    <UserControl.CommandBindings>
        <CommandBinding Command="{x:Static diagram:DiagramNode.expandCommand}" 
            CanExecute="ExpandCommand_CanExecute"
            Executed="ExpandCommand_Executed"/>

        <CommandBinding Command="{x:Static diagram:DiagramNode.collapseCommand}" 
            CanExecute="CollapseCommand_CanExecute"
            Executed="CollapseCommand_Executed"/>

    </UserControl.CommandBindings>

    <UserControl.Resources>

        <ControlTemplate x:Key="expandCollapseButton" TargetType="{x:Type Button}">
            <Grid Width="20" Height="20" Background="Transparent">
                <Ellipse Width="20" Height="20" Fill="DarkGray"/>
                <Ellipse Width="16" Height="16" Fill="WhiteSmoke"/>
                
                <Label x:Name="lbl" Content="{TemplateBinding Content}" 
                       HorizontalAlignment="Center" VerticalAlignment="Center"
                       Background="Transparent"
                       FontFamily="Arial Black" FontSize="10"/>
            </Grid>
            <ControlTemplate.Triggers>
                <Trigger Property="IsEnabled" Value="false">
                    <Setter TargetName="lbl" 
                      Property="Foreground" Value="DarkGray"/>
                </Trigger>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="BitmapEffect">
                        <Setter.Value>
                            <DropShadowBitmapEffect />
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

    </UserControl.Resources>
    
    
    <Canvas Background="Transparent" 
                   Width="90" Height="80" >
        <Label x:Name="NodeName" 
           Content="Name" FontFamily="Arial Black" 
           FontSize="14" Canvas.ZIndex="1"/>
        
        <Button x:Name="btnNavigate" 
                Template="{StaticResource simpleImageButtonTemplate}"
                Click="btnNavigate_Click" Width="60" Height="60" 
                Canvas.Left="20" Canvas.Top="20"
                Canvas.ZIndex="0"/>
        
        <Button x:Name="ExpandButton" Content="+" 
                Canvas.Top="30" Canvas.Left="0"
                Template="{StaticResource expandCollapseButton}"
                Command="{x:Static diagram:DiagramNode.expandCommand}" />


        <Button x:Name="CollapseButton" Content="-" 
                Canvas.Top="55" Canvas.Left="0"
                Template="{StaticResource expandCollapseButton}"
                Command="{x:Static diagram:DiagramNode.collapseCommand}" />
    </Canvas>
</UserControl>

每个 DiagramNode 的外观如下:

已知问题

只有一个已知问题,但事实上,这几乎是 WPF 中所有图表解决方案都会遇到的情况,包括 Vertigo 的 FamilyShow 示例,以及我在上一份工作中评估过的许多其他解决方案。基本上,为了动态定位图表节点,您必须使用支持 X/Y 定位的容器面板,这意味着使用 Canvas

这没问题,但这意味着您必须确保 Canvas 足够大,能够容纳您的算法所需的最大的布局位置。问题不大,但您需要注意。

为此,您会发现在演示应用程序中,DragViewer 控件内嵌入的 DiagramViewer 的大小固定为 2000 x 2000,声明如下:

<UserControl x:Class="SpiderTreeControl.Diagram.DragViewer"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:diagram="clr-namespace:SpiderTreeControl.Diagram;assembly="                
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Height="auto" Width="auto">
    <diagram:FrictionScrollViewer x:Name="sv" 
           Style="{StaticResource ScrollViewerStyle}">
        <diagram:DiagramViewer x:Name="diagramViewer" 
           Margin="0" Width="2000" Height="2000"/>
    </diagram:FrictionScrollViewer>
</UserControl>

除了这个单一问题之外,正如我所说,您可能会在几乎所有 WPF 的图表组件中看到,没有已知的问题。

© . All rights reserved.