Tauriでメニューバーにアプリを常駐しよう[JavaScript編]
はじめに
前回Tauri v2 の System Tray API を使って、メニューバーに常駐するアプリケーションを作成しました。
今回は同機能を JavaScript 側の API を使って実装してみたいと思います。
メニューバーにアイコンを表示
まずはメニューバーにアイコンを表示させます。
アイコンの表示は、
await TrayIcon.new(options?: TrayIconOptions);
を使うことで表示できます。
アイコンを作成する関数を作り、非同期処理の中で呼び出すようにしておきます。
この時、icon
のパスを src-tauri
を起点としたパスで記載する必要があります。
今回の場合、src-tauri/icons/icon.png
を参照します。
(icon
を指定しない場合透明な状態で作成されるため、
メニューバーに追加されていることに気づかず、若干ハマりました。)
import { useEffect } from "react";
import { TrayIcon } from "@tauri-apps/api/tray";
async function createTray(): Promise<TrayIcon> {
const tray = await TrayIcon.new({
id: "js_tray_icon",
// `src-tauri`を起点にしたパス
icon: "icons/icon.png",
});
return tray;
}
export function useTray() {
useEffect(() => {
async function setup() {
// トレイアイコンを作成
const tray = await createTray();
}
setup();
}, []);
}
メニューの表示
続いて、メニューアイテムを表示していきます。
メニューアイテムは
await Menu.new(opts?: MenuOptions);
でMenu
インスタンス作成し、そのインスタンスに
await Menu.append(items: MenuItemOptions | MenuItemOptions[])
してアイテムを追加していきます。
最後に、Menu
インスタンスを
await TrayIcon.setMenu(menu: Menu | Submenu | null);
とすることで、メニューバー内で機能するメニューとして利用できるようになります。
// 追加
import { Menu, MenuItemOptions } from "@tauri-apps/api/menu";
// 追加
async function createMenu(): Promise<Menu> {
// メニューを作成
const menu = await Menu.new();
// メニューアイテムを追加
const menuItems: MenuItemOptions[] = [
{
id: "menuID1",
text: "メニュー1",
action: async (id) => {
console.log(`menu "${id}" clicked`);
},
},
{
id: "menuID2",
text: "メニュー2",
action: async (id) => {
console.log(`menu "${id}" clicked`);
},
},
];
await menu.append(menuItems.map((menu) => menu));
return menu;
}
export function useTray() {
useEffect(() => {
async function setup() {
// トレイアイコンを作成
const tray = await createTray();
// メニューを作成
const menu = await createMenu();
await tray.setMenu(menu);
}
setup();
}, []);
}
メニューの追加
続いて、フロントエンドの操作をメニューバーに反映させていきます。
今回はボタンをクリックした時に、入力されたタイトルがメニューバーに追加されるようにしてみます。
まず、Menu
とTrayIcon
をステートで管理し、useEffect
の外でも使えるようにします。
export function useTray() {
// 追加
const [menu, setMenu] = useState<Menu | null>(null);
const [tray, setTray] = useState<TrayIcon | null>(null);
useEffect(() => {
async function setup() {
// 省略
// 追加
setTray(tray);
setMenu(menu);
}
setup();
}, []);
}
続いて、ハンドラーを追加し、引数で受け取った文字列をメニューに追加できるようにします
export function useTray() {
const [menu, setMenu] = useState<Menu | null>(null);
const [tray, setTray] = useState<TrayIcon | null>(null);
// 追加
async function addMenu(text: string) {
if (!menu) return;
// メニューアイテムを追加
await menu.append({
id: crypto.randomUUID(),
text: text,
});
setMenu(menu);
}
useEffect(() => {
async function setup() {
// 省略
}
setup();
}, []);
// 追加
return { addMenu };
}
また、Menu
が更新された際に、TrayIcon
にセットしなおすようにしておきます。
export function useTray() {
const [menu, setMenu] = useState<Menu | null>(null);
const [tray, setTray] = useState<TrayIcon | null>(null);
// 追加
const resetMenu = useCallback(async () => {
if (!tray) return;
await tray.setMenu(menu);
}, [tray, menu]);
async function addMenu(text: string) {
// 省略
}
useEffect(() => {
async function setup() {
// 省略
}
setup();
}, []);
// 追加
useEffect(() => {
resetMenu();
}, [resetMenu]);
return { addMenu };
}
最後に、App.tsx
を以下のように修正し、上記で作成したフックスを利用するようにします。
import { useState } from "react";
import "./App.css";
import { useTray } from "./hooks/useTray";
function App() {
const { addMenu } = useTray();
const [text, setText] = useState("");
async function handleAddMenu(text: string) {
await addMenu(text);
setText("");
}
return (
<main className="container">
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button onClick={() => handleAddMenu(text)}>追加</button>
</main>
);
}
export default App;
Tauri を
npm run tauri dev
で立ち上げて動作を確認すると、
以下のように入力したテキストをメニューバーに追加することができました。
最終的なコードは以下の通りです。
import { useCallback, useEffect, useState } from "react";
import { TrayIcon } from "@tauri-apps/api/tray";
import { Menu, MenuItemOptions } from "@tauri-apps/api/menu";
async function createTray(): Promise<TrayIcon> {
const tray = await TrayIcon.new({
id: "js_tray_icon",
// `src-tauri`を起点にしたパス
icon: "icons/icon.png",
});
return tray;
}
async function createMenu(): Promise<Menu> {
// メニューを作成
const menu = await Menu.new();
// メニューアイテムを追加
const menuItems: MenuItemOptions[] = [
{
id: "menuID1",
text: "メニュー1",
action: async (id) => {
console.log(`menu "${id}" clicked`);
},
},
{
id: "menuID2",
text: "メニュー2",
action: async (id) => {
console.log(`menu "${id}" clicked`);
},
},
];
await menu.append(menuItems.map((menu) => menu));
return menu;
}
export function useTray() {
const [menu, setMenu] = useState<Menu | null>(null);
const [tray, setTray] = useState<TrayIcon | null>(null);
// メニューをリセット
const resetMenu = useCallback(async () => {
if (!tray) return;
await tray.setMenu(menu);
}, [tray, menu]);
async function addMenu(text: string) {
if (!menu) return;
// メニューアイテムを追加
await menu.append({
id: crypto.randomUUID(),
text: text,
});
setMenu(menu);
}
useEffect(() => {
async function setup() {
// トレイアイコンを作成
const tray = await createTray();
// メニューを作成
const menu = await createMenu();
await tray.setMenu(menu);
setTray(tray);
setMenu(menu);
}
setup();
}, []);
useEffect(() => {
resetMenu();
}, [resetMenu]);
return { addMenu };
}
import { useState } from "react";
import "./App.css";
import { useTray } from "./hooks/useTray";
function App() {
const { addMenu } = useTray();
const [text, setText] = useState("");
async function handleAddMenu(text: string) {
await addMenu(text);
setText("");
}
return (
<main className="container">
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button onClick={() => handleAddMenu(text)}>追加</button>
</main>
);
}
export default App;
まとめ
前回に引き続き、Tauri v2 の System Tray API について紹介しました。
フロントエンドから操作できる API が追加されたことで、
「データフェッチしてきたものをメニューバーに追加」などのユースケースがより簡単に実装できるようになったと感じています。
個人的には Tauri は触っていてとても楽しいので、今後も別の機能を紹介していければと思います。
Discussion