最近在项目中有这样一个需求,在界面上添加一条线段,可以拖拽移动两个端点来改变线段位置。对于这种需求,考虑采用自定义控件来做。
界面结构
这个控件的界面结构可以分解为一个画板、一条线段和两个端点,端点用于拖拽移动,线段用于连接端点,画板用于盛放端点和线段。由于在实际使用时,需要线段有一定的宽度,所以两个端点都是有一定的宽高,线段本身更像是长方形。
<UserControl x:Class="WPFDemo.Metro.MarkLine"
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:WPFDemo.Metro"
mc:Ignorable="d" >
<Canvas x:Name="cvs">
<Polyline x:Name="lineMain" Stroke="Blue" StrokeThickness="2" Points="{Binding Path=DataPoints,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type UserControl}}}"></Polyline>
<Thumb x:Name="thmStart" Canvas.Left="0" Canvas.Top="0" Cursor="Hand" IsEnabled="{Binding Path=IsEdit,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type UserControl}}}" DragDelta="thmStart_DragDelta">
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="16" Height="16" Fill="Blue"></Ellipse>
</ControlTemplate>
</Thumb.Template>
</Thumb>
<Thumb x:Name="thmEnd" Canvas.Left="0" Canvas.Top="0" Cursor="Hand" IsEnabled="{Binding Path=IsEdit,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type UserControl}}}" DragDelta="thmEnd_DragDelta">
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="16" Height="16" Fill="Red"></Ellipse>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
</UserControl>
为了提高绘制效率,线段采用了PolyLine。因为两个端点在形式是宽高分别x的圆形,所以线段实际端点的位置应该是在圆形的圆心,相差(x/2,x/2)。这里为了明显x=16。
同时考虑到美观性,要把两个端点圆放在线段的上一层,确保端点圆能够遮住线段边缘的棱角。
数据结构
理论上只需要确定两个端点的位置,就能绘制出整条线段。因此数据结构只需要提供两个端点坐标即可。下面就为这个控件添加一个自定义属性。
#region 端点数据集
public PointCollection DataPoints
{
get { return (PointCollection)GetValue(DataPointsProperty); }
set { SetValue(DataPointsProperty, value); }
}
// Using a DependencyProperty as the backing store for DataPoints. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataPointsProperty =
DependencyProperty.Register("DataPoints", typeof(PointCollection), typeof(MarkLine), new PropertyMetadata(new PointCollection() { new Point(8, 8), new Point(16, 16) }, DataPointsChanged));
private static void DataPointsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MarkLine line = (MarkLine)d;
if (e.NewValue != e.OldValue)
{
//数据集改变时重新绘制线段和设置端点圆形位置
PointCollection points = (PointCollection)e.NewValue;
Canvas.SetTop(line.thmStart, points[0].Y - 8);
Canvas.SetLeft(line.thmStart, points[0].X - 8);
Canvas.SetTop(line.thmEnd, points[1].Y - 8);
Canvas.SetLeft(line.thmEnd, points[1].X - 8);
}
}
#endregion
端点拖拽方法
线段两端的圆形使用的是可拖拽控件Thumb,拖拽功能的实现很简单,关键点在于拖拽过程中更改数据集并重新绘制线段。
private PointCollection _points = new PointCollection();
private void thmStart_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Thumb thm = (Thumb)sender;
double y = Canvas.GetTop(thm) + e.VerticalChange;
double x = Canvas.GetLeft(thm) + e.HorizontalChange;
Canvas.SetTop(thm, Canvas.GetTop(thm) + e.VerticalChange);
Canvas.SetLeft(thm, Canvas.GetLeft(thm) + e.HorizontalChange);
_points = this.DataPoints;
this.DataPoints = new PointCollection()
{
new Point(x+thm.ActualWidth/2,y+thm.ActualHeight/2),
_points[1]
};
}
private void thmEnd_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Thumb thm = (Thumb)sender;
double y = Canvas.GetTop(thm) + e.VerticalChange;
double x = Canvas.GetLeft(thm) + e.HorizontalChange;
Canvas.SetTop(thm, Canvas.GetTop(thm) + e.VerticalChange);
Canvas.SetLeft(thm, Canvas.GetLeft(thm) + e.HorizontalChange);
_points = this.DataPoints;
this.DataPoints = new PointCollection()
{
_points[0],
new Point(x+thm.ActualWidth/2,y+thm.ActualHeight/2)
};
}
测试示例
编写测试数据,添加一条线段到前台。
private void AddMarkLine()
{
MarkLine line = new MarkLine();
line.DataPoints = new PointCollection()
{
new Point(30,24),
new Point (160,180)
};
this.gridMain.Children.Add(line);
}
保存编译并运行,最终呈现效果如下:
从上面的示例继续拓展,因为PolyLine是折线,修改数据集的结构,可以实现多端折线且每个端点均可以拖拽。