MFC的一个bug

来源:互联网 发布:纯粮白酒怎么优化口感 编辑:程序博客网 时间:2024/04/29 03:24

FROM:http://bbs.sjtu.edu.cn/bbstcon?board=VC&reid=1067088001

1,问题的提出

(1)Dll的创建
是这样的,手头上有一个ActiveX,它通过一些列的方法实现了一定的功能,然而
接口函数特别多。为了完成一个特定的功能,也许要进行一系

列的方法调用,这特别讨厌。
为了解决这个问题,想了一个方法,就是用一个动态库封装这个ActiveX,这个动
态库导出三个函数,一个是初始化(InitializeDll),另一个

是功能函数(Function),再一个是反初始化函数(UninitializeDll)。
为了封装这个ActiveX,简单的方法就是在这个dll中必须创建一个ActiveX控件,
大家知道,创建一个ActiveX的简单方法,就是在MFC生成的代

码中,通过VC插入一个这个控件,并且新创建一个对话框,然后在对话框资源模板
里面,插入一个这个控件就好了。
haha,我就是这么做的。
于是,我用Appwizard生成了一个MFC Regular Dll,然后在资源中插入了一个对话
框,并且插入了一个控件(为了试验起见,就用WebBrowser2

控件了),然后在InitializeDll函数中,创建这个对话框就可以了,对了,这个
时候的对话框,最好是无模态对话框,我使用了

CDialog::Create来创建非模态对话框。
这样,这个初始化函数就这样了:

extern "C" void WINAPI InitializeDll()
{
    static CTestDialog dlg;
    dlg.Create();
}
其中,
BOOL CTestDialog::Create()
{
    return CDialog::Create(IDD);
}
然后,我们就认为,在初始化函数中,这个对话框,以及其中的控件,都应该创建
成功了,就可以了。

解下来的工作,应该是创建一个测试器,来看看这些代码有没有工作。

(2)Tester的编写
写这种Tester,我喜欢用VC里面所谓的“基于对话框”的程序来做,用Appwizard
生成一个基于对话框的程序,很好,自动在对话框资源里面加

入了两个按钮,我的习惯是处理OK按钮的事件,这样子,这个函数就这样了:
void CTesterDlg::OnOk()
{
    // load library
    HINSTANCE hInst = ::LoadLibrary(_T("Test.dll"));
    if (hInst != NULL)
    {
        // get func
        typedef void (WINAPI* InitFunc)();
        InitFunc init = (InitFunc)::GetProcAddress(hInst,
_T("InitializeDll"));
        if (init != NULL)
            init();
        //::FreeLibrary(hInst);
    }
}
hehe,简单点,先不释放动态库(否则,如果动态库成功创建了,后面你又把动态
库释放了,是要出现错误的,因此,暂时这么着吧)。
好了,可以测试运行了,

(3)测试
运行程序,你发现,那个dll里面的对话框没有显示出来!在确信动态库的
InitializeDll()函数被调用了之后,哦,可能是dll里面的那个对话

框资源的Visible属性没有设置,然后回头设置一下,发现这个对话框,还是没有
显示,难道对话框创建不成功???
有可能哦。
记得对话框里面需要插入ActiveX的时候,需要预先调用一条
AfxEnableControlContainer函数,好像是这样的,查查MSDN,的确是这样的,于


是,不客气,在对话框的InitInstance中加入这条语句,这样子,函数成为这样了

BOOL CTestApp::InitInstance()
{
    ::AfxEnableControlContainer();
    return CWinApp::InitInstance();
}
这里,放心好了,CTestApp()是在DllMain中调用的,肯定在InitializeDll()之前
调用,因此,调用顺序上没有问题。
再次运行,现象依旧!!!问题麻烦了。看来,很多自以为是的东西,其实不然啊


2,调试

(1)首先怀疑这个控件是否有问题
(当然我们知道WebBrowser2控件是没有问题的,不过,如果这个控件不是你熟悉
的空间呢???)
这个好办,我就在我的测试器(Tester)的对话框中插入这个控件,然后再次运行,
看看测试器的对话框是否能成功创建。
测试的结果显示:
乖乖!不但我的测试器对话框成功显示了,而且,那个Dll中的对话框也成功显示
了,包括其中的控件啊!!!一个令人振奋的消息!
然后我把测试器对话框中的WebBrowser2控件去掉,可怜得很,那个dll中的对话框
随之又创建不成功了。
顺便把Dll中的对话框里面的WebBrowser控件去掉,发现这个对话框又能创建了,
可见问题就是出现在动态库的对话框中使用了ActiveX控件导

