1

In my sample wpf app i have a picture of a house onto which i have drawn 4 humidity sensors using ellipse in xaml. To draw the sensors in the correct location i have used grid columns and rows. To display the sensor values i created a HumidityView which draws a rectangle and a dockpanel containing the actual measured humidity value.

<Window x:Class="WpfHouseExample.Views.MainView"
        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:WpfHouseExample.Views"
        mc:Ignorable="d"
        Background="Transparent"
        Title="MainView" Height="450" Width="300">

    <Grid ShowGridLines="True">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <ContentControl Grid.Row="2" Grid.Column="0" Grid.RowSpan="2" x:Name="Humidity1" Margin="0,0,0,2"  HorizontalAlignment="Right"/>
        <ContentControl Grid.Row="6" Grid.Column="0" Grid.RowSpan="2" x:Name="Humidity2" Margin="0,0,0,2"/>
        <ContentControl Grid.Row="2" Grid.Column="5" Grid.RowSpan="2" x:Name="Humidity3" Margin="0,0,0,2"/>
        <ContentControl Grid.Row="6" Grid.Column="5" Grid.RowSpan="2" x:Name="Humidity4" Margin="0,0,0,2"/>
        <Image Grid.Column="1" Grid.ColumnSpan="4" Grid.RowSpan="11" Source="pack://application:,,,/Images/House.png" Margin="20"/>
        <Ellipse Width="20" Height="20" Fill="LightGreen" Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="3" Grid.RowSpan="2"/>
        <Ellipse Width="20" Height="20" Fill="LightGreen" Grid.Column="3" Grid.ColumnSpan="2" Grid.Row="3" Grid.RowSpan="2"/>
        <Ellipse Width="20" Height="20" Fill="LightGreen" Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="5" Grid.RowSpan="2"/>
        <Ellipse Width="20" Height="20" Fill="LightGreen" Grid.Column="3" Grid.ColumnSpan="2" Grid.Row="5" Grid.RowSpan="2"/>
        <Line Stroke="LightGreen" StrokeThickness="2" Grid.Column="1" Grid.Row="3" Stretch="Fill" X2="1" Y2="1"/>
        <Line Stroke="LightGreen" StrokeThickness="2" Grid.Column="1" Grid.Row="6" Stretch="Fill" X2="1" Y1="1"/>
        <Line Stroke="LightGreen" StrokeThickness="2" Grid.Column="4" Grid.Row="3" Stretch="Fill" X2="1" Y1="1"/>
        <Line Stroke="LightGreen" StrokeThickness="2" Grid.Column="4" Grid.Row="6" Stretch="Fill" X2="1" Y2="1"/>
    </Grid>
</Window>

My question is about drawing lines from the sensor to the view control. Now i figured out to use the grid and draw horizontal lines in the grid. What i really would like to do is draw diagonal lines from a sensor to view control.
I have found diagramming solutions but that imlementations use only a canvas which does not support positioning of the controls like a grid does. What is the best way to do this?

[Edit => code in question is updated with option to draw diagonal lines in grid]

RBCSharp
  • 17
  • 5
  • 2
    I would use a `Canvas` instead of a `Grid` for such a drawing. – Mario Vernari Aug 30 '21 at 10:01
  • With a canvas you need absolute positions, the benefit of grid is that placement of the controls is automatically. Can you advise how to combine the placement of the view with the image and the sensors? – RBCSharp Aug 30 '21 at 10:18
  • There is no straightforward way to draw a Line between the centers of two Grid cells in XAML. You may however draw a Line between two adjacent corners of a rectangle that covers multiple cells with either `X2="1" Y2="1"` or `X2="1" Y2="1"` and appropriate ColumnSpans and RowSpans. So to get cell centers you could double the row and column counts. – Clemens Aug 30 '21 at 10:54
  • @RBCSharp your idea is correct, and it's worthwhile whereas you've to deal with many screens. However, it's not simple to code the logic than helps you to "snap" a line to a shape. If you have few shapes and few lines, there's no game between the cost of a Canvas and an automatic arrangement. – Mario Vernari Aug 30 '21 at 15:31

1 Answers1

0

You could create a custom control in order to draw a line between any two controls that are located within a common parent element.

The custom control would take the common parent, and the two elements to be connected as parameters, then get their positon and size in order to compute the correct start and end points for a line between them.

In my example code, I draw the line from the middle of the elements, but given the element rects, you can implement any other logic to determine the desired line end points.

Note the example is just a small demo and might neither be efficient nor completely usable.

Custom Control code:

/// <summary>
/// Custom Line control to draw a line between two other controls
/// </summary>
public class LineConnectorControl : Control
{
    static LineConnectorControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LineConnectorControl), new FrameworkPropertyMetadata(typeof(LineConnectorControl)));
    }

    #region Target Properties for Visual Line

    public double X1
    {
        get { return (double)GetValue(X1Property); }
        set { SetValue(X1Property, value); }
    }

    // Using a DependencyProperty as the backing store for X1.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty X1Property =
        DependencyProperty.Register("X1", typeof(double), typeof(LineConnectorControl), new PropertyMetadata(0d));



    public double X2
    {
        get { return (double)GetValue(X2Property); }
        set { SetValue(X2Property, value); }
    }

    // Using a DependencyProperty as the backing store for X2.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty X2Property =
        DependencyProperty.Register("X2", typeof(double), typeof(LineConnectorControl), new PropertyMetadata(0d));



    public double Y1
    {
        get { return (double)GetValue(Y1Property); }
        set { SetValue(Y1Property, value); }
    }

    // Using a DependencyProperty as the backing store for Y1.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty Y1Property =
        DependencyProperty.Register("Y1", typeof(double), typeof(LineConnectorControl), new PropertyMetadata(0d));


    public double Y2
    {
        get { return (double)GetValue(Y2Property); }
        set { SetValue(Y2Property, value); }
    }

    // Using a DependencyProperty as the backing store for Y2.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty Y2Property =
        DependencyProperty.Register("Y2", typeof(double), typeof(LineConnectorControl), new PropertyMetadata(0d));

    #endregion

    #region Source Elements needed to compute Visual Line

    // Positions are computed relative to this element
    public FrameworkElement PositionRoot
    {
        get { return (FrameworkElement)GetValue(PositionRootProperty); }
        set { SetValue(PositionRootProperty, value); }
    }

    // This is the starting point of the line
    public FrameworkElement ConnectedControl1
    {
        get { return (FrameworkElement)GetValue(ConnectedControl1Property); }
        set { SetValue(ConnectedControl1Property, value); }
    }

    // This is the ending point of the line
    public FrameworkElement ConnectedControl2
    {
        get { return (FrameworkElement)GetValue(ConnectedControl2Property); }
        set { SetValue(ConnectedControl2Property, value); }
    }

    // Using a DependencyProperty as the backing store for PositionRoot.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty PositionRootProperty =
        DependencyProperty.Register("PositionRoot", typeof(FrameworkElement), typeof(LineConnectorControl), new FrameworkPropertyMetadata(new PropertyChangedCallback(ElementChanged)));

    // Using a DependencyProperty as the backing store for ConnectedControl1.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ConnectedControl1Property =
        DependencyProperty.Register("ConnectedControl1", typeof(FrameworkElement), typeof(LineConnectorControl), new FrameworkPropertyMetadata(new PropertyChangedCallback(ElementChanged)));

    // Using a DependencyProperty as the backing store for ConnectedControl2.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ConnectedControl2Property =
        DependencyProperty.Register("ConnectedControl2", typeof(FrameworkElement), typeof(LineConnectorControl), new FrameworkPropertyMetadata(new PropertyChangedCallback(ElementChanged)));

    #endregion

    #region Update logic to compute line coordinates based on Source Elements

    private static void ElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var self = (LineConnectorControl)d;
        self.UpdatePositions();
        var fr = (FrameworkElement)e.NewValue;
        fr.SizeChanged += self.ElementSizeChanged;
    }

    private void ElementSizeChanged(object sender, SizeChangedEventArgs e)
    {
        UpdatePositions();
    }

    private void UpdatePositions()
    {
        if (PositionRoot != null && ConnectedControl1 != null && ConnectedControl2 != null)
        {
            Rect rect1 = GetRootedRect(ConnectedControl1);
            Rect rect2 = GetRootedRect(ConnectedControl2);

            X1 = rect1.Location.X + (rect1.Width / 2);
            Y1 = rect1.Location.Y + (rect1.Height / 2);
            X2 = rect2.Location.X + (rect2.Width / 2);
            Y2 = rect2.Location.Y + (rect2.Height / 2);
        }
    }

    private Rect GetRootedRect(FrameworkElement childControl)
    {
        var rootRelativePosition = childControl.TransformToAncestor(PositionRoot).Transform(new Point(0, 0));
        return new Rect(rootRelativePosition, new Size(childControl.ActualWidth, childControl.ActualHeight));
    }

    #endregion
}

Custom Control visual style in Generic.xaml

<Style TargetType="{x:Type local:LineConnectorControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:LineConnectorControl}">
                <Line X1="{TemplateBinding X1}" X2="{TemplateBinding X2}" Y1="{TemplateBinding Y1}" Y2="{TemplateBinding Y2}" Stroke="Red" StrokeThickness="2"></Line>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Usage example

<Grid Name="parentGrid">

    <Grid Name="myGrid" ShowGridLines="True">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" MinWidth="50"/>
            <ColumnDefinition Width="Auto" MinWidth="50"/>
            <ColumnDefinition Width="Auto" MinWidth="50"/>
            <ColumnDefinition Width="Auto" MinWidth="50"/>
            <ColumnDefinition Width="Auto" MinWidth="50"/>
            <ColumnDefinition Width="Auto" MinWidth="50"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Border x:Name="Humidity1" Grid.Row="0" Grid.Column="4" MinWidth="30" Background="Yellow" HorizontalAlignment="Right"/>
        <Border x:Name="Humidity2" Grid.Row="3" Grid.Column="0" Grid.RowSpan="2" Background="Green"/>
    </Grid>
    <!--connecting line-->
    <local:LineConnectorControl PositionRoot="{Binding ElementName=parentGrid}" ConnectedControl1="{Binding ElementName=Humidity1}" ConnectedControl2="{Binding ElementName=Humidity2}"/>
</Grid>
grek40
  • 13,113
  • 1
  • 24
  • 50
  • Thanks for this great answer, i will have to try it out. – RBCSharp Aug 31 '21 at 16:41
  • This option also works. I added DependencyProperties for Stroke and StrokeThickness and changed the logic so line connects to side instead of center. – RBCSharp Aug 31 '21 at 20:32
  • @RBCSharp glad to hear that you can use my sample as a starting point for a control that satisfies your specific requirements. For a clean implementation (didn't really have time for it) I would probably use [DependencyProperty.RegisterReadOnly](https://stackoverflow.com/a/1122611/5265292) for X1, X2, ... also the triggers for re-computation of the points can probably be optimized. – grek40 Aug 31 '21 at 20:56
  • @RBCSharp if your question is answered, consider [accepting it](https://stackoverflow.com/help/someone-answers) / [How does accepting an answer work?](https://meta.stackexchange.com/a/5235/397416) – grek40 Aug 31 '21 at 20:58