Wpf 在图像和 mvvm 控件之间绘制对角线
Wpf drawing diagonal lines between image and mvvm control
在我的示例 wpf 应用程序中,我有一张房子的图片,我在 xaml 中使用椭圆在上面绘制了 4 个湿度传感器。为了在正确的位置绘制传感器,我使用了网格列和行。为了显示传感器值,我创建了一个 HumidityView,它绘制了一个矩形和一个包含实际测量的湿度值的停靠面板。
<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>
我的问题是关于从传感器到视图控件的画线。现在我想出了使用网格并在网格中绘制水平线。我真正想做的是从传感器绘制对角线以查看控件。
我找到了图表解决方案,但实现仅使用 canvas,它不支持像网格那样定位控件。
执行此操作的最佳方法是什么?
[编辑 => 有问题的代码更新为在网格中绘制对角线的选项]
您可以创建自定义控件,以便在位于同一父元素内的任意两个控件之间画一条线。
自定义控件会将公共父元素和要连接的两个元素作为参数,然后获取它们的位置和大小,以便为它们之间的直线计算正确的起点和终点。
在我的示例代码中,我从元素的中间画线,但是给定元素矩形,您可以实现任何其他逻辑来确定所需的线端点。
请注意,该示例只是一个小演示,可能既不高效也不完全可用。
自定义控件代码:
/// <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
}
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>
使用示例
<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>
在我的示例 wpf 应用程序中,我有一张房子的图片,我在 xaml 中使用椭圆在上面绘制了 4 个湿度传感器。为了在正确的位置绘制传感器,我使用了网格列和行。为了显示传感器值,我创建了一个 HumidityView,它绘制了一个矩形和一个包含实际测量的湿度值的停靠面板。
<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>
我的问题是关于从传感器到视图控件的画线。现在我想出了使用网格并在网格中绘制水平线。我真正想做的是从传感器绘制对角线以查看控件。
我找到了图表解决方案,但实现仅使用 canvas,它不支持像网格那样定位控件。
执行此操作的最佳方法是什么?
[编辑 => 有问题的代码更新为在网格中绘制对角线的选项]
您可以创建自定义控件,以便在位于同一父元素内的任意两个控件之间画一条线。
自定义控件会将公共父元素和要连接的两个元素作为参数,然后获取它们的位置和大小,以便为它们之间的直线计算正确的起点和终点。
在我的示例代码中,我从元素的中间画线,但是给定元素矩形,您可以实现任何其他逻辑来确定所需的线端点。
请注意,该示例只是一个小演示,可能既不高效也不完全可用。
自定义控件代码:
/// <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
}
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>
使用示例
<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>