再実装FlutterをC#でやってみた

# はじめに
Flutter力を高めたいなーと思い、Flutterについて深掘りしようと思いました。
そこで、以下の記事に挑戦してみようかなと思いました。
内容はFlutterのフレームワークを実装してみるという内容です。

https://zenn.dev/fastriver/books/reimpl-flutter

記事ではKotlinで書かれてますが、C#で実装してみようかなと思い至りました。
ただ写経するだけでも勉強になると思いますが、違う言語に変換が必要だとさらに理解が深まるかなと思ったからです。
（ただし、結構大変です）

また、上記の記事は有料なので、詳細な内容の解説は書きません。
本記事ではC#でやる際の環境構築、導入を書いていこうかと思います。

**現時点では、上記の記事を完遂したわけではないので、感想とか困ったところとかは終わった後に追記しようと思ってます。**

**最近、本も出版されたみたいですので、興味がある方はぜひ。**

https://www.amazon.co.jp/%E5%86%8D%E5%AE%9F%E8%A3%85Flutter-UI%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%92%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E8%87%AA%E4%BD%9C%E3%81%99%E3%82%8B-%E9%80%9F%E5%B7%9D-%E6%B9%A7%E6%B0%97/dp/4295601748

# 環境構築
筆者は、VisualStudio2022で実行してます。
以下、手順をだらだらと書いていきます。

### プロジェクト作成
まず、コンソールアプリで新規プロジェクトを作成します。

### パッケージインストール
続いて、Nugetパッケージマネージャーで`SkiaSharp`と`OpenTK`をインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/7204c720-f884-ac18-dc27-81b88c50db5d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/e9f50212-1c90-96a2-1203-22fd8f00689b.png)

:::note warn
検索しても出てこない方は右上のパッケージソースが`nuget.org`になっているか確認してください。
:::

### unsafeを許可
上記がおわったら、unsafeを使うので、プロジェクトのプロパティをひらいて下記の箇所にチェックを入れておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/d671eb0a-5b2a-c655-07ee-5a17f44426b4.png)


### 簡単に解説
#### SkiaSharp
SkiaはFlutterの描画に使われているグラフィックライブラリで、SkiaSharpはそれのC#版です。
再実装Flutterでは図形やテキストの描画に使ってます。

https://github.com/mono/SkiaSharp

#### OpenTK
OpenTKはC#でOpenGLを使うことができるパッケージで、OpenGLもグラフィックライブラリです。
再実装Flutterではウィンドウの表示に使ってます。

https://qiita.com/okajima/items/8082c0d2473949ec51e8

https://opentk.net/learn/index.html

#### つまり
OpenTKでウィンドウを作成し、SkiaSharpでそのウィンドウにいろいろかくって感じです。

### 余談
余談ですが久しぶりにコンソールプロジェクトを作成したらコードが下記のみだったのでびっくりしました。

```c#
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
```

mainはどこにいった？ってなりましたが、ドキュメントを読むと下記のコードと同じ内容みたいです。

```c#
using System;

namespace MyApp // Note: actual namespace depends on the project name.
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}
```

# いろいろ書いてみた

Windowを表示して、SkiaSharpでいろいろ書いてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/d11c1bdd-43c8-9ae8-6b2c-22431d4d69b6.png)

再実装FlutterをC#でやってみたいって方がいれば、下記のコードを参考にしていただければ進められるかと思います。

