MFC

MFC 내부구조 #2 MFC Internals - 대충 읽어볼 글

디버그정 2008. 8. 31. 15:32
MFC 내부구조 #2 MFC Internals

2004/07/25 06:48

복사 http://blog.naver.com/synchrog/140004387209

2. MFC와 C/SDK

, 두번째다. 이번엔 MFC와 기존의 API를 비교해가며 분석해보자.

Windows 프로그렘은 2개의 핵심 요소로 이루어 저있다.

(1)   main application 그 자체

(2)   message를 처리할 수 있는 1개 이 상의 window.

MFC가 개발된 궁극적인이유, 다시한번 강조하지만 역시, 시간의 효율성을

최적화한, 생산성 향상에있다고 생각한다.(뭐 절대 틀린말은 아니겠지?)

기존의 API를 살펴보면 최소한 boilerplate code 가 90줄 이상 copy and paste로 베이스 깔고 시작해야한다.(MS에서도 window app 개발에 인가되버린 기술이 바로 copy and paste복사해다 붙여버리기! 이다!)

           boilerplate code: 왜 API 시작할때 꼭 윈도우하나 만들어주는 그 코     드를 말한다.

 

2.1 Boilerplate code

 

이녀석을 쓸 수 밖에 없는이유는 event-driven operating system이 어떤 응

용프로그렘을 작동시키기 위해서는 엄청난 양의 overhead를 감수해야 한다

는 점에서보터 시작된다. Windows는 Event-driven 환경의 OS이고 app를

작동시키기위해 많은 것들이 설치되어 있다. 바로 이 업청난 양의 boilerplat

e code가 app의 event 처리를 위해 사용된다.

 

Windows 가 message 처리과정을 수행하기 위해 필요한 사항

           (1). app프로그렘은 message handler를 구성하고, 이를 Windows에      등록해주어야 한다.그래서 어디로 msg를 보내야하는지 알게 된다.

           (2) Windows는 app의 instance를 추적할 수 있어야한다.

           (3) app는 Windows에게 message queue 안에 있는 next event를

           요구할 수 있으며, 그 message를 적당한 message handler에게 전

           달할 수 있어야한다.

          (4) app가 끝날 때까지 이러한 동작은 꼐속되어야 한다.

 

Windows app가 갖추어야 할 사항

           WinMain(): 이놈은 app 의 시작 위치처럼 동작한다. App는 WinMain

           ()을 통해서 Windows로부터 필요한 정보를 얻는다. 이 정보에는 app

           자신의 instance handle, app가 마지막으로 수행되었을 때의 instanc

           e handle, Commande line argument 그리고 window를 보여주는 상

           태등이 포함된다.(UI)

           Message Loop: Windows 환경에서 app가 message pump를 구성하

           는 것은 의무 사항이다.

           Initialize: 당연한것

           Message handler: 이것이 바로 window procedure이다. App를 종료

           하기위해서는 window procedure가 WM_DESTROY message를 처리

           하여야 한다.(안그러면 프로세스로 메모리를 먹고있게됨)

 

2.2 Code 비교

 

우선 같은 결과를 가저오는 C/SDK(API)로 제작된 소스와 MFC로 제작된 소

스를 살짝 보자.(자세히 볼필요는 전혀 없음. 그냥 많다! 적다 만알면됨)

API

#include <windows.h>

 

LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

HINSTANCE g_hInst;

LPCTSTR lpszClass="First"; //g_hInst와 lpszClass 를 두개의 전역변수로 사용하는 프로그렘.

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

                             ,LPSTR lpszCmdParam,int nCmdShow)

{

             HWND hWnd;

             MSG Message;

             WNDCLASS WndClass;

             g_hInst=hInstance;//hInstance->1인칭 프로그렘자체를 나타내는 정수값.

            

             WndClass.cbClsExtra=0;

             WndClass.cbWndExtra=0;

             WndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);

             WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);

             WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);

             WndClass.hInstance=hInstance;

             WndClass.lpfnWndProc=(WNDPROC)WndProc;

             WndClass.lpszClassName=lpszClass;

             WndClass.lpszMenuName=NULL;

             WndClass.style=CS_HREDRAW | CS_VREDRAW|CS_DBLCLKS;

             RegisterClass(&WndClass);

 

             hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW|WS_VSCROLL,

                             CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,

                             NULL,(HMENU)NULL,hInstance,NULL);

             ShowWindow(hWnd,nCmdShow);

            

             while(GetMessage(&Message,0,0,0)) {

                           TranslateMessage(&Message);

                           DispatchMessage(&Message);

             }

             return Message.wParam;

}

 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

             HDC hdc;

             PAINTSTRUCT ps;

            

             switch(iMessage) {

             case WM_CREATE:

                           return 0;

             case WM_PAINT:

                           hdc = BeginPaint(hWnd, &ps);

                           EndPaint(hWnd, &ps);

                           return 0;

             case WM_LBUTTONDOWN:

                           Messagebox(hWnd,fucker,fucker,NULL);

                           Return 0;

             case WM_DESTROY:

                           PostQuitMessage(0);

                           return 0;

             }

             return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

