Make nodes center align

Hi,

How to make nodes on same layer center align?

Thanks,
Peter

I’m working on a sample for you.

Here’s a modified version of the TLayout demo that has a custom layout that does what I think you want. First, the modified TLayout.xaml:

http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:go="http://schemas.nwoods.com/GoXam" xmlns:local="clr-namespace:TLayout">

<local:CustomTreeLayout TreeStyle="{Binding ElementName=TreeStyleRadioButtonGroup, Path=Tag}"

Angle="{Binding ElementName=AngleTextBox, Path=Text}" Alignment="{Binding ElementName=AlignmentRadioButtonGroup, Path=Tag}" NodeIndent="{Binding ElementName=NodeIndentTextBox, Path=Text}" NodeIndentPastParent="{Binding ElementName=NodeIndentPastParentTextBox, Path=Text}" NodeSpacing="{Binding ElementName=NodeSpacingTextBox, Path=Text}" LayerSpacing="{Binding ElementName=LayerSpacingTextBox, Path=Text}" LayerSpacingParentOverlap="{Binding ElementName=LayerSpacingParentOverlapTextBox, Path=Text}" Compaction="{Binding ElementName=CompactionRadioButtonGroup, Path=Tag}" BreadthLimit="{Binding ElementName=BreadthLimitTextBox, Path=Text}" RowSpacing="{Binding ElementName=RowSpacingTextBox, Path=Text}" RowIndent="{Binding ElementName=RowIndentTextBox, Path=Text}" SetsPortSpot="{Binding ElementName=SetsPortSpotCheckBox, Path=IsChecked}" SetsChildPortSpot="{Binding ElementName=SetsChildPortSpotCheckBox, Path=IsChecked}" Sorting="{Binding ElementName=SortingRadioButtonGroup, Path=Tag}" Comparer="{StaticResource theSimpleComparer}"

AlternateAngle="{Binding ElementName=AlternateAngleTextBox, Path=Text}" AlternateAlignment="{Binding ElementName=AlternateAlignmentRadioButtonGroup, Path=Tag}" AlternateNodeIndent="{Binding ElementName=AlternateNodeIndentTextBox, Path=Text}" AlternateNodeIndentPastParent="{Binding ElementName=AlternateNodeIndentPastParentTextBox, Path=Text}" AlternateNodeSpacing="{Binding ElementName=AlternateNodeSpacingTextBox, Path=Text}" AlternateLayerSpacing="{Binding ElementName=AlternateLayerSpacingTextBox, Path=Text}" AlternateLayerSpacingParentOverlap="{Binding ElementName=AlternateLayerSpacingParentOverlapTextBox, Path=Text}" AlternateCompaction="{Binding ElementName=AlternateCompactionRadioButtonGroup, Path=Tag}" AlternateBreadthLimit="{Binding ElementName=AlternateBreadthLimitTextBox, Path=Text}" AlternateRowSpacing="{Binding ElementName=AlternateRowSpacingTextBox, Path=Text}" AlternateRowIndent="{Binding ElementName=AlternateRowIndentTextBox, Path=Text}" AlternateSetsPortSpot="{Binding ElementName=AlternateSetsPortSpotCheckBox, Path=IsChecked}" AlternateSetsChildPortSpot="{Binding ElementName=AlternateSetsChildPortSpotCheckBox, Path=IsChecked}" AlternateSorting="{Binding ElementName=AlternateSortingRadioButtonGroup, Path=Tag}" AlternateComparer="{StaticResource theSimpleComparer}" />

Second, the modified TLayout.xaml.cs file:

/* Copyright © Northwoods Software Corporation, 2008-2014. All Rights Reserved. */
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Northwoods.GoXam;
using Northwoods.GoXam.Layout;
using Northwoods.GoXam.Model;
namespace TLayout {
public partial class TLayout : UserControl {
public TLayout() {
InitializeComponent();
myDiagram.Model = new GraphLinksModel<SimpleData, String, String, LinkData>();
myDiagram.Model.Modifiable = true;
GenerateTree_Click(null, null);
}
Random rand = new Random();
// Takes the random collection of nodes and creates a random tree with them.
// Respects the minimum and maximum number of links from each node.
// (The minimum can be disregarded if we run out of nodes to link to)
private ObservableCollection GenerateLinks(ObservableCollection nodes) {
var linkSource = new ObservableCollection();
if (nodes.Count == 0) return linkSource;
int minLinks, maxLinks;
if (!int.TryParse(txtMinLinks.Text, out minLinks))
minLinks = 1;
if (!int.TryParse(txtMaxLinks.Text, out maxLinks) || minLinks > maxLinks)
maxLinks = minLinks;
List available = nodes.ToList();
foreach (SimpleData next in nodes) {
available.Remove(next);
int children = rand.Next(minLinks, maxLinks + 1);
for (int i = 1; i <= children; i++) {
if (available.Count == 0) break;
SimpleData to = available[0];
available.Remove(to);
linkSource.Add(new LinkData() { From = next.Key, To = to.Key });
}
}
return linkSource;
}
// Creates a collection of randomly colored nodes.
// Respects the maximum and minimum number of nodes.
private ObservableCollection GenerateNodes() {
var nodeSource = new ObservableCollection();
int minNodes, maxNodes;
if (!int.TryParse(txtMinNodes.Text, out minNodes))
minNodes = 0;
if (!int.TryParse(txtMaxNodes.Text, out maxNodes) || minNodes > maxNodes)
maxNodes = minNodes;
int numberOfNodes = rand.Next(minNodes, maxNodes + 1);
for (int i = 0; i < numberOfNodes; i++) {
nodeSource.Add(new SimpleData() {
Key = i.ToString(),
Color = String.Format("#{0:X}{1:X}{2:X}", 120+rand.Next(100), 120+rand.Next(100), 120+rand.Next(100)),
Width = (rand.NextDouble() > 0.5 ? 50 : 0) + 10,
Height = (rand.NextDouble() > 0.5 ? 50 : 0) + 10
});
}
// Randomize the nodes a little:
for (int i = 0; i < nodeSource.Count; i++) {
int swap = rand.Next(0, nodeSource.Count);
SimpleData temp = nodeSource[swap];
nodeSource[swap] = nodeSource[i];
nodeSource[i] = temp;
}
return nodeSource;
}
// Generates a random tree respecting MinNodes/MaxNodes/MinLinks/MaxLinks
private void GenerateTree_Click(object sender, RoutedEventArgs e) {
var nodes = GenerateNodes();
myDiagram.Model.NodesSource = nodes;
var lmodel = myDiagram.Model as GraphLinksModel<SimpleData, String, String, LinkData>;
lmodel.LinksSource = GenerateLinks(nodes);
}
// When a RadioButton becomes checked, set the Tag of the button container to the button’s content (string)
// For internationalization, this should use RadioButton.Tag to hold the value, not the RadioButton.Content
private void RadioButton_Checked(object sender, RoutedEventArgs e) {
RadioButton rb = sender as RadioButton;
if (rb != null) {
FrameworkElement group = VisualTreeHelper.GetParent(rb) as FrameworkElement;
if (group != null) {
group.Tag = rb.Content;
}
}
}
}
// Compare vertexes by comparing the suffix of each node data key,
// treated as integer values rather than as strings.
public class SimpleComparer : IComparer {
public int Compare(TreeVertex x, TreeVertex y) {
SimpleData a = x.Node.Data as SimpleData;
SimpleData b = y.Node.Data as SimpleData;
int num1 = int.Parse(a.Key.Replace(“Node”, “”));
int num2 = int.Parse(b.Key.Replace(“Node”, “”));
return num1.CompareTo(num2);
}
}

// add some properties for each node data
public class SimpleData : GraphLinksModelNodeData {
public SimpleData() {
this.Width = 40;
this.Height = 40;
}
public String Color {
get { return _Color; }
set { if (_Color != value) { String old = _Color; _Color = value; RaisePropertyChanged(“Color”, old, value); } }
}
private String _Color = “White”;
public double Width { get; set; }
public double Height { get; set; }
}
// no additional properties are needed for link data
public class LinkData : Northwoods.GoXam.Model.GraphLinksModelLinkData<String, String> {}
public class CustomTreeLayout : TreeLayout {
public override TreeNetwork CreateNetwork() {
return new CustomTreeNetwork();
}
protected override void AssignTreeVertexValues(TreeVertex v) {
if (v.ChildrenCount == 0) return;
// when we see a root vertex, first sort the whole tree
if (v.Parent == null) SortTree(v);
var horiz = (v.Angle == 0 || v.Angle == 180);
//// aligning all children?
//if (horiz) {
// // use maximum width needed for all immediate child vertexes
// var maxw = v.Children.Max(tv => tv.Width);
// foreach (var c in v.Children) {
// c.Bounds = new Rect(0, 0, maxw, c.Bounds.Height);
// c.Focus = new Point(maxw/2, c.Focus.Y);
// }
//} else {
// // use maximum height needed for all immediate child vertexes
// var maxh = v.Children.Max(tv => tv.Height);
// foreach (var c in v.Children) {
// c.Bounds = new Rect(0, 0, c.Bounds.Width, maxh);
// c.Focus = new Point(c.Focus.X, maxh/2);
// }
//}
if (v.Alignment == TreeAlignment.Start &&
(v.Parent == null || v.Parent.Children[0] != v)) {
// compute and set maximum width needed for first chain
SetAllFirstChild(v, horiz, MaxFirstChild(v, horiz));
}
}
private void SortTree(TreeVertex v) {
if (v == null) return;
base.SortTreeVertexChildren(v);
foreach (TreeVertex c in v.Children) {
SortTree©;
}
}
protected override void SortTreeVertexChildren(TreeVertex v) {
// no-op, because sorting is now done much earlier, during AssignTreeVertexValues
}
private double MaxFirstChild(TreeVertex v, bool horiz) {
var val = (horiz ? v.Height : v.Width);
if (v.ChildrenCount > 0) {
val = Math.Max(val, MaxFirstChild(v.Children[0], horiz));
}
return val;
}
private void SetAllFirstChild(TreeVertex v, bool horiz, double z) {
if (horiz) {
v.Bounds = new Rect(0, 0, v.Bounds.Width, z);
v.Focus = new Point(v.Focus.X, z/2);
} else {
v.Bounds = new Rect(0, 0, z, v.Bounds.Height);
v.Focus = new Point(z/2, v.Focus.Y);
}
if (v.ChildrenCount > 0) {
SetAllFirstChild(v.Children[0], horiz, z);
}
}
}
public class CustomTreeNetwork : TreeNetwork {
public override TreeVertex CreateVertex() {
return new CustomTreeVertex();
}
}
public class CustomTreeVertex : TreeVertex {
public override void CommitPosition() {
Node node = this.Node;
if (node != null) {
// don’t move node to this.Position, but move it so that the LocationElement’s center is at this.Focus
var off = node.GetRelativeElementPoint(node.LocationElement, Spot.Center);
var pt = new Point(this.Bounds.X + this.Focus.X - off.X, this.Bounds.Y + this.Focus.Y - off.Y);
node.Move(pt, true);
}
}
}
}

To use this new layout:
Copy the three classes named “CustomTree…” into your C# sources.
Change your use of go:TreeLayout to be local:CustomTreeLayout in your XAML.

Great!

This is what I want.

Thanks walter!Wink