午後わてんのブログ

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

WPF、PolyLineの頂点にThumb表示、マウスドラッグで頂点移動

動作テスト結果

Animation20220609_123906.gif

  • PolyLine要素の頂点の追加と削除
  • 頂点にはThumbを表示
  • Thumbをマウスドラッグ移動で頂点も移動


コード

2022WPF/20220608_PathとThumb/20220608_PathとThumb at master · gogowaten/2022WPF

github.com



デザイン画面

MainWindow.xaml

<Window x:Class="_20220608_PathとThumb.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:_20220608_PathとThumb"
        mc:Ignorable="d"
        Title="MainWindow" Height="460" Width="600">
  <Grid UseLayoutRounding="False">
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>
    <StackPanel Grid.Column="1">
      <Button x:Name="MyButton1" Content="add" Click="MyButton1_Click"/>
      <Button x:Name="MyButton2" Content="remove" Click="MyButton2_Click"/>
      <Button x:Name="MyButton3" Content="thumbVisibility" Click="MyButton3_Click"/>
    </StackPanel>
    <Canvas Name="MyCanvas">

    </Canvas>
  </Grid>
</Window>



MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;
using System.Collections.ObjectModel;


//PolyLineで折れ線表示
//アンカーポイントにThumbを表示
//Thumbをマウスドラッグ移動でアンカーポイントも移動
//アンカーポイントの動的追加と削除

//アンカーポイントとThumbは手動で同期させる
//同期ってのは個数と順番
//アンカーポイント追加するときはThumbも追加する
//アンカーポイント削除するときは最後にクリックされたThumbと対応するアンカーポイントを削除

namespace _20220608_PathとThumb
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        //PolyLineのアンカーポイント
        public PointCollection MyPointC { get; set; } = new();
        //アンカーポイントに表示するThumbを入れておく用コレクション
        public ObservableCollection<Thumb> MyThumbs { get; set; } = new();
        private int MyCount;
        private Thumb? MyActiveThumb;//最後にクリックされたThumbを入れておく用
        bool IsThumbVisible = true;//Thumbの表示の有無フラグ
        public MainWindow()
        {
#if DEBUG
            Left = 100; Top = 100;
#endif
            InitializeComponent();

            MyCanvas.Children.Add(MakePolyline(MyPointC, Brushes.Magenta, 10));
            AddPoint(new Point(100, 100));
            AddPoint(new Point(200, 100));

            #region 負荷テスト用
            //AddPointRound(100);//円環
            //AddPoint円環はお断り(10000);

            //1ピクセル斜めに追加
            //for (int i = 0; i < 5000; i++)
            //{
            //    AddPoint(new Point(i, i));
            //}
            #endregion 負荷テスト用
        }

        private void AddPointRound(int count)
        {
            double r = 200;//半径
            double s = 360.0 / count;
            double x; double y;
            for (double i = 0.0; i < 360.0; i += s)
            {
                double rad = Radian(i);
                x = (Math.Cos(rad) * r) + r;
                y = (Math.Sin(rad) * r) + r;
                AddPoint(new Point(x, y));
            }
        }
        private void AddPoint円環はお断り(int count)
        {
            double r = 200; double s = 3600.0 / count;
            double rr = r / (count * 2.0); double rrr = r;
            double x; double y;
            for (double i = 0.0; i < 3600.0; i += s)
            {
                double rad = Radian(i);
                x = (Math.Cos(rad) * rrr) + r;
                y = (Math.Sin(rad) * rrr) + r;
                AddPoint(new Point(x, y));
                rrr -= rr;
            }
        }

        public static double Radian(double degrees)
        {
            return Math.PI / 180.0 * degrees;
        }
        private void AddPoint(Point p)
        {
            MyPointC.Add(p);
            var t = MakeThumb(p);
            MyThumbs.Add(t);
            MyCanvas.Children.Add(t);
        }
        private void RemovePoint(Thumb? t)
        {
            if (t is not Thumb) return;
            int i = MyThumbs.IndexOf(t);
            MyPointC.RemoveAt(i);
            MyThumbs.Remove(t);
            MyCanvas.Children.Remove(t);
            MyActiveThumb = null;
        }


        private Thumb MakeThumb(Point p)
        {
            Thumb t = new() { Width = 20, Height = 20 };
            t.DragDelta += Thumb_DragDelta;
            t.PreviewMouseDown += Thumb_PreviewMouseDown;
            Canvas.SetLeft(t, p.X); Canvas.SetTop(t, p.Y);
            return t;
        }

        private Polyline MakePolyline(PointCollection pc, Brush stroke, double thickness)
        {
            Polyline pl = new();
            pl.Points = pc;
            pl.Stroke = stroke;
            pl.StrokeThickness = thickness;
            return pl;
        }



        private void Thumb_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (sender is Thumb t)
            {
                MyActiveThumb = t;
            }
        }

        private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            if (sender is not Thumb t) { return; }
            double x = Canvas.GetLeft(t) + e.HorizontalChange;
            double y = Canvas.GetTop(t) + e.VerticalChange;
            Canvas.SetLeft(t, x);
            Canvas.SetTop(t, y); ;
            int i = MyThumbs.IndexOf(t);
            MyPointC[i] = new Point(x, y);
        }

        private void MyButton1_Click(object sender, RoutedEventArgs e)
        {
            AddPoint(new Point(MyCount * 30, MyCount * 20));
            MyCount++;
        }

        private void MyButton2_Click(object sender, RoutedEventArgs e)
        {
            RemovePoint(MyActiveThumb);
        }

        private void MyButton3_Click(object sender, RoutedEventArgs e)
        {
            if (IsThumbVisible)
            {
                //Thumbの表示を最初の1つだけにする
                for (int i = 1; i < MyThumbs.Count; i++)
                {
                    MyThumbs[i].Visibility = Visibility.Collapsed;
                }
                IsThumbVisible = false;
            }
            else
            {
                //全てのThumbを表示する
                for (int i = 1; i < MyThumbs.Count; i++)
                {
                    MyThumbs[i].Visibility = Visibility.Visible;
                }
                IsThumbVisible = true;
            }

        }
    }


}