뭐 이정도. Windows 프로그레밍 첨하면 늘 보는 지겨운 코드일것이다.

 

다음은 MFC

#include <afxwin.h>

 

class Capp : public CWinApp{

 

public:

             virtual BOOL InitIntance();

};

class CAppWindow : public CFrameWnd {

public:

             CAppWindow(){Create(NULL,app);}

             Afx_msg void OnLButtonDown(UINT nFlags, CPoint point);

             DECLARE_MESSAGE_MAP()

};

 

BEGIN_MESSAGE_MAP(CAppWindow,CFrameWnd)

             ON_WM_LBUTTONDOWN()

END_MESSAGE_MAP()

 

Void CAppWind::OnLButtonDown(UINT nFlags, CPoint)

{

             MessageBox(fucker,NULL,MB_OK);

}

BOOL CApp::InitInstance()

{

             m_pMainWnd = new CAppWind();

             m_pMainWnd->ShowWindow(m_nCmdShow);

             m_pMainWnd->UpdateWindow();

 

             return TRUE;

}

 

CApp app;

끝이다. 끝! 대박아닌가? 60%정도의 코드소스를 줄일 수 있다.

WinMain도 존재하지않으며 Message loop 따위도 없다. 그러나 어딘가에

분명히 다숨겨져 있는것이며 실제 구동될 때 반드시 기본적인 요소를 실

행한다. 이제 MFC로 우리가 해야할 일은 override 뿐이다.

 

MFC소스는 API소스와 다음과 같은 차잇점이 있다.

 

1)    memver function인 OnLButtonDown

2)    message map

 

2.3 MFC app의 기본 Component

자 이제 MFC 구조를 면밀히 살펴보는 시간!

 


 


 

CWinApp는 거대한 Class이다. 이놈의 전체 소스는 AFXWIN.H에 있는데

매우 간추려본다.

 

class CWinApp : public CWinThread

{

             DECLARE_DYNAMIC(CWinApp)

public:

 

// Constructor

             CWinApp(LPCTSTR lpszAppName = NULL);     // app name defaults to EXE name

 

// Attributes

             // Startup args (do not change)

             HINSTANCE m_hInstance;

             HINSTANCE m_hPrevInstance;

             LPTSTR m_lpCmdLine;

             int m_nCmdShow;

 

             // Running args (can be changed in InitInstance)

             LPCTSTR m_pszAppName;  // human readable name

                                                                                                          //  (from constructor or AFX_IDS_APP_TITLE)

             LPCTSTR m_pszRegistryKey;   // used for registry entries

             CDocManager* m_pDocManager;

 

             // Support for Shift+F1 help mode.

             BOOL m_bHelpMode;           // are we in Shift+F1 mode?

 

public:  // set in constructor to override default

             LPCTSTR m_pszExeName;       // executable name (no spaces)

             LPCTSTR m_pszHelpFilePath;  // default based on module path

             LPCTSTR m_pszProfileName;   // default based on app name

 

// Initialization Operations - should be done in InitInstance

.

Public: 뭐 기타등등 있다.

 

virtual BOOL InitApplication();

 

Protected: // 이들은 알아서 보시길

..

public:

 

CCommandLineInfo* m_pCmdInfo;

void SetCurrentHandles();

             virtual BOOL InitInstance();

             virtual int ExitInstance(); // return app exit code

             virtual int Run();

             virtual BOOL OnIdle(LONG lCount); // return TRUE if more idle processing

             virtual LRESULT ProcessWndProcException(CException* e, const MSG* pMsg);

 

public:

             virtual ~CWinApp();

 

protected:

             //{{AFX_MSG(CWinApp)

             afx_msg void OnAppExit();

             afx_msg void OnUpdateRecentFileMenu(CCmdUI* pCmdUI);

             afx_msg BOOL OnOpenRecentFile(UINT nID);

             //}}AFX_MSG

