午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPFで矢印ベジェ曲線できた

Shapeクラスを継承して作成

結果

WPFで矢印ベジェ曲線
左がベジェ曲線で、右は同じ頂点座標でのPolyline


デザイナー画面上での矢印ベジェ曲線


Pixtack紫陽花で頂点座標表示

コード

github.com

MainWindow.xaml

<Window x:Class="_20230302_PolyBezierArrowline.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:_20230302_PolyBezierArrowline"
        mc:Ignorable="d"
        Title="MainWindow" Height="500" Width="600">
  <Grid>
    <Canvas>
      <local:PolyBezierArrowline Points="0,0 0,150 100,50 100,200" StrokeThickness="20"
                                 IsBezier="True"
                                 EdgeBeginType="Arrow" EdgeEndType="Arrow" Stroke="SeaGreen"
                                 Canvas.Left="50"/>

      <local:PolyBezierArrowline Points="0,0 0,150 100,50 100,200" StrokeThickness="20"
                                 IsBezier="False"
                                 EdgeBeginType="Arrow" EdgeEndType="Arrow" Stroke="DarkMagenta"
                                 Canvas.Left="250"/>
      
      <local:PolyBezierArrowline Points="50,70 250,150 50,250 50,200 50,150 150,100 250,250" StrokeThickness="20"
                                 IsBezier="True"
                                 EdgeBeginType="Arrow" EdgeEndType="Arrow" Stroke="SeaGreen"
                                 Canvas.Top="150"/>
      
      <local:PolyBezierArrowline Points="50,70 250,150 50,250 50,200 50,150 150,100 250,250" StrokeThickness="20"
                                 IsBezier="False"
                                 EdgeBeginType="Arrow" EdgeEndType="Arrow" Stroke="DarkMagenta"
                                 Canvas.Top="150" Canvas.Left="220"/>
      
    </Canvas>
  </Grid>
</Window>

Pointsに頂点座標指定
IsBezier:trueでベジェ曲線、falseならPolyline(直線)
EdgeBeginType:始点の図形形状指定、Noneで形状なし、Arrowで三角形(矢印)
EdgeEndType:終点の図形形状
Angle:矢印三角の角度指定、初期値は30°




PolyBezierArrowline.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;

namespace _20230302_PolyBezierArrowline
{
    //端点の形状
    public enum EdgeType { None = 0, Arrow, }


    public class PolyBezierArrowline : Shape
    {
        #region 依存プロパティ

