午後わてんのブログ

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

WPFで矢印曲線、ベジェ曲線(Path)と矢印(Polygon)を組み合わせて表現、PolyBezierSegment

今回のアプリのダウンロード先
イメージ 1
クリックしたところをアンカーポイントにしてベジェ曲線の終端に矢印
昨日は直線だったのをベジェ曲線にしてみた
 
 
デザイン画面

f:id:gogowaten:20191213104833p:plain

 
 
C#のコード
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.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace _20180616_ベジェ曲線に矢印
{
    public partial class MainWindow : Window
    {
        PolyBezierSegment MySegment;
        Point OffsetFinePoint;//矢印の先端と左上との差
        Point ContactPoint; //線の終端にする矢印の後ろ座標
        public MainWindow()
        {
            InitializeComponent();

            MyInitialize();
            MyCanvas.MouseLeftButtonDown += MyCanvas_MouseLeftButtonDown;
            MyCanvas.MouseMove += MyCanvas_MouseMove;
            MyCanvas.MouseRightButtonDown += MyCanvas_MouseRightButtonDown;
            this.Loaded += MainWindow_Loaded;

        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            //矢印の先端をマウスカーソル位置に合わせるときのオフセット座標を設定
            OffsetFinePoint = new Point(ArrowHead.ActualWidth / 2, 0);

            //矢印を回転するときの中心になる座標の指定
            //矢印の先端を中心にしたい、先端は左右だと中間なのでxは0.5、上下は上端にあるので0.0を指定
            ArrowHead.RenderTransformOrigin = new Point(0.5, 0.0);

            //線の終端にする座標は矢印の後ろ座標
            //-2は位置調整、0だと矢印と曲線の間に隙間ができる
            //マイナスを大きくすると矢印と線の重なりが大きくなる
            ContactPoint = new Point(ArrowHead.ActualWidth / 2, ArrowHead.ActualHeight - 2);
        }

        private void MyCanvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            MySegment.Points.Clear();
        }
        //Path.Dataを作成
        private void MyInitialize()
        {
            MySegment = new PolyBezierSegment();
            var pf = new PathFigure();
            pf.Segments.Add(MySegment);
            var pg = new PathGeometry();
            pg.Figures.Add(pf);
            MyPath.Data = pg;

        }

        //マウス移動時、終端のアンカーポイント移動とその他の制御点の位置調整
        private void MyCanvas_MouseMove(object sender, MouseEventArgs e)
        {
            PointCollection ps = MySegment.Points;
            Point mouseP = e.GetPosition(MyCanvas);//マウスカーソル位置

            if (ps.Count > 5)
            {

                //終端のアンカーの座標は今のカーソル座標
                ps[ps.Count - 1] = mouseP;

                Point preAP = ps[ps.Count - 4];//一個前のアンカーポイント
                Point prepreAP;//二個前のアンカーポイント
                if (ps.Count < 7)
                {
                    var pg = (PathGeometry)MyPath.Data;
                    var pathFigureCollection = pg.Figures;
                    prepreAP = pathFigureCollection[0].StartPoint;
                }
                else
                {
                    prepreAP = ps[ps.Count - 7];
                }

                //終端アンカーと二個前のアンカーとの距離の1 / 4
                double xDiff = (mouseP.X - prepreAP.X) / 4.0;
                double yDiff = (mouseP.Y - prepreAP.Y) / 4.0;
                //一個前のアンカーポイントの制御点座標
                ps[ps.Count - 5] = new Point(preAP.X - xDiff, preAP.Y - yDiff);//終端から遠いほう
                ps[ps.Count - 3] = new Point(preAP.X + xDiff, preAP.Y + yDiff);//終端から近いほう

                //矢印の回転角度    
                //角度はマウスカーソル座標と一個前のアンカーの手前側の制御点との直線の角度
                double angle = Math.Atan2(mouseP.Y - ps[ps.Count - 3].Y, mouseP.X - ps[ps.Count - 3].X);
                angle = angle / Math.PI * 180;//ラジアンから度数へ変換
                angle += 90;//調整、元の矢印は上向きだけど0度は右向きだから
                //矢印回転
                ArrowHead.RenderTransform = new RotateTransform(angle);
                MyLabel.Content = "Angle = " + angle.ToString();
                //矢印の位置、先端をマウスカーソルに合わせる
                mouseP.Offset(-OffsetFinePoint.X, -OffsetFinePoint.Y);
                Canvas.SetLeft(ArrowHead, mouseP.X);
                Canvas.SetTop(ArrowHead, mouseP.Y);

                //終端座標決定
                //矢印の後ろ(接続座標)をベジェ曲線の終端にする
                //接続座標はTransformfromToVisualで計算
                GeneralTransform gt = ArrowHead.TransformToVisual(MyCanvas);
                Point lastAnc = gt.Transform(ContactPoint);
                ps[ps.Count - 1] = lastAnc;

                //終端(アンカー)と一個前のアンカーの距離の1/4
                xDiff = (lastAnc.X - ps[ps.Count - 3].X) / 4.0;
                yDiff = (lastAnc.Y - ps[ps.Count - 3].Y) / 4.0;
                //終端の制御点座標
                Point lastControlP = new Point(lastAnc.X - xDiff, lastAnc.Y - yDiff);
                ps[ps.Count - 2] = lastControlP;

            }
            else if (ps.Count > 0)
            {
                //終端はカーソル位置
                ps[ps.Count - 1] = e.GetPosition(MyCanvas);//カーソル位置に
                //矢印の位置調整
                mouseP.Offset(-OffsetFinePoint.X, -OffsetFinePoint.Y);
                Canvas.SetLeft(ArrowHead, mouseP.X);
                Canvas.SetTop(ArrowHead, mouseP.Y);
            }

        }

        //マウスクリック時、アンカーポイントと制御点2つを追加
        private void MyCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var p = e.GetPosition(MyCanvas);
            //最初のクリック時だけはStartPointを指定する
            if (MySegment.Points.Count == 0)
            {
                var pathGeometry = (PathGeometry)MyPath.Data;
                var pathFigureCollection = pathGeometry.Figures;
                //始点追加
                pathFigureCollection[0].StartPoint = p;
                //制御点2つとアンカーポイントの合計3つ追加
                MySegment.Points.Add(p);//制御点になる
                MySegment.Points.Add(p);//アンカーポイントになる
                MySegment.Points.Add(p);//制御点になる
            }
            else
            {
                //一個前になるアンカー座標をクリック座標にする
                MySegment.Points[MySegment.Points.Count - 1] = p;
                //制御点2つとアンカーポイントの合計3つ追加
                MySegment.Points.Add(p);
                MySegment.Points.Add(p);
                MySegment.Points.Add(p);
            }

        }
    }
}
元になったコードは
マウスクリックでCanvasベジェ曲線で曲線、PolyBezierSegment ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15544835.html
これに矢印を付け足した感じ
制御点座標の決め方も同じでアンカーからの距離は、前後のアンカー間の1/4で
角度は前後のアンカー間を結んだときの直線と平行になるように
しているけど制御点座標の決め方はもっといい方法があるはず
 
