デル株式会社

CFrameWnd 雑記

Last Updated: Jan.16,2007
使用しているコンパイラは、Microsoft Visual C++ 6.0/7.1 です。これ以外のバージョンでは記述と食い違うことがあるかも知れません。

5. MDI をタブ型にする

タブ型の Web ブラウザに人気が出たりしたこともあってか、タブ型化する MDI アプリケーションが増えています。私も MDI の場合、子フレームウィンドウを最大化して利用することが多いので、タブがあるのは子フレームウィンドウの切り替えにとても便利です。

自作の MDI アプリケーションをタブ型化するにあたり、最初に頭に浮かんだのは、CDialogBar を使用して、CTabCtrl を貼り付ける方法です。そこで、CDialogBar や CControlBar のソースを眺めてみると、

afx_msg LRESULT OnSizeParent(WPARAM wParam, LPARAM lParam);

という面白いメッセージ応答関数が見つかりました。以下は引用です。

コントロール バーの配置アルゴリズムは単純です。フレーム ウィンドウは WM_SIZEPARENT メッセージをコントロール バーの範囲に含まれる ID を持つすべての子コントロールに送ります。このメッセージと共に、親のクライアント領域を示す四角形へのポインタが渡されます。このメッセージは Z オーダーで子コントロールへ送られます。子コントロール バーはこの情報に基づいて自分の配置を決定し、その分だけ親ウィンドウのクライアント領域を縮小します。最終的に通常のクライアント領域として残された部分 (コントロール バーを除いた部分)を表す四角形に、メイン クライアント ウィンドウ (MDI クライアント、ビュー、分割ウィンドウなど)が表示されます。(MFC ライブラリ リファレンス「テクニカル ノート 31: コントロール バー」より)

これを読むと、CDialogBar や CControlBar の派生クラスでなくても MDI フレームウィンドウの子でさえあれば、このメッセージを受け取って、サイズや位置を決めることができることになります。そこで、CTabCtrl から直接派生させた CMDITabCtrl を作成することにしました。

OnSizeParent の定義

WM_SIZEPARENT に応答する OnSizeParent の定義です。

// *** WM_SIZEPARENT に応答 ***
LRESULT CMDITabCtrl::OnSizeParent(WPARAM, LPARAM lParam)
{
    if (GetItemCount() <= 0) return 0;
    AFX_SIZEPARENTPARAMS* lpLayout = (AFX_SIZEPARENTPARAMS*)lParam;
    CRect rc;
    rc.CopyRect(&lpLayout->rect);
    AdjustRect(FALSE, &lpLayout->rect);
    if (lpLayout->hDWP != NULL)
        AfxRepositionWindow(lpLayout, m_hWnd, &rc);
    return 0;
}

送られてきた AFX_SIZEPARENTPARAMS 構造体の rect が MDI フレームウィンドウのクライアント領域です。そのすべてを自分の領域として、新たに自分の領域内に(くりぬくように)クライアント領域を指定して AfxRepositionWindow を呼び出すだけです。実際のウィンドウのサイズと位置の変更はこの関数から行われます。

構築用関数の定義

構築にデフォルトの値が適用されるようにした、便利な Create 関数を作っておきます。

// *** デフォルトでの構築 ***
BOOL CMDITabCtrl::Create(CMDIFrameWnd* pParentWnd, UINT nID) 
{
    ASSERT(pParentWnd->IsKindOf(RUNTIME_CLASS(CMDIFrameWnd)));
    DWORD dwStyle = TCS_FOCUSNEVER|TCS_HOTTRACK;
    CRect rect;
    rect.SetRectEmpty();
    if (!CTabCtrl::Create(dwStyle, rect, pParentWnd, nID))
        return FALSE;
    ModifyStyleEx(0, WS_EX_STATICEDGE);
    SendMessage(WM_SETFONT, (WPARAM)::GetStockObject(DEFAULT_GUI_FONT), 0);
    return TRUE;
}

// *** 必ず指定されなければならないスタイルの追加 ***
BOOL CMDITabCtrl::PreCreateWindow(CREATESTRUCT& cs)
{
    cs.style |= (WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS);
    return CTabCtrl::PreCreateWindow(cs);
}

実際に CMainFrame に登録して使ってみましょう。

class CMainFrame : public CMDIFrameWnd
{
public:
    CMDITabCtrl m_tabCtrl;

