Wails 実践講座 #4 トレイ常駐とグローバルショートカット — バックグラウンドから素早くキャプチャ
これまでのノートアプリは、ウィンドウを閉じるとプロセスが終わります。メモツールの本当の価値は必要な瞬間に即座に現れることにあるので、この記事はアプリをトレイに常駐させ、グローバルショートカットでどこからでも新規ノートをキャプチャするバックグラウンドアプリに変えます。ここで Wails v2 の一つの制約を正直に扱ってから進みます。
全 10 編(2 部構成)のうち 4 番目の記事です。
- #1 実践プロジェクトの設計 · #2 SQLite ローカルデータベース · #3 全文検索とデータフロー
- #4 トレイ常駐とグローバルショートカット — バックグラウンドから素早くキャプチャ ← この記事
- #5 署名と公証 — デプロイしたアプリが信頼される仕組み
- #6 CI/CD 自動リリース — GitHub Actions で 3 プラットフォームへ配信
制約から: v2 には内蔵トレイがない #
正直に始めます。Wails v2 にはシステムトレイの API が内蔵されていません。 この機能は #1 で触れたとおり v3 で正式に入ります。v2 でトレイを使うには外部の Go ライブラリを付ける必要があり、最も広く使われるのが getlantern/systray です。ウィンドウ管理は Wails ランタイムが、トレイアイコン・メニューは systray が担う組み合わせです。
この制約を知ることが重要なのは、古いチュートリアルや AI が「Wails にトレイを付けよう」と言って v2 にない API を呼ぶコードを出すことがあるからです。v2 では外部ライブラリ、v3 では内蔵 API — この区別を基準にします。
ウィンドウの閉じるを終了ではなく非表示に #
バックグラウンドアプリの最初の条件は、ウィンドウを閉じてもプロセスが生きていることです。Wails の OnBeforeClose フックで終了を止めてウィンドウを隠せばよいです。
// main.go の options.App に接続: OnBeforeClose: app.beforeClose
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
runtime.WindowHide(ctx) // ウィンドウを隠して
return true // 実際の終了は止める
}true を返すと Wails が終了をキャンセルします。これでウィンドウの閉じるボタンは、アプリを終了する代わりにトレイにしまう動作になります。実際の終了はトレイメニューの「終了」項目で runtime.Quit(ctx) として別に提供します。この区別がないとユーザーがアプリを完全に閉じる方法がなくなるので、必ず一緒に置きます。
トレイ: アイコンとメニュー #
systray は自前のイベントループを回すので、Wails が起動した後に別のゴルーチンで立ち上げます。トレイメニューには最低限「新規ノート」「ウィンドウを開く」「終了」を置きます。
func (a *App) startTray() {
systray.Run(func() {
systray.SetIcon(trayIcon) // //go:embed で入れたアイコンのバイト
systray.SetTooltip("ノート")
mNew := systray.AddMenuItem("新規ノート", "クイックキャプチャのウィンドウを開く")
mShow := systray.AddMenuItem("ウィンドウを開く", "メインウィンドウを表示")
systray.AddSeparator()
mQuit := systray.AddMenuItem("終了", "アプリを終了")
for {
select {
case <-mNew.ClickedCh:
a.openQuickCapture()
case <-mShow.ClickedCh:
runtime.WindowShow(a.ctx)
case <-mQuit.ClickedCh:
runtime.Quit(a.ctx)
return
}
}
}, nil)
}メニュー項目のクリックは各項目の ClickedCh チャネルに届くので、select で受けて該当する動作を実行します。ウィンドウを再び表示するのは 入門 #4 で扱った runtime.WindowShow です。
グローバルショートカット: アプリの外からキャプチャ #
メモツールの核心の体験は、別のアプリを使っていてもショートカット一つで即座にキャプチャすることです。アプリがフォーカスを持たない状態でも動く必要があるのでグローバルショートカットが必要ですが、これも v2 ランタイムになく、外部ライブラリ(golang.design/x/hotkey など)を使います。
func (a *App) registerHotkey() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyN)
if err := hk.Register(); err != nil {
runtime.LogError(a.ctx, "ショートカットの登録に失敗: "+err.Error())
return
}
go func() {
for range hk.Keydown() {
a.openQuickCapture() // Ctrl+Shift+N が押されたらキャプチャウィンドウ
}
}()
}openQuickCapture は小さなキャプチャウィンドウを前面に持ってくる関数です。ウィンドウを表示してフォーカスを与えると、ユーザーはタイトルを入力して保存するだけでノートを残せます。保存は #3 で作った CreateNote をそのまま呼び、このとき発行される notes:changed イベントがメインウィンドウの一覧まで自動で更新します。アプリの外で追加してもすべてのウィンドウが最新になるこの流れが、#3 でデータの真実を Go に置いた設計の見返りです。
グローバルショートカットには二つの注意があります。一つは別のアプリがすでに使う組み合わせと衝突しうること(できればユーザーが変えられるようにします)、もう一つは macOS でアクセシビリティ(入力監視)の権限を要求しうることです。初回起動時に権限の案内を用意しておきます。
v3 ではどう変わるか #
Wails v3 はシステムトレイをランタイムに内蔵し、マルチウィンドウを正式に支援します。つまり v3 では systray のような外部ライブラリなしでトレイアイコン・メニューを Wails の API で直接作り、キャプチャウィンドウも別のウィンドウとして綺麗に表示できます。この記事の概念(閉じるを非表示に、トレイメニューの構成、グローバルショートカットでキャプチャ、イベントで更新)は v3 でもそのままで、変わるのはトレイを外部ライブラリの代わりに内蔵 API で作るという点です。
まとめ #
- Wails v2 にはシステムトレイとグローバルショートカットが内蔵されていません。
getlantern/systray、golang.design/x/hotkeyのような外部ライブラリで付けます。v3 はトレイを内蔵します。 - バックグラウンドアプリの最初の条件は、
OnBeforeCloseでtrueを返してウィンドウの閉じるを終了ではなく非表示に変えることです。実際の終了はトレイメニューから別に提供します。 - トレイメニュー項目のクリックは
ClickedChチャネルで受け、selectで処理します。最低限「新規ノート・ウィンドウを開く・終了」を置きます。 - グローバルショートカットはアプリがフォーカスを持たなくても動きます。保存は既存の
CreateNoteをそのまま呼び、notes:changedイベントがすべてのウィンドウを更新します。 - 次の記事では、このアプリを署名・公証して、他の人の端末で警告なしに実行されるようにします。