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可以方便实现各种想要的动画效果。

 

posted @ 2022-04-07 16:05  荀幽  阅读(835)  评论(0编辑  收藏  举报