2024年6月28日金曜日

Tauri でドラッグアンドドロップでファイルを受け取る

今回は、ドラッグアンドドロップでファイルを受け取るのをやっていく。

コンテナだと動作確認が無理なので、 Windows に環境を入れて実施した。

前提

  • OS: Windows 11 Pro 23H2 ビルド 22631.3737
  • Tauri:
    • rustup: 1.27.1
    • rust: 1.79.0
    • node: v20.11.1
    • tauri-cli: 10.2.4

プロジェクト作成

> cargo install create-tauri-app --locked
> cargo create-tauri-app
✔ Project name · draganddrop
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, bun)
✔ Choose your package manager · npm
✔ Choose your UI template · React - (https://react.dev/)
✔ Choose your UI flavor · TypeScript

Template created! To get started run:
  cd draganddrop
  npm install
  npm run tauri dev

実装

今回は、さくっとコードを載せるだけ。コメントを見れば大体わかるでしょう。

編集対象はフロントエンドのみ。

src/App.tsx:

import { useEffect, useState } from "react";
import "./App.css";
import { appWindow } from "@tauri-apps/api/window";

function App() {

  const [dropFiles, setDropFiles] = useState<string[]>([]);

  useEffect(() => {
    let unlisten: any = null;
    if (!unlisten) {
      (async () => {
        // ファイルのドロップを購読
        unlisten = await appWindow.onFileDropEvent((event) => {
          if (event.payload.type === 'hover') {
            console.log('User hovering', event);
          } else if (event.payload.type === 'drop') {
            console.log('User dropped', event);
            setDropFiles(event.payload.paths);
          } else {
            console.log('File drop cancelled');
          }
        });
      })();
    }

    return () => {
      if (unlisten) {
        unlisten();
      }
    }
  })

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      <h2>Drop files:</h2>
      <ul>
        { /* ドラッグアンドドロップで受け取ったファイルのファイルパスを表示 */
          dropFiles.map((filePath) => <li key={filePath}>{filePath}</li>)
        }
      </ul>

    </div>
  );
}

export default App;

動作確認

npm run tauri dev で実行し、ファイルをドロップすると、 画面下部にドラッグされたファイルのファイルパスが表示される。

良さそう。

以上。

参考資料

2024年6月26日水曜日

Tauri でマルチウィンドウ(動的生成編)

今回は、マルチウィンドウ定義を動的に作るのをやっていく。

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0

前回のものに追加する形でやっていくので、差分を見てください。

実装

今回は、以下の順に実装していく。

  1. 初期ウィンドウ表示を main だけにする
  2. フロントエンド実装
    1. src-tauri/tauri.conf.json にウィンドウ操作の許可設定を追加
    2. 実装
  3. バックエンドエンド実装

フロントエンド実装・バックエンド実装はそれぞれ独立しているので必要などちらか片方だけでも OK。

初期ウィンドウ表示を main だけにする

起動時に、メインウィンドウとのみが表示されるように、前回追加したサブの定義を削除。

