Posted at

WinMainとCreateWindowとGetMessage(PeekMessage)の関係


はじめに

もはや意識する必要もほとんどなくなってはきましたが、やはりたまにWindows OSの奥底というか基礎的な部分を検証する必要が出てきたりします。今回は、Win32アプリのメニューの反応がなぜか悪い(数秒待たされることがある)という問題が発生したので、その検証のついでにWin32アプリの基本構造を探ってみました。


気の早い人向け

先に結論を書いておくと、

CreateWindowとPeekMessage(GetMessage)は同じスレッドから呼ばないとウィンドウメッセージが受け取れず、メニューやクローズボタンが反応しなくなったり、SetParentした際に他のウィンドウに悪い影響を与えるので注意しましょう。

です。ま、当たり前の話なんですが、あまりこのことに明確に触れたドキュメントが見当たらなかったので、念のため書いておきます。


Win32アプリの基本構造

Win32アプリはWinMainというエントリポイントから始まります(プロジェクトのプロパティで変更できるけど)。さて、ここには何を書きましょう?

※ここではサンプルとして、DirectX12のMicrosoft公式サンプルD3D12HelloWorld(の中のD3D12HelloTriangle)を使います。

https://github.com/Microsoft/DirectX-Graphics-Samples

※若干特殊なサンプルではありますが(DirectXを使うお仕事なので…)、ウィンドウベースアプリと基本的な構造は変わりません。


WinMain

WinMainではD3D12HelloTriangleというサンプルクラスを生成してWin32ApplicationクラスのRunという関数を呼んでいるだけです。


Main.cpp

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)

{
D3D12HelloTriangle sample(1280, 720, L"D3D12 Hello Triangle");
return Win32Application::Run(&sample, hInstance, nCmdShow);
}

さらにその中を見ると、まずはコマンドラインの解析。


Win32Application.cpp

int Win32Application::Run(DXSample* pSample, HINSTANCE hInstance, int nCmdShow)

{
// Parse the command line parameters
int argc;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
pSample->ParseCommandLineArgs(argv, argc);
LocalFree(argv);

次に大事なウィンドウの生成。ウィンドウプロシージャのWindowProcを登録していますが、これは後述。

    // Initialize the window class.

WNDCLASSEX windowClass = { 0 };
windowClass.cbSize = sizeof(WNDCLASSEX);
windowClass.style = CS_HREDRAW | CS_VREDRAW;
windowClass.lpfnWndProc = WindowProc;
windowClass.hInstance = hInstance;
windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
windowClass.lpszClassName = L"DXSampleClass";
RegisterClassEx(&windowClass);

RECT windowRect = { 0, 0, static_cast<LONG>(pSample->GetWidth()), static_cast<LONG>(pSample->GetHeight()) };
AdjustWindowRect(&windowRect, WS_OVERLAPPEDWINDOW, FALSE);

// Create the window and store a handle to it.
m_hwnd = CreateWindow(
windowClass.lpszClassName,
pSample->GetTitle(),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
windowRect.right - windowRect.left,
windowRect.bottom - windowRect.top,
nullptr, // We have no parent window.
nullptr, // We aren't using menus.
hInstance,
pSample);

あとはサンプルクラス自体の初期化とウィンドウの表示。

    // Initialize the sample. OnInit is defined in each child-implementation of DXSample.

pSample->OnInit();

ShowWindow(m_hwnd, nCmdShow);

そして大事なメインループ。PeekMessageを呼ぶことでOSからのメニューとかマウスとかクローズとか様々なメッセージを受け取り、登録したウィンドウプロシージャに渡されることでアプリ独自の処理を行います。(普通のアプリでは同期待ちを行うGetMessageを使いますが、やってることは同じです)

    // Main sample loop.

MSG msg = {};
while (msg.message != WM_QUIT)
{
// Process any messages in the queue.
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

ちなみに、whileループになっていますが、PeekMessageが呼ばれたときにWindowsのシステム側が判断して、やることがない場合は別のアプリに実行権を渡したりするため、無限ループでCPUがぶん回ることはありません。

で、ウィンドウのクローズボタンを押すとWM_QUITというメッセージが送信されるので、それを受け取るとwhileループを抜けて、アプリを終了します。

    pSample->OnDestroy();

// Return this part of the WM_QUIT message to Windows.
return static_cast<char>(msg.wParam);
}

簡単。

サンプルのメイン処理はウィンドウプロシージャWindowProcに…

LRESULT CALLBACK Win32Application::WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

{
DXSample* pSample = reinterpret_cast<DXSample*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));