フィールド
どこからでも参照できる変数
Thumbをまとめておくコレクションには、変更通知があるコレクションのObservableCollection型を使っているけど、変更通知は使っていないので普通のList型でもよかった

アプリ起動時の処理
PolyLineを作成してCanvasに追加
頂点を2つ追加

頂点追加処理
頂点座標PointをPointCollectionに追加
Thumb作成してThumbコレクションに追加
Canvasに追加して表示

Thumb作成時
ドラッグ移動イベントのDragDeltaに処理追加
マウスダウンイベント時の処理も追加
表示位置を指定

Thumbのイベント時の処理
マウスダウンイベントではフィールドのMyActiveThumbに自身を追加、これでクリックしたThumbがどれなのか取得できる、削除するときに使用している
ドラッグ移動イベントでは移動と、PolyLineの頂点の移動もしている
動かしているThumbが、どの頂点と対応しているかの判別は、単純にコレクションのIndex番号でしている。これはもっとまともな方法があるだろうし、ホントはBindingしたかったんだけど方法がわからなかった

削除処理
最後(直前)にクリックしたThumbを指定して削除
ここでもIndexを取得して、対応するPointをPointCollectionから削除
CanvasからもThumb削除

できなかったこと

PolyLineの頂点座標も、それに対応するThumbの表示座標も同じPoint型の集まりのPointCollection型だから、これを一つにまとめたかった、けどできなかった
2日くらいググったりダクっ(duckduckgo)たり、捏ねくり回していたんだけど、Point型っていうか構造体がBindingに向いていない感じ、できないことはなさそうなんだけど難しそうだから諦めた


表示数を増やしたときの負荷

環境は
CPU AMD Ryzen 5 2400G
Visual Studio 2022

10,000個、ウィンドウ外あり
10,000個の頂点を1ピクセル右斜したに並べて、Thumbを表示
Animation20220609_144728.gif
このGIFアニメーションfpsは10で作成している
割りと動く、瞬間的にPolyLineが変なところに表示されたりするけど動く
CPUの負荷は20%前後

実際の要素数

ビジュアル ツリー
ビジュアルツリーで見ると要素数は60,000個!
Thumbは立体(エンボス)的に表示して見せるためにBorder要素を4つとGrid要素1つの合計5要素と自身を含めて6要素で構成されている?から10000x6で60,000個ってことみたい
ってことはThumbのTemplateをいじって、Gridだけの平坦なThumbにすれば、20,000要素まで減らせば動きは良くなるかも?機会があれば試したい

Thumb10,000個をすべてウィンドウ内に表示
10,000個の座標を半径200の円環に並べてThumbを表示
定期的に流行る三角関数を使ってみたかっただけ

Animation20220609_145731.gif
カックカク、最初は「動かない!?」と思ったくらいマウスの動きに追従できない、fpsだと1か2くらい
CPU負荷は23~25%、CPUは物理4コアの論理8コアなので、物理コア1個を使い切っている感じ
Thumbを1個だけ表示して動かした場合は、さっきの斜めに表示したときと同じくらいの動きになった。CPU負荷は14%前後で、これだと論理コア1個を使い切っている?もしかしてPolyLineの表示とThumb群の表示は別スレッドで動いているのかしら


Thumb1,000個表示

Animation20220609_151314.gif
多少カクカクするけど破綻した動きはなくなった
Thumbを1個表示なら、普通になめらかに動く


Animation20220609_152032.gif
Thumb100個表示
問題なくなめらかに動く
CPU負荷は7%


円環はお断り
指定個数の頂点を半径の半分まで10周で並べる
10,000個並べると、鋼の錬金バームクーヘンできた
なにこれ
PolyLineは蚊取り線香みたいになっているはず
なってなかった
PolyLineの線が太すぎた
なってた
先の太さを10から4にした
やっぱりバームクーヘンじゃないか!


1000個で蚊取り線香
これならThumbが並んでいるのがわかる
PolyLineも曲線に見える



100個で蚊取り線香っぽくない
当たるも十卦、当たらぬも十卦
100個頂点PolyLine


これが目的の訳じゃないけど流れで

蚊取り線香できた
あんまり見なくなってからも久しいねえ


関連記事

268日後、ついにAdornerを使う
gogowaten.hatenablog.com

次回
gogowaten.hatenablog.com
動作管理部分を別クラスに分けてみた

前回のWPF記事は3日前
gogowaten.hatenablog.com

4年前
マウスクリックでCanvasに直線を描画その2、Polyline、WPFC# - 午後わてんのブログ

gogowaten.hatenablog.com

8年前
複数の画像を並べて重ねて1枚の画像にするPixtack紫陽花2.4.9.80、図形2頂点の追加と削除 - 午後わてんのブログ gogowaten.hatenablog.com
複数の画像を並べて重ねて1枚の画像にするPixtack紫陽花2.4.8.79、直線の描画 - 午後わてんのブログ gogowaten.hatenablog.com
このときはWindowsForm+VBで、WindowsFormでの描画は半透明処理がないから期待しない表示になっている。これを改善するのにかなり時間がかかったんだけど、WPFでは自動で半透明処理してくれるから、当時はかなり驚いたとともに、これからはWPFだ!これならもっといいものを作れる!!って思ったんだよなあ、あれから6年位は経ったのかな、未だにできてないw