I make an application for drawing up a dialog tree by drawing up a flowchart. The block displays the text of the message and below are the response options. When deleting the answer option, I need to move all the following elements up, as well as move their transition lines so that they do not hang at the pin’s place before the moment of deletion.
In general, the structure is as follows: the State element contains a StackPanel , which in turn contains many TransitionPin elements (a pin for an outgoing connection), and also a State contains one pin for an incoming connection. Each TransitionPin can refer to one Transition element (transition line). Transition already contains the starting coordinates (X1, Y1) and ending (X2, Y2). Transition refers to the connected Node object.
Suppose I delete the first TransitionPin , then all the rest are shifted up and I, respectively, need to specify their new starting coordinates for all their lines. But since at the moment of removing the object itself from the contents of the window it is still on the screen, the coordinates are recalculated from already obsolete data. It turns out that I need to wait until the item is removed from the screen graphically (when all the other elements have already moved up) and only then recalculate the values.
Just in case, I attach all the main files. Also, if someone comes up with a way to simplify this model, I would be very grateful.
TransitionPin.xaml.cs
public partial class TransitionPin : UserControl { public delegate void ActionDelegate(TransitionPin sender); public event ActionDelegate onremove; public event ActionDelegate onbeginconnect; public event ActionDelegate onendconnect; public Transition transition; public TransitionPin() { InitializeComponent(); } private void Remove(object sender, RoutedEventArgs e) { onremove?.Invoke(this); } private void BeginConnect(object sender, MouseButtonEventArgs e) { onbeginconnect?.Invoke(this); } private void EndConnect(object sender, MouseButtonEventArgs e) { onendconnect?.Invoke(this); } public void UpdateTransition() { Point from = Point.Add(Pin.TransformToAncestor(MainWindow.main).Transform(new Point(10, 10)), MainWindow.offset); transition.X1 = from.X; transition.Y1 = from.Y; } } TransitionPin.xaml
<UserControl x:Class="Conversator.TransitionPin" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Conversator" mc:Ignorable="d" d:DesignWidth="290"> <Grid Cursor="Arrow"> <Thumb x:Name="Pin" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,-17,0" PreviewMouseLeftButtonDown="BeginConnect" PreviewMouseLeftButtonUp="EndConnect" DragDelta="Drag" Style="{DynamicResource PinStyle}"/> <Border BorderThickness="2" BorderBrush="#FF787878" Background="#FFAAAAAA" CornerRadius="4" Padding="5"> <Grid> <Button HorizontalAlignment="Right" VerticalAlignment="Top" Width="20" Height="20" Click="Remove" Cursor="Hand"> <Button.Template> <ControlTemplate TargetType="{x:Type Button}"> <Border x:Name="Border" BorderThickness="2" CornerRadius="4" BorderBrush="#FFFF3232" Background="#FFFF7878"> <Path Fill="White" Data="M2,0 L6,4 L10,0 L12,2 L8,6 L12,10 L10,12 L 6,8 L2,12 L0,10 L4,6 L0,2" VerticalAlignment="Center" HorizontalAlignment="Center"/> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="Border" Property="Background" Value="#FFFF6E6E"/> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter TargetName="Border" Property="Background" Value="#FFFF6464"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Button.Template> </Button> </Grid> </Border> </Grid> </UserControl> State.xaml.cs
public partial class State : Node { public State() { InitializeComponent(); } private void BeginConnect(TransitionPin sender) { Transition transition = new Transition(); Point from = sender.Pin.TransformToAncestor(MainWindow.main).Transform(new Point(0, 0)); from = Point.Add(Point.Add(from, new Vector(10, 10)), MainWindow.offset); MainWindow.main.span.Children.Insert(0, transition); MainWindow.activeTransition = transition; transition.X1 = from.X; transition.Y1 = from.Y; transition.X2 = from.X; transition.Y2 = from.Y; } private void EndConnect(TransitionPin sender) { Point mouse = Mouse.GetPosition(MainWindow.main); HitTestResult hitTest = VisualTreeHelper.HitTest(MainWindow.main, mouse); if (hitTest != null && hitTest.VisualHit.GetType() == typeof(Border) && ((Border)hitTest.VisualHit).Tag?.ToString() == "Input") { Border border = ((Border)hitTest.VisualHit); object control = border.TemplatedParent; while (!control.GetType().IsSubclassOf(typeof(Node))) control = ((FrameworkElement)control).Parent; Node node = (Node)control; if (node != this) { if (sender.transition != null) { MainWindow.main.span.Children.Remove(sender.transition); Transitions.Remove(sender.transition); } Transition transition = MainWindow.activeTransition; Point to = border.TransformToAncestor(MainWindow.main).Transform(new Point(10, 10)); transition.X2 = to.X + MainWindow.offset.X; transition.Y2 = to.Y + MainWindow.offset.Y; transition.target = (Node)control; ((Node)control).onreposition += transition.TargetReposition; Transitions.Add(transition); sender.transition = transition; } } else { if (MainWindow.activeTransition != null) MainWindow.main.span.Children.Remove(MainWindow.activeTransition); } MainWindow.activeTransition = null; } private void RemovePin(TransitionPin sender) { Pins.Children.Remove(sender); MainWindow.main.span.Children.Remove(sender.transition); Transitions.Remove(sender.transition); foreach (TransitionPin pin in Pins.Children) pin.UpdateTransition(); } private void Add(object sender, MouseButtonEventArgs e) { TransitionPin pin = new TransitionPin(); Pins.Children.Add(pin); pin.Margin = new Thickness(0, 5, 0, 0); pin.onremove += RemovePin; pin.onbeginconnect += BeginConnect; pin.onendconnect += EndConnect; } } State.xaml
<local:Node x:Class="Conversator.State" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Conversator" mc:Ignorable="d" VerticalAlignment="Top" HorizontalAlignment="Left"> <Grid x:Name="Grid" Width="310"> <Thumb Tag="Input" HorizontalAlignment="Left" VerticalAlignment="Top" Style="{DynamicResource PinStyle}" IsEnabled="False"/> <Border Margin="10,10,0,0" BorderThickness="2" BorderBrush="#AAAAAA" Background="#F0F0F0" CornerRadius="4" Padding="5" Cursor="SizeAll"> <Grid> <StackPanel> <StackPanel x:Name="Pins"/> <Button Margin="0,5,0,0"> <Button.Template> <ControlTemplate TargetType="{x:Type Button}"> <Border BorderThickness="2" BorderBrush="#FF969696" Background="#FFC8C8C8" CornerRadius="4" MouseLeftButtonDown="Add" Cursor="Hand" Padding="5"> <TextBlock Text="<Добавить вариант ответа>" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5"/> </Border> </ControlTemplate> </Button.Template> </Button> </StackPanel> </Grid> </Border> </Grid> </local:Node> Node.cs
public class Node : UserControl { public delegate void RepositionDelegate(Vector direction); public event RepositionDelegate onreposition; public void OnReposition(Vector direction) { onreposition?.Invoke(direction); } public List<Transition> Transitions = new List<Transition>(); } Transition.xaml.cs
public partial class Transition : UserControl { public Node target; public double X1 { get { return (double)GetValue(X1Property); } set { SetValue(X1Property, value); } } public static readonly DependencyProperty X1Property = DependencyProperty.RegisterAttached(nameof(X1), typeof(double), typeof(Transition), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (obj, val) => { ((Transition)obj).Recount((double)val.NewValue, null, null, null); })); public double Y1 { get { return (double)GetValue(Y1Property); } set { SetValue(Y1Property, value); } } public static readonly DependencyProperty Y1Property = DependencyProperty.RegisterAttached(nameof(Y1), typeof(double), typeof(Transition), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (obj, val) => { ((Transition)obj).Recount(null, (double)val.NewValue, null, null); })); public double X2 { get { return (double)GetValue(X2Property); } set { SetValue(X2Property, value); } } public static readonly DependencyProperty X2Property = DependencyProperty.RegisterAttached(nameof(X2), typeof(double), typeof(Transition), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (obj, val) => { ((Transition)obj).Recount(null, null, (double)val.NewValue, null); })); public double Y2 { get { return (double)GetValue(Y2Property); } set { SetValue(Y2Property, value); } } public static readonly DependencyProperty Y2Property = DependencyProperty.RegisterAttached(nameof(Y2), typeof(double), typeof(Transition), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (obj, val) => { ((Transition)obj).Recount(null, null, null, (double)val.NewValue); })); public void Recount(double? newX1, double? newY1, double? newX2, double? newY2) { double x1 = newX1 == null ? X1 : (double)newX1; double y1 = newY1 == null ? Y1 : (double)newY1; double x2 = newX2 == null ? X2 : (double)newX2; double y2 = newY2 == null ? Y2 : (double)newY2; Margin = new Thickness(x1, y1, 0, 0); double deltaX = x2 - x1; double deltaY = y2 - y1; SetValue(LengthProperty, Math.Sqrt(Math.Pow(deltaX, 2) + Math.Pow(deltaY, 2))); SetValue(RotationProperty, Math.Atan2(deltaY, deltaX) / (2 * Math.PI) * 360); } public double Length { get { return (double)GetValue(LengthProperty); } } public static readonly DependencyProperty LengthProperty = DependencyProperty.RegisterAttached(nameof(Length), typeof(double), typeof(Transition)); public double Rotation { get { return (double)GetValue(RotationProperty); } } public static readonly DependencyProperty RotationProperty = DependencyProperty.RegisterAttached(nameof(Rotation), typeof(double), typeof(Transition)); public void TargetReposition(Vector delta) { X2 = X2 + delta.X; Y2 = Y2 + delta.Y; } ~Transition() { if (target != null) target.onreposition -= TargetReposition; } public Transition() { InitializeComponent(); } } Transition.xaml
<UserControl x:Name="userControl" x:Class="Conversator.Transition" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Conversator" mc:Ignorable="d" d:DesignHeight="50" d:DesignWidth="50" VerticalAlignment="Top" VerticalContentAlignment="Top" HorizontalContentAlignment="Left" HorizontalAlignment="Left"> <Grid RenderTransformOrigin="0,0" Height="2" Margin="0,-1,0,0" Width="{Binding Length, ElementName=userControl, Mode=OneWay}" Background="#FF141414"> <Grid.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform Angle="{Binding Rotation, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Transition}}}"/> <TranslateTransform/> </TransformGroup> </Grid.RenderTransform> <TextBlock Text="{Binding Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Transition}}}" VerticalAlignment="Center" HorizontalAlignment="Center"/> </Grid> </UserControl> MainWindow.xaml.cs
public partial class MainWindow : Window { List<State> states = new List<State>(); Point pos = new Point(0, 0); public static Transition activeTransition = null; public static MainWindow main; public static Vector offset; public MainWindow() { main = this; InitializeComponent(); } private void AddState(object sender, RoutedEventArgs e) { State state = new State(); states.Add(state); span.Children.Add(state); Point mouse = Point.Add(Mouse.GetPosition(this), offset); state.Margin = new Thickness(mouse.X - 10, mouse.Y - 10, 0, 0); } private void Scroll(object sender, ScrollChangedEventArgs e) { offset = new Vector(e.HorizontalOffset, e.VerticalOffset); } } MainWindow.xaml
<Window x:Class="Conversator.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Conversator" xmlns:controls="clr-namespace:Conversator" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525" WindowState="Maximized"> <Grid> <Grid.ContextMenu> <ContextMenu> <MenuItem Header="Добавить положение" Click="AddState"/> </ContextMenu> </Grid.ContextMenu> <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible" ScrollChanged="Scroll"> <Grid x:Name="span" VerticalAlignment="Top" HorizontalAlignment="Left"/> </ScrollViewer> </Grid> </Window> App.xaml
<Application x:Class="Conversator.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Conversator" StartupUri="MainWindow.xaml"> <Application.Resources> <Style TargetType="{x:Type Thumb}" x:Key="PinStyle"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Thumb}"> <Border Tag="{TemplateBinding Tag}" Name="Border" CornerRadius="10" BorderThickness="2" Width="20" Height="20" Background="#FF323232" Cursor="Hand" BorderBrush="#FF141414"/> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="Border" Property="Background" Value="#FF505050"/> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter TargetName="Border" Property="Cursor" Value="Arrow"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </Application.Resources> </Application> Here is what happens when you delete the first element at the moment:

OnVisualChildrenChanged- Andrey NOPTransitionPin) are located in theStackPanel. Each element is summed line (Transition). When the first one disappears, all the following elements move upwards, respectively, I need to find out the new position of each of them and re-lead the line to it. - Puro