    ...

CMDITabCtrl::Create は CMainFrame::OnCreate で行いますが、CMDITabCtrl はコントロールバーと違って、領域取りが特殊なので、他のすべての子ウィンドウ(ツールバー、ステータスバー、ダイアログバーなど)が構築された後で行わなければいけません。2 番目の引数 ID_WND_MDITABCTRL は、独自に定義したリソース ID です。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ...

    m_tabCtrl.Create(this, ID_WND_MDITABCTRL);
    return 0;
}

この ID の値設定については、以下の引用を参考にして下さい。

コントロール バー オブジェクトおよびウィンドウは、フレーム ウィンドウの子ウィンドウになります。通常、コントロール バー オブジェクトおよびウィンドウは、フレームのクライアント領域(MDI クライアントやビュー)の兄弟オブジェクトとして作成されます。コントロール バーの子ウィンドウ ID 値を指定するときは注意が必要です。コントロール バーが既定の配置で動作するのは、コントロール バーの ID が AFX_IDW_CONTROLBAR_FIRST から AFX_IDW_CONTROLBAR_LAST の範囲に含まれている場合だけです。コントロール バー ID の取り得る値は 256 個ありますが、このうち最初の 32 個は印刷プレビュー処理で使用される特殊な ID です。(MFC ライブラリ リファレンス「テクニカル ノート 31: コントロール バー」より)

この段階でビルドして実行してもタブアイテムがないのでタブは表示されません。その理由は、OnSizeParent の最初でタブアイテムがないときに処理をスキップしているからです。ためしにこの行をコメントアウトしてみるとちゃんと表示されることを確認できます。

タブアイテムの挿入と削除

タブは子フレームウィンドウである CMDIChildWnd の切り替えに使いたいのですから、必要な情報は、

  1. CMDIChildWnd へのポインタ
  2. 表示するタイトル文字列

であり、また、情報を更新するタイミングは、

  1. 新規に構築されたとき(アクティブ)
  2. アクティブになったとき
  3. 「名前を付けて保存」などでタイトルが変更されたとき(アクティブ)
  4. 消滅したとき

です。例によって CMDIChildWnd のソースを眺めます。

virtual void OnUpdateFrameTitle(BOOL bAddToTitle);

お誂え向きの仮想関数がありました。この関数は CMDIFrameWnd に対してタイトルの更新を知らせるものです。これをオーバライドすれば上記の 1〜3 については解決しそうです。のこりの 4 はデストラクタか OnDestroy あたりで処理することにします。ここで、CMDITabCtrl::UpdateItems は第 1 引数に CMDIChildWnd へのポインタ、第 2 引数に追加・更新か削除かを示す真偽値をとるものとします。

CChildFrame::~CChildFrame()
{
    // *** 削除 ***
    ((CMainFrame*)AfxGetMainWnd())->m_tabCtrl.UpdateItems(this, FALSE);
}

void CChildFrame::OnUpdateFrameTitle(BOOL bAddToTitle)
{
    // *** 追加と更新***
    ((CMainFrame*)AfxGetMainWnd())->m_tabCtrl.UpdateItems(this, bAddToTitle);
    CMDIChildWnd::OnUpdateFrameTitle(bAddToTitle);
}

CMDIChildWnd のポインタがあり、アクティブな状態であればドキュメントのタイトルも取得できますので、それを踏まえて CMDITabCtrl::UpdateItems を定義します。

// *** タブアイテムを探す ***
int CMDITabCtrl::FindItem(LPARAM lParam) const
{
    TCITEM item;
    item.mask = TCIF_PARAM;
    int nCount = GetItemCount();
    for (int i = 0; i < nCount; i++)
    {
        GetItem(i, &item);
        if (item.lParam == lParam)
            return i;
    }
    return -1;
}

// *** タブアイテムの更新(bAdd が真なら追加か更新、偽で削除) ***
void CMDITabCtrl::UpdateItems(CMDIChildWnd* pChild, BOOL bAdd)
{
    ASSERT(GetParent()->IsKindOf(RUNTIME_CLASS(CMDIFrameWnd)));

    CMDIFrameWnd* pFrameWnd = (CMDIFrameWnd*)GetParent();
    int nItem = FindItem((LPARAM)pChild);
    if (bAdd)
    {
        CDocument* pDoc = pChild->GetActiveDocument();
        if (pDoc)
        {
            CString s = pDoc->GetTitle();
            TCITEM item;
            item.mask = TCIF_PARAM|TCIF_TEXT;
            item.lParam = (LPARAM)pChild;
            item.pszText = s.GetBuffer(s.GetLength());
            if (nItem == -1) // *** 新規 ***
                nItem = InsertItem(GetItemCount(), &item);
            else // *** 既存 ***
                SetItem(nItem, &item);
            s.ReleaseBuffer();
            SetCurSel(nItem);
            if (GetItemCount() == 1)
                pFrameWnd->RecalcLayout();
        }
    }
    else if (nItem != -1) // *** 削除 ***
    {
        DeleteItem(nItem);
        if (GetItemCount() == 0)
            pFrameWnd->RecalcLayout();
    }
}

