午後わてんのブログ

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

WPF、ベジェ曲線で直線表示、アンカー点の追加と削除

テスト結果

Animation20220614_101406.gif
見た目は直線だけど、中はベジェ曲線
ベジェ曲線のアンカー点の追加と削除

コード

2022WPF/20220612_PolyBezierCanvas/20220612_PolyBezierCanvas at master · gogowaten/2022WPF

github.com

MainWindow.xaml

<Window x:Class="_20220612_PolyBezierCanvas.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:_20220612_PolyBezierCanvas"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="600">
  <Grid x:Name="MyGrid" UseLayoutRounding="True">
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>
    <StackPanel Grid.Column="1">
      <Button x:Name="MyButton1" Content="点追加" Click="MyButton1_Click"/>
      <Button x:Name="MyButton2" Content="選択点削除" Click="MyButton2_Click"/>
      <TextBlock Text="{Binding MyAnchorPoints.Count, StringFormat=アンカー点個数 0}"/>
      <ListBox x:Name="MyListBox" ItemsSource="{Binding MyAnchorPoints}"/>
    </StackPanel>
    <Canvas Name="MyCanvas">
    </Canvas>
  </Grid>
</Window>



MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace _20220612_PolyBezierCanvas
{
    public partial class MainWindow : Window
    {
        private PolyBezierCanvas2 MyPolyBezierCanvas2 { get; set; }

        public MainWindow()
        {
#if DEBUG
            Left = 100; Top = 100;
#endif
            InitializeComponent();

            MyPolyBezierCanvas2 = new PolyBezierCanvas2(
                new Point(100, 100), new Point(200, 200), Brushes.DarkMagenta, 10);
            MyCanvas.Children.Add(MyPolyBezierCanvas2.MyBezierPath);
            MyPolyBezierCanvas2.AddAnchorPoint(new Point(300, 150));
            DataContext = MyPolyBezierCanvas2;
        }

        private void MyButton1_Click(object sender, RoutedEventArgs e)
        {
            //ランダムな位置にアンカー点追加
            Random r = new(DateTime.Now.Millisecond);
            int x = r.Next(300); int y = r.Next(300);
            MyPolyBezierCanvas2.AddAnchorPoint(new Point(x, y));
        }

        private void MyButton2_Click(object sender, RoutedEventArgs e)
        {
            MyPolyBezierCanvas2.RemoveAnchorPoint(MyListBox.SelectedIndex);
        }
    }

    /// <summary>
    /// ベジェ曲線のアンカー点の追加と削除のテスト用
    /// </summary>
    public class PolyBezierCanvas2
    {
        public Path MyBezierPath;//ベジェ曲線表示用
        //StartPointの管理に使う
        public PathFigure MyBezierFigure { get; }
        //BezierSegmentのPoints
        public PointCollection MyBezierPoints { get; } = new();
        //ベジェ曲線を表示するPathの構成
        //Path                               MyBezierPath    ベジェ曲線表示用
        //┗Data(PathGeometry型)
        // ┗PathFigures
        //  ┗PathFigure                      MyBezierFigure  StartPointの指定で使う
        //   ┗Segments
        //    ┗PolyBezierSegment
        //     ┗Points(PointCollection型)    MyBezierPoints  StartPoint以外の全てのPoint

        //アンカー点管理用Collection
        public ObservableCollection<Point> MyAnchorPoints { get; } = new();
        //ベジェ曲線の頂点はアンカー点と制御点の2種類で構成されている
        //頂点の追加や削除はアンカー点を基準に行い、制御点だけを追加削除することはしない
        //アンカー点追加時は同時に制御点2つを追加
        //アンカー点削除時は同時に制御点2つを削除

        public PolyBezierCanvas2(Point anchor0, Point anchor1, Brush stroke, double thickness)
        {
            MyAnchorPoints.CollectionChanged += MyAnchorPoints_CollectionChanged;
            MyBezierPath = new() { Stroke = stroke, StrokeThickness = thickness };
            //ベジェ曲線のdata作成
            PolyBezierSegment seg = new(); seg.Points = MyBezierPoints;
            MyBezierFigure = new(); MyBezierFigure.Segments.Add(seg);
            PathGeometry geo = new(); geo.Figures.Add(MyBezierFigure);
            MyBezierPath.Data = geo;
            //スタートポイントとアンカー点追加
            MyBezierFigure.StartPoint = anchor0;
            AddAnchorPoint(anchor0);
            AddAnchorPoint(anchor1);
        }

        //アンカー点の追加、削除時
        private void MyAnchorPoints_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
        {
            //追加時は制御点も2点追加する
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                if (e.NewItems?[0] is not Point p) { return; }

                if (MyAnchorPoints.Count > 1)
                {
                    //制御点2つとアンカー点を追加
                    //1個めの制御点座標は1個前のアンカー点と同じにする
                    //2個めの制御点座標は追加されたアンカー点と同じにする
                    int pi = e.NewStartingIndex - 1;
                    Point preAncor;//1個前のアンカー点
                    if (pi == 0) { preAncor = MyBezierFigure.StartPoint; }
                    else { preAncor = MyAnchorPoints[pi]; }
                    MyBezierPoints.Add(preAncor);//制御点
                    MyBezierPoints.Add(p);//制御点
                    MyBezierPoints.Add(p);//アンカー点
                }

            }
            //削除時、アンカー点に付随する制御点2点削除する
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                //削除されたアンカー点のIndex
                int pi = e.OldStartingIndex;
                //BezierSegmentのPointsから削除する点のIndex
                int api = pi * 3 - 2;
                //削除したアンカー点が始点だった場合
                if (pi == 0)
                {
                    //FigureのstartPointの入れ替え(前に詰める)
                    MyBezierFigure.StartPoint = MyAnchorPoints[0];
                    api = 0;
                }
                //削除したアンカー点が終点だった場合
                else if (pi == MyAnchorPoints.Count)
                {
                    api = pi * 3 - 3;
                }
                //削除
                //Collectionからの削除は削除はだるま落としなので同じIndex指定
                MyBezierPoints.RemoveAt(api);
                MyBezierPoints.RemoveAt(api);
                MyBezierPoints.RemoveAt(api);
            }
        }

        public void AddAnchorPoint(Point p)
        {
            MyAnchorPoints.Add(p);
        }

        public void RemoveAnchorPoint(int pi)
        {
            //無効なIndexなら削除しない
            if (0 > pi || pi > MyAnchorPoints.Count) { return; }
            //アンカー点が2個なら削除しない(2個以上を保つ)
            if (MyAnchorPoints.Count == 2) { return; }

            MyAnchorPoints.RemoveAt(pi);
        }


    }
}



