土田 拓也

テクノロジやビジネス、デザイン、ライフスタイルについて思惟するシステムエンジニア

  • About
  • Blog
    • Technology
    • Business
    • Design
    • Lifestyle
  • Contact
  • Works
    • ReControl
  • Github
  • LinkedIn
  • RSS
  • Twitter
© 2026 Takuya Tsuchida

Visual Studio 2026 で Win32 アプリケーションを開発する

2026年2月8日

Visual Studio 2026 で Win32 アプリケーションのプロジェクトを作成し、MessageBox 関数による Hello, World! から、最小限のウィンドウ表示、通知エリアへの常駐化、バルーン通知までを解説します。C++ と WinUI 3 で開発を進めてきた ReControl プロジェクトを Win32 アプリケーションへ移行するにあたり、その過程で調査した内容を記事にまとめました。

Win32 プロジェクトを作成する

本節では、Visual Studio 2026 で Win32 プロジェクトを作成します。

Visual Studio Community 2026 を「C++ によるデスクトップ開発」ワークロードにチェックを入れてインストールします。なお、私の Visual Studio Community 2026 環境は言語パックを英語にしているため、Visual Studio Community 2026 の画面は英語表示になり、説明上も一部で英語名称を使用しています。

空のプロジェクトを作成したいので、新しいプロジェクトを作成するときに Windows Desktop Wizard を選択します。

プロジェクト名を Win32App として、Create をクリックします。

ウィザードが立ち上がるので、Application type で Desktop Application (.exe) を選択し、Empty project にチェックを入れて OK をクリックします。

これで Win32 アプリケーションのプロジェクトが作成できました。

Git > Create Git Repository… で、ローカル Git リポジトリを作成します。.gitignore テンプレートは Default (VisualStudio) にします。

自動的にコミットが2つ作成されるので、必要に応じて下記の Git コマンドでコミットログを修正してください。

git rebase -i --root

また、Build > Configuration Manager… から、使用しない 32-bit プラットフォーム (x86 / Win32) を削除しても構いません。

私は、Visual Studio Community 2026 上の JetBrains ReSharper C++ が有効な環境で開発しています。また、下記の .clang-format をプロジェクトに追加しています。そのため、本稿で提示するコードはこの環境を前提に整形されています。

BasedOnStyle: Microsoft
AllowShortIfStatementsOnASingleLine: true

MessageBox で Hello, World! を表示する

本節では、前節で作成したプロジェクトにコードを追加し、MessageBox で Hello, World! を表示します。

下記のコードで Win32App.cpp を追加します。

#include <windows.h>

int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ LPWSTR, _In_ int)
{
    MessageBox(nullptr, L"Hello, World!", L"Win32 App", MB_OK);
    return 0;
}

1行目は Win32 で必須となる Windows ヘッダーの読み込みです。3行目は WinMain アプリケーションエントリポイントの定義ですが、Unicode 対応の wWinMain 関数を定義するのが一般的です。静的解析での警告を回避するために SAL も付与しています。5行目は MessageBox 関数による Hello, World! です。6行目は正常終了の0を戻り値としています。

ビルドして実行すると下記のようなメッセージボックスが表示され、OK をクリックするとアプリケーションが終了します。

ボタンがクラシックであることと、文字が滲んでいることに気付くかと思います。それらも解決しておきましょう。

まず、ボタンをモダンにするにはビジュアルスタイルの有効化が必要です。そのためには、共通コントロールのバージョン6を使用するように指定する必要があります。アプリケーションマニフェストでの指定が王道なのですが、今回はプロジェクトのプロパティにあるリンカーの Additional Manifest Dependencies で指定します。Debug と Release の両方が編集されるように、Configuration が All Configurations になっていることを確認してください。

下記の行を編集画面で追加して、OK をクリックしてください。

"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'"

ビルドして実行すると下記のようにモダンなメッセージボックスが表示されるようになります。

さらに、文字を明瞭にするにはプロセスの既定の DPI 認識を設定する必要があります。こちらもアプリケーションマニフェストでの設定が王道なのですが、今回はプロジェクトのプロパティにあるマニフェストツールの DPI Awareness で High DPI Aware を指定することで対応します。

ビルドして実行すると下記のようにモダンで明瞭なメッセージボックスが表示されるようになります。

これで MessageBox 関数による Hello, World! の表示が完成しました。なお、ReControl では MessageBox のダークモード対応なども行っていますが、複雑になりすぎるので本稿では割愛します。

