🪟

React Native for WindowsでWindowsネイティブ機能を呼び出す

2021/02/08に公開

この記事でやること

  • React Native for Windows(以下 RN4W)のプロジェクトを作成してそこに C++を使って Windows ネイティブの機能を追加します。
  • 今回はサンプルとしてボタンがクリックされた時にイベントが発火するようなものを作っていきます。

前提

こちらに書いてある通りなのですが一応書いておくと

  • nodejs
  • Visual Studio 2019
    • Universal Windows Platform development
    • Desktop development with C++

またそれに加えてこの記事では

  • VSCode
  • VSCode の React Native Tools 拡張機能

も使うのでこれらをインストールしておいてください。

1. プロジェクトを作成

それではこちらに従って新しいプロジェクトを作成していきます。

> npx react-native init rn4wSampleApp --template react-native-template-typescript@6.5.*

新しい React Native のプロジェクトを作ります。今回は TypeScript を使います。

> cd .\rn4wSampleApp\
> npx react-native-windows-init --overwrite

RN4W を入れます。

2. VSCode を設定して React Native パッケージャとデバッガを起動

VSCode に React Native Tools を入れて起動設定を作るだけで JS 側はほぼ設定いらずでデバッグ等ができてとても便利です。

  1. まず VSCode でrn4wSampleAppのフォルダを開きます。
  2. .vscode/launch.jsonを作成し以下のように書き換えてください。
.vscode/launch.json
{
  "configurations": [
    {
      "name": "Launch UWP",
      "cwd": "${workspaceFolder}",
      "type": "reactnative",
      "request": "launch",
      "platform": "windows"
    },
    {
      "name": "Attach",
      "cwd": "${workspaceFolder}",
      "type": "reactnative",
      "request": "attach"
    },
    {
      "name": "Launch Android",
      "cwd": "${workspaceFolder}",
      "type": "reactnativedirect",
      "request": "launch",
      "platform": "android"
    }
  ]
}

これで VSCode 左部からデバッグができるようになります。Launch UWPを選んで実行すれば起動するのですが今回はそれではなくAttachを選択して実行しておいてください。
すると VSCode がアタッチ待ちの状態になり、React Native パッケージャーが起動するのでそのまま次に進んでください。

3. アプリを起動

  1. windows/rn4wSampleApp.slnを Visual Studio で開きます。
  2. 上部のコンボボックスからx86x64を選び実行します。初回は結構時間がかかるので気長に待ちます。
  3. ウインドウが表示され、先ほど起動しておいた VSCode のデバッガにアタッチされるはずです。その後しばらく待ち、「Welcome to React」の画面が表示されたら成功です。

4. ネイティブコードを書く

ここまでの手順だけでも普通に RN4W を使うことができますが、やはりネイティブ機能を使いたいときが出てくるでしょう。RN4W では UWP の機能を呼び出すことができます。その機能を使って今回はマウスボタンがクリックされたときにイベントを JS 側に送るようにしたいと思います。

  1. Visual Studio でソリューション内にあるrn4wSampleAppプロジェクトを右クリックし、「追加」->「新しい項目」と選択します。
  2. 出てきた画面で「ヘッダーファイル (.h)」を選択し、ここでは「MouseHandler.h」と名づけることにしてファイルを新規作成します。
  3. rn4wSampleApp内にあるReactPackageProvider.cppに今作ったファイルを include させます。include の末尾に次の行を加えてください。
    windows/rn4wSampleApp/ReactPackageProvider.cpp
    #include "MouseHandler.h"
    
  4. MouseHandler.hを編集します。以下のように書き換えてください。実行してエラーが出なければ成功です。
windows/rn4wSampleApp/MouseHandler.h
#pragma once

#include "pch.h"

#include <functional>

#include "NativeModules.h"

#include <winrt/Windows.UI.Core.h>
#include <winrt/Windows.UI.Input.h>
#include <winrt/Windows.UI.Xaml.h>
#include <winrt/Windows.UI.Xaml.Input.h>
#include <winrt/Windows.System.h>

namespace winrt
{
  using namespace Windows::UI::Input;
  using namespace Windows::UI::Xaml;
  using namespace Windows::UI::Xaml::Input;
  using namespace Windows::System;
}

namespace MouseHandler
{

  REACT_MODULE(MouseHandler);
  struct MouseHandler
  {
    REACT_METHOD(Init);
    void Init() noexcept {
      winrt::Window::Current().CoreWindow().PointerPressed(
      [=](winrt::CoreWindow const&, winrt::PointerEventArgs const args) {
        winrt::PointerPointProperties pointerProps = args.CurrentPoint().Properties();
        if (pointerProps.IsXButton1Pressed()) {
          XButton1Click();
        }
        else if (pointerProps.IsXButton2Pressed()) {
          XButton2Click();
        }
        else if (pointerProps.IsLeftButtonPressed()) {
          LeftButtonClick();
        }
        else if (pointerProps.IsRightButtonPressed()) {
          RightButtonClick();
        }
        else if (pointerProps.IsMiddleButtonPressed()) {
          MiddleButtonClick();
        }
        return;
      });
    }
    REACT_EVENT(XButton1Click);
    std::function<void()> XButton1Click;
    REACT_EVENT(XButton2Click);
    std::function<void()> XButton2Click;
    REACT_EVENT(RightButtonClick);
    std::function<void()> RightButtonClick;
    REACT_EVENT(LeftButtonClick);
    std::function<void()> LeftButtonClick;
    REACT_EVENT(MiddleButtonClick);
    std::function<void()> MiddleButtonClick;
  };
}

