※本記事は下記のエントリから始まる連載記事となります。
.NET5.0/C#9.0でオートシェイプ風図形描画ライブラリを作ろう!(Chapter0)
Chapter1 画面上に図形を描画するための仕組みを作ろう
実装イメージ
具体的な実装に入る前に、まずは図形を描画する部分のクラス構成をざっくりと考えてみます。
- 四角形や楕円などの図形は、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
にメソッドを追加したので、RectangleShape
のDraw()
メソッドが記述できるようになりました。
輪郭を描かない(線なし)、塗りつぶさない(塗りつぶしなし)の場合に対応するためにStroke
とFill
を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
側での実装は完了です。
続いてCoreShape
(RectangleShape
)を利用する側の実装に移ります。
IGraphics
のSkiaSharp
実装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の描画メソッドを利用する場合、座標や色などの指定にSkRect
やSkColor
といったSkiaSharp独自のオブジェクトを利用する必要があります。
CoreShape
でも独自にRectangle
やColor
構造体を定義しています。また、描画パラメータStroke
やFill
に相当する値を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
を生成し、Fill
とStroke
を設定します - イベント引数の
SKPaintSurfaceEventArgs
から取得できるSKCanvas
オブジェクトを渡してSkiaGraphics
を生成します。 - 生成した
SkiaGraphics
のインスタンスを渡してRectangleShape
のDraw()
メソッドを実行します。
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);
}
}
ここまで来れば後は実行するのみです!
はい、ようやく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
のクラスを用意 -
OvalShape
のDraw()
メソッド内に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);
}
}
次回
次回の予定は
です。