.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう！(Chapter1)


※本記事は下記のエントリから始まる連載記事となります。
[.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう！(Chapter0)](https://qiita.com/pierusan2010/items/9cd5af84d70fe345c968)

# Chapter1 画面上に図形を描画するための仕組みを作ろう

## 実装イメージ
具体的な実装に入る前に、まずは図形を描画する部分のクラス構成をざっくりと考えてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/08a0c249-c0ef-a733-497e-1b3766f0baa8.png)

- 四角形や楕円などの図形は、IShapeインターフェースを実装するものとします。今は描画処理`Draw()`のみが実装されています。
- 描画処理の実装部分は`IGraphics`インターフェースを`Draw()`メソッドで受け取ることで外部から注入します（Method Injecton）
- IGraphicsのSkiaSharp用の実装は`CoreShape.Extensions.SkiaSharp`に用意します。

ひとまずはこの構成にしたがって実装を進めてみたいと思います。

## Chapter1.1 四角形を描画してみよう
それでは四角形（矩形）を描画する処理を例に具体的な実装について見てみましょう。

### RectangleShape

IShapeインターフェースを継承したRectangleShapeクラスを作成します。
`Draw()`メソッドの実装はひとまず空のままにしておきます。

```cs
public class RectangleShape : IShape
{
    public void Draw(IGraphics g)
    {
    }
}
```

#### 座標や色情報を格納する構造体を用意する
図形描画のためのパラメータとして以下が必要になります。

- 位置（Point）
- サイズ（Size）
- 色（Color）