最小限のウィンドウを表示する

本節では、前節のコードを修正し、最小限のウィンドウを表示します。メッセージループが登場し、Win32 アプリケーションらしいコードになります。

下記のコードで Win32App.cpp を更新します。

#include <windows.h>

int WINAPI wWinMain(_In_ const HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPWSTR, _In_ const int nShowCmd)
{
    const WNDCLASS wc{
        .lpfnWndProc = [](const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) -> LRESULT {
            if (uMsg == WM_DESTROY)
            {
                PostQuitMessage(0);
                return 0;
            }

            return DefWindowProc(hWnd, uMsg, wParam, lParam);
        },
        .hInstance = hInstance,
        .hbrBackground = GetSysColorBrush(COLOR_WINDOW),
        .lpszClassName = L"Win32AppMainWindow",
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)
    const auto atomClass = RegisterClass(&wc);
    if (!atomClass) return static_cast<int>(GetLastError());

    const auto hWnd = CreateWindow(MAKEINTATOM(atomClass), L"Win32 App", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
                                   CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hInstance, nullptr);
    if (!hWnd) return static_cast<int>(GetLastError());

    ShowWindow(hWnd, nShowCmd);

    MSG msg{};
    while (const auto bRet = GetMessage(&msg, nullptr, 0, 0))
    {
        if (bRet == -1) return static_cast<int>(GetLastError());

        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return static_cast<int>(msg.wParam);
}

3行目に、インスタンスハンドルである hInstance パラメーターと、メインウィンドウの初期表示フラグである nShowCmd パラメーターを追加しています。

int WINAPI wWinMain(_In_ const HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPWSTR, _In_ const int nShowCmd)

5~18行目はウィンドウクラス属性を格納する WNDCLASS 構造体を指示付き初期化しています。

    const WNDCLASS wc{
        .lpfnWndProc = [](const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) -> LRESULT {
            if (uMsg == WM_DESTROY)
            {
                PostQuitMessage(0);
                return 0;
            }

            return DefWindowProc(hWnd, uMsg, wParam, lParam);
        },
        .hInstance = hInstance,
        .hbrBackground = GetSysColorBrush(COLOR_WINDOW),
        .lpszClassName = L"Win32AppMainWindow",
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)

6~14行目は、ウィンドウに送信されたメッセージを処理する WNDPROC コールバック関数をラムダ式で定義しています。ここは独立した WndProc 関数として定義しても構いません。

        .lpfnWndProc = [](const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) -> LRESULT {
            if (uMsg == WM_DESTROY)
            {
                PostQuitMessage(0);
                return 0;
            }

            return DefWindowProc(hWnd, uMsg, wParam, lParam);
        },

7行目で、ウィンドウが破棄されるときに送信される WM_DESTROY メッセージを判定し、9行目で PostQuitMessage 関数を正常終了コードである0を引数として実行し、10行目で0を戻り値とすることで WNDPROC コールバック関数としてのメッセージ処理を完了しています。13行目はアプリケーションが処理しないメッセージを処理する既定のウィンドウプロシージャである DefWindowProc 関数を実行しています。

            if (uMsg == WM_DESTROY)
            {
                PostQuitMessage(0);
                return 0;
            }

            return DefWindowProc(hWnd, uMsg, wParam, lParam);

15行目はインスタンスハンドルを指定しています。16行目は GetSysColorBrush 関数で取得した背景ブラシを指定しており、この値でウィンドウの背景が描画されます。17行目はウィンドウクラス名を指定しています。ウィンドウクラス名はプロセス内で一意である必要があり、二重起動の防止処理を考慮するとグローバルに一意であることが理想です。18行目のコメントはすべてのフィールドを指示付き初期化していないという警告を抑制しています。

        .hInstance = hInstance,
        .hbrBackground = GetSysColorBrush(COLOR_WINDOW),
        .lpszClassName = L"Win32AppMainWindow",
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)

19行目は RegisterClass 関数でウィンドウクラスを登録し、戻り値のクラスアトムを変数に格納しています。20行目は簡易的なエラー処理をしており、GetLastError 関数で戻されるエラーコードを wWinMain 関数の戻り値としています。本来は、エラーコードから FormatMessage 関数でエラー文字列を取得し、MessageBox 関数で表示したり、システムログに出力したりするなどのエラー処理がよいと思うのですが、複雑になりすぎるので本稿では割愛しています。

    const auto atomClass = RegisterClass(&wc);
    if (!atomClass) return static_cast<int>(GetLastError());

