MFC程序的出生和結束

本文的寫作對象:
  本文主要針對編寫過1、2個Windows程序,對C++比較熟悉,瞭解SDK程序設計的基本知識,同時對MFC運行方式感到困惑的MFC初學者。
 
序、產生

  在MFC程序設計的學習過程中最令人感到難受,甚至於有時會動搖學習者信心的就是一種對於程序的一切細節都沒有控制權的感覺,而這種感覺的出現會使大家認爲自己離開了書本上的例子就無法設計編制程序。事實上這是在MFC學習過程中經常出現的一種正常現象。產生這種情況的原因就是MFC最大的優點——封裝。

 

  衆所周知,MFC將Windows程序設計中的一些重複、冗綴的部分進行了封裝,使得在所有程序中都必然會出現的變化不大的部分不再由程序員手動輸入,而是使用程序自動生成,程序員則可以集中精力於程序的核心部分。但是這種封裝實在非常的嚴密以至於對於初學者來說已經完全看不到程序的入口處了,由此必然會導致對程序沒有一個整體的感覺,對習慣了面向過程而不是面向對象的程序員來說更是如此。爲了解決這種情況,本文將從MFC Framework的內部將程序的整體運行流程拆出,展示給讀者來看,並希望這樣做能有利於大家在MFC程序設計上更加得心應手。

 
第一章、應用程序的初生和結束
第一節、對比
  首先我們來看看古老的SDK程序是怎樣開始的,爲此請觀察下面一段代碼。
 
#include <windows.h>

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內部機制和實現方法,希望各位繼續支持。

 

選自:http://blog.chinaunix.net/uid-21169302-id-446230.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章