スレッドへのメッセージ送信
新しく作成したスレッド(マルチスレッドの項を参照)は、そのままではSendMessage関数などで別のスレッドからメッセージを送ることができません。
メインスレッドがメッセージを受け取れるのはメッセージループがあるからなので、新スレッドにもメッセージループを作成すればメッセージを受信できます。
メッセージはウィンドウに関連付けられるのではなく、スレッドに関連付けられます。
ウィンドウを持たないスレッドやコンソールアプリ等でもメッセージの送信が可能です。
PeekMessage関数
メッセージの受信はGetMessage関数を使用しますが、PeekMessage
関数を使用することもできます。
この関数はマルチスレッドプログラム用のものではなく、シングルスレッドプログラムでも使用されます。
- BOOL PeekMessageW(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg
); - メッセージキューからメッセージを取得し、lpMsgに格納する。
メッセージを取得した場合は0以外を、メッセージを取得しなかった場合は0を返す。
GetMessage関数やPeekMessage関数を実行すると、スレッドにメッセージキューが作成されメッセージの受信が可能になります。
GetMessage関数はメッセージキューに何らかのメッセージが送信されるまで待機しますが、PeekMessage関数はメッセージの有無に関係なく即座に制御を返す点が異なります。
メッセージがある場合はMSG
構造体に格納します。
引数の意味はGetMessage関数と同じですが、第五引数wRemoveMsg
が追加されています。
ここには以下の定数を指定します。
定数 | 説明 |
---|---|
PM_NOREMOVE | 処理後にメッセージキューからメッセージを削除しない。 |
PM_REMOVE | 処理後にメッセージキューからメッセージを削除する。 |
PM_NOYIELD |
この関数の実行中、システムは呼び出し元スレッドを解放して他のスレッドに処理の権利を渡さないようにする。PM_NOREMOVE またはPM_REMOVE と組み合わせて使用する。
|
GetMessage関数と同等の処理を行う場合はPM_REMOVE
フラグを指定します。
また、処理するメッセージを制限する以下の定数の組み合わせを指定することもできます。
(既定では全てのメッセージを処理します)
PM_QS_INPUT | マウスとキーボードのメッセージ |
PM_QS_PAINT | 描画メッセージ |
PM_QS_POSTMESSAGE |
ポストされたメッセージ (Post○○系のメッセージ関数) |
PM_QS_SENDMESSAGE |
センドされたメッセージ (Send○○系のメッセージ関数) |
GetMessage関数とPeekMessage関数のどちらを使うべきかはスレッドの目的によります。
メインスレッドでは多くの場合でGetMessage関数を使用しますが、これは何かメッセージが来ない限り処理をしないため、何も処理をしていない空き時間(アイドル状態)が結構あります。
この空き時間を有効に使うためにGetMessage関数をPeekMessage関数に置き換えることができます。
その他、ゲームアプリなどでアニメーション処理を行う場合、GetMessage関数ではフレーム毎の描画が難しいのでPeekMessage関数を使用することがあります。
サンプルコード
メインスレッドのGetMessage関数をPeekMessage関数に置き換えて、アニメーションを行うサンプルコードです。
このコードはシングルスレッドプログラムです。
#include <windows.h>
#include <math.h>
HINSTANCE hInst; //インスタンス
WCHAR szTitle[] = L"テストアプリ"; //タイトル
WCHAR szWindowClass[] = L"MyTestApp"; //ウィンドウクラス名
//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
HWND InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void ClipPoint(RECT*, POINT*, int*, int*)
//次のフレームの描画を要求する独自メッセージ
#define WM_NEXTFRAME (WM_APP + 1)
int APIENTRY wWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPWSTR lpCmdLine,
int nCmdShow)
{
MyRegisterClass(hInstance);
HWND hWnd = InitInstance(hInstance, nCmdShow);
if (!hWnd)
{
return 0;
}
MSG msg;
ULONGLONG nowTime, lastTime = 0;
unsigned int frameRate = ceil(1000.0 / 30); //秒間30フレーム
//メッセージループ
while (1)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
//何らかのメッセージが来た場合
//WM_QUITメッセージがきたらループを抜ける
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else {
//何もメッセージが来ていないときの処理
//ウィンドウプロシージャの処理中は一時停止する
nowTime = GetTickCount64();
if (nowTime - lastTime >= frameRate) {
PostMessage(hWnd, WM_NEXTFRAME, 0, 0);
lastTime = nowTime;
}
//CPU負荷を下げるための処理
Sleep(1);
}
}
return (int)msg.wParam;
}
//ウィンドウクラスの登録
ATOM MyRegisterClass(HINSTANCE hInstance)
{ //省略 }
//メインウィンドウの作成
HWND InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance;
HWND hWnd = CreateWindow(
szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
if (!hWnd) {
return NULL;
}
ShowWindow(hWnd, nCmdShow);
return hWnd;
}
//座標POINTが領域RECT外にはみ出さないようにする
void ClipPoint(RECT* rtClip, POINT* point, int* vecX, int* vecY)
{
if (point->x > rtClip->right) {
point->x = rtClip->right;
if (*vecX > 0) *vecX *= -1;
}
else if (point->x < rtClip->left) {
point->x = rtClip->left;
if (*vecX < 0) *vecX *= -1;
}
if (point->y > rtClip->bottom) {
point->y = rtClip->bottom;
if (*vecY > 0) *vecY *= -1;
}
else if (point->y < rtClip->top) {
point->y = rtClip->top;
if (*vecY < 0) *vecY *= -1;
}
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
RECT rtClient; //クライアント領域
static RECT rtClip; //描画可能領域
static RECT rtShape; //図形の領域
static POINT ptShape; //図形の中心座標
static int width, height; //図形のサイズ
static int vecX, vecY; //移動する力の向き
HDC hdc;
PAINTSTRUCT ps;
SYSTEMTIME systemTime; //システム時間
FILETIME fileTime; //ファイルタイム(UNIX時間を得るために使用)
switch (message)
{
case WM_CREATE: //ウィンドウの作成
//図形のサイズを定義
width = height = 50;
//UNIX時間で乱数の種を初期化
GetSystemTime(&systemTime);
SystemTimeToFileTime(&systemTime, &fileTime);
srand((unsigned)fileTime.dwLowDateTime);
//描画可能領域を算出
GetClientRect(hWnd, &rtClient);
rtClip = rtClient;
rtClip.left += width / 2;
rtClip.right -= width / 2;
rtClip.top += height / 2;
rtClip.bottom -= height / 2;
//図形位置と移動向きの初期値をランダムに得る
ptShape.x = rand() % (rtClip.right - width);
ptShape.y = rand() % (rtClip.bottom - height);
vecX = (rand() % 5 + 5) * (rand() % 2 == 0 ? -1 : 1);
vecY = (rand() % 5 + 5) * (rand() % 2 == 0 ? -1 : 1);
//図形の位置を描画可能領域に収める
ClipPoint(&rtClip, &ptShape, &vecX, &vecY);
break;
case WM_SIZE: //ウィンドウサイズの変更
GetClientRect(hWnd, &rtClient);
rtClip = rtClient;
rtClip.left += width / 2;
rtClip.right -= width / 2;
rtClip.top += height / 2;
rtClip.bottom -= height / 2;
break;
case WM_NEXTFRAME: //次のフレームを描画
ptShape.x += vecX;
ptShape.y += vecY;
ClipPoint(&rtClip, &ptShape, &vecX, &vecY);
rtShape.left = ptShape.x - width / 2;
rtShape.right = ptShape.x + width / 2;
rtShape.top = ptShape.y - height / 2;
rtShape.bottom = ptShape.y + height / 2;
InvalidateRect(hWnd, NULL, TRUE);
break;
case WM_PAINT: //画面の描画
hdc = BeginPaint(hWnd, &ps);
SelectObject(hdc, GetStockObject(BLACK_BRUSH));
Ellipse(hdc, rtShape.left, rtShape.top, rtShape.right, rtShape.bottom);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
秒間30フレームでクライアント領域内を黒円が跳ね回るアニメーションを描画しています。
ウィンドウの移動やサイズ変更時などはウィンドウメッセージを処理するため、その間はアニメーションは一時停止します。
PostThreadMessage関数
前置きが長くなりましたが、このページの主題である「スレッドへのメッセージの送信」にはPostThreadMessage
関数を使用します。
- BOOL PostThreadMessageW(
DWORD idThread,
UINT Msg,
WPARAM wParam,
LPARAM lParam
); - スレッドidThreadのメッセージキューにメッセージMsgをポストする。
追加のデータとしてwParam、lParamを指定できる。
成功した場合は0以外を、失敗した場合は0を返す。
この関数の動作はPostMessage関数とほぼ同じですが、送信先の指定(第一引数)をウィンドウハンドルではなくスレッドIDで行います。
スレッドIDはCreateThread関数(_beginthreadex
関数)の最後の引数から得ることができます。
ちなみにスレッドIDはスレッドの作成から終了まで変更されず、システム全体で重複することはありません。
GetCurrentThreadId関数
他にもGetCurrentThreadId
関数で実行元のスレッドのIDを得ることができます。
- DWORD GetCurrentThreadId();
- 呼び出し元スレッドの識別子を返す。
サンプルコード
先ほどのサンプルコードと(ほぼ)同等の動作を、マルチスレッドスレッドを使用して書き直したコードです。
#define _CRT_RAND_S
//↑rand_s関数を使用する場合に必要
#include <windows.h>
#include <stdlib.h>
#include <math.h>
#include <process.h>
#define WM_NEXTFRAME (WM_APP + 1)
#define WM_THREADPAUSE (WM_APP + 2)
//ウィンドウの生成等は省略
//rand_s関数のラッパー関数
unsigned int Random()
{
static char init;
if (init == 0) {
SYSTEMTIME systemTime;
FILETIME fileTime;
GetSystemTime(&systemTime);
SystemTimeToFileTime(&systemTime, &fileTime);
srand((unsigned)fileTime.dwLowDateTime);
init = 1;
}
unsigned int r;
return rand_s(&r) == 0 ? r : 0;
}
//座標POINTが領域RECT外にはみ出さないようにする
void ClipPoint(RECT* rtClip, POINT* point, int* vecX, int* vecY)
{
if (point->x > rtClip->right) {
point->x = rtClip->right;
if (*vecX > 0) *vecX *= -1;
}
else if (point->x < rtClip->left) {
point->x = rtClip->left;
if (*vecX < 0) *vecX *= -1;
}
if (point->y > rtClip->bottom) {
point->y = rtClip->bottom;
if (*vecY > 0) *vecY *= -1;
}
else if (point->y < rtClip->top) {
point->y = rtClip->top;
if (*vecY < 0) *vecY *= -1;
}
}
//スレッドパラメーター
typedef struct {
HWND hWnd; //メインスレッドのウィンドウハンドル
int width; //図形の幅
int height; //図形の高さ
} ThreadParam;
//スレッド開始関数
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
MSG msg;
//早めにメッセージキューを作成して
//メッセージを受け付けるようにしておく
//PM_NOREMOVEフラグである事に注意
PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE);
ThreadParam* tp = (ThreadParam*)lpParameter;
//描画に必要なデータの初期化
RECT rtClient;
RECT rtClip;
RECT rtShape;
POINT ptShape;
int vecX, vecY;
int width = tp->width;
int height = tp->height;
GetClientRect(tp->hWnd, &rtClient);
rtClip = rtClient;
rtClip.left += width / 2;
rtClip.right -= width / 2;
rtClip.top += height / 2;
rtClip.bottom -= height / 2;
ptShape.x = Random() % (rtClip.right - width);
ptShape.y = Random() % (rtClip.bottom - height);
vecX = (Random() % 5 + 5) * (Random() % 2 == 0 ? -1 : 1);
vecY = (Random() % 5 + 5) * (Random() % 2 == 0 ? -1 : 1);
ClipPoint(&rtClip, &ptShape, &vecX, &vecY);
//フレームレートの計算
ULONGLONG nowTime, lastTime = 0;
unsigned int frameRate = ceil(1000.0 / 30); //30フレーム
//一時停止用フラグ
BOOL bPause = FALSE;
//メッセージループ
while (1) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
switch (msg.message) {
case WM_SIZE: //ウィンドウサイズ変更
rtClient.right = LOWORD(msg.lParam);
rtClient.bottom = HIWORD(msg.lParam);
rtClip = rtClient;
rtClip.left += width / 2;
rtClip.right -= width / 2;
rtClip.top += height / 2;
rtClip.bottom -= height / 2;
break;
case WM_THREADPAUSE: //一時停止状態の変更
bPause = !bPause;
break;
case WM_QUIT: //スレッドの終了
return (DWORD)msg.wParam;
}
}
if (bPause) { //一時停止中
Sleep(1);
continue;
}
nowTime = GetTickCount64();
if (nowTime - lastTime >= frameRate) {
ptShape.x += vecX;
ptShape.y += vecY;
ClipPoint(&rtClip, &ptShape, &vecX, &vecY);
rtShape.left = ptShape.x - width / 2;
rtShape.right = ptShape.x + width / 2;
rtShape.top = ptShape.y - height / 2;
rtShape.bottom = ptShape.y + height / 2;
SendMessage(tp->hWnd, WM_NEXTFRAME, (WPARAM)&rtShape, 0);
lastTime = nowTime;
}
Sleep(1);
}
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//描画に必要なデータはほとんど
//新スレッド側に移している
static RECT rtShape;
static int width, height;
static DWORD dwThreadID;
static ThreadParam tp;
HANDLE hThread;
HDC hdc;
PAINTSTRUCT ps;
switch (message)
{
case WM_CREATE:
//図形のサイズを定義
width = height = 50;
//スレッドのパラメーター
tp.hWnd = hWnd;
tp.width = width;
tp.height = height;
//新しいスレッドの作成
hThread = (HANDLE)_beginthreadex(
NULL, 0,
(LPTHREAD_START_ROUTINE)ThreadProc,
&tp, 0, &dwThreadID);
if (hThread) {
CloseHandle(hThread);
}
break;
case WM_SIZE: //ウィンドウサイズ変更
//スレッドにそのままポスト
PostThreadMessage(dwThreadID, WM_SIZE, wParam, lParam);
break;
case WM_LBUTTONUP: //マウス左ボタンアップ
//スレッドの(描画処理の)一時停止/停止解除
PostThreadMessage(dwThreadID, WM_THREADPAUSE, 0, 0);
break;
case WM_NEXTFRAME: //フレーム描画要求
rtShape = *((RECT*)wParam);
InvalidateRect(hWnd, NULL, TRUE);
break;
case WM_PAINT: //ウィンドウの描画
hdc = BeginPaint(hWnd, &ps);
SelectObject(hdc, GetStockObject(BLACK_BRUSH));
Ellipse(hdc, rtShape.left, rtShape.top, rtShape.right, rtShape.bottom);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
実行結果は先ほどのサンプルコードと同じですが、描画に必要な計算は全て別スレッドに移しています。
メインスレッドは計算に必要なデータを別スレッドに渡し、結果を受け取り描画しているだけです。
_beginthreadex
関数の最後の引数にDWORD型のポインタを指定して、新しく作成されるスレッドのIDを取得しています。
メッセージはこのIDを使用して送信します。
(スレッドハンドルは使用しないのですぐに閉じています)
WM_SIZE
メッセージでは、サイズ変更をスレッド側に伝えるためにPostThreadMessage
関数でパラメーターをそのまま再送しています。
WM_LBUTTONUP
メッセージでは、スレッドの描画を一時停止するための独自メッセージを送信しています。
(WPARAMとLPARAMは使用しません)
スレッド側では、スレッド作成直後の段階ではメッセージキューが存在しないため、出来るだけ早い段階でPeekMessage
関数を実行してスレッドにメッセージキューを作成しておきます。
この関数実行より前の段階でスレッドにメッセージを送信しても失敗します。
データの初期化処理が終わったらPeekMessage関数(またはGetMessage関数)でメッセージループを作成します。
MSG構造体のmessage
メンバをチェックして、メッセージ毎に処理を振り分けます。
DefWindowProc
関数は使えないので終了のためのメッセージ(今回はWM_QUIT
メッセージ)の処理も自分で定義する必要があります。
新スレッド側からメインスレッドへのメッセージの送信はSendMessage
関数やPostMessage
関数を使用します。
SendMessage関数はメインスレッド側の処理が終わるまで呼び出し元に制御が戻らないのは同じですが、その間も呼び出し元スレッドに送信されるメッセージの処理は行われます。
そのため、ウィンドウの移動等をしてもアニメーションは停止しません。
その他、追加の処理としてクライアント領域を左クリックするとアニメーションを一時停止するようにしています。
スレッドへのメッセージの送信を可能にすると、スレッドの柔軟な制御が可能になります。
今回のサンプルコードでは、ランダム値の生成にrand
関数ではなくrand_s
関数を使用しています。
rand関数はマルチスレッドプログラムで(色々なスレッドから同時に)使用すると問題が発生する可能性があります。
rand_s関数はマルチスレッドプログラムでも問題が発生しないことが保証されるマイクロソフトの独自関数です。
マルチスレッドプログラムで使用しても問題の発生しない関数やコードをスレッドセーフといいます。
rand_s関数を使用するには、コードの先頭に#define _CRT_RAND_S
が必要です。
それでも使用できない場合は#include <stdlib.h>
を追加してみてください。
rand_s関数は引数にunsigned int型のポインタを要求し、そこにランダム値(0~UINT_MAX
までの整数値)を格納します。
関数が成功した場合は0を返し、失敗した場合はエラーコード(errno_t型)を返します。
そのままでは少し使いづらいので今回のコードではrand_s関数の初期化等を行う関数(ラッパー関数)を定義して使用しています。