Chinaunix首页 | 论坛 | 博客
  • 博客访问: 310389
  • 博文数量: 59
  • 博客积分: 1410
  • 博客等级: 上尉
  • 技术积分: 570
  • 用 户 组: 普通用户
  • 注册时间: 2009-03-21 09:31
文章分类

全部博文(59)

文章存档

2011年(1)

2009年(58)

我的朋友

分类: LINUX

2009-05-26 23:30:17

本文的写作对象:
  本文主要针对编写过1、2个Windows程序,对C++比较熟悉,了解SDK程序设计的基本知识,同时对MFC运行方式感到困惑的MFC初学者。
 
序、产生

   在MFC程序设计的学习过程中最令人感到难受,甚至于有时会动摇学习者信心的就是一种对于程序的一切细节都没有控制权的感觉,而这种感觉的出现会使大家 认为自己离开了书本上的例子就无法设计编制程序。事实上这是在MFC学习过程中经常出现的一种正常现象。产生这种情况的原因就是MFC最大的优点——封 装。

 

  众所周知,MFC将Windows程序设计中的一些 重复、冗缀的部分进行了封装,使得在所有程序中都必然会出现的变化不大的部分不再由程序员手动输入,而是使用程序自动生成,程序员则可以集中精力于程序的 核心部分。但是这种封装实在非常的严密以至于对于初学者来说已经完全看不到程序的入口处了,由此必然会导致对程序没有一个整体的感觉,对习惯了面向过程而 不是面向对象的程序员来说更是如此。为了解决这种情况,本文将从MFC Framework的内部将程序的整体运行流程拆出,展示给读者来看,并希望这样做能有利于大家在MFC程序设计上更加得心应手。

 
第一章、应用程序的初生和结束
第一节、对比
  首先我们来看看古老的SDK程序是怎样开始的,为此请观察下面一段代码。
 
#include

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT ("HelloWin") ;
    HWND hwnd ;
    MSG msg ;
    WNDCLASS wndclass ;     

    wndclass.style = CS_HREDRAW | CS_VREDRAW ;
    wndclass.lpfnWndProc = WndProc ;
    wndclass.cbClsExtra = 0 ;
    wndclass.cbWndExtra = 0 ;
    wndclass.hInstance = hInstance ;
    wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
    wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
    wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
    wndclass.lpszMenuName = NULL ;
    wndclass.lpszClassName = szAppName ;

    if (!RegisterClass (&wndclass))    //注册窗口类
    {
        MessageBox (NULL, TEXT ("This program requires Windows NT!"),
        szAppName, MB_ICONERROR) ;
        return 0 ;
    }

    hwnd = CreateWindow (szAppName,   //窗口类名称
              TEXT ("The Hello Program"),   //窗口标题
              WS_OVERLAPPEDWINDOW,   //窗口类型
              CW_USEDEFAULT,   //初始X坐标
              CW_USEDEFAULT,   //初始Y坐标
              CW_USEDEFAULT,   //窗口宽度
              CW_USEDEFAULT,   //窗口高度
              NULL,   //父窗口句柄
              NULL,   //窗口菜单句柄
              hInstance,   //程序实例句柄
              NULL) ;   //创建参数

    ShowWindow (hwnd, iCmdShow) ;    //显示窗口
    UpdateWindow (hwnd) ;    //刷新窗口

    while (GetMessage (&msg, NULL, 0, 0))    //消息循环
    {
        TranslateMessage (&msg) ;
        DispatchMessage (&msg) ;
    }
    return msg.wParam ;
}

   上面的代码对于见过SDK源程序的程序员来说是并不陌生的,它们摘自Petzold的《Programming Windows》第三章的例程。这段代码说明了绝大多数Windows程序的开始方式,它在所有的Windows程序种基本都存在,并且区别不大,所以它 们成为了MFC的封装对象。但是MFC在对程序的初始代码进行封装时并不是什么都没做,事实上MFC所作的变动比任何人想象的都要多。下面我们就一点一点 的将这些被MFC特意隐藏起来的代码挖掘出来,因为这对于我们了解自己的程序是很有好处的。
 

   打开任一个MFC工程,使用Class View都可以看到一个Globals组,该组中用来存储程序所用到的全局变量和全局函数。当我们新建一个MFC工程后该组中都会被自动添加一个 theApp对象,虽然很少有人会对它进行操作,但对于一个MFC程序来说它拥有相当于WinMain的地位,确切的说它对程序起引发的作用。下面我们就 从对theApp的探讨来开始我们的挖掘工作。


