🚀

Flutter for windowsでWin32 APIを使ったプラグイン開発の始め方

2022/02/16に公開

はじめに

自己紹介

こんにちは、あらさん(@arasan01_me)です。最近youtube始めようと思ってネタを考えてたらFlutter for windowsのプラグインを作成していました。何を言ってるかは分からねぇが俺も分からねぇ。

youtube始めるとなると色々準備しなきゃー、と思ってarasan01.devというドメインを取得したところ、pub.devでpublisherを登録するときにはドメインが必要とのことですごくちょうど良かったです。ウレシイですね。

Flutterは何年か前にiOS, Androidの開発で試しに触ってみた程度で離れていて、Windows開発ができると聞いて戻ってきました。なのでFlutter開発経験はほとんどゼロです。いつもはiOSアプリを開発しています(Swiftはいいぞ)。

何を作ったのか

preview image

https://pub.dev/packages/drag_and_drop_windows

Flutter for windowsはそのままだとドラッグ&ドロップでファイルを受け取ることができません。もちろんWidgetなどをドラッグアウトしてファイルの書き出しもできません。

今回は簡単にできそうなドラッグ&ドロップを実験的に実装してみることにしました。Flutterのネイティブにまたがるプラグインは初めてだったのですごく楽しかったです。

簡単とはいえ初めてのプラグイン作成、しかもFlutter for windowsのネイティブを呼び出すものは中々検索にも引っかからず調べる時間がかなりかかってしまいました。そこでこの記事では何も知らないFlutter初心者がプラグインを作った過程を紹介します。

開発本題

開発に入る前に開発周辺の話

Flutter for windowsは最近ニュースになったレベルでプラットフォーム機能をゴリゴリ使ったライブラリはそんなに多くないように見えます。またiOS, Android, Webよりも近づきにくいです。

調べた限りだとWindows Embedderを生かしたプラグイン開発になるようで、主に使う言語は C++ です。C++もC++11以降になってくれば書きやすくなってきます。しかし,初心者の敵とも呼ばれるポインタや、Rustで人気のムーブセマンティック、他の言語でもお馴染みになってきたジェネリクス(template、厳密には違うけど)などそもそもC++に慣れるまでに時間がかかる人も多いでしょう。人にもよりますが普通にDartより書くのはめんどくさいように感じる部分が多いです。

また、Win32 APIは歴史ある昔から使われているものです。普通に2008年とかのVBで書かれたWin32 APIの使い方とかの記事が出てきます。もちろんMicrosoftのドキュメントを見れば公式の仕様は出てきます。が歴史がある割にはいつもどおりのMicrosoftのドキュメントと言った感じで翻訳が崩壊してるので笑いながら流していきましょう。英語版しか参考にできないです。むしろ英語版をChromeのサイト内翻訳で見たほうがきれいな日本語サイトになります、Microsoft頑張って。

https://twitter.com/arasan01_me/status/1493248913566203904

さて、かなり楽しい雰囲気が出てきたFlutter for windows開発ですがどのような知識が使えるのでしょうか。昔に作られた記事はWeb開発などをしていると使い物にならないことが多々あります。iOSでも似たようなものの場合が多いです。

しかしながらWin32 APIをベースに作っているFlutter for windowsは一味違います。概念は昔から同じものなのでVisual Basicなどで書かれた実装などはかなり参考になります。どのように処理を回すか適当にWebサイトを回遊して公式の仕様書を引いて細部を見ていきます。

Win32 API周りは上記のものでなんとかなりました。さて、Flutter for windowsのプラットフォーム側を作るための知見が欲しいです、どこを見れば書いてあるのでしょうか。

調べた限りではgithubで気になったコードをall repositoryで全文検索すると似たようなことをやっている人がいたりするので参考になります。またgithubのflutter/pluginsのwindows実装を見ることもかなり参考になります。C++が書けなくても雰囲気で読んで似たようなコードを書けばなんとかなります、C++がロクに書けない私も真似すればなんとかなりました。

辛くなったら初心に返ってAnnouncing flutter for windowsを読みましょう。ここにはWindows向けに作られたFlutter Architectureが載っています。全体像を引いて見ることで解決の道が見えてきたりします。

https://medium.com/flutter/announcing-flutter-for-windows-6979d0d01fed

いざ実装

ここまでで開発に入る前の調べ先や考え方を話してきました。それでは実際に開発していきます。
開発の方法はiOS, Androidなどと変わりません。いつもどおりflutter createをすればひな形が出来上がります。

flutter create --template=plugin --platforms=windows <my-plugin>