致的。而ActiveX是没有问题的!

(2)只好跟踪MFC了:首次跟踪
实在是没有办法的办法了。
在dll创建对话框的地方设置一个断点吧,开始逐条语句跟踪,一个函数一个函数
跟踪,哇,很辛苦的,调入堆栈很深的,没有发现什么意外啊

,不过,到是发现了不少有趣的事情:
——MFC在创建窗口的时候,为了维护CWnd和HWND的对应关系,使用了Windows的钩
子技术(HOOK),设置了一个WH_CBT钩子,以监视创建窗口的

消息,并且以此来维护CWnd和HWND的对应关系;
——MFC在创建对话框的时候,首先要检测其中是否包含了ActiveX控件,如果包含
了,还要单独列出来,先创建不包含ActiveX的部分,接着创

建ActiveX控件;
——如果对话框包括了ActiveX,那么就要创建一个ActiveX Container,这也是了
,因为需要一个Container Site,这些东东MFC为我们创建了

,的确方便;涉及到一个类COleControlContainer;还有一个类,怪怪的,
COccManager;
——等等吧,还有一些,不详细列举了;
——还有一个关键的东西,呵呵,就是MFC接管了WM_INITDIALOG消息,并且在其中
来创建对话框中的ActiveX控件,这个是非常重要的线索啊!

疑问:那么我们用ClassWizard不是可以加入消息WM_INITDIALOG的消息响应函数么
?那这条消息到底是MFC还是我们的函数在处理?看看MFC为

我们生成的这条消息的处理函数吧,其实,它不是个消息处理函数,只是一个虚函
数罢了!如下:
函数的声明(Prototype):
    virtual BOOL OnInitDialog();
注意,函数的声明前面,也没有消息前缀:afx_msg,察看消息映射宏,你找不到
类似下面的消息映射:
    ON_WM_INITDIALOG()
注意,没有这样的消息映射,看看msdn中,你发现,其实它只是一个虚函数!只是
该虚函数在MFC的WM_INITDIALOG消息处理中被调用了,这就是

全部。

(3)再次跟踪
再次跟踪,注意,发现问题了,进入到了一个关键的MFC函数:
BOOL CWnd::CreateDlgIndirect(LPCDLGTEMPLATE lpDialogTemplate, CWnd*
pParentWnd, HINSTANCE hInst)
这个函数作了好多事情哦,包括创建对话框的预处理,借用MFC代码中的注释来说
明吧(以下代码摘自MFC):
#ifndef _AFX_NO_OCC_SUPPORT
        // separately create OLE controls in the dialog template
        if (pOccManager != NULL)
        {
            if (!SetOccDialogInfo(&occDialogInfo))
                return FALSE;

            lpDialogTemplate = pOccManager->PreCreateDialog(&occDialogInfo,
                lpDialogTemplate);
        }

        if (lpDialogTemplate == NULL)
            return FALSE;
#endif //!_AFX_NO_OCC_SUPPORT
翻译出来,大概是说,对话框模板中的OLE控件(们,哈哈)要分离出来然后创建
吧。大概PreCreateDialog这个函数就是做这个事情的,跟踪

进去,发现的确是这样的,其中调用了一个函数,如下:
const DLGTEMPLATE* COccManager::PreCreateDialog(_AFX_OCC_DIALOG_INFO*
pDlgInfo,
    const DLGTEMPLATE* pOrigTemplate)
{
    ASSERT(pDlgInfo != NULL);

    pDlgInfo->m_ppOleDlgItems =
        (DLGITEMTEMPLATE**)malloc(sizeof(DLGITEMTEMPLATE*) *
            (DlgTemplateItemCount(pOrigTemplate) + 1));

    if (pDlgInfo->m_ppOleDlgItems == NULL)
        return NULL;

    DLGTEMPLATE* pNewTemplate = SplitDialogTemplate(pOrigTemplate,
        pDlgInfo->m_ppOleDlgItems);
    pDlgInfo->m_pNewTemplate = pNewTemplate;

    return (pNewTemplate != NULL) ? pNewTemplate : pOrigTemplate;
}
其中一个函数名就是了:SplitDialogTemplate,翻译出来,莫非:分割对话框模
板???
分割完了,该处理字体了,借用MFC代码如下:
        // On DBCS systems, also change "MS Sans Serif" or "Helv" to system