```C#
// See https://aka.ms/new-console-template for more information
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;
using SkiaSharp;

ShowWindow(640, 480, "Window");

unsafe void ShowWindow(int width, int height, String title)
{
    GLFW.Init();
    GLFW.DefaultWindowHints();
    var windowHandle = GLFW.CreateWindow(width, height, title, null, null);
    GLFW.SwapInterval(1);
    GLFW.ShowWindow(windowHandle);
    GLFW.MakeContextCurrent(windowHandle);
    GLFW.GetCurrentContext();

    var grgInterface = GRGlInterface.Create();
    var grContext = GRContext.CreateGl(grgInterface);
    var renderTarget = new GRBackendRenderTarget(
        width,
        height,
        0,
        8,
        new GRGlFramebufferInfo(0, (uint)SizedInternalFormat.Rgba8));
    var surface = SKSurface.Create(grContext, renderTarget, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
    var canvas = surface.Canvas;

    while (!GLFW.WindowShouldClose(windowHandle))
    {
        using (var paint = new SKPaint()
        {
            Color = SKColors.White,
            IsAntialias = true,
            Style = SKPaintStyle.Fill,
            TextAlign = SKTextAlign.Center,
            TextSize = 24
        })
        {

            canvas.Clear(SKColors.CornflowerBlue);

            paint.Color = SKColors.White;
            canvas.DrawCircle(new SKPoint(50, 50), 30, paint);

            paint.Color = SKColors.Yellow;
            canvas.DrawRect(new SKRect(100, 100, 160, 130), paint);

            paint.Color = SKColors.Red;
            canvas.DrawLine(new SKPoint(200, 200), new SKPoint(230, 300), paint);

            paint.Color = SKColors.Black;
            canvas.DrawText($"Hello World!!", 128, 300, paint);
        }


        grContext.Flush();

        GLFW.SwapBuffers(windowHandle);
    }
}
```

# OpenTKについての余談
色々と調べていると、OpenTKには`GameWindow`というクラスがありました。
その名の通り、ゲームとか作れるのかなーと思って興味がわいたので、少し触ってみました。
すると、下記みたいに時間の表示ができたので共有です。
C#にはUnityがあるので、これを使う方はいないと思いますが。
ただ、ゲームのフレームワークを理解するために、ライブラリを自前で実装してみるってのは良いかもしれません。

![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/a876dc71-7df9-1937-f08e-4c20fb5bab39.gif)


ソース
```c#
// See https://aka.ms/new-console-template for more information
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;
using SkiaSharp;

using(var window = new MyWindow(640, 480))
{
    window.Run();
}

class MyWindow : GameWindow
{
    GRGlInterface grgInterface;
    GRContext grContext;
    SKSurface surface;
    SKCanvas canvas;
    GRBackendRenderTarget renderTarget;
    SKPaint TestBrush;
    double _time = 0;

    public MyWindow(int width, int height)
        : base(GameWindowSettings.Default,
              new NativeWindowSettings()
              {
                  Size = (width, height),
                  Title = "MyWindow"
              })
    {

    }

    protected override void OnUpdateFrame(FrameEventArgs args)
    {
        base.OnUpdateFrame(args);
        _time += args.Time;
    }


    protected override void OnLoad()
    {
        base.OnLoad();
        //Context.MakeCurrent();
        grgInterface = GRGlInterface.Create();
        grContext = GRContext.CreateGl(grgInterface);
        renderTarget = new GRBackendRenderTarget(
            ClientSize.X,
            ClientSize.Y,
            0,
            8,
            new GRGlFramebufferInfo(0, (uint)SizedInternalFormat.Rgba8));
        surface = SKSurface.Create(grContext, renderTarget, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
        canvas = surface.Canvas;

        TestBrush = new SKPaint
        {
            Color = SKColors.White,
            IsAntialias = true,
            Style = SKPaintStyle.Fill,
            TextAlign = SKTextAlign.Center,
            TextSize = 24
        };

    }

    protected override void OnUnload()
    {
        TestBrush.Dispose();
        surface.Dispose();
        renderTarget.Dispose();
        grContext.Dispose();
        grgInterface.Dispose();
        base.OnUnload();
    }

    protected override void OnRenderFrame(FrameEventArgs args)
    {
        canvas.Clear(SKColors.CornflowerBlue);

        TestBrush.Color = SKColors.Black;
        canvas.DrawText($"Time:{(int)_time}", 128, 300, TestBrush);

        canvas.Flush();
        SwapBuffers();
    }
}
```