switch (message)
{

WM_PAINTイベントが来たときに呼ばれているようです。

    case WM_PAINT:

if (pSample)
{
pSample->OnUpdate();
pSample->OnRender();
}
return 0;


試しに別スレッドで実行してみる

で、サンプルではすべての処理をWinMain(から呼ばれる関数)で行っていましたが、それってWinMainと別スレッドで実行してもいいんだっけ? ということで、スレッドを立ててWin32Application::Runをそちらで実行してみます。


Main.cpp

struct ThreadParameter

{
HINSTANCE hInstance;
DXSample* sample;
int nCmdShow;
};

DWORD WINAPI RunThread(LPVOID lpThreadParameter)
{
auto threadParameter = (ThreadParameter*)lpThreadParameter;
return Win32Application::Run(threadParameter->sample, threadParameter->hInstance, threadParameter->nCmdShow);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
D3D12HelloTriangle sample(1280, 720, L"D3D12 Hello Triangle");

ThreadParameter threadParameter = { hInstance, &sample, nCmdShow };
DWORD threadID = 0;
auto threadHandle = CreateThread(nullptr, 0, RunThread, &threadParameter, 0, &threadID);
WaitForSingleObject(threadHandle, INFINITE);
DWORD exitCode = 0;
GetExitCodeThread(threadHandle, &exitCode);
return exitCode;
}


はい、問題なく動きました。


メインループを別スレッドにしてみる

次はメインループ(メッセージループ)を別スレッドにしてみます。

DWORD WINAPI LoopThread(LPVOID lpThreadParameter)

{
auto pSample = (DXSample*)lpThreadParameter;
MSG msg = {};
while (msg.message != WM_QUIT)
{
// Process any messages in the queue.
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (pSample)
{
pSample->OnUpdate();
pSample->OnRender();
}
}
return static_cast<char>(msg.wParam);
}

ShowWindowのあとをこのように書き換えます。

    ShowWindow(m_hwnd, nCmdShow);

DWORD threadID = 0;
auto threadHandle = CreateThread(nullptr, 0, LoopThread, pSample, 0, &threadID);
WaitForSingleObject(threadHandle, INFINITE);

pSample->OnDestroy();

DWORD exitCode = 0;
GetExitCodeThread(threadHandle, &exitCode);
return exitCode;

ちゃんとウィンドウが表示されたものの、マウスカーソルがくるくるまわってクローズボタンが押せなくなりましたね!!(VisualStudioかタスクマネージャから止めてください)


まとめ

まとめると、


  • WinMainは同期オブジェクトで止めてもかまわない。

  • CreateWindowとPeekMessage(GetMessage)はWinMain以外から呼んでも大丈夫。

  • でも、CreateWindowとPeekMessage(GetMessage)は同じスレッドから呼ばないとダメ。

ということが分かりました。

今回のこちらのトラブルは、処理を各CPUに振り分ける自前のジョブシステムを使っていたため、たまにCreateWindowPeekMessageの実行が違うスレッドになって、ウィンドウメッセージを受け取るのが遅れる=反応が悪くなる、ということでした。(荒い作り方だな!!)

ここらへんのコードは昔から何度も書いていますが、ちょっとすっきりしました。基本重要。