font.
        if ((!bSetSysFont) && GetSystemMetrics(SM_DBCSENABLED))
        {
            bSetSysFont = (strFace == _T("MS Shell Dlg") ||
                strFace == _T("MS Sans Serif") || strFace == _T("Helv"));
            if (bSetSysFont && (wSize == 8))
                wSize = 0;
        }

        if (bSetSysFont)
        {
            CDialogTemplate dlgTemp(lpDialogTemplate);
            dlgTemp.SetSystemFont(wSize);
            hTemplate = dlgTemp.Detach();
        }
自己翻译吧。
接着开始设置钩子了,并且开始创建对话框:
        // create modeless dialog
        AfxHookWindowCreate(this);
        hWnd = ::CreateDialogIndirect(hInst, lpDialogTemplate,
            pParentWnd->GetSafeHwnd(), AfxDlgProc);
执行到这里,好像还是正常啊,那么对话框是否创建成功了呢?接着跟踪:
接着这样的代码,也没有问题的:
#ifndef _AFX_NO_OCC_SUPPORT
    if (pOccManager != NULL)
    {
        pOccManager->PostCreateDialog(&occDialogInfo);
        if (hWnd != NULL)
            SetOccDialogInfo(NULL);
    }
#endif //!_AFX_NO_OCC_SUPPORT

    if (!AfxUnhookWindowCreate())
        PostNcDestroy();        // cleanup if Create fails too soon
这里看不出有什么问题,接着,问题出现了:
    // handle EndDialog calls during OnInitDialog
    if (hWnd != NULL && !(m_nFlags & WF_CONTINUEMODAL))
    {
        ::DestroyWindow(hWnd);
        hWnd = NULL;
    }
到这个if这条语句,发现,hwnd的确不是NULL的,怎么?m_nFlags=0x100,而
WF_CONTINUEMODAL=0x10,导致这个判断的结果为TRUE,从而导致

执行了括号中的内容了,而这个内容就是销毁窗口啊。不是吧,我们是在创建窗口
啊,怎么销毁窗口了?看看m_nFlags的值吧,怎么其中没有

WF_CONTINUEMODAL呢?察看前面的代码,发现在设置钩子前面,有这样的语句:
        // setup for modal loop and creation
        m_nModalResult = -1;
        m_nFlags |= WF_CONTINUEMODAL;
这不是使得m_nFlags中包含了WF_CONTINUEMODAL么?怎么执行到后面就没有了?难
道在创建窗口的时候,m_nFlags的值被改变了???

(4)m_nFlags
该查查这个值在哪个函数里面改变了,这个工作,好办,呵呵,我习惯了使用VC的
Find In Files功能了,在MFC的source code中查找这个字符

串,发现总共55行代码中出现了m_nFlags,然后使用VC的Find功能,在output窗口
里面查找WF_CONTINUEMODAL,定位在
D:/MSVS/VC98/MFC/SRC/WINCORE.CPP(3526):     m_nFlags &= ~WF_CONTINUEMODAL;

很有意思啊,这个cpp文件是WinCore,这一行语句是去掉WF_CONTINUEMODAL的,为
什么?定位看看,这个函数是这样的:
void CWnd::EndModalLoop(int nResult)
{
    ASSERT(::IsWindow(m_hWnd));

    // this result will be returned from CWnd::RunModalLoop
    m_nModalResult = nResult;

    // make sure a message goes through to exit the modal loop
    if (m_nFlags & WF_CONTINUEMODAL)
    {
        m_nFlags &= ~WF_CONTINUEMODAL;
        PostMessage(WM_NULL);
    }
}
原来这样子啊,好办,在这个函数中设置一个断点,然后断点处停下的时候,察看
堆栈,发现堆栈如下:
...
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT*
pResult)
LRESULT CDialog::HandleInitDialog(WPARAM, LPARAM)
void CDialog::EndDialog(int nResult)
其中,调用了EndModalLoop这个函数!!!