(P.S:由于篇幅所限我无法向大家提供完整的例程,但各位可以使用VC++ 6.0的AppWizard生成一个单窗口MFC程序,由于程序的初始化代码对所有程序都具有同一性,所以我们可以使用各自生成的代码进行讨论)
 
第二节、基础

  theApp是一个CWinApp类的对象,CWinApp是CWinThread的子类,CWinThread又是CCmdTarget的子类,CCmdTarget则是直接派生自CObject这个几乎所有MFC类的老祖宗的。

(P.S:这里列出CWinApp的所有父类对于当前所谈到的程序初始化工作并没有太大的用处,读者现在也不必对此深究,但对于后面的涉及消息映射和命令传递的章节来说却是极为重要的,因此现在提前将他们列出来以便将来谈到时不会让大家感到突兀。)
 

   仔细观察SDK版的程序源码,我们可以看到一个Windows程序从WinMain函数开始,经过注册窗口类、创建窗口、显示和刷新窗口才使得该程序的 窗口界面为用户可见,而用户对此界面所作的任何操作都会被Windows作为消息传递给程序的窗口函数,并由窗口函数对消息进行分类处理,这些工作都是被 WinMain函数独自包办的。但在MFC程序中WinMain函数的地位被CWinApp类取代了,它所负责的全部初始化工作和对消息解释及分派都有 CWinApp类的内部函数来完成,但是WinMain仍然存在,并且扮演着驾驭CWinApp的角色。CWinApp中几个最重要的函数如下:

virtual BOOL InitApplication();

virtual BOOL InitInstance();
virtual int Run();
 
   那么在SDK程序设计中至关重要的主窗口句柄(就是那个hwnd,几乎程序所有有关窗口的操作都必须用到该句柄,它为Windows定位所要输出的信息 的目的窗口)跑到哪里去了呢?它被存储在CWinThread中名为m_MainWnd的成员变量中,而CWinThread是CWinApp的父类。
 
  再来说MFC对SDK程序中的窗口函数所进行的变化。原来的WndProc窗口函数与WinMain一样被变形后由单独生成的类进行了替代,替代它的是名为CFrameWnd的类,该类一般因程序不同而被继承为不同的模样,比较有代表性的一般形态如下:
class CMyFrameWnd : public CFrameWnd
{
public:
    CMyFrameWND();
    afx_msg void OnPaint();
    afx_msg boid OnAbout();
    DECLARE_MESSAGE_MAP();
};
   虽然每一条消息都有很明显与之对应的函数进行处理,但却令人无法看出MFC是如何实现对于一条消息调用相应的函数的,要知道已经不再存在一个使用 switch...case 格式的窗口函数负责函数的调用了。事实上MFC在此使用了消息映射机制,类定义的最后一行“DECLARE_MESSAGE_MAP();”就是一个属于 消息映射过程的宏,在以后的章节中我们将详细介绍这种消息映射机制,不过在此还是希望大家认识一下其中使用的一些宏,这是你的每一个MFC程序中都必然存 在的代码:
BEGIN_MESSAGE_MAP(CNyFrameWnd,CFrameWnd)
    ON_WM_PAINT()
    ON_COMMAND(IDM_ABOUT,OnAbout)
EBD_MESSAGE_MAP()
第三节、步进
1.必备知识
  MFC对原SDK程序初始码进行的基本封装正如上所述,现在让我们一步一步的看一看程序启动的过程,为了能够对这一过程中的一些对象的使用并不感到莫名其妙,我们有必要先弄清楚对象的几种生存方式及其存活期。以下摘自侯俊杰先生的《深入浅出MFC》第二章,略作改动:
      第1种是在堆栈(stack)中产生,例如:
void MyFunc()
{
    CFoo foo;    //在堆栈(stack)中产生foo对象
    ...    //other segments
}
  这样的对象经常是一个局部对象,如同局部变量一样,其存活期为从对象诞生到程序流程将离开该对象活动范围时(一般为离开该对象所处的子函数时)。
  第2种是在堆(heap)中产生,例如:
void MyFunc()
{
    ...    //other segments
    CFoo* pFoo=new CFoo();    //在堆(heap)中产生对象
}
  这样的对象一般都是使用new进行分配空间,因此其存活期为从调用new分配内存空间开始直到使用delete释放空间(如果使用malloc,对应的就是free)。
  第3种是产生一个全局对象同时也必然是一个静态对象,例如:
CFoo foo;    //在任何函数范围之外作此操作
  其存活期为程序一开始(比Main或WinMain还要早)即存在,直到最后程序退出之前(比一切收尾工作更晚,全局对象在程序中是最后被释放的)才销毁。

(P.S: 上述全局变量的初始化工作由一些名为startup的代码来完成,它们是由编译器提供并链接到程序中的。在编译时由编译器维护一个存有程序中所使用的全部 静态对象的链表,程序开始执行时由startup码段负责遍历此链表并根据表内数据指针调用所指定的构造函数及其参数,完成后才将控制权转交给Main或 WinMain函数。)
  第4种是产生一个局部静态对象,例如:
void MyFunc()
{
    static CFoo foo;    //在函数范围(scope)之内的一个静态对象
    ...    //other segments
}
  类似静态局部变量,其存活期从程序第一次运行到其声明处开始直到程序退出,但比全局对象要早一步被释放。
 
2.theApp
  有了上面的准备我们再来看MFC的程序初始化代码就可以捕捉到一些头绪了。首先我们知道theApp是一个全局对象,因此它应该比WinMain更早被创建,那么在它的构造函数中都进行了一些什么操作呢?下面是一段AppCore.cpp文件中的代码:
CWinApp::CWinApp(LPCTSTR lpszAppName)
{
    m_pszAppName=lpszAppName;
    //initialize CWinThread state
    AFX_MODULE_THREAD_STATE* pThreadState== _AFX_CMDTARGET_GETSTATE();
    AFX_MODULE_THREAD_STATE* pThreadState = pModuleState->m_thread;
    ASSERT(AfxGetThread() == NULL);
    pThreadState->m_pCurrentWinThread = this;
    ASSERT(AfxGetThread() == this);
    m_hThread = ::GetCurrentThread();
    m_nThreadID = ::GetCurrentThreadId();

    // initialize CWinApp state
    ASSERT(afxCurrentWinApp == NULL); // only one CWinApp object please
    pModuleState->m_pCurrentWinApp = this;
    ASSERT(AfxGetApp() == this);

    // in non-running state until WinMain
    m_hInstance = NULL;
    m_pszHelpFilePath = NULL;
    m_pszProfileName = NULL;
    m_pszRegistryKey = NULL;
    m_pszExeName = NULL;
    m_pRecentFileList = NULL;
    m_pDocManager = NULL;
    m_atomApp = m_atomSystemTopic = NULL;
    m_lpCmdLine = NULL;
    m_pCmdInfo = NULL;

    ...
}
  可见大量的CWinApp之中的成员变量由于theApp的诞生而进行了配置或被赋初值,因此我们完全可以说theApp这个Application object是整个程序的引爆器。
 
3.封装后的WinMain
   theApp配置完成之后,操作权应该转交给我们所熟知的WinMain函数,但现在一个大问题是我们并不知道WinMain是否还存在,如果存在有隐 藏在哪里呢?虽然并不是很重要的问题,但找不到它总会令人对程序由一种摸不着头脑的感觉,就好像DOS程序找不到main一样。不用担心,让我们仔细看一 看VC++ 6.0的安装目录下是不是隐藏了些什么?在..\Microsoft Visual Studio\VC98\MFC\SRC目录下我们可以找到一个名为WinMain.cpp的文件,这是否是WinMain的藏身之所呢?打开此文件之 后,我们将看到如下一段代码:
int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE                      hPrevInstance,LPTSTR lpCmdLine, int nCmdShow)
{
    ASSERT(hPrevInstance == NULL);

    int nReturnCode = -1;
    CWinThread* pThread = AfxGetThread();
    CWinApp* pApp = AfxGetApp();

    // AFX internal initialization
    if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine,         nCmdShow))
    goto InitFailure;

    // App global initializations (rare)
    if (pApp != NULL && !pApp->InitApplication())
    goto InitFailure;

    // Perform specific initializations
    if (!pThread->InitInstance())
    {
        if (pThread->m_pMainWnd != NULL)
        {
            TRACE0("Warning: Destroying non-NULL m_pMainWnd\n");
            pThread->m_pMainWnd->DestroyWindow();
        }
        nReturnCode = pThread->ExitInstance();
        goto InitFailure;
    }
    nReturnCode = pThread->Run();

    InitFailure:
    #ifdef _DEBUG
    // Check for missing AfxLockTempMap calls
    if (AfxGetModuleThreadState()->m_nTempMapLock != 0)
    {
        TRACE1("Warning: Temp map lock count non-zero (%ld).\n",
        AfxGetModuleThreadState()->m_nTempMapLock);
    }
    AfxLockTempMaps();
    AfxUnlockTempMaps(-1);
    #endif

    AfxWinTerm();
    return nReturnCode;

}
  果然,这就是MFC保存WinMain的地方,那么我们就马上开始分析分析这个由MFC直接提供的WinMain都作了些什么工作吧。通过整理,我们可以挑出标为红色的地方作为认真研究的对象。
  首先,AfxGetApp是一个全局函数,其定义在AFXWIN1.INL中:
_AFXWIN_INLINE CWiNApp* AFXAPI AfxGetApp()
{
    return afxCurrentWinapp;
}
  而其中用到的afxCurrentWinapp由定义于AFXWIN.H中:
#define afxCurrentWinapp afxGetModuleState()->m_pCurrentWinApp
  对比前面提到的CWinApp构造函数中相应的语句,我们很容易看出AfxGetApp事实上就是获取当前程序的CMyApp对象指针。所以WinMain中以下代码:
    CWinApp* pApp=AfxGetApp();
    pApp->InitApplication();
    pApp->InitInstance();
    nReturnCode=pApp->run();
就等价于:
    CMyWinApp::InitApplication();
    CMyWinApp::InitInstance();
    CMyWinApp::Run();
  这样我们就确定了整个MFC应用程序的初始代码主要就隐藏在上面这三个函数之中,下面我们就来对整个WinMain有重点的进行全面分析
4.WinMain详解
首先是AfxWinInit,它隐藏在APPINIT.CPP中,代码如下:
BOOL AFXAPI AfxWinInit(HINSTANCE hInstance, HINSTANCE hPrevInstance,
            LPTSTR lpCmdLine, int nCmdShow)
{
    ASSERT(hPrevInstance == NULL);

    // handle critical errors and avoid Windows message boxes
    SetErrorMode(SetErrorMode(0) |
            SEM_FAILCRITICALERRORS|SEM_NOOPENFILEERRORBOX);

    // set resource handles
    AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
    pModuleState->m_hCurrentInstanceHandle = hInstance;
    pModuleState->m_hCurrentResourceHandle = hInstance;

    // fill in the initial state for the application
    CWinApp* pApp = AfxGetApp();
    if (pApp != NULL)
    {
        // Windows specific initialization (not done if no CWinApp)
        pApp->m_hInstance = hInstance;
       pApp->m_hPrevInstance = hPrevInstance;
       pApp->m_lpCmdLine = lpCmdLine;
       pApp->m_nCmdShow = nCmdShow;

        pApp->SetCurrentHandles();
    }

    // initialize thread specific data (for main thread)
    if (!afxContextIsDLL)
        AfxInitThread();

    return TRUE;
}

