8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-16

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

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

実装イメージ

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

image.png

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

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

Chapter1.1 四角形を描画してみよう

それでは四角形(矩形)を描画する処理を例に具体的な実装について見てみましょう。

RectangleShape

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

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

座標や色情報を格納する構造体を用意する

図形描画のためのパラメータとして以下が必要になります。

  • 位置(Point)
  • サイズ(Size)
  • 色(Color)

これらを構造体として定義します。また、位置(Point)とサイズ(Size)を組み合わせて四角形を表す構造体(Rectangle)も定義しておきます。
これらは構造体のデザインのガイドラインにしたがってIEquatableを実装した書き換え不可な構造体として定義します。

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

[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Point
{
    public float X { get; init; }
    public float Y { get; init; }
}
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Size
{
    public float Width { get; init; }
    public float Height { get; init; }
}
[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)) { }
}
[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クラスで管理します。
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)
    {
    }
}
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;
    }
}
public class Fill
{
    public Color Color { get; set; } = Color.White;
    public Fill(Color color)
    {
        Color = color;
    }
}

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

IGraphics の実装

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

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

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

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

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側での実装は完了です。
続いてCoreShapeRectangleShape)を利用する側の実装に移ります。

IGraphicsSkiaSharp実装SkiaGraphicsクラス

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

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

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

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の描画メソッドを利用する場合、座標や色などの指定にSkRectSkColorといったSkiaSharp独自のオブジェクトを利用する必要があります。

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

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

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

(型変換用)

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の値を設定)

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 変数宣言はC#8.0以降で利用できるようになった機能です。

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を生成し、FillStrokeを設定します
  • イベント引数のSKPaintSurfaceEventArgsから取得できるSKCanvasオブジェクトを渡して SkiaGraphicsを生成します。
  • 生成したSkiaGraphicsのインスタンスを渡して RectangleShapeDraw()メソッドを実行します。
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

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

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

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

  • IGraphics に楕円描画用のメソッド(DrawOval() FillOval())を追加
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のクラスを用意
  • OvalShapeDraw() メソッド内にDrawOval() FillOval()を実行する処理を記述
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()の実装を記述
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);
    }
}

次回

次回の予定は

です。

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?