AviUtl2の蛇色さんのダークモードプラグインの仕様把握メモ

🐍AviUtl2 ダークモードプラグイン「al2_jd」

はじめに

色々弄ってたので折角なので個人的メモを公開用に編集して共有しますが、内容に間違い、勘違いがあるかもしれませんが自己責任でお願いします。
al2_jd-r6の情報です
見にくいのは仕様です


概要

AviUtl2のUIをダークモード化するための入力プラグイン
Windowsの標準描画処理に介入し、UI要素の配色やスタイルを動的に変更することで、アプリケーション全体の外観を統一感のあるダークモードに切り替えている


コアアーキテクチャ:フック、レンダラー、ペイント

本プラグインは、責務が明確に分離された3層のアーキテクチャで構成されている


3層構造の概要

1. フック層 (kuro/hook/)
Windows APIへの介入を担当。描画関連のAPI呼び出しを捕捉

2. レンダラー層 (kuro/gdi/, kuro/theme/)
捕捉した描画処理を、コントロールの種類に応じて適切な描画ロジックに振り分け

3. ペイント層 (kuro/paint/)
実際の描画に使われる色、ブラシ、フォントなどの「描画素材」を管理・提供

システム構成図

┌─────────────────────────────────────────────────────────────┐
│                Windowsアプリケーション (AviUtl2)           │
└─────────────────────────────────────────────────────────────┘
      │                                       │
      │ (描画API呼び出し)                      │ (ウィンドウメッセージ)
      ↓                                       ↓
┌─────────────────────────────────────────────────────────────┐
│ 【フック層】 DetoursによるAPIフック                       │
│   - GDI API (FillRect, ExtTextOutW, etc.)                   │
│   - Theme API (DrawThemeBackground, etc.)                   │
│   - Window Message (WM_CREATE, WM_PAINT, etc.)              │
└─────────────────────────────────────────────────────────────┘
      │
      │ (フックした描画処理をディスパッチ)
      ↓
┌─────────────────────────────────────────────────────────────┐
│ 【レンダラー層】 GDI / Theme レンダラー                       │
│   - GdiRenderer (サブクラス化でMSGを処理)                 │
│   - ThemeRenderer (テーマハンドルで処理を分岐)               │
│   - ButtonRenderer, ListBoxRenderer, etc.(各コントロール専用)│
└─────────────────────────────────────────────────────────────┘
      │
      │ (描画に必要な色やスタイルを問い合わせ)
      ↓
┌─────────────────────────────────────────────────────────────┐
│ 【ペイント層】 マテリアル / パレット / スタイラス               │
│   - Material: コントロールごとの描画素材定義                   │
│   - Palette: 状態(通常/ホバー等)と色のマッピング               │
│   - Pigment: 具体的な色情報 (背景/前景/境界)                │
│   - Stylus: 描画実行ユーティリティ                            │
└─────────────────────────────────────────────────────────────┘




第1層:描画フックシステム (kuro/hook/)

UI描画を掌握するための入り口。2種類の方法で描画処理に介入

1. APIフック (Detours)

Microsoft製のDetoursライブラリを使用し、描画関連のWindows APIを直接フック

.GDI APIフック (gdi.hpp)

FillRect, ExtTextOutW, DrawEdgeなど、テーマが適用されないレガシーなUIコンポーネントが使用するGDI関数をフック。標準コントロールの基本的な描画をカスタム描画に置き換える

.テーマAPIフック (theme.hpp)

DrawThemeBackground, DrawThemeText, OpenThemeDataなど、
Windowsの視覚スタイル(テーマ)を扱うAPIをフック。OpenThemeDataをフックすることで、コントロールがテーマハンドルを取得する際に、そのハンドルとカスタムレンダラーを紐付け、UIの描画を掌握

2. ウィンドウフック (call_wnd_proc_ret.hpp)

SetWindowsHookEx を用いて WH_CALLWNDPROCRET フックを設定