             DECLARE_MESSAGE_MAP()

 

CWinApp는 멤버 함수와 멤버 변수로 가득차있다.

 

l       CwinApp는 WinMain()으로 들어오는 Command line parameter의 집합을 포함하고 있다. 그 집합에는 current instance handle(m_hInstance), prevous instance handle(m_hPreInstance), command line parameter(m_lpCndLine), 그리고 show window flag(m_nCmdShow)가 포함되어 있다.

l       CWinApp는 응용프로그렘의 이름을 m_szAppName에 복사해둔다.

l       CWinApp는 응용 프로그렘의 실행 파일 이름(m_pszExeName), 응용프로그렘의 help파일의 경로(m_pszHelFilePath), 응용프로그렘의 profile의 이름(m_pszProfilename)을 유지한다.

l       CWinApp는 command line parameter를 유지하기 위해 CCommandLineInfo라는 구조를 사용한다. 수년 동안 command line parameterdml 표준을 발전시켜 왔다. 예를 들면 OLE 프로그렘이 OLE documentdp 포함된 객체로써 시작할 때 , Windows는  응용프로그렘을 /Embeddding 이라는 comman line parameter를 사용하여 구동시킨다. 또한, Windows는 응용프로그렘에게 여러가지 동작을 전달하기 위해 C L P를 사용한다. CCommandLineInfo 는 한 장소에 모든 표준 parameter를 저장한다. CCommandLineInfo는 간단한 유틸이지만 C L P 를 알아야할 필요가 있을경우에는 매우 편리하다.

class CCommandLineInfo : public CObject

{

public:

       // Sets default values

       CCommandLineInfo();

 

       //plain char* version on UNICODE for source-code backwards compatibility

       virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast);

#ifdef _UNICODE

       virtual void ParseParam(const char* pszParam, BOOL bFlag, BOOL bLast);

#endif

 

       BOOL m_bShowSplash;

       BOOL m_bRunEmbedded;

       BOOL m_bRunAutomated;

       enum { FileNew, FileOpen, FilePrint, FilePrintTo, FileDDE,

                    AppUnregister, FileNothing = -1 } m_nShellCommand;

 

       // not valid for FileNew

       CString m_strFileName;

 

       // valid only for FilePrintTo

       CString m_strPrinterName;

       CString m_strDriverName;

       CString m_strPortName;

 

       ~CCommandLineInfo();

// Implementation

protected:

       void ParseParamFlag(const char* pszParam);

       void ParseParamNotFlag(const TCHAR* pszParam);

#ifdef _UNICODE

       void ParseParamNotFlag(const char* pszParam);

#endif

       void ParseLast(BOOL bLast);

};

l       모든 APP는 자신을 초기화 시켜주는 장소가 필요하다. MFC에서는 CWinApp::InitInstance()가 바로 그곳이다. MFC는 APP의 실질적인 어떤 일이 발생하기 전에 저놈을 호출한다. 예를 들면 main window를 보여주는 곳은 InitInstance()이다.

l       반대로 대부분의 APP는 자신의 memory를 지우고 종료를 수행하는 곳이 필요하다.MFC는 CWinApp::ExitInstance()가 이런 목적으로 제공된다.

l       MFC app에서는 message pump가 CWinApp의 일부분이다. CWinApp::Run()을 호출함으로써 GetMessage() ~ DispatchMessage() loop 가 제공된다.(여기에 숨겨져있다!!)

l       Windows와 같은 event-driven 환경의 OS의 특징중에는 message queue에 아무런 event가 없어서 응용프로그렘이 쉬는 순간이 있다. 이때 MFC는 CWinApp:OnIdle()을 호출 할 수 있는 기능을 제공한다. CWinApp::Run()은 Message queue가 비었을 때 OnIdle()을 호출한다. 즉 OnIdle에다가 코딩을 해놓으면 사용자가 아무짓도 안할 때 지가 알아서 띠띠띠 거리며 작동할 수 있게 설계되있다는 뜻.

CWinApp에 대해서 간단히 살펴보았다. 그럼 Window와 관련된 모든 class의 base class인 CWnd class 에대해 알아볼 차례다.(점점 실제 생활에 필요없는 내부구조로 들어간다. 마치 사람이 살아가는데 혈액속에 혈소판이 무슨역할을 하는지 알아내는 것처럼. 이 글을 읽고 있는 당신은 이미 매니아)

 

2.4

CWnd window class의 base class

 

CWinApp에서 파생된 객체외에 MFC app는 화면에 window를 나타내기 위한 객체를 포함하고있다. CWnd를 여기서 보여주기에는 너무고 거하다.CWnd 의 정의는 위와 마찬가지로 AFXWIN.H에 있다. CWnd는 2가지의 기능을 제공하는 부분으로 나눌 수 있다.

첫째, Windows API function을 포장하는 부분,상위 클레스의 MFC관련기능을 제공하는 부분이다.

 

1. Window API 포장하기(Wraped 라구하지)

 

지금부터 사실 MFC Internal의 진가이다. 지금까지 설명해 왔던건 사실 API보다 MFC가 더 간편하고 모든면에서 편리하게 되있따<- 정도도였으니까. 그 예로

 

C/SDK 의 경우

HWND hWnd;

ShowWindow(hWnd, SW_SHOWNORMAL);

 

이런게

 

CWnd* pWnd;

pWnd->ShowWindow(SW_SHOWNORMAL); 로 바꼈다는 뜻.

 

