MFC ダイアログでファイル読込、集計、通信待ちのような重い処理を UI スレッドで実行すると、ウィンドウが「応答なし」に見えます。この記事では、AfxBeginThread でワーカースレッドを起動し、進捗・完了・キャンセルを PostMessage で UI スレッドへ戻す実装手順を確認します。帳票/CSV取り込みのようなバッチ処理を想定し、スレッドから CWnd を直接触らない理由も扱います。
まず確認するもの
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* を渡して、ワーカースレッドから直接メンバ関数を呼ぶ形にしないのが重要です。UI に通知したい先は 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;
CWinThread* pThread = AfxBeginThread(&CMyDialog::WorkerProc, param);
if (pThread == nullptr)
{
delete param;
ShowStartError();
return;
}
m_workerThreadId = pThread->m_nThreadID;
SetRunningUiState();
}
WorkerProcでは重い処理だけを実行する
ワーカースレッドの処理本体では、時間のかかる処理だけを行います。進捗や完了状態は、ユーザー定義メッセージとして UI スレッドへ投げます。通知前に 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)
{
if (::IsWindow(param->notifyHwnd))
{
::PostMessage(param->notifyHwnd, WMU_WORKER_COMPLETE, TRUE, index);
}
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 がまだ固まる | 重い処理が 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は、MFC でワーカースレッドを作る基本手段です。- UI フリーズを防ぐには、時間のかかる処理を UI スレッドから切り離します。
- ワーカースレッドへ渡す情報は、
HWND、キャンセルフラグ、処理件数など最小限にします。 - 進捗・完了・キャンセルの画面更新は、
PostMessageを受けた UI スレッド側で行います。 - ワーカースレッドから
CWndやコントロールを直接操作しないようにします。 - キャンセルは強制終了ではなく、フラグで要求して安全な区切りで抜けます。
- 閉じる処理では、スレッドが残ったまま画面を破棄しないように確認します。
