MFCダイアログでファイル読込や集計、通信待ちのような重い処理をUIスレッドで実行すると、ウィンドウが「応答なし」になります。AfxBeginThreadでワーカースレッドを起動し、進捗・完了・キャンセルをPostMessageでUIスレッドへ戻すと、これを避けられます。帳票やCSVの取り込みのようなバッチ処理を想定したコードです。
まず確認するもの
AfxBeginThreadを使う前に、別スレッドへ移す処理と、UIへ戻す情報を分けておきます。分けておけば、CWnd* をスレッドと共有せずに済みます。
| やりたいこと | 使う手段 | UIへ戻す情報 | 注意点 |
|---|---|---|---|
| 重い処理を裏で進める | AfxBeginThread | 完了件数、現在の処理、完了結果 | 処理本体はUIスレッドから切り離す |
| 進捗バーや処理キューを更新する | ::PostMessage | 現在値、処理中の項目、最大件数 | ワーカースレッドからコントロールを直接操作しない |
| キャンセルを受け付ける | 共有フラグ | 停止済みか、何件まで終わったか | TerminateThreadで強制終了しない |
| 完了後にボタン状態を戻す | ユーザー定義メッセージ | 成功、キャンセル、エラー | UI変更はメッセージハンドラ側で行う |
検証環境と前提
コード例はMFCのダイアログアプリが前提です。ワーカースレッドが重い処理を受け持ち、画面の更新はUIスレッドだけで行います。
| 項目 | 内容 |
|---|---|
| OS | Windows 11 |
| IDE | Visual Studio 2026 |
| プロジェクト | MFCダイアログベースアプリ |
| 文字セット | Unicode |
| MFCの使用 | 共有DLLでMFCを使用 |
| 構成 | Debug / Release、Win32 / x64 |
AfxBeginThreadで作るワーカースレッドの全体像
ワーカースレッドは、UIを持たないバックグラウンド処理です。MFCではAfxBeginThreadにスレッド関数とパラメータを渡すだけで、CWinThreadオブジェクトの作成とスレッド開始を任せられます。
// UI スレッドへ戻すメッセージを決める
constexpr UINT WMU_WORKER_PROGRESS = WM_APP + 1811;
constexpr UINT WMU_WORKER_COMPLETE = WM_APP + 1812;
// AfxBeginThread に渡す関数は、AFX_THREADPROC 相当の形にする
static UINT AFX_CDECL WorkerProc(LPVOID pParam);
UINT AFX_CDECL CMyDialog::WorkerProc(LPVOID pParam)
{
// 重い処理をここで実行する
return 0;
}
// UI 側からスレッドを開始する
CWinThread* pThread = AfxBeginThread(&CMyDialog::WorkerProc, pParam);
if (pThread == nullptr)
{
// 起動失敗時は UI スレッド側で後始末する
}