.目的:WM_CREATE メッセージの処理完了時を捕捉し、新しく作成された全てのウィンドウにGDIレンダラーをアタッチ。これにより、動的に生成されるダイアログやコントロールも漏らさずダークモードの対象にする


第2層:レンダラーシステム (kuro/gdi/, kuro/theme/)

フック層から渡された描画処理を、具体的なロジックに繋ぎこむ中核部分

GDIレンダラー (gdi::Renderer)

レガシーUIの描画を担当する、非常に巧妙なシステム(きもい)

ウィンドウのサブクラス化

WH_CALLWNDPROCRETフックによってアタッチされた後、対象のウィンドウをSetWindowSubclassでサブクラス化し、そのウィンドウメッセージを subclass_proc で全て傍受

メッセージディスパッチ

subclass_proc は、WM_CTLCOLORxxxや NM_CUSTOMDRAW などの描画関連メッセージを検知すると、on_ctl_color()などの仮想関数を呼び出し、具体的な描画処理へ繋ぐ

thread_localによる呼び出し元特定

GDIのフック関数(例: FillRectのフック)が呼ばれた際、どのウィンドウの描画処理なのかを特定する必要がある

この問題を解決するのが thread_local 変数 current_message_state:

  1. subclass_proc がメッセージを受け取るたびに、このスレッドローカル変数に現在のウィンドウハンドルやメッセージ情報を保存

  2. その後、FillRectなどのGDIフック関数が呼ばれると、この変数から呼び出し元のウィンドウを特定

  3. そのウィンドウに紐付いたレンダラーのメソッド (on_fill_rect()) を呼び出し

テーマレンダラー (theme::Renderer)

モダンUIの描画を担当

動的なレンダラー差し替え

OpenThemeDataフックの時点で、ウィンドウのクラス名(Button, Editなど)を基に対応するカスタムレンダラー
(ButtonRenderer, EditboxRendererなど)がテーマハンドルに紐付けられる

仮想関数による描画

その後、DrawThemeBackgroundなどが呼ばれると、テーマハンドルに紐付いたレンダラーの仮想関数 (on_draw_theme_background()) が呼び出され、カスタム描画が実行される


第3層:ペイントシステム (kuro/paint/)

「何を」「どのように」描画するかを定義する、デザインシステムの役割

マテリアル (Material)

ボタン、リストボックスといったUIコントロールの種類ごとに定義される「描画素材」の集合体

パレット (Palette)

マテリアル内に保持され、コントロールの部品ID (part_id) と状態ID (state_id) のペアをキーとして、具体的な描画情報 (Pigment) をマッピング

例:

  • (BP_PUSHBUTTON, PBS_NORMAL) → 通常状態のボタン用のPigment

  • (BP_PUSHBUTTON, PBS_HOT) → ホバー状態のボタン用のPigment

ピグメント (Pigment)

描画に必要な色の組み合わせ(背景色、前景色、境界色など)を保持する構造体。JSONファイルから読み込まれたテーマ設定がここに格納される

スタイラス (Stylus)

Pigment情報を受け取り、矩形描画やテキスト描画といった実際のGDI操作を実行するユーティリティクラス


ダークモード実装の詳細:色の決定から描画まで

ダークモードの実装は、ペイント層とレンダラー層の連携によって実現

1. 色の定義 (外部ファイル連携)

