続・FlutterでWindows用のアプリを作成する

今回の記事は、[CyberAgent Developers Blog](https://developers.cyberagent.co.jp/blog/archives/31736/)の続きとなります。記事の公開後、いくつか問題に直面したので、どのように解決したのかを書いていきたいと思います。

## 1. msixのインストーラが使えない
Windowsアプリを公開する場合に、インストーラを配布します。flutterでは、msixのインストーラを作ってくれる[msix](https://pub.dev/packages/msix/install)パッケージがあります。テスト時にはアンインストール・アップデートも問題なく成功していました。

実際に店舗へインストールしようとときに、問題が出てきました。msixが使えるバージョンはWindows 10 1709の以降であったこと。Windows 10であっても、パッチを当ててないと使えない機能で、Windows 8でも使われる話になり、msixそのものが使えなくなりました。

解決した方法としては、msiのインストーラを作ることにしました。Visual Studioにmsiを作成する拡張機能があり、それを使って手動でインストーラを作ります。

手順としては、一般のアプリを作るのとほとんど変わらないのですが、[msvcp140.dll、vcruntime140.dll、vcruntime140_1.dllが別途必要](https://docs.flutter.dev/desktop#building-your-own-zip-file-for-windows)になるので、build/windows/runner/Releaseフォルダにあるファイルと一緒に含めるようにしてください。

別途、コードサイニング証明書をインストーラに適用すれば、完了です。

```sign_app.bat
signtool.exe sign /f 証明書.pfx /d "説明" /p パスワード /v /t "http://timestamp.comodoca.com/authenticode" インストーラ.msi
```

## 2. エラーログ
薬急便はWebで展開しているサービスで、何らかのエラーが起こった場合には、GCPの[Error Reporting](https://cloud.google.com/error-reporting)に送るようにしています。iOS/AndroidであればFirebaseを使うのも手なんですが、まとめて管理したいので、Error Reportingに報告したいと思いました。

手頃なパッケージがなかったので、[stackdriver-dart](https://pub.dev/packages/stackdriver_dart)を自作しました。Webで使っている[stackdriver-js](https://github.com/GoogleCloudPlatform/stackdriver-errors-js)を参考にして作ってます。

APIはOpenAPIで吐き出しているんですが、ApiClientのソースを少し修正して、エラーが起こったときにレポートするようにしています。

```api_client.dart
var _client = ExtendedClient(
    inner: Client() as BaseClient,
    extensions: [
      StackDriverReportExtension(reporter: reporter),
    ],
  );
```

## 3. 印刷できない
印刷処理は、[printing](https://pub.dev/packages/printing)のパッケージを使っています。テスト環境でまったく問題なく、品質もきれいで重宝していて、あっさりいくと思っていたところで、つまづきました。

実装としては通常使うプリンタを検索して、印刷するようにしていたんですが、実際の環境では何も印刷されず、エラーも発生しない状態です。

印刷の設定に何かあると思っていて、調査したところ、Windowsのプリンタ設定では、固有のプリンタの設定をデフォルトにした複数のプリンタを登録できます。そのときに、プリンタ名に固有設定を追記して設定するんですが、printingのパッケージでは、プリンタを特定できずに、印刷に失敗するようでした。

回避策として、プリンタ名単体の設定を選択できるように、オプションを追加して、通常使うプリンタではなく、プリンタを選ぶようにしました。

## 4. システムトレイ対応
今回開発したアプリは、常駐型で閉じられないようにしたかったので、閉じるを無効にし、最小化した場合に、タスクバーに残さないようにしています。ここはDartだけではできないので、C++のコードを書き換えて対応しています。

メッセージループしているところに最小時にパネルを消す

```windows/runner/win32_window.cpp
LRESULT
Win32Window::MessageHandler(HWND hwnd,
                            UINT const message,
                            WPARAM const wparam,
                            LPARAM const lparam) noexcept {
  switch (message) {
// ...
// 省略
// ...
    case WM_SYSCOMMAND:
      if (wparam == SC_MINIMIZE) {
        SetWindowPos(
          hwnd, nullptr, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_HIDEWINDOW);
        return 0;
      }
      break;
  }

  return DefWindowProc(window_handle_, message, wparam, lparam);
}

```

閉じるボタンを無効にする

```windows/runner/win32_window.cpp
bool Win32Window::OnCreate() {
  HMENU hMenu = GetSystemMenu(GetHandle(), 0);
  RemoveMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
  return true;
}
```

システムトレイを表示するのは、[system_tray](https://pub.dev/packages/system_tray)を使っています。[bitsdojo_window](https://pub.dev/packages/bitsdojo_window)を使えば、appWindowでアプリのウィンドウを制御して、システムトレイ経由でアプリを終了できます。

```app.dart
final _systemTray = SystemTray();

Future<void> _initSystemTray() async {
    final path =
        Platform.isWindows ? 'assets/app_icon.ico' : 'assets/app_icon.png';
    final menu = [
      MenuItem(label: '表示', onClicked: () => appWindow.show()),
      MenuItem(label: '非表示', onClicked: () => appWindow.hide()),
      MenuSeparator(),
      MenuItem(
        label: '終了',
        onClicked: () => appWindow.close(),
      ),
    ];

    await _systemTray.initSystemTray(
      title: "Panel",
      iconPath: path,
      toolTip: "Panel",
    );

    await _systemTray.setContextMenu(menu);
  }
```

## 多重機動防止
Flutterで作成したアプリはいくつも立ち上げられるようになっています。多重機動防止を防止するには、起動時にすでにアプリが立ち上がってないか、チェックする必要があります。

よくある手法として、mutexかFindWindowを使うんですが、Flutterのアプリのclass名は`"FLUTTER_RUNNER_WIN32_WINDOW"`で固定化されているのと、今回のアプリはタイトル名は変わるので、FindWindowは使わず、mutexで処理しました。

```windows/runner/win32_window.cpp
bool Win32Window::CreateAndShow(const std::wstring& title,
                                const Point& origin,
                                const Size& size) {
  Destroy();

  mutex_ = CreateMutex(nullptr, TRUE, L"Mutexのキー");
  if(GetLastError() == ERROR_ALREADY_EXISTS)
  {
      CloseHandle(mutex_);
      // 既存のパネルを表示する処理
      return false;
  }
}

void Win32Window::Destroy() {
  OnDestroy();

  if (mutex_ != nullptr) {
    ReleaseMutex(mutex_);
    CloseHandle(mutex_);
    mutex_ = nullptr;
  }
}
```

## 6. テスト
GitHub Actionsでテストを実行しているのですが、エラーが発生したときに、ログを見るのも面倒だなと思い、ログを整形して、簡単にまとめてくれる[Git Hub Action](https://github.com/marketplace/actions/flutter-test-report)を作りました。

出力は、[サンプル](https://github.com/ueki-tomohiro/flutter-ci-test/pull/2)のように、Unit TestとCoverageをまとめて出力しています。[コメント](https://github.com/ueki-tomohiro/flutter-ci-test/pull/2)もするようになっています。

## まとめ
想定した環境と違ったことがあり、いろいろとはまったんですが、FlutterのWindows利用はいけると思います。

ただ、どうしてもOSに依存する機能では、Windowsプログラミングを知らないと辛いところでした。Unityでゲーム開発をしたことがあったんですが、そのときでも、要望に合わせてネイティブのプラグインを書くことはあったので、ネイティブの知識は必要になってくるのは、Flutterでも変わらないと思いました。

今後も開発を続けて、気がついたところを投稿できればと思います。

1. msixが対応しているのは、Windows 10 1709以降
1. 印刷できないときは、インストールされる端末の設定を確認
1. Windows固有の機能は、windows/runner/配下のC++のファイルを修正