(5)HandleInitDialog
EndDialog这个函数没什么,关键的问题是,HandleInitDialog这个函数为什么会
调用EndDialog。
HandleInitDialog?莫非就是对话框的WM_INITDIALOG消息处理函数?不管是不是,
反正是起了这个作用了!
看看代码吧:
#ifndef _AFX_NO_OCC_SUPPORT
    // create OLE controls
    COccManager* pOccManager = afxOccManager;
    if ((pOccManager != NULL) && (m_pOccDialogInfo != NULL))
    {
        BOOL bDlgInit;
        if (m_lpDialogInit != NULL)
            bDlgInit = pOccManager->CreateDlgControls(this, m_lpDialogInit,
                m_pOccDialogInfo);
        else
            bDlgInit = pOccManager->CreateDlgControls(this,
m_lpszTemplateName,
                m_pOccDialogInfo);

        if (!bDlgInit)
        {
            TRACE0("Warning: CreateDlgControls failed during dialog
init./n");
            EndDialog(-1);
            return FALSE;
        }
    }
#endif
注意其中的EndDialog,前面是CreateDlgControls,莫非就是创建对话框中的控件
?难道创建WebBrowser2控件不成功?跟踪

CreateDlgControls函数,看怎么回事,这样就到了下面这个函数:
BOOL COccManager::CreateDlgControls(CWnd* pWndParent, void* lpResource,
    _AFX_OCC_DIALOG_INFO* pOccDlgInfo)
这个函数也没什么,其中调用了如下的函数:
    // Create the OLE control now.
    hwNew = CreateDlgControl(pWndParent, hwAfter, bDialogEx,
        pDlgItem, nMsg, (BYTE*)lpnRes, dwLen);
两个函数名,好像就差一个s,注意,这里就是创建OLE控件了,WebBrowser控件,就
应该是这个函数创建吧!跟踪进去,看看,hoho,有了:
    if (SUCCEEDED(hr) &&
        pWndParent->InitControlContainer() &&
        pWndParent->m_pCtrlCont->CreateControl(NULL, clsid, NULL,
pItem->style,
            rect, pItem->id, pMemFile, (nMsg == WM_OCC_LOADFROMSTORAGE),
            bstrLicKey, &pSite))
    {
        ...
    }
InitControlContainer(),这个函数跟踪进去,其实没什么啦,关键就是后面的哪
个函数,再次跟踪进去,就到了
BOOL COleControlContainer::CreateControl( CWnd* pWndCtrl, REFCLSID
clsid,
    LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, UINT nID,
    CFile* pPersist, BOOL bStorage, BSTR bstrLicKey,
    COleControlSite** ppNewSite )
哦,到了COleControlcontainer这个类了,这个函数本身也简单,调用了另一个成
员函数,CreateControl,就是C++中的,重载?多态?分不

清楚,反正就是函数的参数类型,参数数目不同,函数名字相同的那种,到了下面
的函数:
BOOL COleControlContainer::CreateControl(CWnd* pWndCtrl, REFCLSID
clsid,
    LPCTSTR lpszWindowName, DWORD dwStyle, const POINT* ppt, const SIZE*
psize,
       UINT nID, CFile* pPersist, BOOL bStorage, BSTR bstrLicKey,
       COleControlSite** ppNewSite)
{
    COleControlSite* pSite = NULL;

    TRY
    {
        pSite = afxOccManager->CreateSite(this);
    }
    END_TRY

    if (pSite == NULL)
        return FALSE;

    BOOL bCreated = SUCCEEDED( pSite->CreateControl(pWndCtrl, clsid,
        lpszWindowName, dwStyle, ppt, psize, nID, pPersist, bStorage,
      bstrLicKey ) );

    if (bCreated)
    {
        ASSERT(pSite->m_hWnd != NULL);
        m_siteMap.SetAt(pSite->m_hWnd, pSite);
        if (ppNewSite != NULL)
            *ppNewSite = pSite;
    }
    else
    {
        delete pSite;
    }

    return bCreated;
}
这里首先准备一个COleControlSite,然后通过这个Site对象来创建控件,
CreateSite函数很简单,就是new了一个对象,就看pSite->CreateControl了(原来
,创建一个OLE控件这么麻烦!),顺次到了如下的函数:
HRESULT COleControlSite::CreateControl(CWnd* pWndCtrl, REFCLSID clsid,
    LPCTSTR lpszWindowName, DWORD dwStyle, const POINT* ppt, const SIZE*
psize,
    UINT nID, CFile* pPersist, BOOL bStorage, BSTR bstrLicKey)
{
    ...
    // Initialize OLE, if necessary
    _AFX_THREAD_STATE* pState = AfxGetThreadState();
    if (!pState->m_bNeedTerm && !AfxOleInit())
        return hr;

    if (SUCCEEDED(hr = CreateOrLoad(clsid, pPersist, bStorage,
bstrLicKey)))
    {
        ...
    }

    if (SUCCEEDED(hr))
    {
        ...
    }

    return hr;
}
我把代码都删掉了,否则没法看,太长了。
这样看来,就简单了,因为OLE控件,涉及到OLE,就必须初始化OLE,所以,函数
调用了AfxOleInit来初始化OLE(注意,ActiveX本身就是基于

COM的,使用了Automation吧,是OLE对象的一种),
代码执行还好,接着,就执行CreateOrLoad函数了,这个函数执行就不对了!执行
结果,hr = 0x800401f0,这个值是不能让人接受的,使用

ErrorLookup吧,看看这个值,好像不是那么特殊的值啊,怎么意思很特殊呢:

        尚未调用 CoInitialize。

不会吧!我们不是调用了AfxOleInit么?难道这个函数没起作用???

(6)AfxOleInit
这个函数到底怎么工作啊,再次使用Find In Files功能,看看这个函数:
BOOL AFXAPI AfxOleInit()
{
    ...
    if (afxContextIsDLL)
    {
        pState->m_bNeedTerm = -1; // -1 is a special flag
        return TRUE;
    }

    // first, initialize OLE
    SCODE sc = ::OleInitialize(NULL);
    ...
}
哦,问题原来就在这里啊!!!

看看吧,afxContextIsDll表示当前这个模块是否dll,显然,我们讨论的就是dll
,所以这个值是true,自然就执行括号中的语句了,那就是直

接返回true,搞错啊,没进行初始化就返回成功了!后面的OleInitialize根本就
没有执行啊!

(7)修正后再次测试
修正这个错误吧,我看,我们还是别修改MFC了,毕竟MFC好庞大,牵一动全身啊,
修改自己的代码吧,
前面不是说过DLL的InitInstance么?在其中AfxEnableControlContainer前面加上
一条调用:
    OleInitialize(NULL)
BOOL CTestApp::InitInstance()
{
    ::OleInitialize(NULL);
    ::AfxEnableControlContainer();
    return CWinApp::InitInstance();
}

再次运行,OK!!!!!!!!通过了!

至此,调试结束,问题解决。

3,结论
在动态库dll中生成对话框,如果其中包括了ActiveX控件,在创建对话框失败时,
需要考虑在CAPP::InitInstance()中加上如下的语句:
OleInitialize(NULL) & AfxEnableControlContainer()
说起来,很简单哦。

4,解释
下面倒过来解释为什么我们在测试器Tester中插入了WebBrowser2后,Dll不加入
OleInitialize也可以创建带有ActiveX的控件。这个解释好像

就比较简单了,因为测试器在创建WebBrowser2的时候,就调用了OleInitialize来
初始化COM环境(测试器是一个exe,而不是dll,所以前面代

码中AfxOleInit中的所谓afxContextIsDLL就是FALSE了,从而它将调用
OleInitialize),而COM的初始化是针对线程的(而不是模块),恰好

,我们的Dll中的对话框和测试器的对话框都运行在一个线程中,那就是界面线程
,所以就能够创建带有ActiveX的对话框!

5,引申
其实,这个调试过程的确很复杂,然而,如果注意到另外的一些事实,也许不用这
么麻烦,就很容易找到问题所在了,这里要说的是VC在调试

的时候的Output窗口中的Trace信息,我们好像很少关心这些信息(至少我很少关
心MFC的这些信息),这次的调试,其实在Output窗口里面已

经显示了错误的信息,让我们借鉴,复制过来供参考:
...
CoCreateInstance of OLE control {8856F961-340A-11D0-A96B-00C04FD705A2}
failed.
>>> Result code: 0x800401f0
>>> Is the control is properly registered?
Warning: CreateDlgControls failed during dialog init.
...
这里就显示了错误码,0x800401f0,就是COM没有初始化,MFC给出的错误提示包括
:控件创建失败,失败的错误码,翻译过来,发现它在猜测

控件没有注册,却没有怀疑自己的AfxOleInit这个函数没有工作。这却让我对应起
来我们现实生活中的许多人:

问题出来的时候,总怀疑别人的工作出问题了,极少怀疑自己的工作中存在缺陷!
!!