AFXWIN2.INL에는 CWnd의 기본적인 동작에 대한 code가 포함되이싿. (참! INL이 무슨 확장자인가? 이는 inline 함수들이 수두룩하게 있다는 뜻이야) CWnd의 몇몇 member function은 inline함수가 아니다. 그 이유는 그함수들이 OLE control을 참조할 때 특별한 처리를 요구하기 때문이지. 그 함수들은 inline을 하기에는 너무크다. Windows API를 포장하고 있는 CWnd 의 맴버함수는 m_hWnd를 사용하여 해당 API함수를 호출한다.

어쨋건 CWnd는 단순히 API 함수 포장맨이 아닌 그이상의 기능을 가진다.

(뭐 사실 MFC 로 짠다해도 가끔은 API가 필요할때가 있다. 그래서 이놈이 존재하는데, CWnd를 단지 API 함수를 쓰기위해 존재한다고 얕보면 안된다)

2. 그외의 CWnd 특징

CWnd의 부모는 CCmdTarget이구 할아버지는 바로 CObject이다! 씨 오브젝트의 피를 이어받는 놈들은 무조건 가지게되는 특징있다. Dynamic run-time info, 영속성기능 등등이 있지. 나중에 예기하게되겠지만 CWnd는 MFC의 message-routing scheme를 보유하게된다. 왜? CCmdTarget때문에. 글구 CWnd는 자체 Message 처리능력이있다. API의 보일러플레잇 코드를 떠올려보자. 메시지 들어올 때 switch로 내려가 WM_씨리즈로 처리하는거 알꺼다.그래서 이것저것 처리하다가 할꺼없는녀석들은 DefWindProc()으로 쫒겨나는것도 알것이다. MFC 마찬가지로 저곳으로 보내는건 마찬가지지만 처리방법이 틀리다. 알다시피 MFC Message handler map이 처리한다. On.. 씨리즈 말하는것이지. MFC 짜다보면알 것이다. WM_Paint 와 같은 처리가 ON_PAINT 라는것을 그리고 쓸데 없는 메시지가 들어올떄 CWnd::Defalut()를 호출하는데 이게 DefWindowProc()을 실행시키는거다.

 

2.5 Message Map에 관하여

마지막으로 이놈에대해 조금 떠들다 끝내고자 한다.

 

Window Handle들을 Window Object로 바꾸는과정, 즉 MFC의 작동 메커니즘인 mapping을 시키기위해 C++ 스럽게 모두 class화 또는 객체화 시키는 것을 말한다. API프로그레밍 하다보면 죄다 첫번째인자에 핸들을 넣지않는가? 그러나 MFC엔 그것이 없는 이유이다. 구체적으로 말하자면 MFC에서는 CWnd를 상속받은 객체와 message dispatch 메커니즘을 사용한다.MFC는 CWnd-derived(한국말로 하겠다.CWnd에서 파생된) 객체와 window handle을 연계(map)시켜 주는 무언가를 제공해야하는데 그게 바로 CHandleMap이다!

문제는 이 CHandleMap이라는 클레스는 문서화되있지않다. MFC가 COM으로 되있기떄문에 이진코드로된 블렉박스인 COMPONENT를 열어볼 길은 없지만 그것을 분석해낸 미친놈들이 있다.(흐흐 그들이 파해처 놓은걸 내가 공부해서 여따 쓰는거지)

MFC는 어떻게든 윈도 프로그레밍을 편하게할라고 API 를 감추려한다. MFC는 종종 natice handle과 MFC wrapper를 섞어서 쓰기 때문에 window handle과 CWnd-derived객체의 연계는 항상 동일 한 방법으로 이뤄진다.

CHandleMap은 CMapPtrToPtr형의 두 맴버 변수를 가지고있다.m_permanentMap과 m_temporaryMap 이다. CHandleMap은 CWnd-derived 객체돠 Window handle의 연계를 유지시키는데 CMapPtrToPtr을 사용한다. 영구 연계는 프로그렘이 끝날때까지(뭐 m_hWnd등이 그렇겠찌) 임시 연계는 해당 메시지가 존재하는동안만 존재한다.으어;;

 

아무튼 대략적으로 정리만하자, 어찌됬든간에 메시지가 오가고 윈도우에 전달하고 어쩌고 저쩌고 할라면 HWND 같은것이 개입되야하는데 이것을 없앤다기보다는 객체화시켜서 이예 이미 끼고 들어가서 프로그레밍 할 수 있게 만드는것이다. 즉 API가 말그대로 포장만되있고 사실 포장된 그내용물들은 다 쓰여지고 있다는 예기다. 다만 연계되어서 외부적으로 조작되는것일 뿐이지.

 

다음편 MFC에서 WinMain을 찾아보자를 기대해주세요.

[출처] MFC 내부구조 #2|작성자