これらを構造体として定義します。また、位置（Point）とサイズ（Size）を組み合わせて四角形を表す構造体（Rectangle）も定義しておきます。
これらは[構造体のデザインのガイドライン](https://docs.microsoft.com/ja-jp/dotnet/standard/design-guidelines/struct)にしたがってIEquatable<T>を実装した書き換え不可な構造体として定義します。

今回は「[C#9.0 SourceGeneratorでReadonly構造体を生成するGeneratorを作ってみました。](https://qiita.com/pierusan2010/items/d66b835240af30955da6)」で紹介した`ReadonlyStructGenerator`を使って実現します。
下記のように構造体を定義し、必要なプロパティをinitアクセサー付きで追加します。
`IEquatable<T>`とその他諸々の実装はGeneratorによって自動生成されます。

```cs
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Point
{
    public float X { get; init; }
    public float Y { get; init; }
}
```

```cs
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Size
{
    public float Width { get; init; }
    public float Height { get; init; }
}
```

```cs
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Rectangle
{
    public Point Location { get; init; }
    public Size Size { get; init; }

    public float Left  => Location.X;
    public float Top => Location.Y;
    public float Right { get; }
    public float Bottom { get; }

    public Rectangle(Point location,Size size)
    {
        Location = location;
        Size = size;
        Right = location.X + size.Width;
        Bottom = location.Y + size.Height;
    }

    public Rectangle(float left, float top, float width, float height)
        : this(new Point(left, top), new Size(width, height)) { }
}
```

```cs
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Color
{
    public byte R { get; init; }
    public byte G { get; init; }
    public byte B { get; init; }
    public byte A { get; init; }

    public Color(byte r, byte g, byte b, byte a = 255) => (R, G, B, A) = (r, g, b, a); 
}
```

#### プロパティの追加

- 四角形の位置、サイズの情報はRectangle構造体（Boundsプロパティ）で管理します。
- 輪郭の色や太さはStrokeクラス、塗りつぶしの色はFillクラスで管理します。

```cs
public class RectangleShape : IShape
{
    public Rectangle Bounds { get; protected set; }
    public Stroke? Stroke { get; set; }
    public Fill? Fill { get; set; }

    public RectangleShape(Rectangle bounds)
    {
        Bounds = bounds;        
    }

    public void Draw(IGraphics g)
    {
    }
}
```

```cs
public class Stroke
{
    public Color Color { get; set; } = Color.Black;
    public float Width { get; set; } = 1f;

    public Stroke(Color color , float width)
    {
        Color = color;
        Width = width;
    }
}
```

```cs
public class Fill
{
    public Color Color { get; set; } = Color.White;
    public Fill(Color color)
    {
        Color = color;
    }
}
```

これで四角形の描画に必要なパラメータは揃いました。

### IGraphics の実装

次に、実際に輪郭や塗りつぶしを描画する処理を定義するIGraphicsインターフェースに四角形を描画するためのメソッドを追加します。
輪郭は`DrawRectangle()`メソッド、塗りつぶしは`FillRectangle()`メソッドを使って描画します。
パラメータには位置とサイズを表す`Rectangle`と、輪郭の色とサイズを指定する`Stroke` または　塗りつぶしの色を指定する`Fill`を受け取ります。

```cs
public interface IGraphics
{
    void DrawRectangle(Rectangle rectangle, Stroke stroke);
    void FillRectangle(Rectangle rectangle, Fill fill);
}
```

### `RectangleShape.Draw()`メソッドの実装

`IGraphics`にメソッドを追加したので、`RectangleShape`の`Draw()`メソッドが記述できるようになりました。
輪郭を描かない（線なし）、塗りつぶさない（塗りつぶしなし）の場合に対応するために`Stroke`と`Fill`をNULL許容としておきます。
（参照型も既定でNULL非許容となっているので、クラス名の後に？を付けてNULL許容を明示する必要があります。）

```cs
public class RectangleShape : IShape
{
    public Rectangle Bounds { get; protected set; }
    public Stroke? Stroke { get; set; }
    public Fill? Fill { get; set; }

    public virtual void Draw(IGraphics g)
    {
         if (Fill is not null)
        {
            g.FillRectangle(Bounds, Fill);
        }
        if (Stroke is not null)
        {
            g.DrawRectangle(Bounds, Stroke);
        }
    }
}        
```

ここまでで`CoreShape`側での実装は完了です。
続いて`CoreShape`（`RectangleShape`）を利用する側の実装に移ります。

### `IGraphics`の`SkiaSharp`実装`SkiaGraphics`クラス

`CoreShape.Extensions.SkiaSharp`のプロジェクトへ移動し、`IGraphics`インターフェースを継承した`SkiaGraphics`クラスを作成します。

`SkiaSharp`では`SKCanvas`クラスが図形描画を行うメソッドを持っています。
コンストラクタで`SKCanvas`のオブジェクトを受け取って利用するようにします。

後は `DrawRectangle()` 、`FillRectangle()` でSkCanvasの`DrawRect()` を実行するだけです。

```cs
public class SkiaGraphics : IGraphics
{
    protected virtual SKCanvas Canvas { get; set; }

    public SkiaGraphics(SKCanvas canvas)
    {
        Canvas = canvas;
    }

    public virtual void DrawRectangle(Rectangle rectangle, Stroke stroke)
    {
        var rect = new SKRect(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom);
        var paint = new SKPaint()
        {
            Style = SKPaintStyle.Stroke,
            Color = new SkColor(stroke.Color.R, stroke.Color. G,stroke. Color.B, stroke.Color.A),
            StrokeWidth = stroke.Width
        }
        Canvas.DrawRect(rect, paint);
    }

    public virtual void FillRectangle(Rectangle rectangle, Fill fill)
    {
        var rect = new SKRect(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom);
        var paint = new SKPaint()
        {
            Style = SKPaintStyle.Fill,
            Color = new SkColor(fill.Color.R, fill.Color.G, fill.Color.B, fill.Color.A)
        }
        Canvas.DrawRect(rect,paint);
    }
}
```

### 型変換用拡張メソッドの定義
`SkiaGraphics`の実装はこれで良いのですが、1つだけ（ちょっとした）問題があります。
SkiaSharpの描画メソッドを利用する場合、座標や色などの指定に`SkRect`や`SkColor`といったSkiaSharp独自のオブジェクトを利用する必要があります。

`CoreShape` でも独自に`Rectangle`や`Color`構造体を定義しています。また、描画パラメータ`Stroke`や`Fill`に相当する値を`SkiaSharp`では`SkPaint`で指定する必要があります。

したがって、`CoreShape`から渡ってきたこれらの値を一旦`SkiaSharp`用の値に変換してあげる処理が必要になります。これらの処理を色々なところで毎回行うのは非常に面倒なので、拡張メソッドとしてまとめて定義してしまいましょう。

`CoreShape.Extensions.SkiaSharp`のプロジェクトに以下のクラスを追加します。

（型変換用）

```cs
public static class TypeConvertExtensions
{
    public static SKPoint ToSk(this Point p) => new SKPoint(p.X, p.Y);
    public static SKSize ToSk(this Size s) => new SKSize(s.Width, s.Height);
    public static SKRect ToSk(this Rectangle rect) => new SKRect(rect.Left, rect.Top, rect.Right, rect.Bottom);
    public static SKColor ToSk(this Color color) => new SKColor(color.R, color.G, color.B, color.A);
}
```

(SkPaintにStroke,Fillの値を設定）

```cs
public static class SkPaintExtensions
{
    public static SKPaint SetStroke(this SKPaint paint, Stroke stroke)
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.Color = stroke.Color.ToSk();
        paint.StrokeWidth = stroke.Width;
        return paint;
    }
    public static SKPaint SetFill(this SKPaint paint, Fill fill)
    {
        paint.Style = SKPaintStyle.Fill;
        paint.Color = fill.Color.ToSk();
        return paint;
    }
}
```

これらを使って先ほどの`DrawRectangle()`、`FillRectangle()`を書き換えると...

(2020/12/28追記)　SKPaintはIDisposableを実装しているので using を付けた変数宣言を利用するように修正しました。ちなみに、[using 変数宣言](https://ufcpp.net/study/csharp/oo_dispose.html#using-declaration)はC#8.0以降で利用できるようになった機能です。

```cs
public virtual void DrawRectangle(Rectangle rectangle, Stroke stroke)
{
    //Canvas.DrawRect(rectangle.ToSk(), new SKPaint().SteStroke(stroke));
    //usingを使うように修正
    using var paint = new SKPaint().SteStroke(stroke);
    Canvas.DrawRect(rectangle.ToSk(), paint);
}

public virtual void FillRectangle(Rectangle rectangle, Fill fill)
{
    //Canvas.DrawRect(rectangle.ToSk(), new SKPaint().SetFill(fill));
    //usingを使うように修正
    using var paint = new SKPaint().SetFill(fill);
    Canvas.DrawRect(rectangle.ToSk(), paint);
}
```

なんということでしょう！こんなにすっきりと記述できました！
長い道のりでしたが、画面に四角形を描画するまであと一息です！

### Windowに四角形を描画する

SampleWPFのプロジェクトへ移動します。
MainWindowのコードビハインド　MainWindow.xaml.cs　に追加した `sKElement_PaintSurface` イベントハンドラに以下のコードを記述します。

- `RectangleShape`を生成し、`Fill`と`Stroke`を設定します
- イベント引数の`SKPaintSurfaceEventArgs`から取得できる`SKCanvas`オブジェクトを渡して `SkiaGraphics`を生成します。
- 生成した`SkiaGraphics`のインスタンスを渡して `RectangleShape`の`Draw()`メソッドを実行します。

```cs
public partial class MainWindow : Window
{
    private void sKElement_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
    {
        var shape = new RectangleShape(100, 100, 200, 150)
        {
            Stroke = new Stroke(CoreShape.Color.Red, 2),
            Fill = new Fill(CoreShape.Color.LightSkyBlue)
        };

        var g = new SkiaGraphics(e.Surface.Canvas);
        shape.Draw(g);
    }
}
```

ここまで来れば後は実行するのみです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/c5a28be0-a3e0-c553-1191-65eb3419fa43.png)

はい、ようやくWindowに四角形を描くことに成功しました！

これで図形を描画する仕組みが整いましたので、後はこの仕組みに乗っかって他の図形を描画するクラスを追加していけば良いですね。

試しに楕円を描画する処理を追加してみましょう。（2020/12/28 ソースコード追記)

-  `IGraphics` に楕円描画用のメソッド（`DrawOval()` `FillOval()`）を追加

```cs
public interface IGraphics
{
    void ClearCanvas(Color color);
    void DrawRectangle(Rectangle rectangle, Stroke stroke);
    void FillRectangle(Rectangle rectangle, Fill fill);
    void DrawOval(Rectangle rectangle, Stroke stroke);
    void FillOval(Rectangle rectangle, Fill fill);
}
```

- `RectangleShape`を継承した`OvalShape`のクラスを用意
-  `OvalShape`の`Draw()` メソッド内に`DrawOval()` `FillOval()`を実行する処理を記述

```cs
public partial class OvalShape : RectangleShape
{
    public OvalShape(Rectangle bounds) : base(bounds)
    {}

    public OvalShape(Point location, Size size)
        : this(new Rectangle(location, size)) { }

    public OvalShape(float left, float top, float width, float height)
        : this(new Rectangle(left, top, width, height)) { }

    public override void Draw(IGraphics g)
    {
        if (Fill is not null)
        {
            g.FillOval(Bounds, Fill);
        }
        if (Stroke is not null)
        {
            g.DrawOval(Bounds, Stroke);
        }
    }
}
```

- `IGraphics`を継承したクラスで`DrawOval()` `FillOval()`の実装を記述

```cs
public class SkiaGraphics : IGraphics
{
    //...

    public virtual void DrawOval(Rectangle rectangle, Stroke stroke)
    {
        using var paint = new SKPaint().SetStroke(stroke);
        Canvas.DrawOval(rectangle.ToSk(), paint );
    }

    public virtual void FillOval(Rectangle rectangle, Fill fill)
    {
        using var paint = new SKPaint().SetFill(fill);
        Canvas.DrawOval(rectangle.ToSk(), paint);
    }
}
```


### 次回

次回の予定は

- [Chapter2 ドラッグ操作で図形を動かしてみよう](https://qiita.com/pierusan2010/items/5862ac88303ddbdf15b4)

です。