22~23行目は CreateWindow 関数でアトムクラスからウィンドウを作成し、戻り値のウィンドウハンドルを変数に格納しています。24行目は20行目と同様に簡易的なエラー処理をしています。

    const auto hWnd = CreateWindow(MAKEINTATOM(atomClass), L"Win32 App", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
                                   CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hInstance, nullptr);
    if (!hWnd) return static_cast<int>(GetLastError());

26行目は ShowWindow 関数でウィンドウを nShowCmd の状態で表示しています。

    ShowWindow(hWnd, nShowCmd);

28~35行目はメッセージループです。28行目はメッセージ情報を保持する MSG 構造体を初期化しています。29行目は GetMessage 関数でメッセージを取得し、WM_QUIT メッセージを取得するまでループします。31行目は GetMessage 関数の戻り値がエラーを示す-1であるときに簡易的なエラー処理をしています。33行目は TranslateMessage 関数で仮想キーメッセージを文字メッセージに変換しています。34行目は DispatchMessage 関数でウィンドウプロシージャにメッセージをディスパッチしています。

    MSG msg{};
    while (const auto bRet = GetMessage(&msg, nullptr, 0, 0))
    {
        if (bRet == -1) return static_cast<int>(GetLastError());

        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

37行目は PostQuitMessage 関数で指定された引数が格納される wParam を wWinMain 関数の戻り値としています。

    return static_cast<int>(msg.wParam);

ビルドして実行すると下記のようなウィンドウが表示され、✕をクリックするとウィンドウが閉じられ、アプリケーションが終了します。

これで最小限のウィンドウが完成しました。

通知エリアへ常駐化する

本節では、前節のコードを修正し、通知エリアへ常駐する Win32 アプリケーションを実装します。通知アイコンやメニューリソースを追加し、コード行数も増加するので、よりアプリケーションのコードらしくなります。

最初に、通知アイコンを右クリックしたときに表示するコンテキストメニューをリソースとして作成します。ソリューションエクスプローラーからリソースを追加します。

リソースの追加ダイアログからメニューを追加します。

メニューの第一階層を ContextMenu とし、第二階層に Exit を追加します。

メニューの ID を IDR_CONTEXTMENU に修正します。

これで、resource.h と Win32App.rc というリソースファイルが生成されます。GUI によって生成されたままでも動作すると思いますが、私は下記のように修正しています。

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Win32App.rc
//
#define IDR_CONTEXTMENU                 101
#define ID_CONTEXTMENU_EXIT             40001

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        102
#define _APS_NEXT_COMMAND_VALUE         40002
#define _APS_NEXT_CONTROL_VALUE         1001
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// English (United States) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE 
BEGIN
    "#include ""winres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

IDR_CONTEXTMENU MENU
BEGIN
    POPUP "ContextMenu"
    BEGIN
        MENUITEM "Exit",                        ID_CONTEXTMENU_EXIT
    END
END

#endif    // English (United States) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

下記のコードで Win32App.cpp を更新します。

#include <windows.h>
#include <windowsx.h>

#include "resource.h"

namespace
{

constexpr GUID NOTIFYICON_GUID = {0xE7DCA55E, 0xBD60, 0x4490, {0x9B, 0x53, 0xF3, 0xA4, 0x69, 0xCC, 0xCC, 0x27}};

constexpr auto WM_NOTIFYICON = WM_APP + 1;
const auto WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");

constexpr auto ERROR_APP = 0x20000000;
constexpr auto ERROR_APP_ALREADY_RUNNING = ERROR_APP + 1;

constexpr auto ERROR_NIM = 0x20010000;
constexpr auto ERROR_NIM_ADD = ERROR_NIM + NIM_ADD;
constexpr auto ERROR_NIM_DELETE = ERROR_NIM + NIM_DELETE;
constexpr auto ERROR_NIM_SETVERSION = ERROR_NIM + NIM_SETVERSION;

int AddNotifyIcon(const HWND hWnd)
{
    NOTIFYICONDATA nid{
        .cbSize = sizeof(NOTIFYICONDATA),
        .hWnd = hWnd,
        .uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_GUID | NIF_SHOWTIP,
        .uCallbackMessage = WM_NOTIFYICON,
        .hIcon = LoadIcon(nullptr, IDI_APPLICATION),
        .szTip = L"Win32 App",
        .uVersion = NOTIFYICON_VERSION_4,
        .guidItem = NOTIFYICON_GUID,
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)
    if (!Shell_NotifyIcon(NIM_ADD, &nid)) return ERROR_NIM_ADD;
    if (!Shell_NotifyIcon(NIM_SETVERSION, &nid)) return ERROR_NIM_SETVERSION;
    return 0;
}

int DeleteNotifyIcon(const HWND hWnd)
{
    NOTIFYICONDATA nid{
        .cbSize = sizeof(NOTIFYICONDATA),
        .hWnd = hWnd,
        .uFlags = NIF_GUID,
        .guidItem = NOTIFYICON_GUID,
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)
    if (!Shell_NotifyIcon(NIM_DELETE, &nid)) return ERROR_NIM_DELETE;
    return 0;
}

void ShowContextMenu(const HWND hWnd, const int x, const int y)
{
    const auto hMenu = LoadMenu(nullptr, MAKEINTRESOURCE(IDR_CONTEXTMENU));
    const auto hSubMenu = GetSubMenu(hMenu, 0);
    SetForegroundWindow(hWnd);
    TrackPopupMenu(hSubMenu, TPM_BOTTOMALIGN, x, y, 0, hWnd, nullptr);
}

LRESULT CALLBACK WndProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_CREATE:
        if (const auto nExitCode = AddNotifyIcon(hWnd)) PostQuitMessage(nExitCode);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(DeleteNotifyIcon(hWnd));
        return 0;

    case WM_NOTIFYICON:
        if (LOWORD(lParam) == WM_CONTEXTMENU) ShowContextMenu(hWnd, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam));
        return 0;

    case WM_COMMAND:
        if (LOWORD(wParam) == ID_CONTEXTMENU_EXIT) DestroyWindow(hWnd);
        return 0;

    default:
        if (uMsg == WM_TASKBARCREATED)
        {
            AddNotifyIcon(hWnd);
            return 0;
        }
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
}

} // namespace