ダークモードの配色は、aviutl2の設定ファイルstyle.confから動的に読み込み、各コントロールのマテリアル定義ファイル内 (kuro/paint/material/*.hpp) で参照している

.色定義の階層構造

aviutl2/
├── data/style.conf          # メインの色設定ファイル(aviutl2標準)
├── al2/config/al2_jd.json   # プラグイン固有の設定(角丸、境界幅など)
└── al2/assets/al2_jd.json   # プリセット情報

.実際の色取得処理

// kuro/paint/material/button.hpp(実際の実装)
struct ButtonMaterial : Material {
    virtual void on_init_material() override {
        // ボタンの通常状態 (PBS_NORMAL)
        auto push_button_normal = Pigment {
            { style.get_COLORREF(Style::Color::ButtonBody), },      // ← style.confから取得
            { style.get_COLORREF(Style::Color::Border), 1, },       // ← style.confから取得
            { style.get_COLORREF(Style::Color::Text), },            // ← style.confから取得
        };

        // ホバー状態 (PBS_HOT)
        auto push_button_hot = Pigment {
            { style.get_COLORREF(Style::Color::ButtonBodyHover), }, // ← style.confから取得
            { style.get_COLORREF(Style::Color::Border), 1, },       // ← style.confから取得
            { style.get_COLORREF(Style::Color::Text), },            // ← style.confから取得
        };

        // パレットに登録
        palette.set(BP_PUSHBUTTON, PBS_NORMAL, push_button_normal);
        palette.set(BP_PUSHBUTTON, PBS_HOT, push_button_hot);
        // ...その他の状態も同様に設定...
    }
};

色の優先順位と動的更新

  1. style.conf(aviutl2の設定)が最優先で読み込まれる

  2. プラグインの設定(角丸、境界幅など)が適用される

  3. 起動時に style.conf を読み込み(自動再読み込みは未実装)

この初期化処理は、プラグイン起動時に paint::manager.init() から一度だけ呼び出され、各マテリアルのパレットに色が設定される

2. 描画プロセス

ユーザーがUIを操作し、再描画が必要になると以下のプロセスが実行される

.描画トリガー

OSがWM_PAINT メッセージを送信するか、アプリケーションが DrawThemeBackground などのAPIを呼び出す

.フック層が捕捉

プラグインのフック層がこのAPI呼び出しやメッセージを捕捉

レンダラーへディスパッチ

フック層は、ウィンドウハンドルやテーマハンドルを基に、適切なレンダラー(例: ButtonRenderer)を特定し、そのメソッド
(例: on_draw_theme_background)を呼び出す。この時、引数としてHDC(デバイスコンテキスト)や描画領域を示すRECT、そしてコントロールの状態を示すpart_idとstate_idが渡される

GDI操作実行

paint::stylusは、Pigmentに含まれる色情報を使って以下の処理を実行:

  • 境界線ありの場合: PenAttribute(ペン)とBrushAttribute(ブラシ)を設定し、Rectangleで描画

  • 境界線なしの場合: CreateSolidBrushで背景色のブラシを作成し、FillRectで塗りつぶし

  • テキスト描画: TextAttributeで背景色・テキスト色を設定し、ExtTextOutWやDrawTextWで描画

スタイラスの具体的な実装例

// stylus.hpp - 矩形描画の実際の実装
BOOL draw_rect(HDC dc, LPCRECT rc, const Pigment* pigment) {
    if (pigment->border.is_valid()) {
        PenAttribute pen_attribute(dc, pigment);    // 境界線用ペン設定
        BrushAttribute brush_attribute(dc, pigment); // 背景用ブラシ設定
        return hive.orig.Rectangle(dc, rc->left, rc->top, rc->right, rc->bottom);
    } else {
        // 境界線なしの場合は塗りつぶしのみ
        my::gdi::unique_ptr<HBRUSH> brush(::CreateSolidBrush(pigment->background.color));
        return hive.orig.FillRect(dc, rc, brush.get());
    }
}

このように、「状態に応じた色をマテリアルから取得し、スタイラスで描画する」という一連の流れが、レンダラーを介して実行されることで、UI全体が統一感のあるダークモードで描画される仕組みになっている


実装ハイライト

スレッドセーフ設計

UI操作は単一のスレッドで行われることを前提としつつ、thread_local変数を活用することで、GDIフックとウィンドウメッセージ処理という異なるコンテキストを連携させている

動的なレンダラー管理

std::unordered_map<HWND, std::shared_ptr<Renderer>> を使用して、ウィンドウハンドルとGDIレンダラーのインスタンスを動的に管理。ウィンドウの生成・破棄に合わせて、レンダラーも自動的に生成・破棄される

フォントプレビュー機能

リストボックスに項目が追加される際 (LB_ADDSTRING)、その文字列がシステムに存在するフォント名かを動的にチェック

フォント名であれば、リストボックスはExtTextOutWフック+サブクラスで実フォント描画、リストビューはNM_CUSTOMDRAWでフォント差し替え、という形でリアルタイムプレビューを実現している


設定システム

設定項目

角丸、境界幅、影の濃度、フォント設定、起動オプションなど、多岐にわたるカスタマイズ項目を提供

設定ファイル

al2_jd.json(実際はモジュール名に由来)に設定を保存し、起動時に読み込むことで永続化を実現


ファイル構成

al2_jd/
├── main.cpp              # エントリポイント、プラグイン情報
├── app.hpp               # アプリケーション全体管理
├── hive.hpp              # グローバル設定・状態管理 (情報の中枢)
├── config_dialog.hpp     # 設定ダイアログ
├── config_io.hpp         # 設定ファイルI/O
├── kuro/                 # ★ダークモード化コア機能
│   ├── hook/             # 【第1層】APIフック管理
│   │   ├── gdi.hpp
│   │   ├── theme.hpp
│   │   └── call_wnd_proc_ret.hpp
│   ├── gdi/              # 【第2層】GDIベース描画
│   │   └── renderer.hpp
│   ├── theme/            # 【第2層】テーマベース描画
│   │   └── renderer.hpp
│   └── paint/            # 【第3層】色・スタイル管理
│       ├── material.hpp
│       ├── palette.hpp
│       └── stylus.hpp
└── common/               # 共通ユーティリティ

まとめ

al2_jdは、Windowsの描画システムのAPIフック、ウィンドウサブクラス化、そして分離したクリーンなアーキテクチャを組み合わせることで、堅牢かつ拡張性の高いUIカスタマイズを実現している

特にthread_local変数を駆使してフックとメッセージ処理のコンテキストを繋いでるのは普通にすごいと思う。これを0から作れって言われても私には絶対にできません。もうこれアルティメットプラグイン2と名乗って良いんじゃないかな…


🔧おまけ:ウィンドウ位置保存機能の追加

app.hppの変更箇所

// dll_init()関数内
// aviutl2ウィンドウを最前面に
::SetForegroundWindow(hive.theme_window);

// aviutl2ウィンドウを最大化
if (hive.maximize_aviutl2)
    ::ShowWindow(hive.theme_window, SW_MAXIMIZE);

// dll_exit()関数内
// 設定を保存(メインウィンドウの位置も含む)
write_config();

config_io.hpp

// ノードからコンフィグを読み込み
virtual BOOL read_node(n_json& root)
{
    MY_TRACE_FUNC("");
    read_fonts(root);
    read_new_project(root);
    read_kuro(root);
    read_window_pos(root, "config_dialog", config_dialog);
    // メインウィンドウの位置を復元
    if (hive.theme_window)
    {
        read_window_pos(root, "main_window", hive.theme_window);
    }
    return TRUE;
}

// ノードにコンフィグを書き込み
virtual BOOL write_node(n_json& root)
{
    MY_TRACE_FUNC("");
    write_fonts(root);
    write_new_project(root);
    write_kuro(root);
    write_window_pos(root, "config_dialog", config_dialog);
    // メインウィンドウの位置を保存
    if (hive.theme_window)
    {
        write_window_pos(root, "main_window", hive.theme_window);
    }
}

この変更(追加)により、前回のウィンドウの位置と大きさでAviUtl2が開くようになります


いいなと思ったら応援しよう!