はじめに
Windhawk という Windows のカスタマイズツールがあります。「Mod」と呼ばれる小さな C++ プログラムを書くことで、Windows のアプリの動作を実行時に書き換えられる、ちょっと変わった開発ツールです。
Windhawk には「Create a new mod」というテンプレート機能があり、これを動かすだけで Mod 開発の基本を一通り体験できます。本記事では、このテンプレートをコンパイル・実行して、ペイントの挙動が実際に書き換わる様子を確認してみます。
なお、テンプレートを動かす過程で予想外の挙動にも遭遇したのですが、その話は別記事に分けています。本記事では、まず 「テンプレートが動くところまで」 を扱います。
シリーズ記事一覧
- 【本記事】Windhawk 入門:公式テンプレートでペイントの挙動を書き換えてみた
- (後日公開)Windhawk 公式テンプレートで詰まった話:Windows 11 の Paint が Direct2D 化していた
対象読者
- Windows のカスタマイズツールに興味がある方
- C++ の関数定義・関数ポインタ・名前空間が読める程度の知識がある方
- Windhawk を初めて触る方
※ Windows API の前提知識は不要です。必要なものは記事中で都度説明します。
この記事のゴール
- Windhawk の公式テンプレートをコンパイル・実行できるようになる
- ペイントの動作が変わるところを実体験できる
- Mod の基本構造(フックを仕掛ける 3 ステップ)を理解できる
環境
- Windows 11 Pro 25H2(OSビルド 26200.8246)
- Windhawk v1.7.3
- Microsoft Paint
💡 Windows のビルド番号は、
Win + Rを押してwinverを実行すると確認できます。
Windhawk とは
Windhawk は、Windows 上で動いているアプリやシステムプロセスの挙動を、実行時に書き換えるためのカスタマイズ基盤です。「Mod」と呼ばれる小さな C++ プログラムを書いて公開・共有することで、世界中のユーザーが気軽に Windows をカスタマイズできる仕組みを提供しています。
何ができるのか
公式の Mod ストア(windhawk.net/mods)にはすでに数百の Mod が公開されており、代表的なものとして以下があります。
- タスクバーのデザイン変更:透明化、角丸化、macOS 風ドック化など
- スタートメニューの拡張:Windows 10 風に戻す、検索を Everything に置換など
- エクスプローラーの挙動変更:タブ追加、コマンドバー復活など
- 特定アプリの動作改変:たとえばペイントの色を強制的に変えるなど(本記事のサンプルです)
他のカスタマイズツールとの違い
Windows のカスタマイズツールには Rainmeter(壁紙の上にウィジェットを重ねる)や Stardock の WindowBlinds(テーマを差し替える)など、老舗のものがいくつかあります。Windhawk はそれらより一歩深いレベルでアプリを改変するのが特徴です。
仕組みとしては、Windhawk のエンジンがターゲットプロセス(explorer.exe や mspaint.exe など)に DLL を注入し、Windows API の呼び出しを横取りして自分のコードに差し替えます。これによって、アプリ自身が想定していない挙動を後から実現できるわけです。
Windhawk のアーキテクチャ。Launcher が起動中の Windows プロセスに Engine と Mod を注入し、Mod のコンパイルは内蔵の VSCodium + LLVM MinGW で行われる。
出典:ramensoftware/windhawk(GPL-3.0 License)
開発者にとって嬉しい点
Windhawk が特によくできているのは、Mod が単一の C++ ファイルだけで書ける点です。
- VSCodium(VS Code の OSS 版)が内蔵されており、エディタのセットアップ不要
- LLVM MinGW コンパイラも内蔵されているため、ツールチェインの準備不要
- ファイルを保存すると自動でコンパイル + 注入が走る
つまり**「インストール直後からコードを書いて、すぐに結果が見える」**環境が用意されており、最初のハードルが極端に低いツールになっています。
Windhawk のインストール
公式サイト(windhawk.net)からインストーラーをダウンロードして実行するだけです。設定項目はほぼなく、ウィザードの指示通り進めれば完了します。
⚠ DLL injection を使うツールの性質上、初回起動時に Windows SmartScreen の警告が出ることがあります。出元が信頼できる場合は「詳細情報」→「実行」で続行できます。
インストール後、初回起動時に Windhawk が必要なシンボル情報をダウンロードするため、数分待つことになります。
新規 Mod の作成
Windhawk のメイン画面から 「Create a new mod」 ボタンを押すと、内蔵の VSCodium が起動し、サンプル Mod のコードが書き込まれた状態のエディタが開きます。
このエディタは Windhawk のためにカスタマイズされた VSCodium で、Mod 開発に必要な機能がすでに揃っています。
- C++ のシンタックスハイライト:Mod は C++ で書くため
-
Windhawk 専用の API 補完:
Wh_LogやWh_SetFunctionHookなどの関数がインテリセンスで出てくる - コンパイル → 注入の自動化:ファイルを保存すると裏でビルドが走り、即座にターゲットプロセスに反映される
特に最後の 「保存するだけでビルド + 注入」 が便利で、コードを書く → 結果を見る、のサイクルを高速で回せます。
開かれたサンプルコードの中身については、次のセクションで詳しく見ていきます。
テンプレートのコードを読む
「Create a new mod」を押すと、ペイントを題材にしたサンプルコードがすでに書き込まれた状態で開きます。一見すると 130 行ほどの C++ ですが、実は 4 つのブロックに分かれていて、それぞれ役割が決まっています。
ここでは、各ブロックが何をしているのかをざっくり眺めていきましょう。コードの細かい動作は後の「フックを仕掛ける 3 ステップ」で説明するので、今は「どこに何があるかを把握する」つもりで読んでください。
メタデータブロック
ファイルの先頭にある、==WindhawkMod== で囲まれた部分です。
// ==WindhawkMod==
// @id new-mod
// @name Your Awesome Mod
// @description The best mod ever that does great things
// @version 0.1
// @author You
// @github https://github.com/nat
// @twitter https://twitter.com/jack
// @homepage https://your-personal-homepage.example.com/
// @include mspaint.exe
// @compilerOptions -lcomdlg32
// @license MIT
// ==/WindhawkMod==
これは Mod の 「身分証」 のような部分で、Windhawk が Mod を識別したり、どのアプリに注入するかを決めたりするのに使われます。特に重要な項目を挙げると:
-
@id:Mod の一意な識別子。ストア内で他の Mod と被ってはいけません -
@name:Windhawk アプリ上に表示される名前 -
@include:注入先のプロセス。ここではmspaint.exeなので、ペイントが起動したときだけ動きます -
@compilerOptions:コンパイル時の追加オプション。ここでは-lcomdlg32でファイルダイアログ用のライブラリをリンクしています -
@license:このコードのライセンス。MIT を指定しておくと再利用されやすくなります
@include は特にミスしやすい項目で、ここに書いたプロセス名と一致するアプリにしか Mod は注入されません。「Mod を作ったのに動かない」というときは、まずここを疑うと良いです。
README ブロック
次にあるのが ==WindhawkModReadme== で囲まれた部分です。
// ==WindhawkModReadme==
/*
# Your Awesome Mod
This is a place for useful information about your mod...
*/
// ==/WindhawkModReadme==
ここは Markdown で書かれた Mod の説明文で、Windhawk のアプリ画面上に「この Mod は何をするものか」を表示するために使われます。
ストアに公開する場合は、ここに「使い方」「設定の意味」「動作環境」「既知の問題」などを書いておくと、利用者にとって親切な Mod になります。Markdown なので、見出しやリスト、画像へのリンクなども使えます。
Settings ブロック
3 つ目のブロックが ==WindhawkModSettings== です。
// ==WindhawkModSettings==
/*
- color:
- red: 255
- green: 127
- blue: 39
$name: Custom color
$description: This color will be used regardless of the selected color.
- blockOpen: true
$name: Block opening files
$description: When enabled, opening files in Paint is not allowed.
*/
// ==/WindhawkModSettings==
ここは Mod のユーザーが設定画面から変更できる項目を YAML ライクな形式で定義する場所です。
- 各項目の名前と初期値(例:
red: 255) -
$name:設定画面に表示される項目名 -
$description:その設定の説明文
ここで定義した項目は、Windhawk が自動的に設定 UI を生成してくれるので、コード側からは Wh_GetIntSetting(L"color.red") のように名前で読み出すだけで使えます。設定 UI を自分で書かなくていいのが、地味に便利なところです。
C++ コード本体
最後が、4 つ目のブロックである C++ コード本体 です。==WindhawkMod...== のような囲みはなく、Settings ブロックの後ろからファイル末尾までが全部このブロックに該当します。
このブロックの中身は、ざっくり次のようになっています。
-
必要なヘッダー(
#include <gdiplus.h>) - Settings 構造体:上の Settings ブロックの値を保持するための変数
-
フック関数:
GdipSetSolidFillColor_HookとGetOpenFileNameW_Hookの 2 つ -
設定読み込み関数:
LoadSettings -
ライフサイクル関数:
-
Wh_ModInit:Mod が読み込まれたときに呼ばれる(初期化) -
Wh_ModUninit:Mod が外されるときに呼ばれる(後片付け) -
Wh_ModSettingsChanged:ユーザーが設定を変えたときに呼ばれる
-
このうち、「フック関数」と「Wh_ModInit」が Mod の心臓部分です。具体的にどう動いているのかは、後ろの「仕組みのおさらい:フックを仕掛ける 3 ステップ」のセクションで詳しく見ていきます。
コンパイルと実行
エディタ上で Ctrl + B を押す(または左側のサイドバーにあるコンパイルボタンをクリックする)と、Mod がコンパイルされ、ターゲットプロセスへの注入準備が整います。
コンパイルが通ると、Windhawk のメイン画面に作成した Mod が表示され、有効化された状態になります。あとは @include で指定したアプリ(このサンプルでは mspaint.exe)を起動すれば、Mod の効果が反映されるはずです。
💡 すでにターゲットアプリが起動している場合、Mod は反映されません。Mod はプロセスの起動時に注入されるため、一度アプリを閉じてから起動し直す必要があります。
次のセクションでは、実際にペイントを開いて Mod が動作しているか確認していきます。
動作確認
コンパイルが通り、Mod が有効化された状態で、実際にペイントを起動して動作を確認します。
💡 ペイントが既に起動している場合は、一度閉じて起動し直してください。
Mod はプロセスの起動時に注入されるため、すでに起動中のペイントには反映されません。
「ファイル → 開く」で Surprise! が出る
ペイントを起動して、メニューから「ファイル → 開く」を選択してみます。
通常であればファイル選択ダイアログが表示されるはずですが、代わりに次のようなメッセージボックスが出てきました。
実際に表示された瞬間は驚きました。「開く」の動作がちゃんと止められていて、コードでアプリの挙動を書き換えている実感が得られて面白かったです。
この挙動は、テンプレート内の GetOpenFileNameW_Hook 関数が、本来呼ばれるはずの Win32 ファイル選択ダイアログ(GetOpenFileNameW 関数)を横取りして、独自のメッセージボックスを表示する処理に差し替えているためです。OK ボタンを押すと、フック関数は元の GetOpenFileNameW を呼ばずに FALSE を返すので、結果として「ファイルを開けない」状態になります。
図形ツールで描くとオレンジになる
次に、ペイントの図形ツールを使って描画してみます。
「ホーム」タブから図形(四角形・楕円など)を選び、色は何でもいいので適当に選択して、キャンバスにドラッグします。
選んだ色に関わらず、図形はすべてオレンジ色(厳密には RGB(255, 127, 39))で塗りつぶされます。これはテンプレートの GdipSetSolidFillColor_Hook 関数が、GDI+ の塗りつぶし色設定の呼び出しを横取りし、色を Settings ブロックで定義された RGB 値に強制的に置き換えているためです。
試しに、Windhawk の Mod 設定画面で color の RGB 値を変更して保存してみると、変更がすぐにペイントの描画に反映されます。
これは Mod 内で定義されている Wh_ModSettingsChanged 関数が呼ばれて、設定値が再読み込みされているためです。
💡 ここまでで、Mod が 2 つの異なる Windows API を横取りして書き換えている ことが確認できました。
GetOpenFileNameW(Win32 のファイルダイアログ)GdipSetSolidFillColor(GDI+ の塗りつぶし色)同じ「フックを仕掛ける」という仕組みで、まったく違う種類の機能を変更できているのがポイントです。
仕組みのおさらい:「フックを仕掛ける 3 ステップ」
ここまででテンプレートを動かすところまで終わりました。せっかくなので、Windhawk Mod がどうやって動いているのかを整理しておきます。
このテンプレートのコードは少し情報量が多いですが、よく見ると 3 つのステップの繰り返しでできています。この型を覚えると、今後どの Mod を読んでも構造が見えるようになります。
「フックを仕掛ける」とは、「ある関数が呼ばれたら、自分の書いた関数に処理を切り替える」ことです。Windhawk ではこれを 3 ステップで実現します。
ステップ 1:型エイリアスを作る
最初にやるのは、「乗っ取りたい関数と同じ形」を表す型を定義することです。
using GdipSetSolidFillColor_t = decltype(&DllExports::GdipSetSolidFillColor);
この一行で、GdipSetSolidFillColor という関数の関数ポインタ型に GdipSetSolidFillColor_t という名前を付けています。
ポイントは decltype(&...) という構文です。これは 「この関数のシグネチャ(引数と戻り値の形)を、コンパイラが自動で取得する」 という意味で、自分でシグネチャを手書きする必要がありません。引数の型を間違えたり、WINAPI のような呼び出し規約を書き忘れたりするミスを防いでくれます。
ステップ 2:フック関数を書く
次に、「乗っ取った後にやりたい処理」を関数として定義します。
GdipSetSolidFillColor_t GdipSetSolidFillColor_Original;
GpStatus WINAPI GdipSetSolidFillColor_Hook(GpSolidFill* brush, ARGB color) {
Wh_Log(L"GdipSetSolidFillColor_Hook: color=%08X", color);
if (Color(color).GetAlpha() == 255) {
color = Color::MakeARGB(255, settings.red, settings.green, settings.blue);
}
return GdipSetSolidFillColor_Original(brush, color);
}
ここでやっているのは 3 つです。
-
_Original変数:元の関数を呼び出すためのポインタを保持する変数(値はステップ 3 で入ります) -
_Hook関数:横取りして処理を差し替える本体。引数のシグネチャは元の関数と完全に一致させます -
最後に
_Original(...)を呼ぶ:色を書き換えたうえで、元の関数に処理を引き渡す
Wh_Log でログを残しておくと、後で「フックが本当に呼ばれているのか」を確認できるので、デバッグの強い味方になります。
ステップ 3:Wh_ModInit で配線する
ここまでは「準備」です。最後にこれらを 配線 することで、初めてフックが有効になります。
HMODULE gdiPlusModule = LoadLibrary(L"gdiplus.dll");
GdipSetSolidFillColor_t GdipSetSolidFillColor =
(GdipSetSolidFillColor_t)GetProcAddress(gdiPlusModule,
"GdipSetSolidFillColor");
Wh_SetFunctionHook((void*)GdipSetSolidFillColor,
(void*)GdipSetSolidFillColor_Hook,
(void**)&GdipSetSolidFillColor_Original);
3 行に分かれていますが、意味は単純です。
-
LoadLibrary:対象の DLL(ここではgdiplus.dll)を読み込む -
GetProcAddress:その DLL からGdipSetSolidFillColorという関数のアドレスを取得する -
Wh_SetFunctionHook:「その関数が呼ばれたら、自分の_Hook関数に飛ばしてください」と Windhawk に依頼する
特に重要なのが 3 つ目で、これが フックの「配線工事」 です。この呼び出しがないと、フック関数をいくら書いても何も起こりません。
この型さえ覚えれば応用が効く
実はこの 3 ステップが、Windhawk Mod の 8 割を占める作業です。テンプレートにはもう 1 つフック(GetOpenFileNameW)が仕掛けられていますが、よく見るとまったく同じ構造になっています。
新しく何かをフックしたい場合は、
- 対象の関数名を調べる
- 上記のステップ 1〜3 を、関数名を変えてコピーする
これだけです。型を一度覚えると、機能を追加するハードルが一気に下がります。
まとめ
本記事では、Windhawk の公式テンプレートを動かして、Mod 開発の基本を一通り体験しました。要点を振り返ると次のようになります。
- Windhawk は DLL injection で Windows のアプリの挙動を実行時に書き換える ツール
- Mod は 単一の C++ ファイル で書けて、IDE もコンパイラも内蔵されているためすぐ始められる
- Mod の構造は メタデータ + README + Settings + C++ コード の 4 ブロック
- Mod の心臓部分は 「フックを仕掛ける 3 ステップ」(型エイリアス → フック関数 → 配線)
- 同じ仕組みで 「動作を止める」「値を書き換える」 の両方ができる
「公式テンプレートを動かしてみる」だけでも、Mod 開発の主要な要素はほぼ全部触れます。ここまでくれば、公式ストアにある他の Mod のソースコードを読んでも、構造が見えるようになっているはずです。
次のステップとしては、
- 公式ストア(windhawk.net/mods)で気になる Mod のソースを読んでみる
- テンプレートを改造して、別のアプリの挙動を変える Mod を作ってみる
-
@includeをnotepad.exeなどに変えて、別のアプリで遊んでみる
などが入口になりそうです。
次回:テンプレートを拡張していたら、予想外の挙動に出会った話
実は、このテンプレートを動かして満足したあと、もう少し遊んでみようと思って 「鉛筆やブラシで描いた線もオレンジ色にしてみる」 という改造を試しました。
シェイプはオレンジで塗られていたので、線描画の方も同じ仕組みでフックすればすぐ動くはずだろうと思っていたのですが、これが意外と一筋縄ではいきませんでした。フックを増やしても色が変わらず、最終的にはテンプレートが想定していない領域まで調査が及びました。
詳細は次回の記事でまとめる予定です。
- 次回(後日公開予定):Windhawk 公式テンプレートで詰まった話:Windows 11 の Paint が Direct2D 化していた
補足
本記事は Claude(Anthropic)と対話しながら執筆しました。コード・コンパイル・動作確認はすべて筆者の環境で実施しており、記事中の挙動・スクリーンショットは実際に観測したものです。