        public bool IsBezier
        {
            get { return (bool)GetValue(IsBezierProperty); }
            set { SetValue(IsBezierProperty, value); }
        }
        public static readonly DependencyProperty IsBezierProperty =
            DependencyProperty.Register(nameof(IsBezier), typeof(bool), typeof(PolyBezierArrowline),
                new FrameworkPropertyMetadata(false,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.AffectsMeasure |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        /// <summary>
        /// 終点の図形形状(エッジタイプ)
        /// </summary>
        public EdgeType EdgeEndType
        {
            get { return (EdgeType)GetValue(EdgeEndTypeProperty); }
            set { SetValue(EdgeEndTypeProperty, value); }
        }
        public static readonly DependencyProperty EdgeEndTypeProperty =
            DependencyProperty.Register(nameof(EdgeEndType), typeof(EdgeType), typeof(PolyBezierArrowline),
                new FrameworkPropertyMetadata(EdgeType.None,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.AffectsMeasure |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
        /// <summary>
        /// 始点の図形形状(エッジタイプ)
        /// </summary>
        public EdgeType EdgeBeginType
        {
            get { return (EdgeType)GetValue(EdgeBeginTypeProperty); }
            set { SetValue(EdgeBeginTypeProperty, value); }
        }
        public static readonly DependencyProperty EdgeBeginTypeProperty =
            DependencyProperty.Register(nameof(EdgeBeginType), typeof(EdgeType), typeof(PolyBezierArrowline),
                new FrameworkPropertyMetadata(EdgeType.None,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.AffectsMeasure |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        /// <summary>
        /// 矢印角度、初期値は30.0にしている。30~40くらいが適当
        /// </summary>
        public double Angle
        {
            get { return (double)GetValue(AngleProperty); }
            set { SetValue(AngleProperty, value); }
        }
        public static readonly DependencyProperty AngleProperty =
            DependencyProperty.Register(nameof(Angle), typeof(double), typeof(PolyBezierArrowline),
                new FrameworkPropertyMetadata(30.0,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.AffectsMeasure |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));


        public PointCollection Points
        {
            get { return (PointCollection)GetValue(PointsProperty); }
            set { SetValue(PointsProperty, value); }
        }

        public static readonly DependencyProperty PointsProperty =
            DependencyProperty.Register(nameof(Points), typeof(PointCollection), typeof(PolyBezierArrowline),
                new FrameworkPropertyMetadata(new PointCollection() { new Point(0, 0), new Point(100, 100) },
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.AffectsMeasure));

        #endregion 依存プロパティ
        protected override Geometry DefiningGeometry
        {
            get
            {
                StreamGeometry geometry = new() { FillRule = FillRule.Nonzero };
                if (Points.Count < 2) { return geometry; }
                if (Points.Count < 4 && IsBezier) { return geometry; }
                //図形はFillで描画して、中間線部分はStrokeで描画している
                //両方同じ色にするのでStrokeで統一
                Fill = Stroke;
                using (var context = geometry.Open())
                {
                    Point begin = Points[0];//始点
                    Point end = Points[^1];//終点
                    //始点図形の描画
                    switch (EdgeBeginType)
                    {
                        case EdgeType.None:
                            break;
                        case EdgeType.Arrow:
                            begin = DrawArrowLine(context, Points[0], Points[1]);
                            break;
                    }
                    //終点図形の描画
                    switch (EdgeEndType)
                    {
                        case EdgeType.None:
                            break;
                        case EdgeType.Arrow:
                            end = DrawArrowLine(context, Points[^1], Points[^2]);
                            break;
                    }
                    //中間線の描画
                    if (IsBezier) { DrawBezier(context, begin, end); }
                    else { DrawLine(context, begin, end); }
                }
                geometry.Freeze();
                return geometry;
            }
        }



        /// <summary>
        /// ベジェ曲線部分の描画
        /// </summary>
        /// <param name="context"></param>
        /// <param name="begin">始点図形との接点</param>
        /// <param name="end">終点図形との接点</param>
        private void DrawBezier(StreamGeometryContext context, Point begin, Point end)
        {
            context.BeginFigure(begin, false, false);
            List<Point> bezier = Points.Skip(1).Take(Points.Count - 2).ToList();
            bezier.Add(end);
            context.PolyBezierTo(bezier, true, false);

        }

        /// <summary>
        /// 直線部分の描画
        /// </summary>
        /// <param name="context"></param>
        /// <param name="begin">始点図形との接点</param>
        /// <param name="end">終点図形との接点</param>
        private void DrawLine(StreamGeometryContext context, Point begin, Point end)
        {
            context.BeginFigure(begin, false, false);
            context.PolyLineTo(Points.Skip(1).Take(Points.Count - 2).ToList(), true, false);
            context.LineTo(end, true, false);
        }

        /// <summary>
        /// アローヘッド(三角形)描画
        /// </summary>
        /// <param name="context"></param>
        /// <param name="edge">端のPoint、始点ならPoints[0]、終点ならPoints[^1]</param>
        /// <param name="next">端から2番めのPoint、始点ならPoints[1]、終点ならPoints[^2]</param>
        /// <returns></returns>
        private Point DrawArrowLine(StreamGeometryContext context, Point edge, Point next)
        {
            //斜辺 hypotenuse ここでは二等辺三角形の底辺じゃない方の2辺
            //頂角 apex angle 先端の角
            //アローヘッドの斜辺(hypotenuse)の角度(ラジアン)を計算
            double lineRadian = Math.Atan2(next.Y - edge.Y, next.X - edge.X);
            double apexRadian = DegreeToRadian(Angle);
            double edgeSize = StrokeThickness * 2.0;
            double hypotenuseLength = edgeSize / Math.Cos(apexRadian);
            double hypotenuseRadian1 = lineRadian + apexRadian;

            //底角座標
            Point p1 = new(
                hypotenuseLength * Math.Cos(hypotenuseRadian1) + edge.X,
                hypotenuseLength * Math.Sin(hypotenuseRadian1) + edge.Y);

            double hypotenuseRadian2 = lineRadian - DegreeToRadian(Angle);
            Point p2 = new(
                hypotenuseLength * Math.Cos(hypotenuseRadian2) + edge.X,
                hypotenuseLength * Math.Sin(hypotenuseRadian2) + edge.Y);

            //アローヘッド描画、Fill(塗りつぶし)で描画
            context.BeginFigure(edge, true, false);//isFilled, isClose
            context.LineTo(p1, false, false);//isStroke, isSmoothJoin
            context.LineTo(p2, false, false);

            //アローヘッドと中間線の接点座標計算、
            //HeadSizeぴったりで計算すると僅かな隙間ができるので-1.0している、
            //-0.5でも隙間になる、-0.7で隙間なくなる
            return new Point(
                (edgeSize - 1.0) * Math.Cos(lineRadian) + edge.X,
                (edgeSize - 1.0) * Math.Sin(lineRadian) + edge.Y);
        }
    }
}

200行もあるけど、100行目まではプロパティの宣言なので、処理部分はそれ以降の100行、それもコメント量が多いので実質50行くらい

直線だけだった前回とほとんど同じ
始点終点の間の中間線の描画をLineToメソッドや、PolylineToメソッドから、ベジェ曲線のPolyBezierToメソッドにしただけ

中間線の描画



ついでに、始点終点図形の描画部分をまとめた

前回の始点と終点図形の描画
間違い探しレベルだったので、まとめた
今回の始点終点図形の描画

環境

感想

ハイポテニュース

二等辺三角形の底辺じゃない方の斜めの2辺って名前なんだっけ?名前あったっけ?
検索したら斜辺って名前だと判明、そうだった!そんな名前だった
斜辺の英語は?
hypotenuse
直角三角形の斜辺の英語がhypotenuseってことなんだけど、二等辺三角形の斜辺と違うの?日本語的には同じだからこれで


関連記事

次回は2日後ついにAdornerを使う
gogowaten.hatenablog.com

前回
gogowaten.hatenablog.com

5年前
WPFで矢印線、直線(PolyLine)と矢印(Polygon)を組み合わせて表現 - 午後わてんのブログ https://gogowaten.hatenablog.com/entry/15558066