int WINAPI wWinMain(_In_ const HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPWSTR, _In_ const int nShowCmd)
{
    const WNDCLASS wc{
        .lpfnWndProc = WndProc,
        .hInstance = hInstance,
        .lpszClassName = L"Win32AppMainWindow",
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)
    const auto atomClass = RegisterClass(&wc);
    if (!atomClass) return static_cast<int>(GetLastError());

    if (FindWindow(MAKEINTATOM(atomClass), nullptr)) return ERROR_APP_ALREADY_RUNNING;

    const auto hWnd =
        CreateWindow(MAKEINTATOM(atomClass), L"Win32 App", 0, 0, 0, 0, 0, nullptr, nullptr, hInstance, nullptr);
    if (!hWnd) return static_cast<int>(GetLastError());

    MSG msg{};
    while (const auto bRet = GetMessage(&msg, nullptr, 0, 0))
    {
        if (bRet == -1) return static_cast<int>(GetLastError());

        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return static_cast<int>(msg.wParam);
}

1~4行目で GET_X_LPARAM マクロと GET_Y_LPARAM マクロを使用するために windowsx.h ヘッダーを、リソースを使用するために resource.h ヘッダーを追加しています。

#include <windows.h>
#include <windowsx.h>

#include "resource.h"

6~7行目で無名名前空間を開始しています。wWinMain 関数だけに記述すると見通しが悪いので関数分割しているためです。F# の規則を参考にプロトタイプ宣言をしていません。

namespace
{

9行目で通知アイコンの GUID を NOTIFYICON_GUID として定義しています。Win32 のヘッダーファイルでは大文字表記の16進数が多いので大文字表記に統一しています。

constexpr GUID NOTIFYICON_GUID = {0xE7DCA55E, 0xBD60, 0x4490, {0x9B, 0x53, 0xF3, 0xA4, 0x69, 0xCC, 0xCC, 0x27}};

GUID の生成には、Visual Studio 2026 の Tools > Create GUID を使用しました。

11~12行目でウィンドウメッセージを定義しています。WM_NOTIFYICON は通知アイコンに関するウィンドウメッセージで、RegisterWindowMessage 関数で定義している WM_TASKBARCREATED はエクスプローラーの再起動などで通知アイコンが消失することに対応するためのウィンドウメッセージです。

constexpr auto WM_NOTIFYICON = WM_APP + 1;
const auto WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");

14~20行目でエラーコードを定義しています。ビット29はアプリケーション定義のエラーコード用として予約されているので、ビット29を1とするエラーコードを定義しています。なお、winerror.h ヘッダーに定義されている HRESULT に準拠する場合、エラーであればビット31を1とする必要などがあります。詳細はヘッダーファイルにコメントで書いてあるので気になる人は読んでみてください。本稿では、HRESULT に準拠すると int に収まらなくなり、型の扱いが面倒になるため、ビット29を1とするに留めています。

constexpr auto ERROR_APP = 0x20000000;
constexpr auto ERROR_APP_ALREADY_RUNNING = ERROR_APP + 1;

constexpr auto ERROR_NIM = 0x20010000;
constexpr auto ERROR_NIM_ADD = ERROR_NIM + NIM_ADD;
constexpr auto ERROR_NIM_DELETE = ERROR_NIM + NIM_DELETE;
constexpr auto ERROR_NIM_SETVERSION = ERROR_NIM + NIM_SETVERSION;

22~37行目で通知アイコンを追加する AddNotifyIcon 関数を定義しています。NOTIFYICONDATA 構造体の uID メンバーを使用せず GUID を使用するので、uFlags メンバーで NIF_GUID フラグを立て、guidItem メンバーに定義済みの NOTIFYICON_GUID を指定しています。通知アイコンはバージョン4を使用するので、マウスオーバーでのチップを表示するために uFlags メンバーで NIF_SHOWTIP フラグを立て、uVersion を NOTIFYICON_VERSION_4 にし、Shell_NotifyIcon 関数に NIM_SETVERSION を個別指定して呼び出しています。

int AddNotifyIcon(const HWND hWnd)
{
    NOTIFYICONDATA nid{
        .cbSize = sizeof(NOTIFYICONDATA),
        .hWnd = hWnd,
        .uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_GUID | NIF_SHOWTIP,
        .uCallbackMessage = WM_NOTIFYICON,
        .hIcon = LoadIcon(nullptr, IDI_APPLICATION),
        .szTip = L"Win32 App",
        .uVersion = NOTIFYICON_VERSION_4,
        .guidItem = NOTIFYICON_GUID,
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)
    if (!Shell_NotifyIcon(NIM_ADD, &nid)) return ERROR_NIM_ADD;
    if (!Shell_NotifyIcon(NIM_SETVERSION, &nid)) return ERROR_NIM_SETVERSION;
    return 0;
}

39~49行目で通知アイコンを削除する DeleteNotifyIcon 関数を定義しています。Shell_NotifyIcon 関数に NIM_DELETE を指定して呼び出しています。

int DeleteNotifyIcon(const HWND hWnd)
{
    NOTIFYICONDATA nid{
        .cbSize = sizeof(NOTIFYICONDATA),
        .hWnd = hWnd,
        .uFlags = NIF_GUID,
        .guidItem = NOTIFYICON_GUID,
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)
    if (!Shell_NotifyIcon(NIM_DELETE, &nid)) return ERROR_NIM_DELETE;
    return 0;
}

51~57行目でコンテキストメニューを表示する ShowContextMenu 関数を定義しています。LoadMenu 関数でメニューリソースの IDR_CONTEXTMENU からメニューを読み込み、GetSubMenu 関数でコンテキストメニューとして使用するサブメニュー部分を取り出しています。TrackPopupMenu 関数を呼び出す前に、現在のウィンドウがフォアグラウンドウィンドウである必要があるので、SetForegroundWindow 関数を呼び出しています。TrackPopupMenu 関数は TPM_BOTTOMALIGN フラグと座標を指定して呼び出すことで、コンテキストメニューはマウスポインターの右上に表示されます。なお、エラー処理は省略しています。

void ShowContextMenu(const HWND hWnd, const int x, const int y)
{
    const auto hMenu = LoadMenu(nullptr, MAKEINTRESOURCE(IDR_CONTEXTMENU));
    const auto hSubMenu = GetSubMenu(hMenu, 0);
    SetForegroundWindow(hWnd);
    TrackPopupMenu(hSubMenu, TPM_BOTTOMALIGN, x, y, 0, hWnd, nullptr);
}

59~87行目でラムダ式で定義していたウィンドウプロシージャーを独立させ WndProc 関数を定義しています。WM_CREATE メッセージはウィンドウの作成を要求したときに、WM_DESTROY メッセージはウィンドウが破棄されるときに、WM_NOTIFYICON メッセージは通知アイコンを操作したときに、WM_COMMAND メッセージはコンテキストメニューを操作したときに、WM_TASKBARCREATED メッセージはタスクバーが作成されたときに送信されます。なお、WM_TASKBARCREATED メッセージを受信した場合、通知アイコンを再登録するために AddNotifyIcon 関数を呼び出しています。

LRESULT CALLBACK WndProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_CREATE:
        if (const auto nExitCode = AddNotifyIcon(hWnd)) PostQuitMessage(nExitCode);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(DeleteNotifyIcon(hWnd));
        return 0;

    case WM_NOTIFYICON:
        if (LOWORD(lParam) == WM_CONTEXTMENU) ShowContextMenu(hWnd, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam));
        return 0;

    case WM_COMMAND:
        if (LOWORD(wParam) == ID_CONTEXTMENU_EXIT) DestroyWindow(hWnd);
        return 0;

    default:
        if (uMsg == WM_TASKBARCREATED)
        {
            AddNotifyIcon(hWnd);
            return 0;
        }
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
}

