miroiu / nodify

Highly performant and modular controls for node-based editors designed for data-binding and MVVM.
https://miroiu.github.io/nodify
MIT License
1.38k stars 224 forks source link

How to Display a Custom Element in the Middle of a Connection Line on Mouse Hover #121

Closed zt199510 closed 4 months ago

zt199510 commented 4 months ago

I hope this message finds you well. I am currently working on a project where I am utilizing your library. I would like to know how I can make a custom element appear in the middle of a connection line when the mouse cursor hovers over it. Could you please provide an example or guide me on how to achieve this?

zt199510 commented 4 months ago

Thank you for your assistance and for maintaining such a useful project.

miroiu commented 4 months ago

Hi, thanks for the kind words. Here are two examples:

Creating a custom connection from scratch

custom-conn

1. Create a user control (or custom control) to draw the connection

<UserControl x:Class="Nodify.Shapes.Canvas.CustomConnectionView"
             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:Nodify.Shapes.Canvas"
             d:DataContext="{d:DesignInstance Type=local:ConnectionViewModel}"
             mc:Ignorable="d">
    <Canvas>
        <Line X1="{Binding Source.Anchor.X}"
              X2="{Binding Target.Anchor.X}"
              Y1="{Binding Source.Anchor.Y}"
              Y2="{Binding Target.Anchor.Y}"
              Stroke="White"
              StrokeThickness="3"
              Cursor="Hand" />

        <Border x:Name="CustomElement"
                Visibility="Hidden">
            <Ellipse Stretch="Fill"
                     Fill="#38754e"
                     Stroke="#5cc180"
                     StrokeThickness="2"
                     Width="50"
                     Height="50" />
        </Border>
    </Canvas>
</UserControl>

2. Override OnMouseEnter and OnMouseLeave

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Nodify.Shapes.Canvas
{
    public partial class CustomConnectionView : UserControl
    {
        public CustomConnectionView2()
        {
            InitializeComponent();
        }

        protected override void OnMouseEnter(MouseEventArgs e)
        {
            var connection = (ConnectionViewModel)DataContext;

            var midPoint = (Vector)((Vector)connection.Target.Anchor + connection.Source.Anchor) / 2;
            var center = new Point(midPoint.X - CustomElement.ActualWidth / 2, midPoint.Y - CustomElement.ActualHeight / 2);

            System.Windows.Controls.Canvas.SetTop(CustomElement, center.Y);
            System.Windows.Controls.Canvas.SetLeft(CustomElement, center.X);
            CustomElement.Visibility = Visibility.Visible;
        }

        protected override void OnMouseLeave(MouseEventArgs e)
        {
            CustomElement.Visibility = Visibility.Hidden;
        }
    }
}

3. Set the ConnectionTemplate of the NodifyEditor to use the custom connection view

<nodify:NodifyEditor ...>
     <nodify:NodifyEditor.ConnectionTemplate>
         <DataTemplate DataType="{x:Type local:ConnectionViewModel}">
             <local:CustomConnectionView />
         </DataTemplate>
     </nodify:NodifyEditor.ConnectionTemplate>
</nodify:NodifyEditor>

Using the built-in connection types

custom-conn2

In the previous example, you can notice that the math to get the midpoint is simple because we use a line for the connection. However, that's not the case for the built-in connection. We can use the GetTextPosition method from the BaseConnection to get the text position returning the midpoint for LineConnection and Connection (bezier curve), and the midpoint of the largest segment for StepConnection and CircuitConnection.

1. Create a connection adapter for the desired connection type

This is needed because the GetTextPosition method is protected and we need it to calculate the midpoint.

using System.Globalization;
using System.Windows;
using System.Windows.Media;

namespace Nodify.Shapes.Canvas
{
    public class StepConnectionAdapter : StepConnection
    {
        private static readonly FormattedText _dummyText = new FormattedText(string.Empty, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface("Times New Roman"), 1, Brushes.Red, 120);

        public Point GetMidpoint()
        {
            (Vector sourceOffset, Vector targetOffset) = GetOffset();
            return GetTextPosition(_dummyText, Source + sourceOffset, Target + targetOffset);
        }
    }
}

2. Create a user control (or custom control) to draw the connection

<UserControl x:Class="Nodify.Shapes.Canvas.StepConnectionView"
             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:Nodify.Shapes.Canvas"
             d:DataContext="{d:DesignInstance Type=local:ConnectionViewModel}"
             mc:Ignorable="d">
    <Canvas>
        <local:StepConnectionAdapter x:Name="StepConnectionRef"
                                Source="{Binding Source.Anchor}"
                                Target="{Binding Target.Anchor}"
                                SourcePosition="{Binding Source.Position}"
                                TargetPosition="{Binding Target.Position}"
                                Cursor="Hand"
                                Stroke="White"
                                StrokeThickness="3" />

        <Border x:Name="CustomElement"
                Visibility="Hidden">
            <Ellipse Stretch="Fill"
                     Fill="#38754e"
                     Stroke="#5cc180"
                     StrokeThickness="2"
                     Width="50"
                     Height="50" />
        </Border>
    </Canvas>
</UserControl>

3. Override OnMouseEnter and OnMouseLeave

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Nodify.Shapes.Canvas
{
    public partial class StepConnectionView : UserControl
    {
        public CustomConnectionView()
        {
            InitializeComponent();
        }

        protected override void OnMouseEnter(MouseEventArgs e)
        {
            var midPoint = StepConnectionRef.GetMidpoint();
            var center = new Point(midPoint.X - CustomElement.ActualWidth / 2, midPoint.Y - CustomElement.ActualHeight / 2);

            System.Windows.Controls.Canvas.SetTop(CustomElement, center.Y);
            System.Windows.Controls.Canvas.SetLeft(CustomElement, center.X);
            CustomElement.Visibility = Visibility.Visible;
        }

        protected override void OnMouseLeave(MouseEventArgs e)
        {
            CustomElement.Visibility = Visibility.Hidden;
        }
    }
}

4. Set the ConnectionTemplate of the NodifyEditor to use the custom connection view

<nodify:NodifyEditor ...>
     <nodify:NodifyEditor.ConnectionTemplate>
         <DataTemplate DataType="{x:Type local:ConnectionViewModel}">
             <local:StepConnectionView />
         </DataTemplate>
     </nodify:NodifyEditor.ConnectionTemplate>
</nodify:NodifyEditor>

Let me know if you have any questions.

zt199510 commented 4 months ago

Thank you