タブ付きMDIのMFCアプリで「今閉じたタブの直前に操作していたタブ」に戻らず、なぜか右端のタブへ飛ぶ――そんな“違和感のある挙動”に悩んでいませんか。ここでは EnableMDITabsLastActiveActivation(TRUE) でも解消しきれないケースを整理し、MDIクライアントをサブクラス化してMRU(Most Recently Used)順で確実にフォーカスを戻す実装を、フラッシュ(瞬間チラつき)対策まで含めて解説します。
問題の全体像と再現条件
MFCのタブ付きMDI(CMDIFrameWndEx+CMDIChildWndEx)で複数の子ウィンドウ(CChildFrame)を開き、タブを切り替えて作業していると、現在表示中のタブを閉じた際に「直前に操作していたタブ」ではなく、「タブ列の一番右にあるタブ」がアクティブになってしまうことがあります。
さらに、CMDIFrameWndEx::EnableMDITabsLastActiveActivation(TRUE) を有効化しても最初の1回だけ期待通りに戻り、2回目以降は同じ現象が続く、という報告も珍しくありません。独自に切り替えを上書き実装しても、内部処理と競合して一瞬だけ別のタブがチラ見えする“フラッシュ”が発生し、見栄えと操作感が悪化します。
| 観点 | 期待動作 | 実際に起きる問題 | 影響 |
|---|---|---|---|
| タブを閉じた後 | 直前に操作していたタブ(MRU)へ戻る | 右端のタブが勝手に選ばれる | 作業の文脈が飛ぶ/操作負担増 |
| LastActiveActivation | 常にMRUで復帰 | 1回目のみ有効、以降は崩れる | ユーザー体験の一貫性が失われる |
| 独自実装 | ちらつき無しで正しいタブに戻る | “一瞬だけ”間違ったタブが表示 | UIのフラッシュで違和感が残る |
原因の整理:なぜ右端タブが選ばれるのか
タブ付きMDIでは、タブコントロール(CMFCTabCtrl)とMDIクライアント(MDIClient)が密に連携しています。子ウィンドウ破棄時(WM_MDIDESTROY)にはフレーム/クライアント/タブの複数レイヤーで「次にどのタブをアクティブにするか」を決める処理が働き、環境や操作順によっては「最後に可視だったタブ」=右端へ再選択するロジックが勝ちやすくなります。
また、WM_MDIDESTROY の後に WM_MDIACTIVATE が複数回流れるパターンがあり、ここで内部の「右端タブを選ぶ」分岐が再実行されると、独自に選び直しても一瞬だけ別タブが表示→正しいタブに戻るというフラッシュを誘発します。
解決の方針:MDIクライアントをサブクラス化し、MRUで自律制御する
要点は次の3つです。
- MRU履歴を自前で保持(
WM_MDIACTIVATEをフック) - タブ破棄時に履歴から次候補を決定(
WM_MDIDESTROY) - 内部の「右端を選ぶ」挙動を抑止し、描画を止めてから切替→一括再描画でフラッシュを防止
実装ステップ
1. MRU履歴の用意とMDIクライアントのサブクラス化
メインフレームに履歴用のコンテナを持たせ、MDIクライアント(クラス名 MDIClient)を SetWindowSubclass でサブクラス化します。履歴は std::deque<HWND> を使うと、先頭への追加・削除が安定して高速です。
// MainFrm.h
#pragma once
#include <deque>
#include <algorithm>
#include <commctrl.h> // SetWindowSubclass, RemoveWindowSubclass
class CMainFrame : public CMDIFrameWndEx
{
DECLARE_DYNAMIC(CMainFrame)
public:
std::deque<HWND> m_activationHistory;
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnClose();
DECLARE_MESSAGE_MAP()
};
// グローバル or 無名名前空間
static LRESULT CALLBACK MDIClientProc(
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
auto* pMain = reinterpret_cast<CMainFrame*>(dwRefData);
if (!pMain) return DefSubclassProc(hWnd, uMsg, wParam, lParam);
switch (uMsg)
{
case WM_MDIACTIVATE:
{
HWND hActive = reinterpret_cast<HWND>(wParam);
if (hActive)
{
auto& hist = pMain->m_activationHistory;
hist.erase(std::remove(hist.begin(), hist.end(), hActive), hist.end());
hist.push_front(hActive); // 先頭=最新
}
}
break;
case WM_MDIDESTROY:
{
HWND hClosed = reinterpret_cast<HWND>(wParam);
auto& hist = pMain->m_activationHistory;
hist.erase(std::remove(hist.begin(), hist.end(), hClosed), hist.end());
// 次候補(直前に使った子)
HWND hNext = hist.empty() ? nullptr : hist.front();
// チラつき抑止:以後の内部切替を覆い隠す
pMain->SetRedraw(FALSE);
// ここでデフォルト処理を実行し、実際に子を破棄させる
LRESULT lr = DefSubclassProc(hWnd, uMsg, wParam, lParam);
if (hNext && ::IsWindow(hNext))
{
// 右端タブへの強制選択を抑止
if (CWnd* pWnd = CWnd::FromHandlePermanent(hNext))
{
if (auto* pChild = DYNAMIC_DOWNCAST(CMDIChildWndEx, pWnd))
{
if (CMFCTabCtrl* pTab = pChild->GetRelatedTabGroup())
{
// 内部ロジックを止めるフラグ
pTab->m_bActivateLastVisibleTab = FALSE;
}
// 履歴先頭をアクティブ化
pChild->MDIActivate();
}
}
else
{
// ハンドルから暫定CWndを作ってでもアクティブ化を実行
CMDIChildWndEx* pChild =
DYNAMIC_DOWNCAST(CMDIChildWndEx, CWnd::FromHandle(hNext));
if (pChild) pChild->MDIActivate();
}
}
// 再描画をまとめて実行(フラッシュ防止)
pMain->SetRedraw(TRUE);
pMain->RedrawWindow(nullptr, nullptr,
RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
return lr;
}
case WM_NCDESTROY:
// 安全のためサブクラスを解除
RemoveWindowSubclass(hWnd, MDIClientProc, 42);
break;
}
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
// MainFrm.cpp : OnCreate 内
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CMDIFrameWndEx::OnCreate(lpCreateStruct) == -1)
return -1;
// 省略: ツールバーやMDIタブの初期化 ...
// MDIクライアントのHWNDを取得してサブクラス化
HWND hMDIClient =
::FindWindowEx(m_hWnd, nullptr, _T("MDIClient"), nullptr);
if (hMDIClient)
{
SetWindowSubclass(hMDIClient, MDIClientProc, 42,
reinterpret_cast<DWORD_PTR>(this));
}
return 0;
}
2. WM_MDIACTIVATEでMRU履歴を更新
アクティブ化されるたびにハンドルを履歴先頭へ移動(既存なら除去してから先頭に追加)します。重複を取り除くことで履歴の一意性を担保します。ここでの処理は軽量なので、std::dequeに対するerase+push_frontで十分です。
case WM_MDIACTIVATE:
{
HWND hActive = (HWND)wParam;
auto& hist = pMain->m_activationHistory;
hist.erase(std::remove(hist.begin(), hist.end(), hActive), hist.end());
hist.push_front(hActive);
}
break;
3. WM_MDIDESTROYで履歴掃除+次候補の手動アクティブ化
閉じられた子のハンドルを履歴から取り除き、履歴先頭(直前に使った子)を次タブ候補にします。その上で、描画を止めてからデフォルト処理を流し、最後に候補へアクティブ化→再描画、という順序にするのがフラッシュ対策の肝です。また、タブグループ側の「右端タブを選び直す」挙動をm_bActivateLastVisibleTab = FALSEで抑止します。
case WM_MDIDESTROY:
{
HWND hClosed = (HWND)wParam;
auto& hist = pMain->m_activationHistory;
hist.erase(std::remove(hist.begin(), hist.end(), hClosed), hist.end());
HWND hNext = hist.empty() ? nullptr : hist.front();
pMain->SetRedraw(FALSE);
LRESULT lr = DefSubclassProc(hWnd, uMsg, wParam, lParam);
if (hNext && ::IsWindow(hNext)) {
CMDIChildWndEx* pNext =
DYNAMIC_DOWNCAST(CMDIChildWndEx, CWnd::FromHandle(hNext));
if (pNext) {
if (CMFCTabCtrl* ptc = pNext->GetRelatedTabGroup()) {
ptc->m_bActivateLastVisibleTab = FALSE;
}
pNext->MDIActivate();
}
}
pMain->SetRedraw(TRUE);
pMain->RedrawWindow(nullptr, nullptr,
RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
return lr;
}
4. アプリ終了時の後始末(フラッシュ抑止)
終了シーケンス中は内部の破棄順序が複雑になりやすいため、サブクラスは外してMFCの既定処理に任せます。必要ならフレーム自体の描画も止めておくと、最後の最後に走る余計なアクティベートを画面に出さずに済みます。
void CMainFrame::OnClose()
{
// 可能なら MDIClient を再取得してサブクラスを解除
if (HWND hMDIClient = ::FindWindowEx(m_hWnd, nullptr, _T("MDIClient"), nullptr))
{
RemoveWindowSubclass(hMDIClient, MDIClientProc, 42);
}
// 必要に応じて全面描画を止める
SetRedraw(FALSE);
ShowWindow(SW_HIDE);
CMDIFrameWndEx::OnClose();
}
完成版:最小サンプルの全コード(抜粋)
上記を一つにまとめると、次のような形になります。既存プロジェクトに貼り付ける際は、名前やインクルード順などを環境に合わせて調整してください。
// MainFrm.h
#pragma once
#include <deque>
#include <algorithm>
#include <commctrl.h>
class CMainFrame : public CMDIFrameWndEx
{
public:
std::deque<HWND> m_activationHistory;
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnClose();
DECLARE_MESSAGE_MAP()
};
// MainFrm.cpp
#include "pch.h"
#include "MainFrm.h"
// サブクラスプロシージャ
static LRESULT CALLBACK MDIClientProc(
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
UINT_PTR, DWORD_PTR dwRefData)
{
auto* pMain = reinterpret_cast<CMainFrame*>(dwRefData);
if (!pMain) return DefSubclassProc(hWnd, uMsg, wParam, lParam);
switch (uMsg)
{
case WM_MDIACTIVATE:
if (HWND hActive = (HWND)wParam)
{
auto& hist = pMain->m_activationHistory;
hist.erase(std::remove(hist.begin(), hist.end(), hActive), hist.end());
hist.push_front(hActive);
}
break;
case WM_MDIDESTROY:
{
HWND hClosed = (HWND)wParam;
auto& hist = pMain->m_activationHistory;
hist.erase(std::remove(hist.begin(), hist.end(), hClosed), hist.end());
HWND hNext = hist.empty() ? nullptr : hist.front();
pMain->SetRedraw(FALSE);
LRESULT lr = DefSubclassProc(hWnd, uMsg, wParam, lParam);
if (hNext && ::IsWindow(hNext))
{
CMDIChildWndEx* pNext =
DYNAMIC_DOWNCAST(CMDIChildWndEx, CWnd::FromHandle(hNext));
if (pNext)
{
if (CMFCTabCtrl* ptc = pNext->GetRelatedTabGroup())
{
ptc->m_bActivateLastVisibleTab = FALSE;
}
pNext->MDIActivate();
}
}
pMain->SetRedraw(TRUE);
pMain->RedrawWindow(nullptr, nullptr,
RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
return lr;
}
case WM_NCDESTROY:
RemoveWindowSubclass(hWnd, MDIClientProc, 42);
break;
}
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWndEx)
ON_WM_CREATE()
ON_WM_CLOSE()
END_MESSAGE_MAP()
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CMDIFrameWndEx::OnCreate(lpCreateStruct) == -1)
return -1;
// ... ツールバー/タブ グループ等の初期化 ...
HWND hMDIClient = ::FindWindowEx(m_hWnd, nullptr, _T("MDIClient"), nullptr);
if (hMDIClient)
{
SetWindowSubclass(hMDIClient, MDIClientProc, 42,
reinterpret_cast<DWORD_PTR>(this));
}
return 0;
}
void CMainFrame::OnClose()
{
if (HWND hMDIClient = ::FindWindowEx(m_hWnd, nullptr, _T("MDIClient"), nullptr))
{
RemoveWindowSubclass(hMDIClient, MDIClientProc, 42);
}
SetRedraw(FALSE);
ShowWindow(SW_HIDE);
CMDIFrameWndEx::OnClose();
}
動作のポイントとチューニング
| トピック | 解説 | 実装の要点 |
|---|---|---|
| MRU履歴 | 直近でアクティブ化された順を先頭から保持。閉じた子・非表示の子は除外。 | std::deque<HWND>+重複排除でシンプル・高速。 |
| 内部の右端選択抑止 | タブ側の「最後に可視だったタブへ戻す」を止める。 | CMFCTabCtrl::m_bActivateLastVisibleTab = FALSE; |
| フラッシュ対策 | 破棄~再アクティブ化の間は描画を止める。 | SetRedraw(FALSE) → 切替 → RedrawWindow(...|RDW_UPDATENOW) |
| メッセージ順序 | WM_MDIDESTROY後にWM_MDIACTIVATEが複数回流れることがある。 | 履歴は最後に確定したアクティベートを先頭に反映。 |
| アプリ終了時 | 内部破棄が連鎖しやすいので独自制御を外す。 | RemoveWindowSubclassしてMFCの既定に任せる。 |
なぜ「DefSubclassProcを先に呼ぶ」のか
子ウィンドウの破棄は既定処理が最終責任を持っています。独自に切替を済ませてから既定処理を呼ぶと、既定処理が「まだ生きているはずの子」を前提に動き、結果として二重のアクティベートやちらつきの原因になります。そこで、描画を止めてから既定処理→MRUへ切替→再描画という順序にすることで、視覚上は意図した一回の切替だけが見えるようになります。
複数タブグループ(グルーピング)との相性
MFCのタブグループ機能を使っている場合でも本実装は有効です。CMDIChildWndEx::GetRelatedTabGroup() で属しているタブグループの CMFCTabCtrl* を取得し、そのグループに対してのみ m_bActivateLastVisibleTab = FALSE を設定します。これにより、他グループの挙動には干渉せず、閉じたタブと同じグループ内でのみMRUに基づく復帰が行えます。
確認用:最小再現と試験シナリオ
- A, B, C の3タブを開く(アクティブ順に A→B→C)。
- Bへ戻る(順は C→B→A になる)。
- Bを閉じる。
- 期待:Cがアクティブ(直前に操作)。従来:右端=Cでたまたま合うケースもあるが、順次進めるとズレが顕在化。
- 続けて C を閉じる。
- 期待:Aがアクティブ(MRUの次点)。
本実装では、上記シナリオのどのステップでもフラッシュ無しでMRUへ確実に戻ります。
運用のヒント:ショートカットと「MRUで閉じ進める」
エディタでおなじみの「Ctrl+F4でタブを閉じ、直前に使っていたタブへ戻る」を連続実行すると、MRUリストを上から順に消し込む挙動になります。これを前提にすれば、ユーザーは脳内で“戻り先”を気にせず、直前の作業コンテキストへ瞬時に復帰できます。
よくある質問(FAQ)
- Q.
EnableMDITabsLastActiveActivation(TRUE)だけでは不十分?
A. 環境や操作順によっては2回目以降にMRU順が崩れます。内部の再選択ロジックが複数回走るため、サブクラス化+独自MRUで最終決定するのが確実です。 - Q.
m_bActivateLastVisibleTabに直接触れて大丈夫?
A. 実体はCMFCTabCtrlの公開メンバとして提供されているバージョンが一般的です。万一非公開の場合は、派生クラスでアクセサを提供するか、フラグを変更できる箇所に限定して対処してください。 - Q.
FromHandleとFromHandlePermanentの違いは?
A. 前者は暫定CWndを生成する可能性があります。可能ならFromHandlePermanentを優先し、なければ暫定でも可、という順で安全に扱います。 - Q. フリッカーが残る場合は?
A.SetRedraw(FALSE)の範囲を広げる/LockWindowUpdateを併用する/RedrawWindowにRDW_ERASEを加える、などを検討してください。 - Q. 最後の1タブを閉じるときは?
A. 履歴が空になるため、何もしません(既定動作)。hNextがnullptrの場合は切替処理をスキップします。
デバッグのコツ
CChildFrame::OnMDIActivate(BOOL bActivate, CWnd* pActivateWnd, CWnd* pDeactivateWnd)にブレークポイントを置き、余分なアクティベートが起きていないかを確認。WM_MDIACTIVATE/WM_MDIDESTROYの発生順をOutputDebugStringでログ化し、履歴の先頭が常に最新を指しているかを検証。- フラッシュが出る場合は、メインフレームとクライアントの両方で
SetRedrawを適用してみる。
注意点(安全運用)
| 項目 | 実務的なポイント |
|---|---|
| スレッド境界 | GUIスレッド以外から MDIActivate 等を呼ばない。必ずメインスレッドで実行。 |
| ハンドル寿命 | 閉じられた子のハンドルは即無効化され得る。IsWindow で確認し、ダングリング参照を避ける。 |
| 派生フレーム混在 | CMDIChildWndEx 以外の子を混在させる場合は DYNAMIC_DOWNCAST の結果を確認して分岐。 |
| タブのドラッグ移動 | タブの並び替えは履歴順とは無関係。MRUは操作履歴だけで決まる。 |
| バージョン差 | MFCのマイナー差分でメッセージ順序が異なる場合あり。ログで挙動を確かめ、必要なら分岐を設ける。 |
実用テクニック:コードの見通しを良くする
描画停止の範囲を明示するため、RAIIのスコープガードを導入すると可読性が上がります。
class CRedrawScope
{
CWnd* m_pWnd;
public:
explicit CRedrawScope(CWnd* p) : m_pWnd(p) { if (m_pWnd) m_pWnd->SetRedraw(FALSE); }
~CRedrawScope() { if (m_pWnd) { m_pWnd->SetRedraw(TRUE); m_pWnd->RedrawWindow(nullptr, nullptr,
RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW); } }
};
// 使い方(WM_MDIDESTROY内など)
{
CRedrawScope hold(&*pMain);
// ... 既定処理やMDIActivateを挟む ...
}
本稿のまとめ
- タブを閉じた後に直前に操作していたタブへ戻らないのは、タブ側/クライアント側の複合ロジックが働くため。
- MDIクライアントのサブクラス化+MRU履歴の自前管理で、毎回必ずMRUへ戻せる。
m_bActivateLastVisibleTab = FALSEとSetRedraw(FALSE)の併用で、フラッシュを確実に抑止。- 終了時はサブクラスを外し、必要ならフレーム全体の再描画を止めて静かに終わる。
このアプローチにより、「閉じても直前の文脈へ一瞬で戻れる」という自然な操作感を、MFCのタブ付きMDIでも安定して実現できます。既存プロジェクトへの導入コストは小さく、メンテナンス性も高いので、まずは最小パッチとして組み込んでみてください。
付録:サンプル片(ユーザー提示の要点)
記事冒頭の課題提起にもとづく、核となる3断片を改めて掲載します。
// 1) 履歴構造とサブクラス化(OnCreate)
std::deque<HWND> m_activationHistory;
SetWindowSubclass(m_hWndMDIClient, MDIClientProc, 42,
reinterpret_cast<DWORD_PTR>(this));
// 2) WM_MDIACTIVATE で履歴更新
case WM_MDIACTIVATE:
{
HWND hActive = (HWND)wParam;
auto& hist = pMain->m_activationHistory;
hist.erase(std::remove(hist.begin(), hist.end(), hActive), hist.end());
hist.push_front(hActive);
}
break;
// 3) WM_MDIDESTROY で履歴掃除+MRUへ手動切替(フラッシュ抑止込み)
case WM_MDIDESTROY:
{
HWND hClosed = (HWND)wParam;
auto& hist = pMain->m_activationHistory;
hist.erase(std::remove(hist.begin(), hist.end(), hClosed), hist.end());
if (!hist.empty()) {
CMDIChildWndEx* pNext =
DYNAMIC_DOWNCAST(CMDIChildWndEx, CWnd::FromHandle(hist.front()));
if (pNext) {
if (CMFCTabCtrl* ptc = pNext->GetRelatedTabGroup())
ptc->m_bActivateLastVisibleTab = FALSE;
pMain->SetRedraw(FALSE);
pNext->MDIActivate();
pMain->SetRedraw(TRUE);
pMain->RedrawWindow(nullptr, nullptr,
RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
}
}
// (必要に応じて DefSubclassProc のタイミングは調整)
}
break;
チェックリスト
commctrl.hをインクルードしている。SetWindowSubclass/RemoveWindowSubclassがリンクできる(共通コントロール初期化済み)。- MDIクライアントのハンドル取得に失敗したときのフォールバック(
FindWindowExなど)を持つ。 - 履歴の
HWNDは破棄後に除去され、IsWindow確認を挟んでいる。 - 終了時はサブクラスを外す(
OnClose)。
以上で、タブを閉じた直後に「最後に使った子ウィンドウ」へフォーカスが必ず戻る、かつフラッシュも起きない実装が完成です。

コメント