89行目で無名名前空間を終了しています。

} // namespace

91~117行目で wWinMain 関数を修正しています。メインウィンドウは非表示のため、ウィンドウクラスから背景を描画するブラシを指定する hbrBackground メンバーを削除し、CreateWindow 関数を簡素化し、ShowWindow 関数を削除しています。また、98行目に FindWindow 関数を使用した二重起動の防止処理を追加しています。

int WINAPI wWinMain(_In_ const HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPWSTR, _In_ const int nShowCmd)
{
    const WNDCLASS wc{
        .lpfnWndProc = WndProc,
        .hInstance = hInstance,
        .lpszClassName = L"Win32AppMainWindow",
    }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)
    const auto atomClass = RegisterClass(&wc);
    if (!atomClass) return static_cast<int>(GetLastError());

    if (FindWindow(MAKEINTATOM(atomClass), nullptr)) return ERROR_APP_ALREADY_RUNNING;

    const auto hWnd =
        CreateWindow(MAKEINTATOM(atomClass), L"Win32 App", 0, 0, 0, 0, 0, nullptr, nullptr, hInstance, nullptr);
    if (!hWnd) return static_cast<int>(GetLastError());

    MSG msg{};
    while (const auto bRet = GetMessage(&msg, nullptr, 0, 0))
    {
        if (bRet == -1) return static_cast<int>(GetLastError());

        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return static_cast<int>(msg.wParam);
}