ベジェ曲線

ベジェ曲線
頂点に連番を表示したところ
00が始点で06が終点
灰色がアンカー点、青が制御点
始点と終点のアンカー点には制御点が1つ
それ以外の中間のアンカー点には制御点が2つ付く

ベジェ曲線表示まで

Bezier:ベジェ、ベジェ曲線ピエール・ベルジェ
Path:道、道筋、経路
Geometry:幾何学
Figure:形、図形
Segment:区切り、区分

  1. PolyBezierSegmentのPointsに始点以外の頂点を指定
  2. PathFigureのSegmentsに1を追加、FigureのStartPointに始点を指定
  3. PathGeometryのFiguresに2を追加
  4. PathのDataに3を指定


  • Path.Data
    • ┗PathGeometry.Figures
      • ┗PathFigure.Segments
        • ┗PolyBezierSegment

ベジェ曲線
このベジェ曲線だと
PolyBezierSegmentのPointsに指定するのは01~06までの座標で、00に当たる始点だけはPathFigureのStartPointに指定する

  • 00だけはPathFigure
  • 01以降はPolyBezierSegment



ベジェ曲線管理用のクラスPolyBezierCanvas2
Canvasって付けてあるけどCanvas関係ないわ、最初書いていたときはCanvasクラスを継承していたから付けたんだけど、継承する必要ないことに気づいてそのままだった…

フィールド
62行目、MyAnchorPoints、アンカー点のコレクションObservableCollectionを用意して、アンカー点単位で追加や削除できるようにした

コンストラク
線を表示するには最低2つの点が必要なので、それと線の色と先の太さを指定

アンカー点追加と削除
追加時は何もしていないから必要なかったw
削除時は2個以下にならないようにしているだけ

アンカー点追加時の処理
アンカー点のコレクションにつかったObservableCollectionには、要素の追加削除時に発生するイベントが用意されているのでそれを利用、ベジェ曲線のSegmentとFigureを操作
追加する場所は線の末尾に付け足す処理に限定しているので、SegmentのPointsにAddしているだけ
制御点座標の決定は手抜きでアンカー点と同じ座標にしているので、見た目は直線になる

追加前
ここに(300, 70)を追加するときの制御点座標
//1個めの制御点座標は1個前のアンカー点と同じにする
一個前のアンカー点ってのはこの画像だと03のことなので(200, 190)
//2個めの制御点座標は追加されたアンカー点と同じにする
これはそのままなので(300, 70)にする
結果は
追加後
こうなる
制御点とアンカー点が重なっているから直線になる
制御点を動かしてみると
制御点移動
こうなるはず
さっきは03と04が同じ座標、05と06が同じ座標で重なっていた

アンカー点削除時の処理
ベジェ曲線
アンカー点削除時の処理
始点と終点以外の場合(03)は、前後の制御点(02, 04)も削除
終点(06)のときは、前の2つの制御点(04, 05)も削除
始点(00)のときは、後ろの2つの制御点(01, 02)も削除して、2番めのアンカー点(03)を始点に指定する


PolyBezierCanvas2を使う側

MainWindow
フィールドにPolyBezierCanvas2を用意しておいて、12行目
そのMyBezierPathをデザイン画面で用意しておいたCanvasに追加して表示、23行目

25行目、DataContextにPolyBezierCanvas2を指定して、デザイン画面のListBoxのBindingで利用

デザイン画面のXAML
17,18行目のBindingで利用している


これでだいたいできそうかな、あとは制御線の表示と、Thumbの表示と、その移動に合わせての座標の書き換えは、前回までのPolyLineの応用でなんとかなるはず


関連記事

次回のWPF記事
gogowaten.hatenablog.com

前回のWPF記事
gogowaten.hatenablog.com

4年前

gogowaten.hatenablog.com

gogowaten.hatenablog.com

gogowaten.hatenablog.com

7年前
gogowaten.hatenablog.com