Flutter on Desktopでウィンドウ位置を保持

https://qiita.com/yoyo-misc/items/dda6f0d090e580d0d4ca

前回の記事の続きです。FlutterのDesktop BuildではMobileやWebと異なりウィンドウの位置やサイズを制御・保持できるアドバンテージがあります。MobileはともかくWebとDesktopでは比較対象になることもあると思いますので、Desktopにも興味がある方向けに説明したいと思います。

# パッケージの入れ替え

https://pub.dev/packages/window_manager

前回はウィンドウのサイズ変更にgoogle/flutter-desktop-embeddingパッケージを利用しましたが、window_managerパッケージではサイズ変更に加えイベントリスナーが利用できるため本記事ではパッケージを入れ替えて動作を確認していきます。

:::note info
本記事ではパッケージのリファレンスのReadMeを読みながらコードに触れています。不明な点はパッケージのリファレンスを参照してください。以降のコードはmain.dartに直接書いていきます。
:::

## パッケージを入れ替え

``` diff_dart
- import 'package:window_size/window_size.dart';
+ import 'package:window_manager/window_manager.dart';
```

## 非同期メソッドを使う宣言
``` :dart
void main() async {
```
main()内で非同期メソッドを使うためasyncを付けます。

## windowdManagerを初期化

``` :dart
WidgetsFlutterBinding.ensureInitialized();
```
``` diff_dart
+ await windowManager.ensureInitialized();
```

WidgetsFlutterBindingの下にwindowdManagerを追加します。非同期なのでawaitを付けます。

## WindowOptionsの定義

``` :dart
  WindowOptions windowOptions = const WindowOptions(
    size: Size(800, 600),
    // center: true,
    backgroundColor: Colors.transparent,
    skipTaskbar: false,
    // titleBarStyle: TitleBarStyle.hidden,
  );
  ```

 WindowManagerでウィンドウのインスタンスを作成する際のプロパティを定義します。centerプロパティをtrueにしたままsetPositionをセットすると反映されない（上書きされない）ので注意してください。TitleBarStyle.hiddenを設定すると本番環境でのウィンドウ操作が詰みます。その際はOSのタスクから閉じてください。

## ウィンドウの位置を初期化
``` :dart
double x = 50;
double y = 50;
```
x,yはdouble型の座標です。アプリが起動されウィンドウを表示する際にディスプレイの左上を原点にX=左からnピクセル、Y=上からnピクセルの位置で描画します。アプリをはじめて起動するとき、ウィンドウの位置はアプリの外部のどこにも記憶されていないためアプリ内で初期値を宣言しておきます。

## CSVから位置を読み込む
``` :dart
 var file = File("D:\\flutter.csv");
  if (file.existsSync()) {
    var contents = await file.readAsString();
    var spl = contents.split(',');

    x = double.parse(spl[0]);
    y = double.parse(spl[1]);
  }
```
WindowsなのでD:\\flutter.csvを配置し読み書きしてみます。ファイルが存在すれば読み込んでx,y座標を書き換えています。

```flutter.csv:csv
0,1
```
ここではまだファイルは存在していませんが、本記事では説明しやすいようにカンマ区切りの簡単なCSV形式にしています。実装する場合はjsonかxmlがいいと思います。

:::note warn
本記事では説明をわかりやすく簡単にするためパーサーが不要なCSVで読み書きしています。CSVでの管理を推奨するものではありません。
:::

## ウィンドウ位置をセット
``` :dart
await windowManager.setPosition(Offset(x, y));
```
WindowOptionsのSizeプロパティでウィンドウサイズを指定しましたが、ウィンドウサイズ変更のメソッドはおそらくsetBoundsあたりで行うと思います（未検証）


## ウィンドウマネージャーの表示
``` :dart
windowManager.waitUntilReadyToShow(windowOptions, () async {
    await windowManager.show();
    await windowManager.focus();
  });
```
async awaitを忘れずに。これでmain()での準備は完了です。

## class _MyHomePageStateにWindowListenerを追加

``` diff_dart
- class _MyHomePageState extends State<MyHomePage> {
+ class _MyHomePageState extends State<MyHomePage> with WindowListener {
```

mixinでリスナーを追加します。ぼくはC#erなのでDartの継承がうまく説明できないため他の方の記事を参考にしてみてください。

## ウィンドウ移動時のイベントを取得し処理をする

``` :dart
@override
  void onWindowMoved() async {
      var position = await windowManager.getPosition();
      var x = position.dx;
      var y = position.dy;
      var str = '$x,$y';
      if (kDebugMode) {
        print(str);
      }
      var file = File("D:\\flutter.csv");
      await file.writeAsString(str);
    setState(() { });
```
:::note warn
訂正
以前公開したコードではsetStateの中でasync/awaitを記述していたことに気付きましたが、非同期処理はメソッドの中に記述してsetStateの中では非同期処理は行わないようにします。
:::

onWindowMovedでウィンドウが移動した後のイベントを取得します。windowManager.getPositionでoffset型の値が取得しdx/dyプロパティで値を取り出します。今回はカンマ区切りのCSVに書き込みたいので`var str = '$x,$y';`をwriteAsStringに渡しています。file.writeAsString()は既定としてファイルが存在しない場合は作成してくれるようです。

## ウィンドウ位置を保持するロジックのまとめ

1. 位置を保持するCSVを読み込む。なければ初期値を使ってウィンドウを表示
1. ウィンドウを移動したらイベントで取得し、 位置を保持するCSVに書き込む

## 考えていただきたいこと

今回はonWindowMovedメソッドのみで実装してみましたが、リサイズされた場合のイベント処理はどうしたらいいか、サイズの保持はどうやったらいいか。CSVだと管理が大変だぞ。みたいにいろいろ考えることが出てくると思います。少しずつ手をかけながら面倒を見てあげるとアプリに愛着が湧いてくるはずなので、私はハウツーのアウトプットよりもそのことをみなさんに伝えたいと思っています。


## この記事の続き

https://qiita.com/yoyo-misc/items/ac5e8057a66035b62463