🌐

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

8 min read

この記事でやること

  • 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#で作る記事です。上に書いた通り自分はうまくいかなかったのですがもしかしたらうまくいくのかもしれません。

Discussion

ログインするとコメントできます