WPFで矢印ベジェ曲線できた
Shapeクラスを継承して作成
結果
左がベジェ曲線で、右は同じ頂点座標でのPolyline
コード
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メソッドにしただけ
ついでに、始点終点図形の描画部分をまとめた
間違い探しレベルだったので、まとめた
環境
- Windows 10 Home バージョン 21H2
- Visual Studio Community 2022 Version 17.5.1
- WPF
- C#
- .NET 6.0
感想
ハイポテニュース
二等辺三角形の底辺じゃない方の斜めの2辺って名前なんだっけ?名前あったっけ?
検索したら斜辺って名前だと判明、そうだった!そんな名前だった
斜辺の英語は?
hypotenuse
直角三角形の斜辺の英語がhypotenuseってことなんだけど、二等辺三角形の斜辺と違うの?日本語的には同じだからこれで
関連記事
次回は2日後ついにAdornerを使う
gogowaten.hatenablog.com
5年前
WPFで矢印線、直線(PolyLine)と矢印(Polygon)を組み合わせて表現 - 午後わてんのブログ
https://gogowaten.hatenablog.com/entry/15558066