スレッドへ渡すパラメータは最小限にする
AfxBeginThreadの第2引数で、ワーカースレッドへ渡せる値は1つだけです。HWND、キャンセルフラグ、処理件数などは、構造体にまとめて渡します。
このとき、CDialog* やCWnd* を渡して、ワーカースレッドからメンバ関数を直接呼ぶ形にはしません。通知したい先はHWNDで持っておき、画面の更新は、メッセージを受けたUIスレッドが行います。
struct WorkerParam
{
HWND notifyHwnd;
volatile LONG* cancelRequested;
int totalItems;
};
void CMyDialog::StartWorker()
{
::InterlockedExchange(&m_cancelRequested, 0);
auto* param = new WorkerParam;
param->notifyHwnd = GetSafeHwnd();
param->cancelRequested = &m_cancelRequested;
param->totalItems = kTaskCount;
// m_nThreadID を読むまで止めておく(走らせると終了時に CWinThread が自分を削除する)
CWinThread* pThread = AfxBeginThread(&CMyDialog::WorkerProc, param,
THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
if (pThread == nullptr)
{
delete param;
ShowStartError();
return;
}
m_workerThreadId = pThread->m_nThreadID;
pThread->ResumeThread(); // ここから先は pThread に触らない
SetRunningUiState();
}
WorkerProcでは重い処理だけを実行する
ワーカースレッドの処理本体には、時間のかかる処理だけを書きます。進捗と完了は、ユーザー定義メッセージでUIスレッドへ投げます。通知の前に、IsWindowでHWNDが有効か見ておきます。
#include <memory>
UINT AFX_CDECL CMyDialog::WorkerProc(LPVOID pParam)
{
if (pParam == nullptr)
{
return 1;
}
std::unique_ptr<WorkerParam> param(
static_cast<WorkerParam*>(pParam));
for (int index = 0; index < param->totalItems; ++index)
{
if (::InterlockedCompareExchange(param->cancelRequested, 0, 0) != 0)
{
if (::IsWindow(param->notifyHwnd))
{
::PostMessage(param->notifyHwnd, WMU_WORKER_COMPLETE, TRUE, index);
}
return 0;
}
DoHeavyWorkOneItem(index);
if (::InterlockedCompareExchange(param->cancelRequested, 0, 0) != 0)
{
// この項目は処理し終えているので、完了件数は index + 1
if (::IsWindow(param->notifyHwnd))
{
::PostMessage(param->notifyHwnd, WMU_WORKER_PROGRESS, index + 1, index);
::PostMessage(param->notifyHwnd, WMU_WORKER_COMPLETE, TRUE, index + 1);
}
return 0;
}
if (::IsWindow(param->notifyHwnd))
{
::PostMessage(param->notifyHwnd, WMU_WORKER_PROGRESS, index + 1, index);
}
}
if (::IsWindow(param->notifyHwnd))
{
::PostMessage(param->notifyHwnd, WMU_WORKER_COMPLETE, FALSE, param->totalItems);
}
return 0;
}

UI更新はPostMessageの受信側で行う
MFCオブジェクトは、複数のスレッドから操作しても安全とは限りません。特にCWnd系のオブジェクトは、Windowsハンドルマップがスレッドごとに管理されるため、ワーカースレッドから直接UIを操作すると、更新されない、別のオブジェクトとして扱われる、タイミングによって動きが変わる、といったことが起きます。
BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
ON_MESSAGE(WMU_WORKER_PROGRESS, &CMyDialog::OnWorkerProgress)
ON_MESSAGE(WMU_WORKER_COMPLETE, &CMyDialog::OnWorkerComplete)
END_MESSAGE_MAP()
LRESULT CMyDialog::OnWorkerProgress(WPARAM wParam, LPARAM lParam)
{
const int completedItems = static_cast<int>(wParam);
const int currentIndex = static_cast<int>(lParam);
m_progress.SetRange32(0, kTaskCount);
m_progress.SetPos(completedItems);
SetTaskState(currentIndex, _T("完了"));
CString text;
text.Format(_T("実行中... %d / %d 件完了"), completedItems, kTaskCount);
m_status.SetWindowText(text);
return 0;
}
キャンセルは強制終了ではなく要求として扱う
ワーカースレッドを止めたいときは、強制終了せずに、キャンセル要求を送ります。ファイル、ロック、メモリ確保、外部リソースを扱う処理を途中で強制的に止めると、後始末ができなくなるからです。
void CMyDialog::CancelWorker()
{
if (!m_isWorkerRunning)
{
return;
}
::InterlockedExchange(&m_cancelRequested, 1);
m_status.SetWindowText(_T("キャンセル要求を送信しました..."));
}

閉じる処理ではスレッドの生存を確認する
ダイアログが破棄されたあとにワーカースレッドがPostMessageすると、通知先のHWNDはもう無効になっているかもしれません。そこで、閉じる操作のときに処理中なら、キャンセル要求だけ出して閉じません。完了メッセージを受けたあとで、閉じられる状態へ戻します。
void CMyDialog::OnClose()
{
if (m_isWorkerRunning)
{
CancelWorker();
m_status.SetWindowText(_T("処理中です。キャンセル完了後に閉じてください。"));
return;
}
CDialogEx::OnClose();
}
よくある失敗パターン
| 症状 | よくある原因 | 確認する場所 | 修正方針 |
|---|---|---|---|
| UIがまだ固まる | 重い処理がUIスレッド側に残っている | イベントハンドラ内のループ、待機、I/O | 重い処理をWorkerProc側へ移す |
| 進捗バーが更新されない | メッセージマップに受信側がない | ON_MESSAGEとハンドラ名 | ユーザー定義メッセージを正しく受ける |
| キャンセルが遅い | キャンセル確認の間隔が長い | ループ内の確認位置 | 安全な区切りごとにフラグを見る |
| 完了後にボタンが戻らない | 完了通知がUI側へ届いていない | PostMessageの宛先HWND | 有効なHWNDを渡す |
| たまに不安定になる | ワーカースレッドからCWndを直接操作している | WorkerProc内のUI操作 | PostMessage経由に戻す |
| 終了時に落ちることがある | 処理中の閉じる操作を許している | OnClose、OnCancel | 処理中はキャンセル完了を待つ |
AfxBeginThreadと他の手段の使い分け
| 手段 | 向いている用途 | 避けたい用途 |
|---|---|---|
| AfxBeginThreadのワーカースレッド | 計算、ファイル処理、短〜中時間のバックグラウンド処理 | スレッド内でウィンドウを所有する処理 |
| MFCのUIスレッド | 別スレッドでメッセージループやウィンドウを持つ処理 | 単純な一括処理 |
| std::thread | MFC非依存の処理 | MFCオブジェクトを直接扱う処理 |
| タイマー分割 | 軽い処理を少しずつ進める画面処理 | CPU負荷や待機が大きい処理 |
実装後に見るべき画面状態
処理が完了するかだけでなく、処理中もUIが応答しているかを見ます。処理キューが進んでいても、画面操作やタイマー更新が止まっているなら、重い処理がUIスレッドに残っている可能性があります。UIスレッドIDとワーカースレッドIDが別の値で表示されているかも、あわせて見ます。

まとめ
- AfxBeginThreadで、時間のかかる処理をUIスレッドから切り離します。
- ワーカースレッドへ渡すのは、HWND、キャンセルフラグ、処理件数など最小限にして、CWndやコントロールは直接触りません。
- 進捗・完了・キャンセルの画面更新は、PostMessageを受けたUIスレッドで行います。
- キャンセルは強制終了せず、フラグで要求して、区切りのよい所で抜けます。閉じる操作では、スレッドが残ったまま画面を破棄しないようにします。
進捗や完了の通知をSendMessageで送ると、ワーカースレッドはUIスレッドがそのメッセージを処理し終えるまで戻りません。そのあいだにUIスレッドがワーカーの終了を待っていたり、ワーカーが持っているロックを取りに行ったりすると、両方のスレッドが止まります。どの組み合わせで固まるかは「PostMessage vs SendMessageの決定的違い」で確かめられます。
