はじめに
もはや意識する必要もほとんどなくなってはきましたが、やはりたまに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
という関数を呼んでいるだけです。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
D3D12HelloTriangle sample(1280, 720, L"D3D12 Hello Triangle");
return Win32Application::Run(&sample, hInstance, nCmdShow);
}
さらにその中を見ると、まずはコマンドラインの解析。
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
をそちらで実行してみます。
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に振り分ける自前のジョブシステムを使っていたため、たまにCreateWindow
とPeekMessage
の実行が違うスレッドになって、ウィンドウメッセージを受け取るのが遅れる=反応が悪くなる、ということでした。(荒い作り方だな!!)
ここらへんのコードは昔から何度も書いていますが、ちょっとすっきりしました。基本重要。