Search code examples
c#wpfmvvmlinediagonal

Wpf drawing diagonal lines between image and mvvm control


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]


Solution

  • 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>