1
votes

The miniframe class CPaneFrameWnd contains bug in smart docking algorithm! This class used in MFC as miniframe for floating panes and can dock its to parent frame dock sites or to tabbed panes. It works fine when all panes can be docked to main frame only, but when panes docked to the child frame in MDI applications this class have a bug. Steps to reproduce the bug:

  1. Undock some pane to float state.
  2. Save docking state in MDI child frame: GetDockingManager()->SaveState(...)
  3. Restore docking state for this MDI child frame: GetDockingManager()->LoadState(...); GetDockingManager()->SetDockState();
  4. And try to dock this pane to same frame side by the mouse.
  5. You can't this. The pane are moved by mouse to frame side but NOT DOCKED.

Bug in CPaneFrameWnd class source. In many places class uses code m_pDockManager != NULL ? m_pDockManager : afxGlobalUtils.GetDockingManager(GetParent()); for access its dockmanager. But in some places of class this code looks like m_pDockManager != NULL ? m_pDockManager : afxGlobalUtils.GetDockingManager(this);. And this is reason of the bug - global function afxGlobalUtils.GetDockingManager() can't get dock manager from this pointer and try to get it from parent window of this pointer. It looks like pManager != NULL ? pManager : GetDockingManager(pWnd->GetParent());. But class CPaneFrameWnd have INLINE NONVIRTUAL GetParent() method that can't be accessed by afxGlobalUtils.GetDockingManager(). So, after some recursions afxGlobalUtils.GetDockingManager() returns dockmanager for MAIN app frame! And of course this dockmanager is not same as docmanager for MDI child frame.

Single right solution is to change all m_pDockManager != NULL ? m_pDockManager : afxGlobalUtils.GetDockingManager(this); to m_pDockManager != NULL ? m_pDockManager : afxGlobalUtils.GetDockingManager(GetParent()); in CPaneFrameWnd source (afxpaneframewnd.cpp file). But this is requires patch to MFC code. And all of us knows how Microsoft lazy. May be somebody knows how to fix this bug in current MFC release ?

1
What gave you the impression that docking to child frames is a supported configuration?Cody Gray
Becouse it is documented by MS23W
"May be somebody knows how to fix this bug in current MFC release ?" - That's an odd question, given that you started that very same paragraph with "Single right solution is ...". If you found a bug, and you want it fixed, you need to talk to the vendor of the product (Microsoft Connect).IInspectable
@IInspectable, yes, you are right. But MS will solve this bug for a long time and probably will not fix it because MFC have minimal priority for MS. So, may be somebody knows how to workaround the bug.23W
I sent the bug request to MS.23W

1 Answers

1
votes

I found the workaround for bug fixing. As mentioned in question, main problem that the mini-frame class CPaneFrameWnd have the m_pDockManager property which is not initialized (it have nullptr value). So, in some situations class CPaneFrameWnd can't find the right dockmanager from parent. Workaround for the bug is force initialize all mini-frames m_pDockManager property. Good place for this is the restoring docking state from registry (step 3 in question).

The sample for right save and load child frame docking state:

// Save docking state for CChildFrame class (inherited from CMDIChildWndEx)
void CChildFrame::SaveBarState(LPCTSTR lpszProfileName) const
{
    const_cast<CChildFrame*>(this)->GetDockingManager()->SaveState(lpszProfileName);

    CObList list;
    const_cast<CChildFrame*>(this)->GetDockingManager()->GetPaneList(list, FALSE, NULL, FALSE);
    if (list.GetCount() > 0) {
        POSITION pos = list.GetTailPosition();
        while (pos != NULL) {
            CMFCToolBar* pToolBar = DYNAMIC_DOWNCAST(CMFCToolBar, list.GetPrev(pos));
            if (pToolBar != nullptr) {
                pToolBar->SaveState(lpszProfileName);
            }
        }
    }
}

// Restore docking state for CChildFrame class (inherited from CMDIChildWndEx)
void CChildFrame::LoadBarState(LPCTSTR lpszProfileName)
{
    CObList list;
    GetDockingManager()->GetPaneList(list, FALSE, NULL, FALSE);
    if (list.GetCount() > 0) {
        POSITION pos = list.GetTailPosition();
        while (pos != NULL) {
            CMFCToolBar* pToolBar = DYNAMIC_DOWNCAST(CMFCToolBar, list.GetPrev(pos));
            if (pToolBar != nullptr) {
                pToolBar->LoadState(lpszProfileName);
            }
        }
    }

    GetDockingManager()->LoadState(lpszProfileName);
    GetDockingManager()->SetDockState();
    GetDockingManager()->ShowDelayShowMiniFrames(TRUE);

    // MFC BUGFIX: force assigning the child frame docking manager to all miniframes.
    for (POSITION pos = GetDockingManager()->GetMiniFrames().GetHeadPosition(); pos != NULL;)
    {
        CWnd* pWndNext = (CWnd*)GetDockingManager()->GetMiniFrames().GetNext(pos);
        if (pWndNext != nullptr && pWndNext->IsKindOf(RUNTIME_CLASS(CPaneFrameWnd))) {
            STATIC_DOWNCAST(CPaneFrameWnd, pWndNext)->SetDockingManager(GetDockingManager());
        }
    }
}

How-to use this code:

// creating child frame and its panes, loading the saved panes docking state.
int CChildFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    bool bRes = TBase::OnCreate(lpCreateStruct) == 0;
    if (bRes)
    {
        // enable docking
        EnableDocking(CBRS_ALIGN_ANY);

        // enable Visual Studio 2005 style docking window behavior
        CDockingManager::SetDockingMode(DT_SMART);

        // Creating toolbar, statusbar and panes. Dock them to default places.
        {
            // ....
        }
    }

    if (bRes) {
       LoadBarState(theApp.GetRegSectionPath(_T("ChildFrame")));
    }
    return bRes ? 0 : 1;
}


// destroy child frame and save panes docking state.
void CChildFrame::OnDestroy()
{
    SaveBarState(theApp.GetRegSectionPath(_T("ChildFrame")));
    TBase::OnDestroy();
}

Full sample source code.