Tauri + windows-rsでスリープ防止アプリを作った話またはその制作手順を公開しTauri DIYを布教する的な何か

この記事は [Rust Advent Calendar 2023 - Qiita](https://qiita.com/advent-calendar/2023/rust) 18日目の記事です[^aisatsu]！

[^aisatsu]: 毎年のことながら遅刻してしまい申し訳ありませんm(_ _)m 前日の19日は[aobat](https://qiita.com/aobat)さんの[privateなcrateの扱いについて](https://qiita.com/aobat/items/4d383177990765776a16)という記事でした。`git`のリポジトリとコミットIDを使用してバージョン固定できるやり方神ですよね、筆者はリポジトリをPublicにしがちなので認証で困ることはあまりなく何度も救われています。

GUIフレームワークの[Tauri](https://tauri.app/)と、Microsoft公式が出しているWindows API用クレートの[windows-rs](https://github.com/microsoft/windows-rs)を使用し、[sleepy-locker](https://github.com/anotherhollow1125/sleepy-locker)というWindows用スリープ防止ユーティリティアプリを作成したので、その仕組の解説記事になります！

作ったアプリ「sleepy-locker」のリポジトリ↓

https://github.com/anotherhollow1125/sleepy-locker

- ダウンロードページ: https://github.com/anotherhollow1125/sleepy-locker/releases/
    - Assets以下の sleepy-locker_0.0.2_x64_ja-JP.msi がインストーラになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/d8147d74-8202-6683-36a1-c733881c6218.png)

地味...！...しかも、**[昨年](https://qiita.com/namn1125/items/fa59ba4e499d770921db)と記事構成も技術スタックもほぼ一緒やないかい！** 修論に追われ、アドカレに向けて新規性のあるネタを用意できませんでした[]

# 作った動機と機能説明

筆者は執筆時点で大学院生のため、PCで論文PDFをじーっと眺めることがよくあります。

すると"ヤツ"は突然訪れます。そう、**画面スリープです！**

その度にマウスをくるくる...動画などは再生中であることを検出してスリープくんは忖度してくれますが、いくらAIが発達した2023年でも「画面を眺める」という人間の行為はPCに認識してもらえません[^detect_face]。

[^detect_face]: 人感センサー付きモニターなんかもありますが筆者は使ってないです。

設定を変えればいい？確かにそのとおりです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/43c71f04-a13c-8609-0353-5fb0efe152fd.png)

...

今度は**モニタがずっと点きっぱなしになってしまいました**。<font color="#888888">自分でそうしたわけですが...</font>

筆者はPCをシャットダウンもスリープもしない派[^not_sleep]なので、365日モニタが点きっぱなしになってしまいます。電力を無駄に消費しますし、モニタが傷んでしまいますね。

[^not_sleep]: シャットダウンやスリープから復帰する時にSSDの読み書き回数が消耗されるという迷信を信じているのが一つ、モニタは別として、PC本体に関しては点けっぱなしでもあまり問題ないと聞いたのが一つ、そしてPCは日常的に使用するため起動時間が待てないというのが一番大きい理由です。でもまぁこれは人それぞれ考えが異なるものだと思います。( わりと本記事の核心に近い内容なのになぜ注釈に書いた[] )

一方で、シェアハウスに住んでいたりする関係でデスクトップPCも人目に触れる機会が多いため、外出や睡眠といった離席時にPCロックはよく掛けます。

ロック時はモニタが点いている必要はありません。つまり**ロック時は消灯していてほしい**のです。

色々悩んだ末、最終的に流れ着いたのが、手動でスリープ防止を切り替えられるアプリケーションの利用でした。

- [パソコンがスリープ(休止状態)になるのを防止するソフト「Sleep Preventer」 | フリーソフトラボ.com](https://freesoftlab.com/detail/sleep-preventer/)
- [Windows 用の PowerToys Awake ユーティリティ | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/powertoys/awake)

PowerToysのAwakeは最近知ったもので、しばらくは１つ目のSleep Preventerというフリーソフトを利用していました。

しかしSleep Preventerには、「ロック中だけ防止無効化」といった便利機能はありません。

結果、離着席のたびに操作が必要になりました。「Sleep Preventerを無効化」 → 「Win + L (ロックのショートカット)」 → 「ロック解除」 → 「Sleep Preventarを有効化」...

別に短時間であればモニタが点きっぱなしでも全然構わないのですが、一々Sleep Preventerのオンオフを気にしていたのでは筆者の脳の消費電力が上がってしまいます。

そこで、「ロック解除後はスリープ防止をするけども、ロック中は普通に画面スリープをする(システムの電源オプションに従う)」アプリケーションが欲しくなりました。

# Just, Do It Yourself！

Don't let your dreams be dreams...

「スリープ防止を行える既存アプリがあるならば、その仕組みを少し流用するだけでお望みのアプリを作れるのでは？」

と考え、Microsoft公式が出している[PowerToys Awake](https://learn.microsoft.com/ja-jp/windows/powertoys/awake)を参考に、どうやってスリープを防止しているかを調べてみました。

https://github.com/microsoft/PowerToys/blob/ee2258191338aa36824ce5cb4b2234a02e1cbdd0/src/modules/awake/Awake/Core/Manager.cs#L85

```C#:C#
using Microsoft.Win32;

// 省略

private static bool SetAwakeState(ExecutionState state)
{
    try
    {
        var stateResult = Bridge.SetThreadExecutionState(state);
        return stateResult != 0;
    }
    catch
    {
        return false;
    }
}
```

おや？ [`SetThreadExecutionState`関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-setthreadexecutionstate) を呼び出しているだけで案外シンプル...？これなら自分でできそう...！

以降ここからは調査しながら手を動かしながらアプリをDIYしていきました。本記事の残りでは、[sleepy-locker](https://github.com/anotherhollow1125/sleepy-locker)を作った軌跡をコミット等を参考にまとめて行きたいと思います！

:::note warn
以降読みやすさの都合上パス等は明記せず、必要な手順を含め省略を多用します。正確なコードや位置関係は[リポジトリ](https://github.com/anotherhollow1125/sleepy-locker)を参考にしていただけると幸いです。
:::

# とりあえずまずスリープ防止

[windows-rs](https://github.com/microsoft/windows-rs) クレートを使って、実際に `SetThreadExecutionState` 関数を試してみました。

```toml:Cargo.toml
[dependencies.windows]
version = "0.52.0"
features = [
    "Win32_System_Power",
    # ...
]
```

`Win32_System_Power` featureは、`windows::Win32::System::Power::SetThreadExecutionState` を使うために必要なfeatureです。

本アプリの`lib.rs`以下に、次の関数を定義しました。

```rust:Rust
use windows::Win32::System::Power::{
    SetThreadExecutionState, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED,
};

pub fn prevent_sleep() {
    unsafe {
        SetThreadExecutionState(ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED | ES_CONTINUOUS);
    }
}

pub fn allow_sleep() {
    unsafe {
        SetThreadExecutionState(ES_CONTINUOUS);
    }
}
```

`ES_*`の意味は[公式ドキュ](https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-setthreadexecutionstate)を参考にした感じ次のような意味を持つみたいです。

|値|説明|
|:-|:-|
|`ES_CONTINUOUS`|次の呼び出しまで同設定を永続的に有効化|
|`ES_SYSTEM_REQUIRED`|システムを動作状態のままにする。スリープ防止|
|`ES_DISPLAY_REQUIRED`|ディスプレイを点けたままにする。画面スリープ防止|

これらの値はフラグとして足し合わせることが可能になっているので、`|`で`BitOr`を取っています。

`prevent_sleep`を呼び出したスレッドはまるで動画再生のような「ディスプレイの点灯が必要」なスレッドとシステムに認識されます。`allow_sleep`を呼び出すことで、フラグはリセットされます。

作った機能のお試しには `examples` ディレクトリを活用するのが便利です[^examples]。`Cargo.toml`と同じ階層に `examples` ディレクトリを置き、そこにエントリポイント(`main`)を持つプログラムを置きます。

[^examples]: 本来の使い方ではないかもしれないですが... `bin` ディレクトリを使う方法もあります。

```rust:sleep_prevent.rs
use dialoguer::Select;
use sleepy_locker::sleep_prevent::{allow_sleep, prevent_sleep};

fn main() {
    let items = ["prevent", "allow", "exit"];

    loop {
        let selection = Select::new()
            .with_prompt("Select Sleep Mode")
            .items(&items)
            .default(0)
            .interact()
            .unwrap();

        match selection {
            0 => {
                prevent_sleep();
                println!("Sleep prevention enabled");
            }
            1 => {
                allow_sleep();
                println!("Sleep prevention disabled");
            }
            _ => return,
        };
    }
}
```

機能を試すにあたり、[dialoguer](https://docs.rs/dialoguer/latest/dialoguer/)クレートを使用しています。こんなシンプルな記述だけでリッチなCLIが生えるのでとてもオススメです！

```plaintext
> cargo run --example sleep_prevent
    Finished dev [unoptimized + debuginfo] target(s) in 0.34s
     Running `target\debug\examples\sleep_prevent.exe`
Select Sleep Mode:
> prevent
  allow
  exit
```

`examples`ディレクトリにあるプログラムは、`cargo run --example ファイル名`で実行できます。実行を通し、 `prevent` を選ぶと `allow` を選ぶか `exit` するまでスリープされなくなることが確認できました！

# TauriでサクッとGUIを生やしてみる

もうSleep Preventerと同じ機能を持ったアプリケーションなら作れそう！と感じたので、早速TauriでGUI化してみました！

```rust:main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use sleepy_locker::sleep_prevent::{allow_sleep, prevent_sleep};
use std::sync::Mutex;

#[tauri::command]
fn set_sleep_prevent_enabled(
    prevent_sleep_enabled: tauri::State<'_, Mutex<bool>>,
    enabled: bool,
) -> Result<bool, String> {
    let mut prevent_sleep_enabled = prevent_sleep_enabled
        .lock()
        .map_err(|e| format!("failed to lock prevent_sleep_enabled: {}", e))?;
    *prevent_sleep_enabled = enabled;
    if enabled {
        prevent_sleep();
        dbg!("Sleep prevention enabled");
    } else {
        allow_sleep();
        dbg!("Sleep prevention disabled");
    }
    Ok(*prevent_sleep_enabled)
}

#[tauri::command]
fn get_sleep_prevent_enabled(
    prevent_sleep_enabled: tauri::State<'_, Mutex<bool>>,
) -> Result<bool, String> {
    let prevent_sleep_enabled = prevent_sleep_enabled
        .lock()
        .map_err(|e| format!("failed to lock prevent_sleep_enabled: {}", e))?;
    Ok(*prevent_sleep_enabled)
}

fn main() {
    let prevent_sleep_enabled = Mutex::new(false);

    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            set_sleep_prevent_enabled,
            get_sleep_prevent_enabled
        ])
        .manage(prevent_sleep_enabled)
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

ごちゃついてますが注目ポイントは2点です！

- `invoke_handler` によるコマンド定義: `set_sleep_prevent_enabled` 及び `get_sleep_prevent_enabled` を登録しています。TypeScript側で `invoke` を使うことで呼び出せるようになります
- `manage` メソッドによるリソース管理: 防止が有効になっているかどうかを保持する変数 `prevent_sleep_enabled` を `manage` 関数によって登録します。すると、`tauri::State` 型の変数を通して各コマンドは引数でその不変参照を得られます！ `std::sync::Mutex`で包むことによって排他的に可変参照を得て、`set_sleep_prevent_enabled` から値を変更できるようにしています

コマンドを晒せば後はTypeScript & ReactでよしなにGUIを作れます！今回UIには[mui](https://mui.com/)を採用しました

```tsx:SleepPreventSwitch.tsx
import Switch from "@mui/material/Switch";
import { useEffect, useState, ChangeEvent } from "react";
import { invoke } from "@tauri-apps/api";

export default function SleepPreventSwitch() {
  const [checked, setChecked] = useState(false);
  const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
    setChecked(event.target.checked);
    const new_state = event.target.checked;
    await invoke<boolean>("set_sleep_prevent_enabled", { enabled: new_state });
    setChecked(new_state);
  };

  useEffect(() => {
    let cancel = false;
    (async () => {
      const res = await invoke<boolean>("get_sleep_prevent_enabled");
      if (!cancel) {
        setChecked(res);
      }
    })();

    return () => {
      cancel = true;
    };
  }, []);

  return (
    <Switch
      checked={checked}
      onChange={handleChange}
      inputProps={{ "aria-label": "controlled" }}
    />
  );
}
```

各コマンドを `invoke` で呼び出しています。スリープ防止有効化という副作用を起こすコマンド `set_sleep_prevent_enabled` は`handleChange`にて呼び出しています。現在有効であるかを最初に取得する目的で、 `get_sleep_prevent_enabled` を `useEffect` から呼び出しています。

プロトタイプの時点でほぼ完成形と同じものができました！見た目はほぼ完成ですが、一方でまだロック中でもスリープ防止が機能しています。あとはロックに合わせてスリープ防止機能のオンオフができれば良さそうです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/80fd90ef-dfcf-b6d4-a941-52f21c3aa937.png)

# ロックイベントの取得 ~ ロックを舐めるな！🎸⚡ ~

ここまで、

1. Rust側で機能作成&テスト
1. Tauri向けにコマンド作成
1. TS & ReactでGUI化

という流れで実装を行いました。単純な機能はほぼこのパターンで全て実装できます！が、たまに曲者がいまして今回はこの **ロックイベントの取得** が大きめの峠になります。

「Windowsをロックした/ロック解除した」というイベント通知をどうやって受け取るか、検索したりChatGPTに聞いたりしたところ、Win32API(厳密には`wtsapi32`？)では、[`WTSRegisterSessionNotification`関数](https://learn.microsoft.com/ja-jp/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotification)というリモートデスクトップサービス向けに用意された関数を使用すればよいようでした。

- [Windowsの画面ロックをC#で検知する #C# - Qiita](https://qiita.com/unasenohkoku/items/380e4fb4613b5abe96ea)
- [C# PC画面ロック 検出 #C# - Qiita](https://qiita.com/Yusuke_Pipipi/items/5c561b81e861e286e0c5)

.NETを使う方針ならば別な手も取れたみたいでしたが、windows-rsでゴリ押したい場合この関数一択のようです。

`WTSRegisterSessionNotification`の引数にある`hwnd`には、ロック/ロック解除通知を受け取りたいウィンドウのウィンドウハンドルを渡します。[リンク先](https://qiita.com/unasenohkoku/items/380e4fb4613b5abe96ea#:~:text=%E7%94%BB%E9%9D%A2%E3%81%8C%E8%A6%81%E3%82%89%E3%81%AA%E3%81%84%E3%82%B1%E3%83%BC%E3%82%B9%E3%81%A7%E3%82%82%E7%94%BB%E9%9D%A2%E3%81%AE%E5%AE%9F%E8%A3%85%E3%82%92%E5%BC%B7%E5%88%B6%E3%81%95%E3%82%8C%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86)でも言及されている通り、どうしてもウィンドウが必要らしいです...

ここで最初、「我々にはTauriのウィンドウがあるじゃあないか！」と考え、[Tauriウィンドウのハンドルを得るメソッド](https://docs.rs/tauri/1.5.3/x86_64-pc-windows-msvc/tauri/window/struct.Window.html#method.hwnd)の使用を試みたのですが、どうやらイベント通知は[ウィンドウプロシージャ](https://learn.microsoft.com/ja-jp/windows/win32/winmsg/using-window-procedures)に直接送られるらしく、詳細は省きます[^peke]がイベント通知を受け取れないことが判明しました。

[^peke]: [`GetMessageW`関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getmessagew)のループに送信されるならば、[`SetWindowsHookExW`関数](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setwindowshookexw)によって`GetMessageW`にフックを仕掛けることができるのでTauriのウィンドウでも捕捉できます。しかしウィンドウプロシージャに直接イベント通知が行くのでフックでは捕捉できないのでした。→ [フックできると思って実装したコードのコミット](https://github.com/anotherhollow1125/sleepy-locker/commit/fb97faf4243e0b88dd3a894d23603c853e44ace1)

結局、通知を受け取るための専用のウィンドウ及びスレッドが必要なようです。面倒だったのでChatGPTに実装してもらいました[]

```rust:lock_hooks.rs
fn dummy_window_for_detect_lock() -> Result<()> {
    unsafe {
        let instance = GetModuleHandleA(None)?;
        debug_assert!(instance.0 != 0);

        // dw stands for dummy_window
        let window_class = s!("sleepy_locker_dw");

        let wc = WNDCLASSA {
            hInstance: instance.into(),
            lpszClassName: window_class,
            lpfnWndProc: Some(wndproc), // (1)
            ..Default::default()
        };

        let atom = RegisterClassA(&wc);
        debug_assert!(atom != 0);

        // (2)
        let hwnd = CreateWindowExA(
            WINDOW_EX_STYLE::default(),
            window_class,
            s!("Dummy Window"),
            WS_OVERLAPPEDWINDOW,
            0,
            0,
            0,
            0,
            None,
            None,
            instance,
            None,
        );

        // (3)
        WTSRegisterSessionNotification(hwnd, NOTIFY_FOR_THIS_SESSION)?;

        let mut message = MSG::default();

        // (4)
        while GetMessageA(&mut message, None, 0, 0).into() {
            DispatchMessageA(&message);
        }

        Ok(())
    }
}
```

- (1): `wndproc` がウィンドウプロシージャで、この関数内でロック関連通知である [`WM_WTSSESSION_CHANGE`](https://learn.microsoft.com/ja-jp/windows/win32/termserv/wm-wtssession-change) メッセージを処理します
- (2): `CreateWindowExA` で、大きさのないダミーウィンドウを生成しています
- (3): ダミーウィンドウを `WTSRegisterSessionNotification` 関数によって登録することで、`WM_WTSSESSION_CHANGE` メッセージをウィンドウプロシージャに通知してもらうようにします。`NOTIFY_FOR_THIS_SESSION`の意味は筆者もよくわかっていませんが、おそらく現在のログインセッションを意味するなら今回の目的では `ALL` ではなく`THIS`で十分でしょう

ダミーウィンドウはTauriとは独立したスレッドで動かすことにします。肝心のウィンドウプロシージャにて `WM_WTSSESSION_CHANGE` を受け取った時は、`std::sync::mpsc::Sender` を利用して別スレッドに通知を飛ばすことにしました。

ところで、グローバル変数を避けがちなRustですが、ウィンドウプロシージャみたいに融通が利かない関数相手の時は [`OnceLock`](https://doc.rust-lang.org/std/sync/struct.OnceLock.html) が便利ですね。スッキリと書くことができています！

```rust:lock_hooks.rs
use std::sync::{mpsc::Sender, OnceLock};

// 省略

// どこかでSenderをsetしておく
static SNDR: OnceLock<Sender<Event>> = OnceLock::new();

// 省略

extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        match message {
            WM_WTSSESSION_CHANGE => {
                let Some(tx) = SNDR.get() else {
                    return DefWindowProcA(window, message, wparam, lparam);
                };

                match wparam.0 as u32 {
                    WTS_SESSION_LOCK => {
                        tx.send(Event::Lock)
                            .expect("failed to send LockEvent::Lock");
                    }
                    WTS_SESSION_UNLOCK => {
                        tx.send(Event::Unlock)
                            .expect("failed to send LockEvent::Unlock");
                    }
                    _ => {}
                }

                LRESULT(0)
            }
            _ => DefWindowProcA(window, message, wparam, lparam),
        }
    }
}
```

これでロック通知を受け取れるようになり、そしてTauri側にロック通知を知らせられるようになりました！

# モード切り替え集約スレッド

最後です！スリープ防止モード切り替え部分を新機能に迎合させます！もともとは`main.rs`の`#[tauri::command]`がついたコマンド関数内で直接切り替えを行っていましたが、これを`lib.rs`の一つのスレッドに集約させてしまいます。

`mpsc`の`Sender`側はクローンして `set_sleep_prevent_enabled` が `manage` メソッドを通して得られるように変更しておき、ユーザの操作イベントを `Receiver` 側へ飛ばすようにします。 `Receiver` によってイベントを一つのスレッドに集約し、受け取ったイベントに従って状態を変化させたのち、スリープ防止の有効化/無効化を操作します。

```rust:lib.rs
pub(crate) fn execute_prevent_or_allow(lock_state: &LockState) {
    match lock_state {
        LockState::Unlock(Mode::Prevent) => prevent_sleep(),
        _ => allow_sleep(),
    }
}

pub fn init_control_thread() -> (Sender<Event>, Arc<Mutex<LockState>>) {
    let (tx, rx) = channel();
    let tx1 = tx.clone();
    // ↓ ダミーウィンドウを生成してロックイベントを受け取るスレッドの初期化
    detect_lock_init(tx1).unwrap();
    let state = Arc::new(Mutex::new(LockState::Unlock(Mode::Allow)));
    let st = state.clone();

    std::thread::spawn(move || {
        for event in rx {
            let mut state = st.lock().unwrap();
            match event {
                // 以下2つは wndproc より
                Event::Lock => {
                    state.lock();
                }
                Event::Unlock => {
                    state.unlock();
                }

                // 以下2つは set_sleep_prevent_enabled より
                Event::Prevent => {
                    state.set_enabled(true);
                }
                Event::Allow => {
                    state.set_enabled(false);
                }
            }
            // スリープ防止モード切り替え
            execute_prevent_or_allow(&state);
        }
    });

    // ↓ main.rs側に渡す
    (tx, state)
}
```

新しいスレッドを作成し、その中で `prevent_sleep` 及び `allow_sleep` が呼ばれるようにしているのには理由があります。

- `for event in rx` で、 `Sender` が生きている間ずっとイベントを処理し続けるため
- `prevent_sleep`と`allow_sleep`で呼ばれる`Set` **Thread** `ExecutionState` 関数は名前の通り、全ての実行が同一スレッド内で行われないと適切に動作しないため、同じスレッドに集約する必要がある。`prevent_sleep`等はウィンドウプロシージャ `wndproc` 等で呼んでしまうとスレッドが異なるので適切な動作にならない

必要があってそうしたのですが、`mpsc`を介すように書けたことで結果的にスッキリした見た目になりました！

# その他細かい工夫

ここまでで欲しかった機能を実装し終えることができました。この後はアイコンを作ったりタスクトレイやスタートアップ機能の追加を行ったりしました。詳しくは[以前書いた記事](https://qiita.com/namn1125/items/8ed4d91d3d00af8750f8)に小ネタとして書いてありますので、参照していただけると恐悦至極です:bow:

https://qiita.com/namn1125/items/8ed4d91d3d00af8750f8

# まとめ・所感

いままでTauriについて「[丁寧なハンズオン](https://qiita.com/namn1125/items/8153f7c0881b7d4127ed)」や「[何が作れるか](https://qiita.com/namn1125/items/fa59ba4e499d770921db)」、「[どんな面白い特徴があるか・小ネタ集](https://qiita.com/namn1125/items/8ed4d91d3d00af8750f8)」といった話題で記事を書いてきましたが、今回は「どんな感覚でTauriによりアプリケーションをDIYするか」という切り口で記事を書いてみました。

筆者としては「Tauriなら、ちょっとずつ作りたいところを固めながら作れる」という特徴を本記事で示せたと信じたいのですが、どうでしたでしょうか？今回は(Rustアドカレということもあって)Rust中心の話題でしたが、今後はTypeScriptやReactといったフロントエンドの話題も拡充させたいなぁと思います。

ここまで読んでいただき誠にありがとうございました！良いお年を！

# 参考・引用

記事中にリンクを載せなかった参考文献です。

- [ウィンドウプロシージャ - Windows API 入門](http://kaitei.net/winapi/window-procedures/)
- [tauriのmsiを日本語にする方法 - Qiita](https://qiita.com/Yuumillar/items/3182bdbf2a314164b2b8)