簡単に解説します。といってもこちらにある通りなのですが、マクロが用意されていて、それを使うことでうまい具合に関数を JS 側から実行させたりできます。
まずREACT_MODULEでモジュールを指定します。そして指定した構造体の中にいろいろ書いていきます。
今回はREACT_METHODREACT_EVENTを使っていますが、REACT_METHODは関数を JS から実行できるものでREACT_EVENTは JS 側で登録しで JS 側のイベントを発火させることができます。
また基本的に C++でつけた名前がそのまま JS でも使われますがマクロの第二引数に文字列を入れると JS 側から別名で呼び出せたりします。

5. JS 側から実行する

さて、このままではうまくいってるんだかわからないので今度は JS 側のコードを書き換えていきます。App.tsxを次のように書き換えます。

App.tsx
import * as React from 'react';

import {useEffect} from 'react';
import {Text, View} from 'react-native';
import {NativeModules, NativeEventEmitter} from 'react-native';

const MouseHandlerEventEmitter = new NativeEventEmitter(
  NativeModules.MouseHandler,
);

NativeModules.MouseHandler.Init();

const MainPage: React.FC = () => {
  const [message, setMessage] = React.useState('');
  useEffect(() => {
    console.log(NativeModules);
    MouseHandlerEventEmitter.addListener('XButton1Click', () => {
      setMessage('x1 button clicked!');
    });
    MouseHandlerEventEmitter.addListener('XButton2Click', () => {
      setMessage('x2 button clicked!');
    });
    MouseHandlerEventEmitter.addListener('RightButtonClick', () => {
      setMessage('Right button clicked!');
    });
    MouseHandlerEventEmitter.addListener('LeftButtonClick', () => {
      setMessage('Left button clicked!');
    });
    MouseHandlerEventEmitter.addListener('MiddleButtonClick', () => {
      setMessage('Middle button clicked!');
    });
    return () => {
      MouseHandlerEventEmitter.removeAllListeners('XButton1Click');
      MouseHandlerEventEmitter.removeAllListeners('XButton2Click');
      MouseHandlerEventEmitter.removeAllListeners('RightButtonClick');
      MouseHandlerEventEmitter.removeAllListeners('LeftButtonClick');
      MouseHandlerEventEmitter.removeAllListeners('MiddleButtonClick');
    };
  }, []);
  return (
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Text>{message}</Text>
    </View>
  );
};

export default MainPage;

このコードを見たら大体わかっていただけると思いますがNativeModules.上のREACT_MODULEで指定した名前.REACT_METHODで指定した名前の形でネイティブの関数を呼び出すことができます。そして NativeEventEmitter のインスタンスを作り addListener の第一引数にREACT_EVENTで指定した名前を入れることでネイティブからのイベントを実行することができるようになります。
というか NativeModules の下は any になってるので TypeScript にした意味あんまりありませんでしたね・・・
実際に使うときはちゃんと型定義を書いた方が良いでしょう。

さて、コードを保存したらアプリ上でCtrl+Shift+Dを押して「Reload Javascript」を選択してください。
アプリ上でなにかマウスのボタンをクリックするととても地味ですが表示が切り替わるはずです!

感想

  • React Native よりも UWP の情報を調べるのと自分が普段 Web 系の言語しか書いてないのもあって C++を書くのにも苦労しました。C++の説明で頓珍漢なこと言ってたら申し訳ないです。
  • 一番きつかったのは UWP の API を調べることでした。最初はどのマウスボタンが押されたのか判別する API を見つけられなくてあきらめかけていました。公式のドキュメントを見ながら頑張りましょう。
  • C++については調べても C++/CX という似て非なるもののコードが載ってたりしてコピペしても動かなかったりしてつらかったのとそもそも C++で UWP を使うコード例がほとんど見当たりませんでした。C#では普通にプロパティでアクセスしているものが C++だと関数になっていたりとかいろいろつらかったです。

その他

  • 一応 C#で作ることも可能なようなのですがうまくいきませんでした。RN4W が C#で作られていたころはうまくいったのかも・・・?
  • UWP アプリ以外は作ることができません。ただこれから WinUI3 をサポートして古いバージョンの Win10 もサポートするみたいなことが書いてあったので将来的には他の形式にもできるかもしれません。
  • 最初に init 関数を呼ばないと動かないのはどうなのかと思ったのですがこれしか方法が見つけられませんでした・・・

参考にしたサイト

https://microsoft.github.io/react-native-windows/docs/native-modules

RN4W の公式ドキュメントです。わかんないことがあればここを見るのがベスト。

https://docs.microsoft.com/ja-jp/uwp/api

UWP の公式ドキュメントです。ここを見るのがいいというかここにしか情報がないです。

https://techcommunity.microsoft.com/t5/windows-dev-appconsult/building-a-react-native-module-for-windows/ba-p/1067893

C#で作る記事です。上に書いた通り自分はうまくいかなかったのですがもしかしたらうまくいくのかもしれません。

この記事は https://note.nazo6.dev/blog/43e24b6b170a40 とのクロスポストです。

GitHubで編集を提案

Discussion