其中用到的AfxInitThread函数位于THRDCORE.CPP中,具体代码如下:
void AFXAPI AfxInitThread()
{
    if (!afxContextIsDLL)
    {
        // set message filter proc
        _AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
        ASSERT(pThreadState->m_hHookOldMsgFilter == NULL);
        pThreadState->m_hHookOldMsgFilter = ::SetWindowsHookEx
            (WH_MSGFILTER,_AfxMsgFilterHook, NULL,
            ::GetCurrentThreadId());

        #ifndef _AFX_NO_CTL3D_SUPPORT
        // intialize CTL3D for this thread
        _AFX_CTL3D_STATE* pCtl3dState = _afxCtl3dState;
        if (pCtl3dState->m_pfnAutoSubclass != NULL)
           (*pCtl3dState->m_pfnAutoSubclass)(AfxGetInstanceHandle());

        // allocate thread local _AFX_CTL3D_THREAD just for automatic

        //termination
        _AFX_CTL3D_THREAD* pTemp = _afxCtl3dThread;
        pTemp; // avoid unused warning
        #endif
    }
}
 
之后是theApp的InitApplication函数,由于程序并没有改写该函数,一次相当于调用CWinApp::InitApplication,其代码位于APPCORE.CPP中,如下所列:
BOOL CWinApp::InitApplication()
{
    if (CDocManager::pStaticDocManager != NULL)
    {
        if (m_pDocManager == NULL)
                m_pDocManager = CDocManager::pStaticDocManager;
        CDocManager::pStaticDocManager = NULL;
    }

    if (m_pDocManager != NULL)
            m_pDocManager->AddDocTemplate(NULL);
    else
        CDocManager::bStaticInit = FALSE;

    return TRUE;
}

  这是MFC为了内部管理所作的工作,我们不必理会。
 
  接下来是pApp->InitInstance,CWinApp类中此函数是虚函数,由于我们的程序改写了该函数,所以现在等于调用我们自己的InitInstance,我们的程序也将从这里开始自己主窗口的生命。我生成的一个简单程序的这一段代码如下:
BOOL CTestApp::InitInstance()
{
    AfxEnableControlContainer();
    #ifdef _AFXDLL
        Enable3dControls();
    #else
        Enable3dControlsStatic();
    #endif
    SetRegistryKey(_T("Local AppWizard-Generated Applications"));

    LoadStdProfileSettings();

 

    CSingleDocTemplate* pDocTemplate;
    pDocTemplate = new CSingleDocTemplate(
        IDR_MAINFRAME,
        RUNTIME_CLASS(CTestDoc),
        RUNTIME_CLASS(CMainFrame), // main SDI frame window
        RUNTIME_CLASS(CTestView));
    AddDocTemplate(pDocTemplate);

    CMainFrame* pMainFrame = new CMainFrame;
    if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
            return FALSE;
    m_pMainWnd = pMainFrame;

   CCommandLineInfo cmdInfo;
    ParseCommandLine(cmdInfo);

    if (!ProcessShellCommand(cmdInfo))
           return FALSE;

    m_pMainWnd->ShowWindow(SW_SHOW);    //这两行与SDK程序
    m_pMainWnd->UpdateWindow();         //极为相似

    return TRUE;

}
  标为红色的一句调用了CMainFrame的构造函数,而在此构造函数中又有堆Create函数的调用,该函数用于创建窗口,共有8个参数。声明如下:
BOOL Create(LPCTSTR lpszClassName,LPCTSTR lpszWindowName,
            DWORD dwStyle=WS_OVERLAPPEDWINDOW,
            const RECT& rect=rectDefault,
            CWnd* pParentWnd=NULL,
            LPCTSTR lpszMenuName=NULL,
            DWORD dwExStyle=0,
            CCreateContext* pContext=NULL);
   所使用的窗口类名称可以作为其第一个参数传入,也可以使用NULL,表示以MFC内建窗口类产生一个标准的外框窗口;第二个参数是窗口标题栏中显示的文 字;第三个参数为窗口风格;第四个参数指定窗口位置和大小;第五个参数指定父窗口,没有父窗口则使用NULL;第六个参数指定菜单,使用在RC资源文件中 定义的菜单名填写;第七个参数为扩展风格,找支持Windows 3.1以后的版本;第八个参数为一个之乡CCreateContext结构的指针,MFC使用它在具备Document/View结构的应用程序中初始化 外框窗口。
 
   SDK程序设计中要求的对窗口类的注册工作在MFC中也是必需的,并且它是由Create函数在创建窗口之前触发的操作,但其中间过程较为复杂,如果要解 释清楚需要向本文加入大量源代码,限于篇幅本文将不介绍此部分,有兴趣的读者可以阅读侯俊杰先生的《深入浅出MFC》P283-P289,有相当详尽的描 述和分析。
 
  窗口的显示和更新较为简单,主要就是后边标红的两行,因此现在我们要把重心放在CWinApp::RUN函数上,其代码位于APPCORE.CPP中,列出如下:
int CWinApp::Run()
{
    if (m_pMainWnd == NULL && AfxOleGetUserCtrl())
    {
        // Not launched /Embedding or /Automation, but has no main window!
        TRACE0("Warning: m_pMainWnd is NULL in CWinApp::Run - quitting                application.\n");
        AfxPostQuitMessage(0);
    }
    return CWinThread::Run();
}
  可以看到,CWinApp::Run事实上指向CWinThread::Run,它位于THRDCORE.CPP中,代码如下:
int CWinThread::Run()
{
    ASSERT_VALID(this);

    // for tracking the idle time state
    BOOL bIdle = TRUE;
    LONG lIdleCount = 0;

    // acquire and dispatch messages until a WM_QUIT message is received.
    for (;;)
    {
        // phase1: check to see if we can do idle work
        while (bIdle &&
               !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
        {
            // call OnIdle while in bIdle state
            if (!OnIdle(lIdleCount++))
                bIdle = FALSE; // assume "no idle" state
        }

            // phase2: pump messages while available
        do
        {
            // pump message, but quit on WM_QUIT
            if (!PumpMessage())
                return ExitInstance();

            // reset "no idle" state after pumping "normal" message
            if (IsIdleMessage(&m_msgCur))
            {
                bIdle = TRUE;
                lIdleCount = 0;
            }

        } while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
    }

    ASSERT(FALSE); // not reachable
}

PumpMessage代码如下:
BOOL CWinThread::PumpMessage()
{
    ASSERT_VALID(this);

    if (!::GetMessage(&m_msgCur, NULL, NULL, NULL))
    {
        return FALSE;
    }

    // process this message

    if (m_msgCur.message != WM_KICKIDLE

          && !PreTranslateMessage(&m_msgCur))
    {
        ::TranslateMessage(&m_msgCur);
        ::DispatchMessage(&m_msgCur);

    }
    return TRUE;
}
   这俨然就是原来的WndProc的一个翻版,很明显正是有着一部分代码来负责整个程序的消息获取、解释、判断和分派的,我们可以通过对它的适当改写来使 得程序的消息流动按我们的意愿进行,另外要使程序能处理空闲时间(Idle Time)也要从改写该函数开始。而在对消息进行了分派之后就是由早已架设好的由前面曾提到的那些宏所实现的消息映射来驱动整个程序了,由于我计划将消息 映射方在后面的章节再来介绍,因此我们对于程序初生的剖析到此也就告一段落了,下面我们来看一看程序的死亡,也就是退出过程。
 
第四节、退出
  MFC程序的死亡相对于初生来说要简单的多,主要是以下几步:
  1.使用者通过点击File/Close或程序窗口由上角的叉号发出WM_CLOSE消息。
  2.程序没有设置WM_CLOSE处理程序,交给默认处理程序。
  3.默认处理函数对于WM_CLOSE的处理方式为调用::DestoryWindow,并因而发出WM_DESTORY消息。
  4.默认的WM_DESTORY处理方式为调用::PostQuitMessage,发出WM_QUIT。
  5.CWinApp::Run收到WM_QUIT后结束内部消息循环,并调用ExinInstance函数,它是CWinApp的一个虚拟函数,可以由用户重载。
  6.最后回到AfxWinMain,执行AfxWinTerm,结束程序。
 
 
  以上就是一个MFC程序的初始化和最后退出的全过程,对于阅读本文的读者我深表感谢,同时若文中有什么地方叙述有错误或者读者仍然存有疑问,请去FrontFree论坛提问,我将尽快回答。在后面的章节中我们将继续探索MFC内部机制和实现方法,希望各位继续支持。
阅读(5197) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~