SubclassWindow() method issues in projects base on MFC Feature Pack
The Problem
Trying to paint a background image into client area of a MDI application build in VC++ 6.0 to VC++ 2005 IDE it’s not a difficult task.
In case you need, you can find easily good references. For instance, there are two references from Microsoft (KB129471 and KB103786) and one I prefer: a FAQ wrote by a friend of mine.
Unfortunately things are changing radically in case you’re following the same steps in a Visual C++ IDE that has MFC Feature Pack support. If you’re building from the scratch a VC++ 2008/VC++ 2010 a MDI project that has MFC Feature Pack support and you’re trying to apply sub-classing steps, you will have a big surprise in the moment you’re starting your application in debug mode. Effectively your application will crash in the moment you are trying to call SubclassWindow() in CMainFrame::OnCreate().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CMDIFrameWndEx::OnCreate(lpCreateStruct) == -1) return -1; // ---- code --- // BANG! IN VS 2010 or VS 2008 with MFC Feature Pack //m_wndMDIClient.SubclassWindow(m_hWndMDIClient); // where m_wndMDIClient is an instance of CMDIClientWnd (http://support.microsoft.com/kb/129471) // ------ code ---- return 0; } |
Problem details
Starting with MFC Feature Pack CMDIFrameWndEx is the new CMainFrame’s parent class instead of CMDIFrameWnd and the problem acts inside of Attach() method:
1 2 3 4 5 6 7 8 9 10 11 |
///////////////////////////////////////////////////////////////////////////// // Extra CWnd support for dynamic subclassing of controls BOOL CWnd::SubclassWindow(HWND hWnd) { if (!Attach(hWnd)) // BANG!!! return FALSE; // the rest of the code return TRUE; } |
and the issue appears in the second ASSERT() macro
1 2 3 4 5 6 7 8 9 |
BOOL CWnd::Attach(HWND hWndNew) { ASSERT(m_hWnd == NULL); // only attach once, detach on destroy ASSERT(FromHandlePermanent(hWndNew) == NULL); // must not already be in permanent map // the rest of the code return TRUE; } |
because CWnd::FromHandlePermanent(HWND hWnd) looks up into a permanent handle map and in returns existing CWnd pointer.
1 2 3 4 5 6 7 8 9 10 11 12 |
Wnd* PASCAL CWnd::FromHandlePermanent(HWND hWnd) { CHandleMap* pMap = afxMapHWND(); CWnd* pWnd = NULL; if (pMap != NULL) { // only look in the permanent map - does no allocations pWnd = (CWnd*)pMap->LookupPermanent(hWnd); ASSERT(pWnd == NULL || pWnd->m_hWnd == hWnd); } return pWnd; } |
CHandleMap is the wrapper that implements the mapping mechanism between the pointers of MFC wrapped classes and the Windows object handles. Internally, this class has to dictionaries (m_permanentMap and m_temporaryMap) implemented as CMapPtrToPtr, m_nHandles – the number of handles, m_nOffset – the offset of handles in the object and it has a m_pClass pointer of CRuntimeClass (a run time class associated with all MFC classes).
In case you’re interest in more details, you can find more information here.
We have a pointer to a CHandleMap instance that is assigned with the returned pointer of a handle map returned by afxMapHWND(). The returned pointer pWnd it’s assigned with the result returned by pMap->LookupPermanent(hWnd). LookupPermanet() effectively search into a the permanent hash map for exiting HANDLEs and in our case it find it.
1 2 |
inline CObject* CHandleMap::LookupPermanent(HANDLE h) { return (CObject*)m_permanentMap.GetValueAt((LPVOID)h); } |
where
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void* CMapPtrToPtr::GetValueAt(void* key) const // find value (or return NULL -- NULL values not different as a result) { ENSURE(this); if (m_pHashTable == NULL) return NULL; UINT nHash = HashKey(key) % m_nHashTableSize; // see if it exists CAssoc* pAssoc; for (pAssoc = m_pHashTable[nHash]; pAssoc != NULL; pAssoc = pAssoc->pNext) { if (pAssoc->key == key) return pAssoc->value; } return NULL; } |
If the item having nHash key was found into m_pHashTable then the condition if (pAssoc->key == key) is TRUE because the attribute m_hWndMDIClient of CMDIFrameWnd is used yet.
So, effectively what LookupPermanent() has found in m_permanentMap map is m_hWndMDIClient. And because pMap->SetPermanent(m_hWnd = hWndNew, this) is one of the next call into Attach() method those ASSERTs are a must.
Even if those ASSERT() calls from Attach() are available only in debug mode (because of ASSERT() macro behavior) a release build would not save the situation. Soon or later you’ll get conflicts and the application will crash.
Trying to find where this has happened is not so complicated as long as we take in consider our CMainFrame class it’s derived from CMDIFrameWndEx a class that extends CMDIFrameWnd. If we are looking into CMDIFrameWndEx class implementation (AfxMDIClientAreaWnd.cpp) we will see that into this class SubclassWindow() method it’s called jet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
BOOL CMDIFrameWndEx::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) { if (!CMDIFrameWnd::OnCreateClient(lpcs, pContext)) { return FALSE; } if (m_bDoSubclass) { m_wndClientArea.SubclassWindow(m_hWndMDIClient); // this is it! } return TRUE; } |
Subclassing a CWnd derived instance that has already a mapped HWND item is an error and these ASSERTs try to avoid this from development moment. Having two different CWnd-derived objects with the same HWND is not possible – the only exception is CDC instances that have 2 HWNDs (m_hDC and m_hAttribDC).
Related to my issue, according to Steve Horne from Microsoft, “anything that uses the MFC Feature Pack will be using CMDIFrameWndEx which is a very different beast. It has this feature built it as you’ve found out”.
The worst part is that “If you were able to subclass the Ex client area, you’d probably end up breaking a lot of the FluentUI features.”
The VS 2008 / VS 2010 wizard generates and use a lot of Feature Pack FluendUI items.
A bad solution
An approach might be trying to adapt sub-classing idea directly into CMainFrame class. So, the steps might be:
1 2 3 4 5 6 7 8 9 |
void CMainFrame::OnPaint() { CWnd* pWnd = CWnd::FromHandle(m_hWndMDIClient); // returns a MFC object pointer // for the given handle if it is valid CPaintDC dc(pWnd); // the rest of the code } |
CWnd::FromHandle() acquires a pointer to an MFC object pointer from CHandleMap via afxMapHWND().
1 2 3 4 5 6 7 8 9 10 11 |
BOOL CMainFrame::OnEraseBkgnd(CDC* pDC) { return FALSE; } void CMainFrame::OnSize(UINT nType, int cx, int cy) { CMDIFrameWndEx::OnSize(nType, cx, cy); Invalidate(); } |
At the very first time everything looked nice. But unfortunately I have to admit Steve Horne’s observations. In different situations (most on resizing or moving messages) some of the FluentUI items were not correctly painted (some Ribbon items painting issues – different cases).
So, a better solution is needed.
A good but not perfect solution
In my research, for projects base on MFC Feature Pack, there is no perfect solution for this issue. I mean something similarly with the good solutions that I mentioned in the beginning of this article but acts fine until the first IDE that use MFC Feature Pack.
As we have seen on top trying to subclass a window with an already mapped is not a good idea.
The solution is based on Joseph M. Newcomery’s idea, a well-known book writer and Microsoft Visual C++ MVP. Joe proposes “temporary” remapping only for the case we need – in my case painting actions. For the rest of the action the mapping process inside of framework continues in the classic way. It’s a “gross and ugly” solution but until having a better solution from Microsoft or others I consider it fine for my needs.
1 2 3 4 5 6 7 8 9 10 11 |
BOOL CMDIClientWnd::OnEraseBkgnd(CDC* pDC) { return FALSE; // let OnPaint() to paint, only } void CMDIClientWnd::OnPaint() { CPaintDC dc(this); // effective painting stuff } |
1 2 3 4 5 6 7 8 9 |
BOOL CMainFrame::PreTranslateMessage(MSG* pMsg) { if (WM_PAINT == pMsg->message) { RedrawClientArea(); return FALSE; } return CMDIFrameWndEx::PreTranslateMessage(pMsg); } |
Here is the RedrawClientArea() public method.
1 2 3 4 5 6 7 8 9 |
void CMainFrame::RedrawClientArea() { CMDIClientWnd wnd_cl; wnd_cl.Attach(m_wndClientArea.Detach()); wnd_cl.Invalidate(); wnd_cl.UpdateWindow(); m_wndClientArea.Attach(wnd_cl.Detach()); } |
So we create locally an instance of CMDIClientWnd and we attach it internally to ChandleMap::m_permanetMap via Attach(), not before detaching m_wndClientArea (an CMDIClientAreaWnd instance, attribute in CMDIFrameWndEx and as we have seen before it subclass the CMDIFrameWndEx in CMDIFrameWndEx::OnCreateClient()).
The idea is that our CMDIClientWnd instance temporary replace m_wndClientArea instance of CMDIClientAreaWnd right before effective WM_PAINT message is dispatched via PreTranslateMessage().
1 2 3 4 5 6 |
void CMainFrame::OnSize(UINT nType, int cx, int cy) { CMDIFrameWndEx::OnSize(nType, cx, cy); RedrawClientArea(); // repaint on WM_SIZE } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void CChildFrame::OnMove(int x, int y) { CMDIChildWndEx::OnMove(x, y); CMainFrame *pMainFrame = (CMainFrame *)GetParentFrame(); ASSERT(pMainFrame); pMainFrame->RedrawClientArea(); } void CChildFrame::OnSize(UINT nType, int cx, int cy) { CMDIChildWndEx::OnSize(nType, cx, cy); CMainFrame *pMainFrame = (CMainFrame *)GetParentFrame(); ASSERT(pMainFrame); pMainFrame->RedrawClientArea(); } |
A disadvantage of this approach is that the interest message (WM_PAINT) is not handled inside the class of m_wndClientArea, but the good point is that the rest of the messages are left at the correct class of the framework and will work correctly.
Demo application (2063 downloads)