WPFのXAMLを使った「クリックするとスプライン曲線が引かれるデモプログラム」

コンピュータ

マウスカーソルの座標の取得とスプライン曲線を引く方法を確認しましたので、マウスクリックで曲線を引くデモプログラムを作成します。

ソースコード

ファイル名:ClickSplineDemo.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:MainWindow.xaml

<Window x:Class="ClickSplineDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Click Spline Demo"
        Width="900" Height="650"
        UseLayoutRounding="True"
        SnapsToDevicePixels="True">

    <Grid Background="#1E1E1E">
        <!-- ヒットテストのため Background は必須 -->
        <Canvas x:Name="CanvasRoot"
                Background="Transparent" />
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Controls;

namespace ClickSplineDemo;

public partial class MainWindow : Window
{
    private readonly List<Point> _points = [];
    private readonly List<Ellipse> _pointDots = [];

    private readonly Path _curvePath = new()
    {
        Stroke = Brushes.DeepSkyBlue,
        StrokeThickness = 4,
        StrokeLineJoin = PenLineJoin.Round,
        StrokeStartLineCap = PenLineCap.Round,
        StrokeEndLineCap = PenLineCap.Round
    };

    public MainWindow()
    {
        InitializeComponent();

        Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        // Canvas
        CanvasRoot.MouseMove += Canvas_MouseMove;
        CanvasRoot.MouseLeftButtonDown += Canvas_MouseLeftButtonDown;
        CanvasRoot.MouseRightButtonDown += Canvas_MouseRightButtonDown;

        // Window
        KeyDown += Window_KeyDown;

        // 描画要素を追加
        CanvasRoot.Children.Add(_curvePath);
    }

    // =========================
    // イベントハンドラ
    // =========================

    // マウス移動イベント
    private void Canvas_MouseMove(object sender, MouseEventArgs e)
    {
        // 座標確認
        Point p = e.GetPosition(CanvasRoot);
        Title = $"Click Spline Demo  |  X={p.X:0.0}, Y={p.Y:0.0}";
    }

    // マウス左ボタン押下イベント
    private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        Point p = e.GetPosition(CanvasRoot);
        _points.Add(p);

        AddDot(p);
        RedrawCurve();
    }

    // マウス右ボタン押下イベント
    private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (_points.Count == 0) return;

        _points.RemoveAt(_points.Count - 1);

        // dotも消す
        var dot = _pointDots[^1];
        _pointDots.RemoveAt(_pointDots.Count - 1);
        CanvasRoot.Children.Remove(dot);

        RedrawCurve();
    }

    // キーボード押下イベント
    private void Window_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.C)
        {
            ClearAll();
        }
    }

    // =========================
    // 描画・ロジック
    // =========================

    private void ClearAll()
    {
        _points.Clear();

        foreach (var d in _pointDots)
            CanvasRoot.Children.Remove(d);
        _pointDots.Clear();

        _curvePath.Data = null;
    }

    private void AddDot(Point p)
    {
        const double r = 4;

        var dot = new Ellipse
        {
            Width = r * 2,
            Height = r * 2,
            Fill = Brushes.White,
            Stroke = Brushes.Black,
            StrokeThickness = 1
        };

        Canvas.SetLeft(dot, p.X - r);
        Canvas.SetTop(dot, p.Y - r);

        _pointDots.Add(dot);
        CanvasRoot.Children.Add(dot);
    }
    // 曲線をレンダリング
    private void RedrawCurve()
    {
        // 点が少ないと曲線にならないので簡易表示
        if (_points.Count < 2)
        {
            _curvePath.Data = null;
            return;
        }

        if (_points.Count == 2)
        {
            // 2点だけなら直線
            var fig = new PathFigure { StartPoint = _points[0] };
            fig.Segments.Add(new LineSegment(_points[1], true));
            _curvePath.Data = new PathGeometry(new[] { fig });
            return;
        }

        // 3点以上:Catmull-Rom → Bezier で滑らかに
        // 端点複製(Catmull-Rom用)
        var pts = new List<Point>(_points);
        pts.Insert(0, pts[0]);
        pts.Add(pts[^1]);

        _curvePath.Data = CreateBezierFromCatmullRom(pts);
    }

    // ================================
    // Catmull-Rom → Cubic Bezier 変換
    // ================================
    private static PathGeometry CreateBezierFromCatmullRom(IList<Point> pts)
    {
        var fig = new PathFigure
        {
            StartPoint = pts[1],
            IsClosed = false,
            IsFilled = false
        };

        for (int i = 0; i < pts.Count - 3; i++)
        {
            Point p0 = pts[i];
            Point p1 = pts[i + 1];
            Point p2 = pts[i + 2];
            Point p3 = pts[i + 3];

            // Catmull-Rom (tension = 0.5) → Bezier
            Point c1 = p1 + (p2 - p0) / 6.0;
            Point c2 = p2 - (p3 - p1) / 6.0;

            fig.Segments.Add(new BezierSegment(c1, c2, p2, true));
        }

        return new PathGeometry(new[] { fig });
    }
}

概要

🖱 マウス操作

■ 左クリック

Canvas上を左クリックすると、その位置に**制御点(白い小さな円)**が追加されます。

  • 2点 → 直線として描画

  • 3点以上 → Catmull-Rom スプラインを元にした
    滑らかな曲線として自動的に再描画されます

クリックするたびに、曲線全体が再計算されます。


■ 右クリック

直前に追加した最後の制御点を1つ削除します。

  • 点を削除すると、それに応じて曲線も即座に更新されます

  • 点が0個になると、曲線は消えます


■ マウス移動

マウスをCanvas上で移動すると、
**現在のマウス座標(Canvasローカル座標系)**が
ウィンドウタイトルバーに表示されます。

これは、

  • MouseEventArgs.GetPosition(Canvas)

  • Canvasのヒットテスト仕様確認

を目的とした、デバッグ兼デモ用の表示です。


⌨ キー操作

■ C キー

すべての制御点と曲線を一括クリアします。

  • 画面を初期状態に戻したい場合に使用します

  • Canvas自体は再生成せず、描画要素のみを削除します


🎨 描画仕様

  • 曲線は Path + PathGeometry を使用して描画

  • Catmull-Rom スプラインを
    Cubic Bézier 曲線の連続セグメントに変換

  • 線端・接合部は Round に設定し、
    視覚的に自然な連続線になるよう調整しています

制御点は視認性確認用のためのもので、
描画ロジック上は Point の配列としてのみ扱われます


🧩 実装上の特徴

  • イベントの紐づけは Loaded 内で一括管理

  • 描画更新は「状態 → 再描画」の単純なフロー

  • MVVMを使用せず、最小構成のコードビハインド設計

教育用・検証用・プロトタイプ用途を想定した、
ロジックが追いやすい構成になっています。

実行

サンプル動画

コメント