灰色がアンカー、紫が制御点
00が始点、06が終点(終端)
 
終端(マウスカーソル座標)の制御点は、終端と一個前のアンカーの手前側の制御点との直線上に置くことに決めてあるので、上の画像だと06と04を結ぶ線上に05を置く
 
イメージ 11
 
矢印の角度はこの線に合わせると自然な感じがしたので、この線の角度を求める

f:id:gogowaten:20191213105346p:plain

変数のpsはベジェ曲線のセグメントのPointsプロパティで、座標リストみたいなの
ps[ps.Count - 3]はさっきの図だと04で制御点になる、これとマウス座標mousePからMath.Atan2を使って角度を求めている、103、104行目
 
 
矢印の回転と移動ができたら、ベジェ曲線の終端座標を決める
マウスカーソル位置を終端にすると
イメージ 10
不自然なので
 
イメージ 9
矢印の後ろを終端にする
 

f:id:gogowaten:20191213105402p:plain

矢印の横は中央、縦は下端の座標(接続座標)をベジェ曲線の終端座標にすれば、くっついて見える
今回はその接続座標をTransformToVisualメソッド使って求めてみた
これを使うとコントロールの変形前の座標から変形後の座標を取得したりできる
 
矢印(ArrowHead)はCanvas(MyCanvas)に表示しているのでそのTransformを
117行目で取得して、
Transformメソッドに接続座標(ContactPoint)を渡して変形後の接続座標を取得している、118行目
これが終端座標になるので119行目で指定
 
接続座標ContactPointはアプリの起動直後に

f:id:gogowaten:20191213105419p:plain

49行目で取得している、元(変形前)の接続座標
 
イメージ 8
矢印から伸びる曲線も違和感ないと思う、角度とか
 
矢印以外の形
●アンカーらしいアンカー

f:id:gogowaten:20191213105429p:plain

矢印に指定するのはPolygonじゃなくてもできそうなので
元の12行目をコメントアウトして
20行目、Ellipseを指定すると
イメージ 7
できた
 
今回のコード
 
 
 
関連記事
昨日、2018/06/20
WPFで矢印線、直線(PolyLine)と矢印(Polygon)を組み合わせて表現 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15558066.html
 
 
2018/6/11は10日前
マウスクリックでCanvasベジェ曲線で曲線、PolyBezierSegment ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15544835.html