dartの方は普通に書けば動くので今回はあまり見ません、主にwindows/の中のプラグイン本体を見ていきます。

最初に見るべきはもちろんヘッダーファイルです。includeにあるヘッダーファイルがこのプラグインの基本となるものです。<PluginName>RegisterWithRegisterのような関数に注目してください。
この関数はFlutterによる自動生成のコードで本体側のgenerated_plugin_registrant.ccから呼ばれます。つまりプラグインのエントリーポイント、イニシャライザのようなものです。

ヘッダーファイルには他に色々記載がありますが見るべきポイントはこれだけです。extern "C"__declspec(dllexport|dllimport)はFlutterの都合とプラグインの都合でひな形に追加されているものです。消すとめんどくさいので残しておくと良いです。

次に本体のファイルを見ていきます。ひな形にはMethodChannelを使ったDart <-> C++間の関数の呼び出しをしています。MethodChannelなどは公式が解説しているのでそちらを見ると良いです。MethodChannelの似たものにEventChannelなどもあります。正しくない表現ですがMethodChannel -> Future, EventChannel -> Streamのように使えます。

https://docs.flutter.dev/development/platform-integration/platform-channels

大きなプラグインを作らない場合はflutter::Pluginを継承したクラスに全部そのまま実装しても困らないです。私が開発したものではこのようになっています。

class DragAndDropWindowsPlugin : public flutter::Plugin {
  public:
    static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);

    DragAndDropWindowsPlugin(flutter::BinaryMessenger* messenger);

    virtual ~DragAndDropWindowsPlugin();

  private:
    std::optional<LRESULT> MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept;
    std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_;
    std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> event_channel_;
};

すべての関数、変数がこのクラスに集中しています。この規模では分離するほどのものもないためです。さて注目するのはRegisterWithRegistrar関数です。
最初の呼び出しポイントから渡されるregistrarを使ってこのプラグインをFlutterに登録しなければいけません。登録するためにはこのクラスを実体化して保持する必要があります、そのため順を追ってコードを見ていきます。