今回は、子フレームウィンドウが存在しないときはタブを表示せずにクライアント領域に割り当てる仕様としたため、タブアイテムが 0 か 1 の場合に CMDIFrameWnd::RecalcLayout を呼び出します。そうすると、WM_SIZEPARENT メッセージが飛んでくるので CMDITabCtrl::OnSizeParent が実行され、領域の再確保が行われます。

タブアイテムの切り替え

最後にタブアイテムをクリックしたときにアクティブな子フレームウィンドウを切り替える処理を追加します。

void CMDITabCtrl::OnSelChange(NMHDR* pNMHDR, LRESULT* pResult) 
{
    TCITEM item;
    item.mask = TCIF_PARAM;
    GetItem(GetCurSel(), &item);
    CMDIChildWnd* pChild = (CMDIChildWnd*)item.lParam;
    if (pChild) pChild->MDIActivate();
    *pResult = 0;
}

これで完成です。

機能の追加

ソースがシンプルなので、機能を追加するのも比較的簡単です。例としてタブアイテムを右クリックするとシステムメニューが表示されるようにしてみます。

// *** コンテキスト メニュー ***
void CMDITabCtrl::OnContextMenu(CWnd* /*pWnd*/, CPoint point)
{
    TCHITTESTINFO hit;
    hit.pt = point;
    ScreenToClient(&hit.pt);
    int i = HitTest(&hit);
    if (i != -1) 
    {
        TCITEM item;
        item.mask = TCIF_PARAM;
        GetItem(i, &item);
        CMDIChildWnd* pChild = (CMDIChildWnd*)item.lParam;
        if (pChild)
        {
            pChild->MDIActivate();
            CMenu* pMenu = pChild->GetSystemMenu(FALSE);
            if (pMenu)
            {
                if (pChild->IsZoomed())
                {
                    pMenu->EnableMenuItem(SC_RESTORE, MF_BYCOMMAND|MF_ENABLED);
                    pMenu->EnableMenuItem(SC_MOVE, MF_BYCOMMAND|MF_GRAYED);
                    pMenu->EnableMenuItem(SC_SIZE, MF_BYCOMMAND|MF_GRAYED);
                    pMenu->EnableMenuItem(SC_MINIMIZE, MF_BYCOMMAND|MF_ENABLED);
                    pMenu->EnableMenuItem(SC_MAXIMIZE, MF_BYCOMMAND|MF_GRAYED);
                }
                else if (pChild->IsIconic())
                {
                    pMenu->EnableMenuItem(SC_RESTORE, MF_BYCOMMAND|MF_ENABLED);
                    pMenu->EnableMenuItem(SC_MOVE, MF_BYCOMMAND|MF_ENABLED);  
                    pMenu->EnableMenuItem(SC_SIZE, MF_BYCOMMAND|MF_GRAYED);
                    pMenu->EnableMenuItem(SC_MINIMIZE, MF_BYCOMMAND|MF_GRAYED);
                    pMenu->EnableMenuItem(SC_MAXIMIZE, MF_BYCOMMAND|MF_ENABLED);
                }
                else
                {
                    pMenu->EnableMenuItem(SC_RESTORE, MF_BYCOMMAND|MF_GRAYED);
                    pMenu->EnableMenuItem(SC_MOVE, MF_BYCOMMAND|MF_ENABLED); 
                    pMenu->EnableMenuItem(SC_SIZE, MF_BYCOMMAND|MF_ENABLED);
                    pMenu->EnableMenuItem(SC_MINIMIZE, MF_BYCOMMAND|MF_ENABLED);
                    pMenu->EnableMenuItem(SC_MAXIMIZE, MF_BYCOMMAND|MF_ENABLED);
                }
                UINT nCmd = pMenu->TrackPopupMenu(
                    TPM_RETURNCMD|TPM_LEFTALIGN |TPM_RIGHTBUTTON,
                    point.x, point.y, this);
                pChild->SendMessage(WM_SYSCOMMAND, nCmd);
            }
        }
    }
}

Back Copyright (c) 2007 AOK. All Rights Reserved.