|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionI recently started a new job where I am employed as a WPF developer. When I arrived the guys there gave me a brief that was to make a cool app, and they really liked the look and feel of the FamilyShow exemplar by Vertigo. Which I also love, that and Tangerine by Infragistics are my favourite WPF demos. What I liked in both where the fluid movements and the diagraming approach used in the FamilyShow exemplar particularly. The guys where I just started working asked me how hard it would be to create something like the diagramming component seen in the FamilyShow exemplar. So without further ado I contacted my favourite partner in weird WPF breifs, Mr Fredrik Bornander, who I love working with on these stranger ideas. We seem to manage to do a reasonable job together, at least I think anyway. This article will describe a tree like diagram component that we have nicknamed the "SpiderControl". Here is a screenshot just to wet your appetite:
What Does It DoThe following is a list of what the control actually does
Hows It MadeSo now onto the nitty gritty, which is the part you are probably wanting to read anyhow. So first lets just have a quick look at the basic structure Ill split this into 2 diagrams for no other reason than I couldnt figure out how to get words SmartArt to add more levels to its standard SmartArt diagrams, curse technology.
It can be seen from the above diagram that the HostWindow holds an instance
of a
<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>
And then if we focus our attention to the actual The
<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>
The
But all of that is simple eye candy, we need to get to nuts and bolts. DragViewer classSo going back to the This is done is code as follows:
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;
}
}
}
Were individual DiagramViewer classIs where all the layout of contained The DiagramViewer uses a radial algorithm to lay out the collection of child
nodes around a parent node. This is done using standard trigonometry maths.
The basic idea is that each
This process is done on the
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();
}
The above process also relies on another process, which is the process which works out the leg distances (the length of the line to draw from one node to its child) between nodes. This is as shown below:
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 classThis is a specialized
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 classThis is a pretty standard WPF UserControl that represents a single
<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>
Each DiagramNode looks like the following:
Known IssuesThere is only 1 known issue, but this is actually the case with pretty much
every diagramming solution I have seen within WPF, including the FamilyShow
exemplar by Vertigo, and lots of others which I had to evaluate in my last job.
Basically in order to dynamically position diagram nodes, you have to use a
container panel that supports X/Y positioning, which means using a Which is fine, but this means you must must ensure that the Canvas is big enough to accomodate the largest layout positions that you algorithm demands. Not a huge problem, but it is one that you need to be aware of. To this end you will find that within the embedded
<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>
Other than this single issue, which as I say you will probably see in practically all diagrmming components for WPF, there are no known issues.
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||