void DragAndDropWindowsPluginRegisterWithRegistrar(
  FlutterDesktopPluginRegistrarRef registrar) {
  DragAndDropWindowsPlugin::RegisterWithRegistrar(
      flutter::PluginRegistrarManager::GetInstance()
          ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}

このコードではヘッダーで定義した関数から渡されるRegistrarRefを使って実際に使うことになるregistrarを取得し引数に登録しています。GetRegistrarでは得られるものとしてPluginRegistrarWindowsというWindowsのイベントなどを仲介してくれるものです。これのポインタが得られるので引数にはポインタが入ります。

クラスのRegisterWithRegistrarの実装を見ていきます。

// static
void DragAndDropWindowsPlugin::RegisterWithRegistrar(
    flutter::PluginRegistrarWindows *registrar) {
  auto plugin = std::make_unique<DragAndDropWindowsPlugin>(registrar->messenger());
  flutter::WindowProcDelegate delegate([plugin_pointer = plugin.get()](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
    return plugin_pointer->MessageHandler(hwnd, message, wparam, lparam);
  });

  registrar->RegisterTopLevelWindowProcDelegate(delegate);
  registrar->AddPlugin(std::move(plugin));
}

registrarmessengerを持っているのでこれをもらいます。messengerはアプリとプラグインの情報をやりとりするものです。これを使ってChannelを作成できます。ポインタの一種であるユニークポインタ(unique_ptr, 一人だけが所有権を持てるポインタのこと)を作成するmake_uniqueを使ってクラスを実体化します。このクラスが実体化されたらクラスをregistrarに登録します。AddPlugin(std::move(plugin))にてregistrarに所有権を譲渡します。

ここで大事なポイントの1つ、RegisterTopLevelWindowProcDelegateが出てきます。これに対して決まった型の関数を渡してあげるとウィンドウのイベントをハンドリングできるようになります。
ここで大事な部分はpluginがユニークポインタなので所有権を譲渡した先の管理になり自身のハンドリング関数を呼び出すことが難しくなります。
そこでラムダ式にてpluginからC++14にて登場したGeneralized captureを使って新しい変数を定義します。ユニークポインタに対する扱いを簡単にする用途で使うことができるラムダ式のキャプチャ方法です。プラグイン開発では多用されているようです。

ここまでで登録処理は終わりました。あとはイベントを処理する部分を書いていくだけです。
iOS, Androidと類似するコードがC++で表現されるだけなので比較的簡単かなと思います。

DragAndDropWindowsPlugin::DragAndDropWindowsPlugin(flutter::BinaryMessenger* messenger) {
  event_channel_ = std::make_unique<flutter::EventChannel<flutter::EncodableValue>>(
      messenger,
      kDragAndDropWindowsChannelName,
      &flutter::StandardMethodCodec::GetInstance());
  auto handler = std::make_unique<flutter::StreamHandlerFunctions<flutter::EncodableValue>>(
      [this](
        const flutter::EncodableValue* arguments,
        std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events) {
        // onListen
        event_sink_ = std::move(events);
        return nullptr;
        },
      [this](const flutter::EncodableValue* arguments) {
        // onCancel
        event_sink_ = nullptr;
        return nullptr;
      }
  );
  event_channel_->SetStreamHandler(std::move(handler));
}

DragAndDropWindowsPlugin::~DragAndDropWindowsPlugin() {
  event_channel_->SetStreamHandler(nullptr);
}

EventChannelを作りChannelが接続されていればEventSinkにてアプリ側に値を渡すように定義するだけです。注意ポイントとして&flutter::StandardMethodCodec::GetInstance()がありますが、いくつかアプリ側とやりとりするCodecが存在します。ものによって何の型がやりとりできるか分かるので自分が使いやすいものを選べば良いです、ただ大抵の場合はそのままのStandardでよいかなと。

std::optional<LRESULT> DragAndDropWindowsPlugin::MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept {
  std::optional<LRESULT> result;
  switch (message) {
    case WM_ACTIVATEAPP: {
      DragAcceptFiles(window, true);
      return 0;
    }
    case WM_DROPFILES: {
      HDROP hdrop = reinterpret_cast<HDROP>(wparam);
      UINT file_count = DragQueryFileW(hdrop, 0xFFFFFFFF, nullptr, 0);
      if (file_count == 0) {
        return 0;
      }
      flutter::EncodableList files;
      for (UINT i = 0; i < file_count; ++i) {
        wchar_t filename[MAX_PATH];
        if (DragQueryFileW(hdrop, i, filename, MAX_PATH)) {
          int iBufferSize = ::WideCharToMultiByte(CP_UTF8, 0, filename, -1, NULL, 0, NULL, NULL);
          char* cpBufUTF8 = new char[iBufferSize];
          ::WideCharToMultiByte(CP_UTF8, 0, filename, -1, cpBufUTF8, iBufferSize, NULL, NULL);
          std::string s(cpBufUTF8, cpBufUTF8 + iBufferSize - 1);
          delete[] cpBufUTF8;
          flutter::EncodableValue file(std::move(s));
          files.push_back(std::move(file));
        }
      }
      DragFinish(hdrop);
      if (event_sink_) event_sink_->Success(files);
      return 0;
    }
  }
  return std::nullopt;
}

このコードが最後のものになります、普通にWin32 APIを叩くだけです。返したい値が作られればEncodableListの型に値を詰め込んでsinkに流すことでDart側にて受信できます。

Dart側は通常通りStreamで受け取ればOKです。

const EventChannel _eventChannel = EventChannel('drag_and_drop_windows');

final Stream<List<String>> _dropEventStream = _eventChannel
    .receiveBroadcastStream()
    .map((event) => List<String>.from(event))
    .asBroadcastStream();

Stream<List<String>> get dropEventStream => _dropEventStream;

おわりに

このプラグインを作ったきっかけとして、自分がFlutter for windowsで試験的に作ってるアプリに対してドラッグ&ドロップ機能を導入したかったところがあります。最初はそのままアプリ本体のwindows/runnerを変更して対処していたんですが、プラグイン化したほうが便利になると思って作成しました。

初めてのFlutter for windowsプラットフォームプラグイン開発でしたがあまりハマり続ける部分はなくすぐに公開と技術記事(この記事)ができてよかったと思っています。

自分はWindowsがメイン機として使うことが多いうえ、なんだかんだWindowsが便利だと感じている人間なのでFlutter for windowsが盛り上がって開発のデファクトスタンダードになることを祈っています。

宣伝

これからなんですが、はじめにも書いたとおり技術解説のyoutubeを始めるつもりで、ネタとしてこのプラグイン開発しました。動画でもFlutter for windows開発やiOS開発などの情報を発信していく(予定)のでぜひチャンネル登録してください。この文章を書いているのも自分にYoutubeはじめろ、というプレッシャーのつもりでやってます、ハイ。

YouTubeのvideoIDが不正ですhttps://www.youtube.com/channel/UCIaF9nAFNZl0EIuRIPri9AQ

Discussion