WPF-画箭头
需求:根据起点和终点,实现自定义方向箭头控件。
方法1:继承UIElement基类,在OnRender中画点。
方法2:参照WPF 源码中的Line等控件,继承Shape,定义Geometry。
下面我两种方式都有实现。
方式1:
using System.Collections.Generic; using System.Windows.Controls; using System.Windows.Media; using System.Windows; using System; namespace ArrowDemo { public class Arrow : Control { public List<Point> Points { get; set; } public int HeadWidth { get; set; } public int HeadHeight { get; set; } public int BodyWidth { get; set; } public int OffSet { get; set; } public Arrow() { BodyWidth = 15; HeadWidth = 60; HeadHeight = 25; OffSet = 10; } protected override void OnRender(DrawingContext drawingContext) { if (Points == null || Points.Count < 2) return; drawingContext.DrawGeometry(Brushes.Blue, new Pen() { DashStyle = new DashStyle(new List<double>() { 3, 1, 5 }, 0), Brush = Brushes.Yellow, Thickness = 2 }, GetArrowGetmetry(Points[0], Points[1])); } private StreamGeometry GetArrowGetmetry(Point start, Point end) { var g = new StreamGeometry(); //与X轴夹角 var theta = Math.PI - Math.Atan2(start.Y - end.Y, start.X - end.X); var angle = theta * 180 / Math.PI; var sint = Math.Sin(theta); var cost = Math.Cos(theta); var point0 = new Point(start.X - sint * BodyWidth / 2, start.Y - cost * BodyWidth / 2); var point1 = new Point(end.X - cost * HeadHeight - sint * BodyWidth / 2, end.Y + sint * HeadHeight - cost * BodyWidth / 2); var point2 = new Point(end.X - cost * HeadHeight - sint * HeadWidth / 2, end.Y + sint * HeadHeight - cost * HeadWidth / 2); var point3 = end; var point4 = new Point(end.X - cost * HeadHeight + sint * HeadWidth / 2, end.Y + sint * HeadHeight + cost * HeadWidth / 2); var point5 = new Point(end.X - cost * HeadHeight + sint * BodyWidth / 2, end.Y + sint * HeadHeight + cost * BodyWidth / 2); var point6 = new Point(start.X + sint * BodyWidth / 2, start.Y + cost * BodyWidth / 2); var offsetX = cost * OffSet; var offsetY = sint * OffSet; var headPoint0 = new Point(point2.X + offsetX, point2.Y - offsetY); var headPoint1 = new Point(point3.X + offsetX, point3.Y - offsetY); var headPoint2 = new Point(point4.X + offsetX, point4.Y - offsetY); using (StreamGeometryContext context = g.Open()) { //头部 context.BeginFigure(headPoint0, false, false); context.LineTo(headPoint1, true, true); context.LineTo(headPoint2, true, true); context.BeginFigure(point0, true, false); context.LineTo(point1, true, true); context.LineTo(point2, true, true); context.LineTo(point3, true, true); context.LineTo(point4, true, true); context.LineTo(point5, true, true); context.LineTo(point6, true, true); context.LineTo(point0, true, true); } return g; } } }
运行效果图:
方式2:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Shapes; namespace ArrowDemo { public static class ShapeHelper { /// <summary> /// 是否为有理数 /// </summary> /// <param name="o"></param> /// <returns></returns> public static bool IsDoubleFinite(object o) { double val = (double)o; return !(double.IsNaN(val) || double.IsInfinity(val)); } } public sealed class DirectionArrow : Shape { #region Dependency Property // 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(DirectionArrow), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty Y1Property = DependencyProperty.Register("Y1", typeof(double), typeof(DirectionArrow), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender), new ValidateValueCallback(ShapeHelper.IsDoubleFinite)); // 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(DirectionArrow), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender), new ValidateValueCallback(ShapeHelper.IsDoubleFinite)); public static readonly DependencyProperty Y2Property = DependencyProperty.Register("Y2", typeof(double), typeof(DirectionArrow), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender), new ValidateValueCallback(ShapeHelper.IsDoubleFinite)); // Using a DependencyProperty as the backing store for ArrowWidth. This enables animation, styling, binding, etc... public static readonly DependencyProperty ArrowWidthProperty = DependencyProperty.Register("ArrowWidth", typeof(double), typeof(DirectionArrow), new FrameworkPropertyMetadata(15d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender), new ValidateValueCallback(ShapeHelper.IsDoubleFinite)); // Using a DependencyProperty as the backing store for ArrowHeight. This enables animation, styling, binding, etc... public static readonly DependencyProperty ArrowHeightProperty = DependencyProperty.Register("ArrowHeight", typeof(double), typeof(DirectionArrow), new FrameworkPropertyMetadata(10d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender), new ValidateValueCallback(ShapeHelper.IsDoubleFinite)); // Using a DependencyProperty as the backing store for ArrowSpace. This enables animation, styling, binding, etc... public static readonly DependencyProperty ArrowSpaceProperty = DependencyProperty.Register("ArrowSpace", typeof(double), typeof(DirectionArrow), new FrameworkPropertyMetadata(5d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender), new ValidateValueCallback(ShapeHelper.IsDoubleFinite)); #endregion [TypeConverter(typeof(LengthConverter))] public double X1 { get { return (double)GetValue(X1Property); } set { SetValue(X1Property, value); } } [TypeConverter(typeof(LengthConverter))] public double Y1 { get { return (double)GetValue(Y1Property); } set { SetValue(Y1Property, value); } } [TypeConverter(typeof(LengthConverter))] public double X2 { get { return (double)GetValue(X2Property); } set { SetValue(X2Property, value); } } [TypeConverter(typeof(LengthConverter))] public double Y2 { get { return (double)GetValue(Y2Property); } set { SetValue(Y2Property, value); } } [TypeConverter(typeof(LengthConverter))] public double ArrowWidth { get { return (double)GetValue(ArrowWidthProperty); } set { SetValue(ArrowWidthProperty, value); } } [TypeConverter(typeof(LengthConverter))] public double ArrowHeight { get { return (double)GetValue(ArrowHeightProperty); } set { SetValue(ArrowHeightProperty, value); } } [TypeConverter(typeof(LengthConverter))] public double ArrowSpace { get { return (double)GetValue(ArrowSpaceProperty); } set { SetValue(ArrowSpaceProperty, value); } } /// <summary> /// 提供自定义数据 /// </summary> protected override Geometry DefiningGeometry { get { var geomertry = new StreamGeometry() { FillRule = FillRule.EvenOdd }; using (var context = geomertry.Open()) { DrawDirectionArrow(context); } geomertry.Freeze(); return geomertry; } } private void DrawDirectionArrow(StreamGeometryContext context) { var startPoint = new Point(X1, Y1); var endPoint = new Point(X2, Y2); var distance = GetDistance(startPoint, endPoint); //与X轴夹角 var theta = Math.PI - Math.Atan2(Y1 - Y2, X1 - X2); var cost = Math.Cos(theta); var sint = Math.Sin(theta); if (distance < (ArrowHeight + ArrowSpace)) { startPoint = new Point((startPoint.X + endPoint.X) / 2, (startPoint.Y + endPoint.Y) / 2); } var count = Math.Floor(distance / (ArrowHeight + ArrowSpace)); var arrowItemLenght = distance / count; var arrowOffsetX = arrowItemLenght * cost; var arrowOffsetY = arrowItemLenght * sint; var arrowWidthOffset = ArrowWidth / 1.5 / count; var arrowWidth = ArrowWidth / 1.5; for (int i = 0; i <= count; i++) { var p0 = new Point(startPoint.X - ArrowHeight / 2 * cost, startPoint.Y + ArrowHeight / 2 * sint); var p2 = new Point(p0.X - arrowWidth * sint, p0.Y - arrowWidth * cost); var p4 = new Point(p0.X + arrowWidth * sint, p0.Y + arrowWidth * cost); var p3 = new Point(startPoint.X + ArrowHeight / 2 * cost, startPoint.Y - ArrowHeight / 2 * sint); var p1 = new Point(p2.X - ArrowHeight * cost, p2.Y + ArrowHeight * sint); var p5 = new Point(p4.X - ArrowHeight * cost, p4.Y + ArrowHeight * sint); context.BeginFigure(p0, true, true); context.LineTo(p1, true, true); context.LineTo(p2, true, true); context.LineTo(p3, true, true); context.LineTo(p4, true, true); context.LineTo(p5, true, true); context.LineTo(p0, true, true); startPoint = new Point(startPoint.X + arrowOffsetX, startPoint.Y - arrowOffsetY); arrowWidth += arrowWidthOffset; } } private double GetDistance(Point p1, Point p2) { return Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2)); } } }
FrameworkPropertyMetadata中选择对Measure和Render有效,值更新就触发重绘逻辑。
MainWindow.xaml
<Window x:Class="ArrowDemo.MainWindow" 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:ArrowDemo" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Canvas x:Name="canvas" MouseDown="Grid_MouseDown" Background="Black"> <StackPanel Orientation="Horizontal" Height="50"> <Button Content="DirectionArrow" Click="Button_Click_1" Width="150"/> <Button Content="Draw" Click="Button_Click" Width="50"/> <Button Content="Clear" Click="Button_Click" Width="50"/> <Button Content="Animation" Click="Button_Click_2" Width="80"/> </StackPanel> </Canvas> </Window>
MainWindow.xaml.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace ArrowDemo { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } List<Arrow> arrows = new List<Arrow>(); List<Point> points = new List<Point>(); List<DirectionArrow> directionArrows = new List<DirectionArrow>(); List<Ellipse> ellipses = new List<Ellipse>(); private void Grid_MouseDown(object sender, MouseButtonEventArgs e) { var pos = e.GetPosition(canvas); //pos = new Point(pos.X - 2, pos.Y - 2); Ellipse ellipse = new Ellipse() { Width = 4, Height = 4, Fill = Brushes.White }; canvas.Children.Add(ellipse); Canvas.SetLeft(ellipse, pos.X - 2); Canvas.SetTop(ellipse, pos.Y - 2); points.Add(pos); ellipses.Add(ellipse); } private void Button_Click(object sender, RoutedEventArgs e) { Button button = sender as Button; if (button.Content.ToString() == "Clear") { Clear(); } else { Arrow arrow = new Arrow() { Points = new List<Point>(points) }; //Clear(); canvas.Children.Add(arrow); Canvas.SetZIndex(arrow, -100); arrows.Add(arrow); } } private void Clear() { foreach (var item in ellipses) { if (canvas.Children.Contains(item)) { canvas.Children.Remove(item); } } foreach (var item in arrows) { if (canvas.Children.Contains(item)) { canvas.Children.Remove(item); } } foreach (var item in directionArrows) { if (canvas.Children.Contains(item)) { canvas.Children.Remove(item); } } points.Clear(); ellipses.Clear(); arrows.Clear(); directionArrows.Clear(); } private void Button_Click_1(object sender, RoutedEventArgs e) { if (points.Count != 2) return; DirectionArrow directionArrow = new DirectionArrow() { X1 = points[0].X, Y1 = points[0].Y, X2 = points[1].X, Y2 = points[1].Y, Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#55e5ed")), Stroke = Brushes.Yellow, StrokeThickness = 2, ArrowSpace = 10, ArrowWidth = 20, ArrowHeight = 20 //StrokeDashArray = new DoubleCollection(new double[] { 2, 2 }) }; canvas.Children.Add(directionArrow); Canvas.SetZIndex(directionArrow, -100); directionArrows.Add(directionArrow); } private void Button_Click_2(object sender, RoutedEventArgs e) { //DoAnimation1(); DoAnimation2(); } private void DoAnimation2() { if (directionArrows.Count < 1) return; var directionArrow = directionArrows[0]; var second = 1; DoubleAnimation x2Animation = new DoubleAnimation() { From = directionArrow.X1, To = directionArrow.X2, AutoReverse = true, Duration = new Duration(TimeSpan.FromSeconds(second)), RepeatBehavior = RepeatBehavior.Forever }; DoubleAnimation y2Animation = new DoubleAnimation() { From = directionArrow.Y1, To = directionArrow.Y2, AutoReverse = true, Duration = new Duration(TimeSpan.FromSeconds(second)), RepeatBehavior = RepeatBehavior.Forever }; directionArrow.BeginAnimation(DirectionArrow.X2Property, x2Animation); directionArrow.BeginAnimation(DirectionArrow.Y2Property, y2Animation); } private void DoAnimation1() { if (directionArrows.Count < 1) return; var directionArrow = directionArrows[0]; var second = 1; var startPoint = new Point(directionArrow.X1, directionArrow.Y1); var endPoint = new Point(directionArrow.X2, directionArrow.Y2); //与X轴夹角 var theta = Math.PI - Math.Atan2(startPoint.Y - endPoint.Y, startPoint.X - endPoint.X); var cost = Math.Cos(theta); var sint = Math.Sin(theta); var count = Math.Floor((startPoint - endPoint).Length / (directionArrow.ArrowHeight + directionArrow.ArrowSpace)); var arrowItemLenght = (startPoint - endPoint).Length / count; var arrowOffsetX = arrowItemLenght * cost; var arrowOffsetY = arrowItemLenght * sint; DoubleAnimation x2Animation = new DoubleAnimation() { From = directionArrow.X2 - arrowOffsetX, To = directionArrow.X2, AutoReverse = true, Duration = new Duration(TimeSpan.FromSeconds(second)), RepeatBehavior = RepeatBehavior.Forever }; DoubleAnimation y2Animation = new DoubleAnimation() { From = directionArrow.Y2 + arrowOffsetY, To = directionArrow.Y2, AutoReverse = true, Duration = new Duration(TimeSpan.FromSeconds(second)), RepeatBehavior = RepeatBehavior.Forever }; DoubleAnimation x1Animation = new DoubleAnimation() { From = directionArrow.X1, To = directionArrow.X1 + arrowOffsetX, AutoReverse = true, Duration = new Duration(TimeSpan.FromSeconds(second)), RepeatBehavior = RepeatBehavior.Forever }; DoubleAnimation y1Animation = new DoubleAnimation() { From = directionArrow.Y1, To = directionArrow.Y1 - arrowOffsetY, AutoReverse = true, Duration = new Duration(TimeSpan.FromSeconds(second)), RepeatBehavior = RepeatBehavior.Forever }; directionArrow.BeginAnimation(DirectionArrow.X2Property, x2Animation); directionArrow.BeginAnimation(DirectionArrow.Y2Property, y2Animation); directionArrow.BeginAnimation(DirectionArrow.X1Property, x1Animation); directionArrow.BeginAnimation(DirectionArrow.Y1Property, y1Animation); } } }
运行效果图:
方式2可以方便实现各种想要的动画效果。