1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クリスマスなのでUnityエディタのアイコンを光らせてみた!

Last updated at Posted at 2025-12-15

本記事は、Applibot Advent Calendar 2025の15日目の記事になります。
遅刻してすみません(土下座)

前日は@shoma3571さんのIP ごとに世界観が違う UI を、デザイントークンで吸収している話でした

はじめに

今年もクリスマスの季節がやってきましたが、皆様いかがお過ごしでしょうか?
お忙しい皆様のことですから、クリスマスのことなんか忘れて仕事に没頭しているのではないでしょうか?
なんとおいたわしや……1

そんな頑張るUnityエンジニアの皆さんに私からのささやかなクリスマスプレゼントです。
エディタのアイコンを光らせて、開発中の気分を爆上げしていきましょう!!!

改めてはじめに

はい、真面目にやります。

最近、同じサイバーエージェントグループのQualiArtsに所属している同期のあたるくんと一緒にUniconというOSSを作っています。
こちらはエディタのアイコンを任意の画像や色に変更し、複数Unityを立ち上げた際に見分けがつくようにできるエディタ拡張で、現在macOSとWindowsに対応しています。

今回、自分の方でWindows版の対応を行ったためその紹介と、仕組みを応用してアイコンをアニメーションをやってみたので記事にしてみました。

本記事はWindowsにおけるアイコン変更の話がメインですが、UniconやmacOSでの実装の話は以下であたるくんが書いてくれているので、ぜひこちらもご覧ください!

今回やること

タスクバーを録画したやつですが、こんな感じで光らせていきます。
ゲーミングですね。
anim.gif

実装

実行ファイルから元アイコンを取得する

さて、アイコンを変更するためには、当然ですが変更後のアイコンを用意する必要があります。
今回はUnityのアイコンを光らせたいため、実行ファイルからアイコンを抽出し、それを加工することとします。

まず抽出にはPrivateExtractIconsという関数を使用します。
こちらは説明に飛んでもらうと分かるかと思いますが、一般使用が想定されていないため通常はExtractIconsの使用をおすすめされています。

それぞれの関数の違いは、サイズ指定ができるかどうかだけだった気がする(うろ覚え)のですが、筆者の環境ではなぜか前者の関数しか動かなかったので、本記事ではそちらを使用した例としたいと思います。

抽出の実装はこんな感じです。

    /// @param path ファイルパス
    /// @param size アイコンサイズ
    /// @return アイコンハンドル
    HICON ExtractIconFromPath(const wchar_t* path, int size)
    {
        std::array<HICON, 1> icons = { nullptr };
        std::array<UINT, 1> iconIds = { 0 };

        auto count = PrivateExtractIcons(path, 0, size, size, icons.data(), iconIds.data(), 1, 0);
        if (count > 0 && icons[0] != nullptr)
        {
            return icons[0];
        }

        return nullptr;
    }

あとはこれをBitmapに変換するなりして、加工してあげればOKです。
ちなみにUniconでは、色々あって一度C#側に持ってきて加工してから、C++側で再度適用しています。
効率を考えるならC++で完結した方がよいと思いますが、それはまぁおいおい……

また注意点ですが、ここに限らずで一部の取得したアイコンは使い終わったらDestroyIconしないとメモリーリークするため、注意が必要です。
なお、共有アイコンの形でメモリ上にあるアイコンハンドルを取得するものに関しては、逆にDestroyIconしてはいけないのですが、どの関数が対象になるかはリファレンスを見ていただけたらと思います。

ウィンドウのアイコンを変える

Windowsにおいて「ウィンドウのアイコン」を変えるには、そのウィンドウに対してハンドル経由でアイコンハンドル付きのWM_SETICONというメッセージを送りつけます。

これだけならC#側からuser32.dllをDllImportしてSendMessageを呼ぶだけでも変わりますが、今回は後述するフック処理のために C++ のネイティブプラグインを作成して、そこから操作することにします。

以下は例で、この関数に対して最初に作ったアイコンハンドルを渡してあげればOKです。

/// @param hWnd ウィンドウハンドル
/// @param hSmallIcon 小アイコンハンドル
/// @param hBigIcon 大アイコンハンドル
void SetIcon(HWND hWnd, HICON hSmallIcon, HICON hBigIcon)
{
    SendMessage(hWnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(hSmallIcon));
    SendMessage(hWnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(hBigIcon));
}

そうするとウィンドウ左上のアプリアイコンが指定したアイコンに切り替わるかと思います。

タスクバーのアイコンを変える

さて、上までを順にやってみた方ならお分かりかもしれませんが、実はこれだけではタスクバーのアイコンは変わらず、依然としてUnityの本来のアイコンが表示されたままになります。

これはどういうことかというと、実はWindowsのタスクバーに表示されるアイコンは単にウィンドウのものがそのまま使われるわけではなく、Application User Model ID(以降AUMID)に紐づけられたアイコンによって決定されます。

AUMIDには以下の種類が存在します。

  • システム定義
  • アプリケーション定義
    • プロセス単位
    • ウィンドウ単位

システム定義のAUMIDはアプリケーション側で何もしなくても、実行ファイルパスからOS側で自動生成されます。
アプリケーション側で何も設定していない場合、こちらのAUMIDが使用され、タスクバーのアイコンはアプリの本来のアイコンになります。

