【MFC×MDI】タブを閉じたときに「最後に使ったタブ」へ確実に戻す:EnableMDITabsLastActiveActivationの限界とMRU実装・フラッシュ抑止の完全ガイド

タブ付き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 &lt;deque&gt;
#include &lt;algorithm&gt;
#include &lt;commctrl.h&gt;   // SetWindowSubclass, RemoveWindowSubclass

class CMainFrame : public CMDIFrameWndEx
{
    DECLARE_DYNAMIC(CMainFrame)
public:
    std::deque&lt;HWND&gt; 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&lt;CMainFrame*&gt;(dwRefData);
    if (!pMain) return DefSubclassProc(hWnd, uMsg, wParam, lParam);

    switch (uMsg)
    {
    case WM_MDIACTIVATE:
        {
            HWND hActive = reinterpret_cast&lt;HWND&gt;(wParam);
            if (hActive)
            {
                auto& hist = pMain-&gt;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&lt;HWND&gt;(wParam);
            auto& hist = pMain-&gt;m_activationHistory;
            hist.erase(std::remove(hist.begin(), hist.end(), hClosed), hist.end());

            // 次候補(直前に使った子)
            HWND hNext = hist.empty() ? nullptr : hist.front();

            // チラつき抑止:以後の内部切替を覆い隠す
            pMain-&gt;SetRedraw(FALSE);

            // ここでデフォルト処理を実行し、実際に子を破棄させる
            LRESULT lr = DefSubclassProc(hWnd, uMsg, wParam, lParam);

            if (hNext &amp;&amp; ::IsWindow(hNext))
            {
                // 右端タブへの強制選択を抑止
                if (CWnd* pWnd = CWnd::FromHandlePermanent(hNext))
                {
                    if (auto* pChild = DYNAMIC_DOWNCAST(CMDIChildWndEx, pWnd))
                    {
                        if (CMFCTabCtrl* pTab = pChild-&gt;GetRelatedTabGroup())
                        {
                            // 内部ロジックを止めるフラグ
                            pTab-&gt;m_bActivateLastVisibleTab = FALSE;
                        }
                        // 履歴先頭をアクティブ化
                        pChild-&gt;MDIActivate();
                    }
                }
                else
                {
                    // ハンドルから暫定CWndを作ってでもアクティブ化を実行
                    CMDIChildWndEx* pChild =
                        DYNAMIC_DOWNCAST(CMDIChildWndEx, CWnd::FromHandle(hNext));
                    if (pChild) pChild-&gt;MDIActivate();
                }
            }

            // 再描画をまとめて実行(フラッシュ防止)
            pMain-&gt;SetRedraw(TRUE);
            pMain-&gt;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&lt;DWORD_PTR&gt;(this));
    }
    return 0;
}

2. WM_MDIACTIVATEでMRU履歴を更新

アクティブ化されるたびにハンドルを履歴先頭へ移動(既存なら除去してから先頭に追加)します。重複を取り除くことで履歴の一意性を担保します。ここでの処理は軽量なので、std::dequeに対するerase+push_frontで十分です。

case WM_MDIACTIVATE:
{
    HWND hActive = (HWND)wParam;
    auto&amp; hist = pMain-&gt;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&amp; hist = pMain-&gt;m_activationHistory;
    hist.erase(std::remove(hist.begin(), hist.end(), hClosed), hist.end());

    HWND hNext = hist.empty() ? nullptr : hist.front();

    pMain-&gt;SetRedraw(FALSE);
    LRESULT lr = DefSubclassProc(hWnd, uMsg, wParam, lParam);

    if (hNext &amp;&amp; ::IsWindow(hNext)) {
        CMDIChildWndEx* pNext =
            DYNAMIC_DOWNCAST(CMDIChildWndEx, CWnd::FromHandle(hNext));
        if (pNext) {
            if (CMFCTabCtrl* ptc = pNext-&gt;GetRelatedTabGroup()) {
                ptc-&gt;m_bActivateLastVisibleTab = FALSE;
            }
            pNext-&gt;MDIActivate();
        }
    }

    pMain-&gt;SetRedraw(TRUE);
    pMain-&gt;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 &lt;deque&gt;
#include &lt;algorithm&gt;
#include &lt;commctrl.h&gt;

class CMainFrame : public CMDIFrameWndEx
{
public:
    std::deque&lt;HWND&gt; 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&lt;CMainFrame*&gt;(dwRefData);
    if (!pMain) return DefSubclassProc(hWnd, uMsg, wParam, lParam);

    switch (uMsg)
    {
    case WM_MDIACTIVATE:
        if (HWND hActive = (HWND)wParam)
        {
            auto&amp; hist = pMain-&gt;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&amp; hist = pMain-&gt;m_activationHistory;
            hist.erase(std::remove(hist.begin(), hist.end(), hClosed), hist.end());
            HWND hNext = hist.empty() ? nullptr : hist.front();

            pMain-&gt;SetRedraw(FALSE);
            LRESULT lr = DefSubclassProc(hWnd, uMsg, wParam, lParam);

            if (hNext &amp;&amp; ::IsWindow(hNext))
            {
                CMDIChildWndEx* pNext =
                    DYNAMIC_DOWNCAST(CMDIChildWndEx, CWnd::FromHandle(hNext));
                if (pNext)
                {
                    if (CMFCTabCtrl* ptc = pNext-&gt;GetRelatedTabGroup())
                    {
                        ptc-&gt;m_bActivateLastVisibleTab = FALSE;
                    }
                    pNext-&gt;MDIActivate();
                }
            }
            pMain-&gt;SetRedraw(TRUE);
            pMain-&gt;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&lt;DWORD_PTR&gt;(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に基づく復帰が行えます。

確認用:最小再現と試験シナリオ

  1. A, B, C の3タブを開く(アクティブ順に A→B→C)。
  2. Bへ戻る(順は C→B→A になる)。
  3. Bを閉じる。
  4. 期待:Cがアクティブ(直前に操作)。従来:右端=Cでたまたま合うケースもあるが、順次進めるとズレが顕在化。
  5. 続けて C を閉じる。
  6. 期待: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-&gt;SetRedraw(FALSE); }
    ~CRedrawScope() { if (m_pWnd) { m_pWnd-&gt;SetRedraw(TRUE); m_pWnd-&gt;RedrawWindow(nullptr, nullptr,
        RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW); } }
};

// 使い方(WM_MDIDESTROY内など)
{
    CRedrawScope hold(&amp;*pMain);
    // ... 既定処理やMDIActivateを挟む ...
}

本稿のまとめ

  • タブを閉じた後に直前に操作していたタブへ戻らないのは、タブ側/クライアント側の複合ロジックが働くため。
  • MDIクライアントのサブクラス化+MRU履歴の自前管理で、毎回必ずMRUへ戻せる。
  • m_bActivateLastVisibleTab = FALSE と SetRedraw(FALSE) の併用で、フラッシュを確実に抑止。
  • 終了時はサブクラスを外し、必要ならフレーム全体の再描画を止めて静かに終わる。

このアプローチにより、「閉じても直前の文脈へ一瞬で戻れる」という自然な操作感を、MFCのタブ付きMDIでも安定して実現できます。既存プロジェクトへの導入コストは小さく、メンテナンス性も高いので、まずは最小パッチとして組み込んでみてください。


付録:サンプル片(ユーザー提示の要点)

記事冒頭の課題提起にもとづく、核となる3断片を改めて掲載します。

// 1) 履歴構造とサブクラス化(OnCreate)
std::deque&lt;HWND&gt; m_activationHistory;
SetWindowSubclass(m_hWndMDIClient, MDIClientProc, 42,
                  reinterpret_cast&lt;DWORD_PTR&gt;(this));

// 2) WM_MDIACTIVATE で履歴更新
case WM_MDIACTIVATE:
{
    HWND hActive = (HWND)wParam;
    auto&amp; hist = pMain-&gt;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&amp; hist = pMain-&gt;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-&gt;GetRelatedTabGroup())
                ptc-&gt;m_bActivateLastVisibleTab = FALSE;

            pMain-&gt;SetRedraw(FALSE);
            pNext-&gt;MDIActivate();
            pMain-&gt;SetRedraw(TRUE);
            pMain-&gt;RedrawWindow(nullptr, nullptr,
                RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
        }
    }
    // (必要に応じて DefSubclassProc のタイミングは調整)
}
break;

チェックリスト

  • commctrl.h をインクルードしている。
  • SetWindowSubclass / RemoveWindowSubclass がリンクできる(共通コントロール初期化済み)。
  • MDIクライアントのハンドル取得に失敗したときのフォールバック(FindWindowEx など)を持つ。
  • 履歴の HWND は破棄後に除去され、IsWindow 確認を挟んでいる。
  • 終了時はサブクラスを外す(OnClose)。

以上で、タブを閉じた直後に「最後に使った子ウィンドウ」へフォーカスが必ず戻る、かつフラッシュも起きない実装が完成です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次