...(snip)
  "tauri": {
    ...(snip)
    "windows": [
      {
        "label": "main",
        "title": "Main Window",
        "url": "index.html",
        "height": 1000
      },
    ],
...(snip)

src-tauri/tauri.conf.json にウィンドウ操作の許可設定を追加

フロントエンドからウィンドウ操作をするには、 src-tauri/tauri.conf.json の allowList に許可設定を追加する必要がある。

今回は、 tauri/allowlist/window/create を true にする。

...(snip)
  "tauri": {
    "allowlist": {
      ...(snip)
      "window": {
        "create": true
      }
      ...(snip)
    },
...(snip)

これで、フロントエンドからウィンドウの生成を実行できるようになる。

フロントエンド実装

src/App.tsx:

フロントエンドから直接ウィンドウを生成するものと、 コマンド経由でバックエンドに生成を依頼するものの、 2 種類計 4 個のボタンを追加。

new してから操作するまでや、エラー取得の流れが独特なので気を付けること。

...(snip)
      <button onClick={() => {
        const mainWindow = new WebviewWindow('main', {
          title: 'Main Window',
          url: 'index.html',
        });
        mainWindow.once('tauri://created', () => {
          // ウィンドウ生成後、何かしたい場合はここに記述する
        });

        mainWindow.once('tauri://error', function(e) {
          console.log(e);
        });
      }}>フロントエンドからメインウィンドウを開く</button>
      <button onClick={() => {
        const subWindow = new WebviewWindow('sub', {
          title: 'Sub Window',
          url: 'index.html',
        });
        subWindow.once('tauri://created', () => {
          // ウィンドウ生成後、何かしたい場合はここに記述する
        });

        subWindow.once('tauri://error', function(e) {
          console.log(e);
        });
      }}>フロントエンドからサブウィンドウを開く</button>

      <button onClick={() => {
        invoke('open_main_window', {});
      }}>バックエンドからメインウィンドウを開く</button>
      <button onClick={() => {
        invoke('open_sub_window', {});
      }}>バックエンドからサブウィンドウを開く</button>
...(snip)

バックエンド実装

src-tauri/src/main.rs:

フロントエンドから open_main_window, open_sub_window のコマンドを呼び出すようにしたので、 それらコマンドを実装し、それぞれ対象のウィンドウにイベントを発行するように実装。

...(snip)
#[tauri::command]
fn open_main_window(app_handle: AppHandle) {
    let result = tauri::WindowBuilder::new(
        &app_handle,
        "main",
        tauri::WindowUrl::App("index.html".into()),
    )
    .build();

    // 既にウィンドウが開いている場合は Err が返ってくるので、match で判定して処理
    match result {
        Ok(main_window) => main_window.set_title("Main Window").unwrap(),
        Err(error) => println!("{:?}", error),
    }
}

#[tauri::command]
fn open_sub_window(app_handle: AppHandle) {
    let result = tauri::WindowBuilder::new(
        &app_handle,
        "sub",
        tauri::WindowUrl::App("index.html".into()),
    )
    .build();

    // 既にウィンドウが開いている場合は Err が返ってくるので、match で判定して処理
    match result {
        Ok(sub_window) => sub_window.set_title("Sub Window").unwrap(),
        Err(error) => println!("{:?}", error),
    }
}
...(snip)
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            open_main_window,
            open_sub_window,
            to_all,
            to_main,
            to_sub
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

動作確認

×でどちらかのウィンドウを消したあと、ウィンドウ作成ボタを押下すると、ウィンドウが表示される。 良さそう。

以上。

参考資料

Tauri でマルチウィンドウ(静的定義編)

今回は、マルチウィンドウ定義を静的に設定するのをやっていく。

そして、前回やらなかった個別ウィンドウへ向けたイベント送信をやってみる。

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0

実装

今回は、以下の順に実装していく。

  1. src-tauri/tauri.conf.json にウィンドウ定義を追加
  2. フロントエンド実装
  3. バックエンドエンド実装

フロントエンド実装・バックエンド実装はそれぞれ独立しているので必要などちらか片方だけでも OK。

src-tauri/tauri.conf.json にウィンドウ定義を追加

起動時に、メインウィンドウとサブウィンドウのふたつが表示されるように設定。

...(snip)
  "tauri": {
    ...(snip)
    "windows": [
      {
        "label": "main",
        "title": "Main Window",
        "url": "index.html",
        "height": 1000
      },
      {
        "label": "sub",
        "title": "Sub Window",
        "url": "index.html",
        "height": 1000
      }
    ],
...(snip)

ここで設定した label で、各ウィンドウの判別が行われる。

どちらも url を index.html としているので、同じ画面を表示する。

フロントエンド実装

src/App.tsx:

初回起動時にイベントを listen.

各種ボタンでそれぞれの対象に対してイベントを発行する。

import { useEffect, useState } from "react";
import "./App.css";
import { WebviewWindow, appWindow, getCurrent } from "@tauri-apps/api/window";
import { emit, listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api";

function App() {
  const currentWindow = getCurrent();
  const [receivedEvent, setReceivedEvent] = useState<Array<any>>([]);

  let initialized = false;

  {/* useEffect を使って `event` という名前のイベントを購読する */}
  useEffect(() => {
    if (!initialized) {
      let unlisten: any = undefined;
      (async () => {
        unlisten = await listen('event', (e: any) => {
          console.log(e);
          setReceivedEvent((prev) => [...prev, e])
        });
      })();

      initialized = true;

      return () => {
        if (unlisten) {
          unlisten();
        }
      };
    }
  }, []);

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      {/* getCurrent で、現在の WebviewWindow オブジェクトが取得できる */}
      <p>This window is: `{currentWindow.label}`</p>

      <h2>フロントエンドからのイベント送信</h2>

      <button onClick={() => {
        emit('event', { message: `${currentWindow.label}からのイベント` })
      }}>表示中の全ウィンドウへイベント送信</button>
      <button onClick={() => {
        appWindow.emit('event', { message: `${currentWindow.label}からのイベント` })
      }}>自分自身へイベント送信</button>
      <button onClick={() => {
        const mainWindow = new WebviewWindow('main');
        mainWindow.emit('event', { message: `${currentWindow.label}からのイベント` })
      }}>mainへのイベント送信</button>
      <button onClick={() => {
        const subWindow = new WebviewWindow('sub');
        subWindow.emit('event', { message: `${currentWindow.label}からのイベント` })
      }}>subへのイベント送信</button>

      <h2>バックエンドからのイベント送信</h2>

      <button onClick={() => {
        invoke('to_all', {});
      }}>表示中の全ウィンドウへイベント送信</button>
      <button onClick={() => {
        invoke('to_main', {});
      }}>mainへのイベント送信</button>
      <button onClick={() => {
        invoke('to_sub', {});
      }}>subへのイベント送信</button>

      <ul>
        {receivedEvent.map((e) => <li key={e.id}>{JSON.stringify(e)}</li>)}
      </ul>
    </div>
  );
}

export default App;

バックエンド実装

src-tauri/src/main.rs:

フロントエンドから to_all, to_main, to_sub のコマンドを呼び出すようにしたので、 それらコマンドを実装し、それぞれ対象のウィンドウにイベントを発行するように実装。

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use serde::Serialize;
use tauri::{AppHandle, Manager};

#[derive(Clone, Serialize)]
struct Payload {
    message:  String,
}

#[tauri::command]
fn to_all(app_handle: AppHandle) {
    app_handle.emit_all("event", Payload { message: "バックエンドからのイベント".to_string() }).unwrap();
}

#[tauri::command]
fn to_main(app_handle: AppHandle) {
    app_handle.emit_to("main", "event", Payload { message: "バックエンドからのイベント".to_string() }).unwrap();
}

#[tauri::command]
fn to_sub(app_handle: AppHandle) {
    app_handle.emit_to("sub", "event", Payload { message: "バックエンドからのイベント".to_string() }).unwrap();
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            to_all,
            to_main,
            to_sub])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

動作確認

各ボタンをポチポチすると、ウィンドウ下部の受信イベント表示欄に受信したイベントが表示されていく。 良さそう。

実行した画面

以上。

参考資料