アプリケーション定義のAUMIDはプロセス単位とウィンドウ単位の両方に設定することができます。
特徴として、プロセス単位でのAUMID設定はSetCurrentProcessExplicitAppUserModelID、ウィンドウ単位ではSHGetPropertyStoreForWindowによるウィンドウプロパティ設定で設定を行い、このうち前者はUIなどを表示する前のアプリ初期化中に呼び出す必要があります。

また、プロセス単位とウィンドウ単位の両方にAUMIDが設定されている場合はウィンドウの方が優先されます。

さて、これらを踏まえた上でUnityではどうしているかというと、私調べだとシステム定義のAUMIDが使用されているようでした。
本来ならプロセス単位でAUMIDの上書きを行いたいところですが、エディタのUIなどの表示前(これには起動時のスプラッシュも含まれる)にAUMIDを設定することが困難なため2、今回はウィンドウ単位での上書きを行います。

変更方法は以下のような感じです。
これで適当なAUMIDを設定してあげることで、ウィンドウのアイコンをタスクバーのアイコンとして表示してあげることができます。

/// @param hWnd ウィンドウハンドル
/// @param appId AUMID
void SetAppId(HWND hWnd, const std::wstring& appId)
{
    IPropertyStore* pPropStore = nullptr;
    auto hr = SHGetPropertyStoreForWindow(hWnd, IID_PPV_ARGS(&pPropStore));
    if (SUCCEEDED(hr))
    {
        PROPVARIANT propVar;
        if (appId.empty())
        {
            PropVariantInit(&propVar);
        }
        else
        {
            hr = InitPropVariantFromString(appId.c_str(), &propVar);
        }

        if (SUCCEEDED(hr))
        {
            hr = pPropStore->SetValue(PKEY_AppUserModel_ID, propVar);
            if (SUCCEEDED(hr))
            {
                pPropStore->Commit();
            }
            PropVariantClear(&propVar);
        }
        pPropStore->Release();
    }
}

子ウィンドウのアイコンを変える

メインウィンドウに関しては、これで変更できるようになりましたが、では子ウィンドウはどうでしょうか?
Unityではインスペクタの切り離しを行えるため、その際に子ウィンドウが作られます。
今までの操作はあくまでメインウィンドウに対してのもので、このままでは子ウィンドウの方にはアイコンが設定されていないので設定するようにします。

基本的には子ウィンドウのハンドルに対してSendMessageしてあげればいいだけなのですが、これには子ウィンドウの生成を検知してハンドルを取得する機構が必要なため、今回はSetWindowsHookExを使用してHCBT_CREATEWNDにコールバックをひっかけるようにします。

実装イメージは以下の通りです。

std::wstring g_appId;
HICON g_smallIcon = nullptr;
HICON g_bigIcon = nullptr;
HHOOK g_hHook = nullptr;

LRESULT CALLBACK EditorWindowProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode == HCBT_CREATEWND)
    {
        auto hWnd = reinterpret_cast<HWND>(wParam);

        auto hasIcon = (g_smallIcon != nullptr) || (g_bigIcon != nullptr);
        if (hasIcon)
        {
            SetIcon(hWnd, g_smallIcon, g_bigIcon);
        }

        auto hasAppId = !g_appId.empty();
        if (hasAppId)
        {
            SetAppId(hWnd, g_appId);
        }
    }

    return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

void StartHook()
{
    if (g_hHook != nullptr)
    {
        return;
    }

    g_hHook = SetWindowsHookEx(WH_CBT, EditorWindowProc, nullptr, GetCurrentThreadId());
}

注意点として、終了時は絶対にUnhookWindowsHookExを呼び出してフックを解除処理を行うようにしてください。

光らせる

ここまででアイコンを変更する方法が分かったかと思います。
あとはよしなにアイコンをsin波辺りを使ってHue値を変えてゲーミングにして、EditorApplication.updateを使用して一定秒数毎にアイコンを変更すれば、無事光らせてアニメーションさせることができるかと思います。

ポイントとしてはアイコン変更はそこまで高FPSで出来るわけではないので、ある程度の速度に落として行ってあげることかなと思います。

今回使用したコード

アニメーション部分に関しては完全におまけなので上げてないのですが、通常のアイコン変更のロジックに関しては、Uniconの方に上がっているのでこちらを見ていただけたらと思います。
コードが汚いのは許して……

最後に

完全にお遊びの記事でしたが、いかがでしたでしょうか?
今回はアイコンでしたが、Win32APIは他にもいろんなことができるので、興味がある方は遊んでみてもいいかもしれません。

ただ、最後のアニメーションは負荷検証などしてないし、おそらくWindows側もあまり想定してないやり方なので、もしかしたらUnityエディタが不安定になるかもしれません。
クリスマスが終わったらちゃんと後片付けしましょう。

また、よければUniconも使っていただけると、私とあたるくんが泣いてよろこびます。

明日は@mkikenさんの記事です!お楽しみに!

  1. 筆者も仕事です;;

  2. 所謂DLLインジェクションみたいなことをすれば、UI表示前に処理を差し込めるかもしれませんが、そこまでするかという

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?