ビルドして実行すると通知アイコンが表示されます。表示されない場合、非表示のアイコンメニューの中に隠れていると思います。右クリックするとコンテキストメニューが表示され、Exit をクリックするとアプリケーションが終了します。

これで最小限の常駐アプリケーションが完成しました。

バルーン通知する

本節では、通知アイコンによるバルーン通知を実装します。おまけのコンテンツなので説明は簡易的です。

下記のコードで Win32App.cpp の71行目にある WM_NOTIFYICON メッセージの処理を更新します。詳細は NOTIFYICONDATA 構造体の NIF_INFO フラグの説明などを確認してください。

    case WM_NOTIFYICON:
        if (LOWORD(lParam) == WM_CONTEXTMENU) ShowContextMenu(hWnd, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam));
        if (LOWORD(lParam) == NIN_SELECT)
        {
            NOTIFYICONDATA nid{
                .cbSize = sizeof(NOTIFYICONDATA),
                .hWnd = hWnd,
                .uFlags = NIF_INFO | NIF_GUID,
                .szInfo = L"Hello, World!",
                .szInfoTitle = L"Win32 App",
                .dwInfoFlags = NIIF_RESPECT_QUIET_TIME,
                .guidItem = NOTIFYICON_GUID,
            }; // NOLINT(clang-diagnostic-missing-designated-field-initializers)
            Shell_NotifyIcon(NIM_MODIFY, &nid);
        }
        return 0;

ビルドして実行すると通知アイコンをクリックしたときにバルーン通知が表示されます。

これで最小限のバルーン通知可能な常駐アプリが完成しました。


本稿では、Visual Studio 2026 で Win32 アプリケーションのプロジェクトを作成し、MessageBox 関数による Hello, World! から、最小限のウィンドウ表示、通知エリアへの常駐化、バルーン通知までを解説しました。次回は、VERSIONINFO リソースや ICON リソースの記事を書こうと思っています。また、Win32 アプリケーションのインストーラーを作成できるように、Microsoft Visual Studio Installer Projects 2022 の記事も検討しています。

カテゴリ:テクノロジ タグ:C++, ReControl, Visual Studio 2026, Win32, Windows