- 윈도 프로그램의 시작과 끝. "Message" -
------------------------------------------------------------
지긋지긋한 추위속의 겨울이 한창입니다.
뜨거웠던 지난여름이 어제 같은데.. 정말 계절의 변화란
신기합니다.
저는 계절의 변화를 그림자의 길이로 가장 먼저 느끼는
버릇이 있습니다. 가령 그림자가 길어지면 겨울이 온다는걸
느끼죠. 햇빛의 입사각에 따라 계절의 분위기를 느끼는 건데
몇일전
정오를 약간 지난 시각에 봄의 분위기를 느낄수 있었습니다.
한창의 겨울은 곧 봄이 옴을 암시하기도 하는거죠.
프로그래머의 실력도 그렇지 않을까 합니다.
한참 능력의 한계에 숨이 턱턱 차오를때,
바로 그때가 한계단 올라서는 순간이 머지 않을때 이기도 하죠.
^_^;
자 .. 한계단 위로, 우리 같이 힘을 냅시다.
4번째 이야기를 해보겠습니다.
-------------------------------------------------------------
- 윈도 프로그램의 시작과 끝. "Message" -
DOS에서의 C프로그램을 기억하는가?
그 유명한 Hello World!
1:void main (void)
2:{
3: printf("Hello World");
4:}
C/C++ 프로그래머 라면 누구나 제일먼저 접했을 만한 코드다.
프로그램은 1번 라인에서 시작해 3번 라인을 수행하고
장렬히 전사한다.
위에서 아래로.. 순서데로 실행한다.
도중에 함수를 만나면 함수안으로 들어갔다가 나와서
계속 길을 간다.
그러다 길이 끝나면 종료한다.
이러한 전통적인 스타일은 거의 모든 프로그래밍 언어에서
사용되는데 C++도 물론이다.
프로그램을 실행시킬때 OS는 main() 함수 한개를 실행하고
프로그램을 끝낸다. 우리가 만드는 모든 프로그램은 main()
함수가 끝나게 되므로써 운명하신다.
우리가 만드는 코드는 main() 이 종료하기 전에
무슨짓을 하도록 하는거다.
이렇게 OS가 프로그램을 실행할때 최초의 시작지점을
엔트리 포인트(Entry Point) 라고 한다.
main() 이 곧 엔트리 포인트지.
그런데 Windows 프로그래밍에서는 main() 이 안보인다.(물론 main도 된다)
흔히 말하는 API로 만들면(SDK) WinMain() 이 있다.
특히 MFC로 만들면 WinMain() 도 없다.
Windows가 프로그램을 실행할때 코드의 시작지점인 엔트리포인트는
도대체 어디인가!!!!!!!
(MSC 6.0 이던가, 7.0 이던가?
처음 윈도우즈 프로그래밍을 접했을때
나는 심각한 정신착란 증세 및 정서불안에 시달렸다. --;
바로 엔트리 포인트 때문에......
시간이 아주 많이 지나서야 겨우 원리를 깨닫게 되었지만
main()에서 시작해 main()에서 끝나는데 길들여졌던 나로서는
완전 딴세상 이었다.
특히 MFC를 처음배울때는 그나마 WinMain()도 없어서
제2의 정신적 붕괴를 경험해야 했다..)
내가 지금 하고 싶은 얘기가 뭐냐?
바로 엔트리포인트를 알자는 거다.
MFC를 알기전에 반드시!
알아야 하는 코드가 있으니
그것이 "Hello World" Windows 판 이다.
헬로월드.
1:int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
2: LPSTR lpszCmdLine, int nCmdShow )
3:{
4: static char szAppName[] = "Hello World";
5: WNDCLASS wc;
6: HWND hWnd;
7: MSG msg;
8:
9: wc.style = 0;
10: wc.lpfnWndProc = (WNDPROC)WndProc;
11: wc.cbClsExtra = 0;
12: wc.hInstance = hInstance;
13: wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
14: wc.hCursor = LoadCursor( NULL, IDC_ARROW );
15: wc.hbrBackground = COLOR_WINDOW + 1;
16: wc.lpszMenuName = NULL;
17: wc.lpszClassName = szAppName;
18:
19: RegisterClass( &wc );
20:
21: hWnd = CreateWindow( szAppName,
22: szAppName,
23: WS_OVERLAPPEDWINDOW,
24: CW_USERDEFAULT,
25: CW_USERDEFAULT,
26: CW_USERDEFAULT,
27: CW_USERDEFAULT,
28: HWND_DESKTOP,
29: NULL,
30: hInstance,
31: NULL );
32:
33: ShowWindow( hWnd, nCmdShow );
34: UpdateWindow( hWnd );
35:
36: while(GetMessage( &msg, NULL, 0, 0 ))
37: {
38: TranslateMessage( &msg );
39: DispatchMessage( &msg );
40: }
41: return msg.wParam;
42:}
43:
44:
45:LRESULT CALLBACK WndProc( HWND hWnd, UINT message,
46: WPARAM wParam, LPARAM lParam )
47:{
48: PAINTSTRUCT ps;
49: HDC hdc;
50: RECT rect;
51:
52: switch(message)
53: {
54: case WM_PAINT:
55: hdc = BeginPaint( hWnd, &ps );
56: GetClientRect( &rect );
57: DrawText( hdc, "Hello World", -1, &rect,
58: DT_SINGLINE | DT_CENTER | DT_VCENTER );
59: EndPaint( hWnd, &ps );
60: return 0;
61:
62: case WM_DESTROY:
63: PostQuitMessage( 0 );
64: return 0;
65: }
66: return DefWindowProc( hWnd, message, wParam, lParam );
67:}
헥헥헥헥.....
(이거 손수 메모장에서 다 타이프 했슴..)
(여러분도 해보세요. 고통을 같이 합시당. -_-; )
(그리고 소스만 프린트 해서 계속 보셔야 할겁니다.)
왜 MFC 프로그래밍 하는데 이런 C소스를 알아야 하느냐!
하면..
MFC도 이걸 기초로 만들어 졌기 때문이다.
이 소스 이해 못하면, 감히 윈도우즈 프로그래머 라고
밖에나가 이야기 못한다.
진짜다.
농담 아니다.
중요하다.
그냥 C언어에 main() 이 있듯이
윈도우즈에선 WinMain() 이 있다.
즉 Windows에서 윈도 프로그램의 시작은 WinMain() 이다.
main() 도 물론 된다.
그런데 이건 윈도 없는 프로그램이다. Windows 에서 돌아가지만.
우리의 성경 MSDN 은 이렇게 말한다.
"The WinMain function is called by the system as the initial entry point for a
Win32-based application. "
라꼬.
즉 WinMain() 이 엔트리포인트 라는 거다.
WinMain() 역시 이 함수가 끝나면 프로그램 끝난다.
그러면 WndProc() 는 뭐지?
음..그건 그냥 함수다. 함수. WndProc() 는 엔트리포인트 아니다.
좀 중요하고 특별해서 그렇지..
따라서.
위 소스에서
1번라인에서 시작해 42번 라인에 오면
프로그램 끝난다.
헉!!
그런데 WinMain() 안을 보면 WndProc() 함수는 한번도
호출 한적이 없다.
그런데 이거 실행시켜보면 WndProc() 도 동작한다.
도대체 WinMain() 어디서 WndProc() 를 호출했지?
이거 아는데 좀 오래 걸렸다.
알고나서는 몰랐던 시절이 우스웠지만
몰랐던 때는 아주 심각했었다.
정답은 무엇인가.
바로 39번 라인의 DispatchMessage( &msg ); 가 범인이다.
(이 대목에서 "헉! 아니야!" 라고 생각하시는 분은 이글 안보셔도 될듯..)
자.. 이제 슬슬 본론으로 들어가보자.
DispatchMessage() 는 프로그램이 받은 메세지를 윈도우프로시저로
전달하는 함수이다. 프로그램이 뭘 받는다고? 메세지를 받는다고?
그리고 윈도우프로시저로 전달한다고?
음.. 그렇다.
메세지를 전달한다.
아시다시피 윈도 프로그램은 어떤 메세지를 받아 그 메세지에 응답하는
구조로 되어있다.
예를 들어 마우스를 움직이면
프로그램은 "마우스가 움직였다! 우짤래?" 라는 메세지를 받는다.
그러면 우리는 "마우스가 움직이면... 그냥 놔둬.." -_-; 라는
식으로 프로그램을 짜는 것이다.
윈도우(창)를 가지는 프로그램은 반드시 메세지를 처리할수 있도록
프로그래밍 되어야 한다.
누가 우리 프로그램에 메세지를 주는가?
그것은 운영체제가 준다.
또는 우리 스스로 어떤 메세지를 우리 자신에게 던져줄수도 있다.
또 다른 프로그램(윈도우)에게도 던질수 있다.
DispatchMessage() 는 프로그램이 받은 메세지를 WndProc()함수가
처리하도록 메세지를 건네주는 함수이다.
그리고 WndProc() 가 메세지를 처리할때 까지, 즉 WndProc()가
리턴할때 까지 그자리에서 서있다. 유식하게 "블럭킹 되었다"라고 한다.
음.. 그런데 말야.
그냥 WndProc()함수 안에 있는 내용을
DispatchMessage() 가 있는자리에서 처리하지 뭐하러 WndProc()라고
다른 함수를 만들었지?
그건 "코드의 재진입"을 위해서 라고 볼수 있는데 밑에서 설명할거다. ^^;
긴장 풀고... 험험..
(윈도프로그래밍을 시작하고 얼마되지 않았을때
WinMain()에 별짓을 다 해봤는데 지금 기억나는것중 인상적인것이
printf()를 써본것이다.
결과는 어땠을까?
^^;)
자.
이제 DispatchMessage()가 출현하기 까지의 소스코드를 천천히
뜯어보자.
5번라인 에서 WNDCLASS 형의 wc 변수를 만들었다.
WNDCLASS 는 윈도창의 성격과 그 처리에 대한 일련의 정보들을
담아두는 구조체이다.
9번행에서 17번 행까지 무언가를 죽~죽~ 설정했다.
주목할것은 10번행이다. (나머지는 책에 설명이 잘 되어있슴)
WNDCLASS의 lpfnWndProc 멤버는 윈도우가 받는 메세지를
처리할 함수인 "윈도우프로시저"함수의 포인터를 말한다.
소스에서 "윈도우프로시저"함수는 WndProc() 이기에 "WndProc"라고
했다.
WNDCLASS 의 설정을 마친뒤 19번행에서 RegisterClass() 를
호출한다. 여기에 wc 즉 WNDCLASS 구조체를 넘겨준다.
(wc .. 왠지 화장실 생각이 안나는지.. O_O; )
RegisterClass() 는 OS 에게
"내가 윈도우창 만들건데 그 윈도우 관련 정보는 wc에 있어.
니가 wc보구 마음의 준비를 해"
뭐 이정도 되는 함수다.
그리고 드디어 21번행에서 CreateWindow() 를 호출한다.
드디어 윈도창을 하나 만드는 거다.
이함수의 첫번째 인자가 등록된 클래스의 이름. 즉
RegisterClass() 로 등록한 윈도클래스의 이름이다.
CreateWindow() 는 윈도클래스라는 놈에 기반해서 만든다.
윈도 클래스를 등록한다는 자체가 우리만의 독자적인 윈도우
형식(모양,성격 등등..)을 만든다는 뜻이다.
만약 CreateWindow() 에서 윈도클래스를 미리 정의된
윈도클래스(즉,윈도형식)으로 만들수도 있다.
대표적인게 컨트롤들.. 즉 BUTTON 이나 COMBOBOX 같은...
버튼이나 콤보박스도 윈도우 니깐.
자..
그다음 33,34 행에서 윈도를 화면에 보이도록 한다.
그다음이 GetMessage() 어쩌고 나오는데....
여기가 오늘의 핵심이닷!
지금까지 우리는 우리만의 윈도형식으로 윈도클래스를
등록해서 그 윈도클래스로 CreateWindow() 를 이용해 윈도우를
만들었다.
그리고 화면에도 띄웠다.
그리고 이제는 메세지를 받아와야 한다!
메세지를 왜 받아오느냐?
앞서 말했지만 윈도우 프로그램은 메세지를 받아서 그에 응답
하는 구조이기 때문에 내가 금방 만든 윈도우로 날라오는
수많은 종류의 메세지들을 받아야 한다.
이게 그냥 있으면 받아지는게 아니다.
그래서 GetMessage() 라는 함수를 호출하는거다.
GetMessage() 는 무슨짓을 하는가?
음..
우리가 윈도우를 하나 만들면,
그에 따라 "메세지큐" 라는걸 운영체제가 생성해 준다.
"메세지큐"는 나만의 바구니 라고 보면 된다.
운영체제가 나에게 넘겨주는 수많은 메세지를 담는 바구니.
GetMessage() 는 그 바구니에 담긴 메세지를 집어오는 함수다.
그런데 집어오는 순서가 있다.
"메세지큐" 라는 이름에서 "큐" 는 선입선출 구조.
즉 먼저 들어온 놈을 먼저 꺼낸다는 거다.
좀더 깊숙히 살펴볼까?
여러가지 이유로 발생하는 메세지들은 처음에 운영체제가
가진다. 운영체제는 이들 메세지 들이 누구에게 갈것인지를
분류하고 해당하는 윈도우의 메세지큐에 넣어주는 것이다.
마치 운영체제는 우체국이고 메세지큐는 각각 집에 있는
우편함 이라고나 할까?
^^;
자. GetMessage() 가 있는 부분으로 다시 돌아가자.
36에서 40번행을 보면
while(GetMessage(&msg, NULL, 0, 0) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
라고 되어 있다.
WinMain() 이 엔트리 포인트 이고 이 함수가 리턴하면
프로그램이 종료한다고 앞서 이야기 했지.
따라서 while() 문이 종료하면
프로그램이 끝난다는 사실...
즉 메세지큐에서 메세지를 가져오는 작업을 중지하는순간
프로그램은 종료하게 되는것이다.
그럼 GetMessage() 가 false 를 리턴되면 while 문을
빠져나와 종료하겠군..
언제 false 가 리턴되느냐 하면...
WM_QUIT 이란 메세지를 가져오면 리턴된다.
GetMessage() 는 이렇다.
"메세지 있냐?"
"오.. 메세지들이 바구니에서 줄을 서있군.."
"맨 먼저 들어온놈 너! 일루와."
"짜식.. 너 가지고 갈란다."
"true 를 리턴해야지."
그런데
"메세지 있냐?"
"오.. 메세지들이 바구니에서 줄을 서있군.."
"맨 먼저 들어온놈 너! 일루와."
"헉!! 너는 WM_QUIT ... "
"흑흑.. false 를 리턴하자..."
음.. 그런데 말야.
바구니에 메세지가 없으면 어떨까?
다 퍼간후 더이상 들어온 메세지가 없으면..
그럴때 GetMessage() 는 메세지를 가져올때 까지 기다린다.
즉 리턴되지 않는다는 거다.
자.. 어쨋든 GetMessage() 로 메세지 하나를 가져왔다 치자.
그 메세지는 msg 변수에 담겨있다.
그럼
TranslateMessage() 란 무엇인가....
요건 키보드 관련 메세지일 경우 virtual-key 를 키보드
드라이버에 의해서 ASCII 값으로 매핑된 키로 바꾼뒤
다시 메세지큐에(바구니에) WM_CHAR 이란 메세지를 넣는다.
헥헥헥... 숨차다. -o-;
음.. 이거 이야기 하자면 분량이 너무 많으니
자세한건 MSDN을 보시기 바람..
자. 중요한건 DispatchMessage() 이다.
이 놈은 또 뭔가.
이놈이 물건이다.
WinMain() 에서 GetMessage() 로 가져온 메세지를
이제 우리가 처리해야 하는데 이 처리는
여기서 하는게 아니고 "윈도우프로시저" 에서 한다.
우리는 윈도클래스를 만들때 이미 메세지를 처리할
윈도프로시저를 지정해 주었다.
10행을 보시라.
당당하게 WndProc 라는 함수에서 처리하라고.. ^^;
DispatchMessage()는 GetMessage로 메세지큐에서 꺼내온
메세지를 윈도우프로시저로 전달한다.
그리고 윈도우프로시저 함수가 리턴할때 까지 기다린다.
따라서
DispatchMessage() 가 호출되는 순간!!
WndProc() 함수가 드디어 호출되는 것이다...
WndProc() 함수를 잘봐라.
앞에 CALLBACK 이라고 되어있다.
즉 누구에 의해 불려지는 함수란 뜻이다.
누구에 의해?
DispatchMessage() 에 의해.. ^^;
그리고 WndProc() 는 드디어 메세지를 처리하는 것이다.
후.. 오늘따라 왜이리 타이핑이 힘든지..
허리가 아프다.. 읔....
자.....
여기까지 숨차게 달려온 우리들.. 정말 대견하다.
그런데 지금까지의 내용은 앞으로의 내용을 설명하기 위한
전초전 이었다...... -_-;
컥..... 돌던지지 마시길~
음.
우리가 재미있는? 장난을 칠수 있음을 혹시 느끼신분?
^^;
음.. 뭐 시답잖은 거지만
우리는 메세지 조작을 할수 있다는 사실이다..
즉 GetMessage() 후에
DispatchMessage() 를 호출하기 전에
msg 변수의 내용을 바꾸는거다. -_-;
가령 WM_LBUTTONUP 이 들어왔는데 이걸 WM_LBUTTONDOWN 으로
바꿔 버린다던지.. ^^;
히히.
우리 프로그램에서 이런짓 해봐야 뭐가 재미있냐고?
음.. 꼭 여기서 설명해야 할건 아니지만 메세지 HOOK 이란게
있는데 이건 프로그램의 메세지를 가로채는 기술인데
이걸로 아주 기상천외한 짓들을 할수있다...
음.... 그런데 꼭 필요한 경우 외에는 나쁜짓 하지 말것. ^^;
(혹시 메세지 가로체서 네트웍으로 전송하고자 하는 위험한
장난을 생각하신건 아닌지.. 헉!! 말해버렸네.. 쩝)
또 아주 중요한 의미가 있다.
즉 DispatchMessage 하기전에 메세지 필터링이 가능하다.
혹시 MFC 에서 PreTranslateMessage() 라는 오버라이드 가능한
함수를 본적이 있는가?
이 함수가 바로 TranslateMessage() 를 호출하기 전에
호출되는 함수다. API 로 만들면 이런 함수 필요 없겠지만
(직접 TranslateMessasge() 호출하기 전에 작업하면 되니깐)
MFC에선 이 메세지 가져오는 작업이 안보이니 그런 함수가
존재하는 거지.
또 그것 뿐이냐!
후후.. 또있다.
이건 아주 유용하게 쓸수도 있는건데...
GetMessage() 는 메세지큐에 메세지가 없으면 바구니에
메세지가 담길때 까지 리턴되지 않고 기다린다고 했다.
그런데 GetMessage 와 비슷한 역할을 하는 함수가 한개있다.
이름하여 PeekMessage().
요놈도 역시 GetMessage 처럼 메세지큐에서 메세지를
가져오는 함수이긴 한데 요놈은 메세지가 있든없든 무조건
리턴한다.
즉.. 비교하면..
"메세지 있냐?"
"음.. 메세지 한놈도 없네."
"그냥 리턴하자."
이런놈이다.
음.. 감이 팍팍 온다.
즉 GetMessage 쓰면 메세지가 발생하지 않을때에
나는 아무짓도 할수 없는데
PeekMessage를 쓰면 메세지가 있든 없든 계속
while 문이 돌게 되므로 while 문 안에서
계속 반복작업을 할수있다.
이건 백그라운드로 무슨짓을 할때 쓰는건데
스레드 쓰기엔 뭐하고 그냥 심심할때 처리할 일을
하는데 유용하게 쓸수 있다. ^^;
그럼 MFC 에서는 이런짓 못하느냐? 하면
후후.. 당근 된다.
그게 바로 OnIdle() 이란 오버라이드 가능한 함수다.
음.. 이건 스레드 설명할때나 해야 하는건데
오늘은 메세지 설명해야 하므로 일단 요기까지.
담에 쓰레드 파헤칠때 다시 알아보자.
간단히 그 원리만 보여준다.
while ( bDoingBackgroundProcessing )
{
MSG msg;
while ( ::PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
{
if ( !PumpMessage( ) )
{
bDoingBackgroundProcessing = FALSE;
::PostQuitMessage( );
break;
}
}
// let MFC do its idle processing
LONG lIdle = 0;
while ( AfxGetApp()->OnIdle(lIdle++ ) )
;
// Perform some background processing here
// using another call to OnIdle
}
자자..
우리는 이제 메세지를 받아서 처리하는 방법을 배웠다.
이제 메세지를 보내는 방법을 알아볼까?
메세지를 보내는 것은 SendMessage()와 PostMessage()라는
두개의 함수로 가능하다.
즉 메세지를 보내는 방법은 2가지가 있는데 하나가
Send 하는것, 하나는 Post 하는 것이다.
이거 두개가 아주 다르다.
그리고 차이점을 정확히 이해해야 버그를 막는다.
PostMessage() 는 메세지를 해당 메세지큐에 집어넣고
빠져나오는 함수다.
즉 우리가
PostMessage( hDestWnd, WM_MYMSG, 0, 0 );
하게 되면
hDestWnd 윈도의 메세지큐에 WM_MYMSG 라는 메세지를
넣어놓고 돌아온다.
그리고 함수는 리턴되고 계속 작업을 할수 있다.
예를 들어볼까?
Hello World 소스에서 WndProc 의 62번행을 보라.
여기서 PostQuitMessage()를 없애고
PostMessage( hWnd, WM_DESTROY ,0,0) 를 호출하자.
그러면 PostMessage는 WM_DESTROY를 우리의 메세지큐에
넣고 리턴된다.
여기서 혼동하지 말아야 할것이 있다.
앞서 이야기 한것 처럼 메세지루프에서 DispatchMessage()
는 WndProc()가 리턴하기 전까지 중지해 있다는 점이다.
마치 DispatchMessage()가 WndProc()를 호출한것 처럼
말이다.
따라서 지금 PostMessage()를 하더라도
그 순간 메세지 큐에서 GetMessage()해서 DispatchMessage()
가 일어나지 않는다는 점이다.
가끔가다가
WndProc()를 스레드로 착각하는 경우를 보는데
절대 그렇지 않다. 절대 아니다.!!
PostMessage()는 메세지큐에 메세지를 넣었을뿐
아직 프로그램 실행은 PostMessage() 다음줄에 와있을 뿐이다.
64번 행에서 return 0;가 호출되어
WndProc가 종료한후 드디어 DispatchMessage()가 리턴되고
다시 GetMessage() 호출되는 것이다.
절대 스레드 처럼 동시에 실행 된다고 생각하면 안된다.
사실 이게 API로 짤때는 모두 당연하게 이해 했지만
MFC에서는 메세지 핸들러 라는 함수로만 프로그래밍 하는
습관이 있어 메세지루프의 구조가 감추어 졌기 때문에
어디에선가 메세지가 오면 무조건 메세지 핸들러가
실행된다고 생각하는 것 때문에 마치 스레드 처럼
동시에 여러개의 메세지 핸들러가 실행될지도 모른다는
착각을 유발하는 거다.
후.. 절대 그렇지 않으니 주의합시다.
자 이제 SendMessage().
SendMessage()는 틀리다.
사실 이놈은 아주 특별한 함수다.
이놈은 메세지를 메세지큐에 넣지 않는다.
헉!
메세지큐에 넣지 않고 어떻게 메세지처리를 기대하냐고?
후후..
SendMessage()는 마치 세치기와 같은원리다.
즉 GetMessage()는 메세지큐에서 들어온 순서데로 메세지를
가져오는데 SendMessage()는 현재 처리되고 있는 수행을
모두 일시 정지 시키고 SendMessage()로 보낸 메세지를
수행하게 한다.
예를 들어보면 쉽겠지..
63번행을 이번에는 SendMessage( hWnd,WM_DESTROY,0,0)로
바꾸어보자.
그러면 무슨일이 벌어지는가.?
SendMessage()를 호출한 순간 WndProc()가 재호출 된다.
즉 프로그램 실행이 48번행으로 옮겨지는 거다!
그리고 switch()문을따라 다시 62번 행으로 온다.
왜냐면 switch()의 message변수에는 SendMessage()가 보낸
WM_DESTROY가 들어 있으니 말이다.
그리고 다시 63번 행이 실행되겠지..
즉 위와같이 하면 영원히 반복한다는 것을 알수 있겠지?
즉 차이점이 무엇인가...
SendMessage()는 PostMessage()와 달리 메세지큐에 메세지를
넣고 임무를 완수하는게 아니고
무조건 WNdProc를 호출해서 보낸 메세지를 처리한뒤에야
임무를 완수한다는 거다.
만약 63번행에서 WM_DESTROY를 보내지 않고 WM_PAINT 를 보낸
다면 결국 60번 행에서 SendMessage() 때문에 호출된 WndProc가
리턴될때
비로소 SendMessage() 다음줄로 실행을 계속 한다는 거다.
후..
간추리자면 이렇다.
SendMessage()는 해당메세지를 처리하기 위해 WndProc를 직접
호출하는거나 마찬가지란 뜻이다.
즉 이렇게 WndProc()안에서 SendMessage()한 결과로 다시
WndProc()가 호출되는 경우를 "WndProc로의 코드 재진입"
이라고 한다.
우리는 여기서 전역변수(MFC에선 멤버변수가 될수 있겠지) 사용에
중대한 위험요소를 발견할수 있다.
즉 전역변수 int a;가 있다고 치자.
최초에 63번 행에서 SendMessage()를 호출하기 직전
a에 0를 넣어 놓았다고 하자.
그리고 54번행의 WM_PAINT메세지 처리부에서 a를 +1 시킨다고
하자.
그러면 SendMessage(WM_PAINT) 이전에 a==0 이던것이
SendMessage()가 리턴한 후엔 a=1 이 된다는 점을
눈치 챘는가??
MFC로 본다면
::OnDestroy()
{
m_a=0;
SendMessage(WM_PAINT);
MessageBox( m_a );
}
::OnPaint()
{
m_a=9;
}
위에서 SendMessage()호출후 m_a값은
0가 아니고 9가 된다는 점..
이게 PostMessage()일때는 0가 될것을..
O_O; 음...
이건 아주 중요한 문제가 될수 있으니 잘 생각해야 한다.
특히 MFC에서는 메세지핸들러로 메세지 처리가 각각의
함수로 분리되어 있기 때문에 SendMessage()로 인해
전역변수 혹은 멤버변수의 값이 항상 변질될수 있음을
간과해선 안된다.
그래서 말이다.
대부분의 메세지 처리는 PostMessage()로 하는게 좋다.
메세지의 흐름을 부드럽게 하고 위와같은 위험에서
자유로우니까 말이다.
SendMessage()는 즉시 실행이 되는 긴급을 요하거나
로직상으로 꼭 필요할때만 쓰는게 좋다.
음.. 이걸 이야기 하고 나니 좀 안심이 되는군.
^_^;
자. 드디어 본격적으로 MFC쪽으로 시야를 돌려보자.
이제 우리는 MFC가 어떻게 Hello World SDK버전(API로짠거)
을 C++화 시켜 감추었는지, 그 MFC 라는 옷을 확! 벋겨보자.
-_-; (음..여기서 부터 미성년자 관람불가 인가...)
VC++를 설치했다면 파일찾기로 Winmain.cpp 이란 파일을
찾을수 있을거다.
이놈을 보면 MFC가 WinMain()과 같은 작업을 어떻게
처리하는지 알수있다.
// 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는 CWinApp를 구현하므로서 하나의 응용프로그램이 된다.
CWinApp는 CWinThread에서 상속 받았음을 잊지 말도록.
위에서 호출되는 InitInstance()를 주목하시라~
우리는 이 함수에서 윈도우를 만들고 m_pMainWnd에
프로그램의 최상위 부모가될 윈도우 포인터를 세트해야 된다.
SDK에서 CreateWindow() 하는 부분이 바로 여기다!
역시 InitInstance()에서 Create()를 호출해야 한다.
우리는 실제작업에서 대부분 위자드를 사용해서 윈도우를
만드는데 도큐멘트/뷰 구조가 아닌 다이얼로그 베이스로
만들면 이 구조를 쉽게 이해할수 있다.
거기에 보면 CWinApp에서 상속한 우리프로그램의 App클래스에서
MFC위자드가 InitInstance()를 오버라이드해 거기에서
DoModal()로 윈도우를 만들고 그 윈도포인터를 m_pMainWnd에
복사하는걸 볼수 있을거다.
만약 m_pMainWnd에 윈도포인터를 지정하지 않으면
WinMain()이 리턴될꺼란걸 위 소스를 보면 알수있지.
즉 InitInstance()는 윈도우를 만들어라고 생긴 함수다.
우리는 여기서 무조건 윈도우를 맨들어야 한다.
그리고
위 소스에서 보면 알겠지만 InitInstance가 false를
리턴하면 ExitInstance()가 실행되고 Run()을 실행하지
않고 WinMain()이 종료됨을 알수 있다.
그럼 또 감이 팍팍! 오는군..
바로 Run()이 SDK에서 메세지 루프를 구현한 부분이다. 후후후.
앞에서 PeekMessage() 설명하면서 잠시 선을 뵈었지만
우리는 Run을 오버라이드 해서 메세지 루프 처리를
수동으로 할수도 있다. ^^
어쨋든 이정도로 일단 감만 잡아도 일반적인 상황에선
충분하다. 이제 SDK의 WndProc() 부분이 MFC에서 어떻게
구현되어 있는지 볼까?
음...
Wincore.cpp 이란 파일을 찾아보시라.
그러면 CWnd의 소스를 볼수 있을 것이다.
쭉 내려가다 보면
CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
이런 놈을 발견하게 될텐데 이놈이 바로 SDK에서
WndProc와 같은 놈이다!!!! 우하하.
이놈이 뭘 호출하는지 보시라.
바로 아래줄에 있는
CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
함수를 호출한다.
이 부분이 메세지맵을 이용해서 메세지를 우리가 만든 메세지 핸들에
처리되게끔 하는 함수다.
음.. 이 함수를 모두 분석하는건 좀 무리일수도 있겠군..
핵심만 간추리면 이렇다.
윈도우즈에서 발생하는 메세지는 수백가지에 이른다.
우리는 SDK에서 WndProc안의 switch case 문으로 메세지를
처리하는 대신에 메세지핸들러 라는 함수로 이걸 처리하는데
어떻게 메세지가 왔을때 메세지핸들러가 실행되냐면
쉽게말해
발생되는 메세지를 처리할 메세지 핸들러는 메세지엔트리
라는 어떤 배열에 저장되고(함수포인터가 저장되겠지..)
해당메세지가 발생했을때 OnWndMsg()에서는 그 메세지엔트리
배열에서 해당 함수를 실행시키는 거다.
음..
쉽게 말한다고 했는데 어려운가...쩝.
예를 보는게 최고지.
우리는 메세지 맵을 아래와 같이 정의해 쓴다.
// 선언부
DECLARE_MESSAGE_MAP()
// 구현부
BEGIN_MESSAGE_MAP(..)
ON_WM_...()
END_MESSAGE_MAP()
이런 코드..
이건 아시다시피 C++언어도 아니다.
이건 정의된 매크로다.
이 매크로는 컴파일 할때 변경된다!!
그 변경되는 코드들은 GetMessageMap() 함수를 구현시킨다.
그리고 이함수의 구현부는 앞서말한 메세지엔트리 배열을
가져올수 있도록 만든다.
즉 우리는!
메세지엔트리 라는 배열에 다가 원하는 메세지 핸들러를
채워주게 함으로써 MFC 소스코드(OnWndMsg())가 GetMessageMap()
을 통해 우리의 메세지엔트리 배열을 가져갈수 있게끔
해주는 거다.
마치 함수를 오버라이드 해서 구현부를 작성하는 원리처럼.~
헥헥헥..
사실 매세지맵의 내부원리나 그 처리과정은 실제 작업에서
그다지 중요한 지식은 아니다.
그냥 "이정도군.." 정도로 감만 잡고 있어도 된다.
나또한 이정도 밖에 모르고 사실 더 깊이는 알고 싶지도 않다.
알아봤자 MFC 소스코드를 바꾸지도 못하니 우리가 응용할수
있는 범위안에서만 그 교양의 지식으로 남겨 놓는게 좋을것
같다.
뭐 MFC 같은 클래스집합을 손수 한번 만들어 보시겠다는
야심찬 선수가 등장한다면 그 선수는 모두 알아야 겠지만..
메세지..
이놈이야말로 윈도 프로그래밍에서 가장 중요한 놈이자 가장
기본이 되는 놈이다.
이놈을 잘쓰면 윈도 프로그램 잘 만드는 거고 그렇지 못하면
못많드는 정도가 아니라 뒤죽박죽 날날이 프로그램이 된다.
win16에서는 메세지 루프와 관련해서 멀티테스킹을 했다.
그만큼 메세지의 처리와 사용은 성능면에서나 구조면에서
지대한 영향을 미친다. 그것보다 무서운건 버그를 만드는
주범이 될 확률도 높다는 것이다.
메세지에 대해서는 언제나 자신 있도록 관련서적도 참고해서
마스터 하는것이 좋지 않을까...
-감사합니다.-
--------------------------------------------------------------
휴...
다적고 보니 좀 길군요. ^^;
다시 읽어보며 좀 줄이긴 했지만 지루하지 않았는지 걱정됩니다.
늦은밤에 끄적거리는 거라 또 오타나 실수가 있을것
같아 미안하군요. -_-;
또 뭔가 많은걸 담아보려고 나름데로 고민 했는데 다시 읽어보니
다른분들에게 별 도움될만한게 많이 없는것 같아 죄송하구요.
앞으로 스레드에 대해 이야기할 기회가 있으면 그때 나머지
부분까지 이야기 할수 있도록 하겠습니다.
아무쪼록 이글을 읽으시는 모든분에게 도움이 되었으면 하는
바램 입니다. 감사합니다.
-태권브이-
2000/2/4
--------------------------------------------------------------
------------------------------------------------------------
지긋지긋한 추위속의 겨울이 한창입니다.
뜨거웠던 지난여름이 어제 같은데.. 정말 계절의 변화란
신기합니다.
저는 계절의 변화를 그림자의 길이로 가장 먼저 느끼는
버릇이 있습니다. 가령 그림자가 길어지면 겨울이 온다는걸
느끼죠. 햇빛의 입사각에 따라 계절의 분위기를 느끼는 건데
몇일전
정오를 약간 지난 시각에 봄의 분위기를 느낄수 있었습니다.
한창의 겨울은 곧 봄이 옴을 암시하기도 하는거죠.
프로그래머의 실력도 그렇지 않을까 합니다.
한참 능력의 한계에 숨이 턱턱 차오를때,
바로 그때가 한계단 올라서는 순간이 머지 않을때 이기도 하죠.
^_^;
자 .. 한계단 위로, 우리 같이 힘을 냅시다.
4번째 이야기를 해보겠습니다.
-------------------------------------------------------------
- 윈도 프로그램의 시작과 끝. "Message" -
DOS에서의 C프로그램을 기억하는가?
그 유명한 Hello World!
1:void main (void)
2:{
3: printf("Hello World");
4:}
C/C++ 프로그래머 라면 누구나 제일먼저 접했을 만한 코드다.
프로그램은 1번 라인에서 시작해 3번 라인을 수행하고
장렬히 전사한다.
위에서 아래로.. 순서데로 실행한다.
도중에 함수를 만나면 함수안으로 들어갔다가 나와서
계속 길을 간다.
그러다 길이 끝나면 종료한다.
이러한 전통적인 스타일은 거의 모든 프로그래밍 언어에서
사용되는데 C++도 물론이다.
프로그램을 실행시킬때 OS는 main() 함수 한개를 실행하고
프로그램을 끝낸다. 우리가 만드는 모든 프로그램은 main()
함수가 끝나게 되므로써 운명하신다.
우리가 만드는 코드는 main() 이 종료하기 전에
무슨짓을 하도록 하는거다.
이렇게 OS가 프로그램을 실행할때 최초의 시작지점을
엔트리 포인트(Entry Point) 라고 한다.
main() 이 곧 엔트리 포인트지.
그런데 Windows 프로그래밍에서는 main() 이 안보인다.(물론 main도 된다)
흔히 말하는 API로 만들면(SDK) WinMain() 이 있다.
특히 MFC로 만들면 WinMain() 도 없다.
Windows가 프로그램을 실행할때 코드의 시작지점인 엔트리포인트는
도대체 어디인가!!!!!!!
(MSC 6.0 이던가, 7.0 이던가?
처음 윈도우즈 프로그래밍을 접했을때
나는 심각한 정신착란 증세 및 정서불안에 시달렸다. --;
바로 엔트리 포인트 때문에......
시간이 아주 많이 지나서야 겨우 원리를 깨닫게 되었지만
main()에서 시작해 main()에서 끝나는데 길들여졌던 나로서는
완전 딴세상 이었다.
특히 MFC를 처음배울때는 그나마 WinMain()도 없어서
제2의 정신적 붕괴를 경험해야 했다..)
내가 지금 하고 싶은 얘기가 뭐냐?
바로 엔트리포인트를 알자는 거다.
MFC를 알기전에 반드시!
알아야 하는 코드가 있으니
그것이 "Hello World" Windows 판 이다.
헬로월드.
1:int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
2: LPSTR lpszCmdLine, int nCmdShow )
3:{
4: static char szAppName[] = "Hello World";
5: WNDCLASS wc;
6: HWND hWnd;
7: MSG msg;
8:
9: wc.style = 0;
10: wc.lpfnWndProc = (WNDPROC)WndProc;
11: wc.cbClsExtra = 0;
12: wc.hInstance = hInstance;
13: wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
14: wc.hCursor = LoadCursor( NULL, IDC_ARROW );
15: wc.hbrBackground = COLOR_WINDOW + 1;
16: wc.lpszMenuName = NULL;
17: wc.lpszClassName = szAppName;
18:
19: RegisterClass( &wc );
20:
21: hWnd = CreateWindow( szAppName,
22: szAppName,
23: WS_OVERLAPPEDWINDOW,
24: CW_USERDEFAULT,
25: CW_USERDEFAULT,
26: CW_USERDEFAULT,
27: CW_USERDEFAULT,
28: HWND_DESKTOP,
29: NULL,
30: hInstance,
31: NULL );
32:
33: ShowWindow( hWnd, nCmdShow );
34: UpdateWindow( hWnd );
35:
36: while(GetMessage( &msg, NULL, 0, 0 ))
37: {
38: TranslateMessage( &msg );
39: DispatchMessage( &msg );
40: }
41: return msg.wParam;
42:}
43:
44:
45:LRESULT CALLBACK WndProc( HWND hWnd, UINT message,
46: WPARAM wParam, LPARAM lParam )
47:{
48: PAINTSTRUCT ps;
49: HDC hdc;
50: RECT rect;
51:
52: switch(message)
53: {
54: case WM_PAINT:
55: hdc = BeginPaint( hWnd, &ps );
56: GetClientRect( &rect );
57: DrawText( hdc, "Hello World", -1, &rect,
58: DT_SINGLINE | DT_CENTER | DT_VCENTER );
59: EndPaint( hWnd, &ps );
60: return 0;
61:
62: case WM_DESTROY:
63: PostQuitMessage( 0 );
64: return 0;
65: }
66: return DefWindowProc( hWnd, message, wParam, lParam );
67:}
헥헥헥헥.....
(이거 손수 메모장에서 다 타이프 했슴..)
(여러분도 해보세요. 고통을 같이 합시당. -_-; )
(그리고 소스만 프린트 해서 계속 보셔야 할겁니다.)
왜 MFC 프로그래밍 하는데 이런 C소스를 알아야 하느냐!
하면..
MFC도 이걸 기초로 만들어 졌기 때문이다.
이 소스 이해 못하면, 감히 윈도우즈 프로그래머 라고
밖에나가 이야기 못한다.
진짜다.
농담 아니다.
중요하다.
그냥 C언어에 main() 이 있듯이
윈도우즈에선 WinMain() 이 있다.
즉 Windows에서 윈도 프로그램의 시작은 WinMain() 이다.
main() 도 물론 된다.
그런데 이건 윈도 없는 프로그램이다. Windows 에서 돌아가지만.
우리의 성경 MSDN 은 이렇게 말한다.
"The WinMain function is called by the system as the initial entry point for a
Win32-based application. "
라꼬.
즉 WinMain() 이 엔트리포인트 라는 거다.
WinMain() 역시 이 함수가 끝나면 프로그램 끝난다.
그러면 WndProc() 는 뭐지?
음..그건 그냥 함수다. 함수. WndProc() 는 엔트리포인트 아니다.
좀 중요하고 특별해서 그렇지..
따라서.
위 소스에서
1번라인에서 시작해 42번 라인에 오면
프로그램 끝난다.
헉!!
그런데 WinMain() 안을 보면 WndProc() 함수는 한번도
호출 한적이 없다.
그런데 이거 실행시켜보면 WndProc() 도 동작한다.
도대체 WinMain() 어디서 WndProc() 를 호출했지?
이거 아는데 좀 오래 걸렸다.
알고나서는 몰랐던 시절이 우스웠지만
몰랐던 때는 아주 심각했었다.
정답은 무엇인가.
바로 39번 라인의 DispatchMessage( &msg ); 가 범인이다.
(이 대목에서 "헉! 아니야!" 라고 생각하시는 분은 이글 안보셔도 될듯..)
자.. 이제 슬슬 본론으로 들어가보자.
DispatchMessage() 는 프로그램이 받은 메세지를 윈도우프로시저로
전달하는 함수이다. 프로그램이 뭘 받는다고? 메세지를 받는다고?
그리고 윈도우프로시저로 전달한다고?
음.. 그렇다.
메세지를 전달한다.
아시다시피 윈도 프로그램은 어떤 메세지를 받아 그 메세지에 응답하는
구조로 되어있다.
예를 들어 마우스를 움직이면
프로그램은 "마우스가 움직였다! 우짤래?" 라는 메세지를 받는다.
그러면 우리는 "마우스가 움직이면... 그냥 놔둬.." -_-; 라는
식으로 프로그램을 짜는 것이다.
윈도우(창)를 가지는 프로그램은 반드시 메세지를 처리할수 있도록
프로그래밍 되어야 한다.
누가 우리 프로그램에 메세지를 주는가?
그것은 운영체제가 준다.
또는 우리 스스로 어떤 메세지를 우리 자신에게 던져줄수도 있다.
또 다른 프로그램(윈도우)에게도 던질수 있다.
DispatchMessage() 는 프로그램이 받은 메세지를 WndProc()함수가
처리하도록 메세지를 건네주는 함수이다.
그리고 WndProc() 가 메세지를 처리할때 까지, 즉 WndProc()가
리턴할때 까지 그자리에서 서있다. 유식하게 "블럭킹 되었다"라고 한다.
음.. 그런데 말야.
그냥 WndProc()함수 안에 있는 내용을
DispatchMessage() 가 있는자리에서 처리하지 뭐하러 WndProc()라고
다른 함수를 만들었지?
그건 "코드의 재진입"을 위해서 라고 볼수 있는데 밑에서 설명할거다. ^^;
긴장 풀고... 험험..
(윈도프로그래밍을 시작하고 얼마되지 않았을때
WinMain()에 별짓을 다 해봤는데 지금 기억나는것중 인상적인것이
printf()를 써본것이다.
결과는 어땠을까?
^^;)
자.
이제 DispatchMessage()가 출현하기 까지의 소스코드를 천천히
뜯어보자.
5번라인 에서 WNDCLASS 형의 wc 변수를 만들었다.
WNDCLASS 는 윈도창의 성격과 그 처리에 대한 일련의 정보들을
담아두는 구조체이다.
9번행에서 17번 행까지 무언가를 죽~죽~ 설정했다.
주목할것은 10번행이다. (나머지는 책에 설명이 잘 되어있슴)
WNDCLASS의 lpfnWndProc 멤버는 윈도우가 받는 메세지를
처리할 함수인 "윈도우프로시저"함수의 포인터를 말한다.
소스에서 "윈도우프로시저"함수는 WndProc() 이기에 "WndProc"라고
했다.
WNDCLASS 의 설정을 마친뒤 19번행에서 RegisterClass() 를
호출한다. 여기에 wc 즉 WNDCLASS 구조체를 넘겨준다.
(wc .. 왠지 화장실 생각이 안나는지.. O_O; )
RegisterClass() 는 OS 에게
"내가 윈도우창 만들건데 그 윈도우 관련 정보는 wc에 있어.
니가 wc보구 마음의 준비를 해"
뭐 이정도 되는 함수다.
그리고 드디어 21번행에서 CreateWindow() 를 호출한다.
드디어 윈도창을 하나 만드는 거다.
이함수의 첫번째 인자가 등록된 클래스의 이름. 즉
RegisterClass() 로 등록한 윈도클래스의 이름이다.
CreateWindow() 는 윈도클래스라는 놈에 기반해서 만든다.
윈도 클래스를 등록한다는 자체가 우리만의 독자적인 윈도우
형식(모양,성격 등등..)을 만든다는 뜻이다.
만약 CreateWindow() 에서 윈도클래스를 미리 정의된
윈도클래스(즉,윈도형식)으로 만들수도 있다.
대표적인게 컨트롤들.. 즉 BUTTON 이나 COMBOBOX 같은...
버튼이나 콤보박스도 윈도우 니깐.
자..
그다음 33,34 행에서 윈도를 화면에 보이도록 한다.
그다음이 GetMessage() 어쩌고 나오는데....
여기가 오늘의 핵심이닷!
지금까지 우리는 우리만의 윈도형식으로 윈도클래스를
등록해서 그 윈도클래스로 CreateWindow() 를 이용해 윈도우를
만들었다.
그리고 화면에도 띄웠다.
그리고 이제는 메세지를 받아와야 한다!
메세지를 왜 받아오느냐?
앞서 말했지만 윈도우 프로그램은 메세지를 받아서 그에 응답
하는 구조이기 때문에 내가 금방 만든 윈도우로 날라오는
수많은 종류의 메세지들을 받아야 한다.
이게 그냥 있으면 받아지는게 아니다.
그래서 GetMessage() 라는 함수를 호출하는거다.
GetMessage() 는 무슨짓을 하는가?
음..
우리가 윈도우를 하나 만들면,
그에 따라 "메세지큐" 라는걸 운영체제가 생성해 준다.
"메세지큐"는 나만의 바구니 라고 보면 된다.
운영체제가 나에게 넘겨주는 수많은 메세지를 담는 바구니.
GetMessage() 는 그 바구니에 담긴 메세지를 집어오는 함수다.
그런데 집어오는 순서가 있다.
"메세지큐" 라는 이름에서 "큐" 는 선입선출 구조.
즉 먼저 들어온 놈을 먼저 꺼낸다는 거다.
좀더 깊숙히 살펴볼까?
여러가지 이유로 발생하는 메세지들은 처음에 운영체제가
가진다. 운영체제는 이들 메세지 들이 누구에게 갈것인지를
분류하고 해당하는 윈도우의 메세지큐에 넣어주는 것이다.
마치 운영체제는 우체국이고 메세지큐는 각각 집에 있는
우편함 이라고나 할까?
^^;
자. GetMessage() 가 있는 부분으로 다시 돌아가자.
36에서 40번행을 보면
while(GetMessage(&msg, NULL, 0, 0) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
라고 되어 있다.
WinMain() 이 엔트리 포인트 이고 이 함수가 리턴하면
프로그램이 종료한다고 앞서 이야기 했지.
따라서 while() 문이 종료하면
프로그램이 끝난다는 사실...
즉 메세지큐에서 메세지를 가져오는 작업을 중지하는순간
프로그램은 종료하게 되는것이다.
그럼 GetMessage() 가 false 를 리턴되면 while 문을
빠져나와 종료하겠군..
언제 false 가 리턴되느냐 하면...
WM_QUIT 이란 메세지를 가져오면 리턴된다.
GetMessage() 는 이렇다.
"메세지 있냐?"
"오.. 메세지들이 바구니에서 줄을 서있군.."
"맨 먼저 들어온놈 너! 일루와."
"짜식.. 너 가지고 갈란다."
"true 를 리턴해야지."
그런데
"메세지 있냐?"
"오.. 메세지들이 바구니에서 줄을 서있군.."
"맨 먼저 들어온놈 너! 일루와."
"헉!! 너는 WM_QUIT ... "
"흑흑.. false 를 리턴하자..."
음.. 그런데 말야.
바구니에 메세지가 없으면 어떨까?
다 퍼간후 더이상 들어온 메세지가 없으면..
그럴때 GetMessage() 는 메세지를 가져올때 까지 기다린다.
즉 리턴되지 않는다는 거다.
자.. 어쨋든 GetMessage() 로 메세지 하나를 가져왔다 치자.
그 메세지는 msg 변수에 담겨있다.
그럼
TranslateMessage() 란 무엇인가....
요건 키보드 관련 메세지일 경우 virtual-key 를 키보드
드라이버에 의해서 ASCII 값으로 매핑된 키로 바꾼뒤
다시 메세지큐에(바구니에) WM_CHAR 이란 메세지를 넣는다.
헥헥헥... 숨차다. -o-;
음.. 이거 이야기 하자면 분량이 너무 많으니
자세한건 MSDN을 보시기 바람..
자. 중요한건 DispatchMessage() 이다.
이 놈은 또 뭔가.
이놈이 물건이다.
WinMain() 에서 GetMessage() 로 가져온 메세지를
이제 우리가 처리해야 하는데 이 처리는
여기서 하는게 아니고 "윈도우프로시저" 에서 한다.
우리는 윈도클래스를 만들때 이미 메세지를 처리할
윈도프로시저를 지정해 주었다.
10행을 보시라.
당당하게 WndProc 라는 함수에서 처리하라고.. ^^;
DispatchMessage()는 GetMessage로 메세지큐에서 꺼내온
메세지를 윈도우프로시저로 전달한다.
그리고 윈도우프로시저 함수가 리턴할때 까지 기다린다.
따라서
DispatchMessage() 가 호출되는 순간!!
WndProc() 함수가 드디어 호출되는 것이다...
WndProc() 함수를 잘봐라.
앞에 CALLBACK 이라고 되어있다.
즉 누구에 의해 불려지는 함수란 뜻이다.
누구에 의해?
DispatchMessage() 에 의해.. ^^;
그리고 WndProc() 는 드디어 메세지를 처리하는 것이다.
후.. 오늘따라 왜이리 타이핑이 힘든지..
허리가 아프다.. 읔....
자.....
여기까지 숨차게 달려온 우리들.. 정말 대견하다.
그런데 지금까지의 내용은 앞으로의 내용을 설명하기 위한
전초전 이었다...... -_-;
컥..... 돌던지지 마시길~
음.
우리가 재미있는? 장난을 칠수 있음을 혹시 느끼신분?
^^;
음.. 뭐 시답잖은 거지만
우리는 메세지 조작을 할수 있다는 사실이다..
즉 GetMessage() 후에
DispatchMessage() 를 호출하기 전에
msg 변수의 내용을 바꾸는거다. -_-;
가령 WM_LBUTTONUP 이 들어왔는데 이걸 WM_LBUTTONDOWN 으로
바꿔 버린다던지.. ^^;
히히.
우리 프로그램에서 이런짓 해봐야 뭐가 재미있냐고?
음.. 꼭 여기서 설명해야 할건 아니지만 메세지 HOOK 이란게
있는데 이건 프로그램의 메세지를 가로채는 기술인데
이걸로 아주 기상천외한 짓들을 할수있다...
음.... 그런데 꼭 필요한 경우 외에는 나쁜짓 하지 말것. ^^;
(혹시 메세지 가로체서 네트웍으로 전송하고자 하는 위험한
장난을 생각하신건 아닌지.. 헉!! 말해버렸네.. 쩝)
또 아주 중요한 의미가 있다.
즉 DispatchMessage 하기전에 메세지 필터링이 가능하다.
혹시 MFC 에서 PreTranslateMessage() 라는 오버라이드 가능한
함수를 본적이 있는가?
이 함수가 바로 TranslateMessage() 를 호출하기 전에
호출되는 함수다. API 로 만들면 이런 함수 필요 없겠지만
(직접 TranslateMessasge() 호출하기 전에 작업하면 되니깐)
MFC에선 이 메세지 가져오는 작업이 안보이니 그런 함수가
존재하는 거지.
또 그것 뿐이냐!
후후.. 또있다.
이건 아주 유용하게 쓸수도 있는건데...
GetMessage() 는 메세지큐에 메세지가 없으면 바구니에
메세지가 담길때 까지 리턴되지 않고 기다린다고 했다.
그런데 GetMessage 와 비슷한 역할을 하는 함수가 한개있다.
이름하여 PeekMessage().
요놈도 역시 GetMessage 처럼 메세지큐에서 메세지를
가져오는 함수이긴 한데 요놈은 메세지가 있든없든 무조건
리턴한다.
즉.. 비교하면..
"메세지 있냐?"
"음.. 메세지 한놈도 없네."
"그냥 리턴하자."
이런놈이다.
음.. 감이 팍팍 온다.
즉 GetMessage 쓰면 메세지가 발생하지 않을때에
나는 아무짓도 할수 없는데
PeekMessage를 쓰면 메세지가 있든 없든 계속
while 문이 돌게 되므로 while 문 안에서
계속 반복작업을 할수있다.
이건 백그라운드로 무슨짓을 할때 쓰는건데
스레드 쓰기엔 뭐하고 그냥 심심할때 처리할 일을
하는데 유용하게 쓸수 있다. ^^;
그럼 MFC 에서는 이런짓 못하느냐? 하면
후후.. 당근 된다.
그게 바로 OnIdle() 이란 오버라이드 가능한 함수다.
음.. 이건 스레드 설명할때나 해야 하는건데
오늘은 메세지 설명해야 하므로 일단 요기까지.
담에 쓰레드 파헤칠때 다시 알아보자.
간단히 그 원리만 보여준다.
while ( bDoingBackgroundProcessing )
{
MSG msg;
while ( ::PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
{
if ( !PumpMessage( ) )
{
bDoingBackgroundProcessing = FALSE;
::PostQuitMessage( );
break;
}
}
// let MFC do its idle processing
LONG lIdle = 0;
while ( AfxGetApp()->OnIdle(lIdle++ ) )
;
// Perform some background processing here
// using another call to OnIdle
}
자자..
우리는 이제 메세지를 받아서 처리하는 방법을 배웠다.
이제 메세지를 보내는 방법을 알아볼까?
메세지를 보내는 것은 SendMessage()와 PostMessage()라는
두개의 함수로 가능하다.
즉 메세지를 보내는 방법은 2가지가 있는데 하나가
Send 하는것, 하나는 Post 하는 것이다.
이거 두개가 아주 다르다.
그리고 차이점을 정확히 이해해야 버그를 막는다.
PostMessage() 는 메세지를 해당 메세지큐에 집어넣고
빠져나오는 함수다.
즉 우리가
PostMessage( hDestWnd, WM_MYMSG, 0, 0 );
하게 되면
hDestWnd 윈도의 메세지큐에 WM_MYMSG 라는 메세지를
넣어놓고 돌아온다.
그리고 함수는 리턴되고 계속 작업을 할수 있다.
예를 들어볼까?
Hello World 소스에서 WndProc 의 62번행을 보라.
여기서 PostQuitMessage()를 없애고
PostMessage( hWnd, WM_DESTROY ,0,0) 를 호출하자.
그러면 PostMessage는 WM_DESTROY를 우리의 메세지큐에
넣고 리턴된다.
여기서 혼동하지 말아야 할것이 있다.
앞서 이야기 한것 처럼 메세지루프에서 DispatchMessage()
는 WndProc()가 리턴하기 전까지 중지해 있다는 점이다.
마치 DispatchMessage()가 WndProc()를 호출한것 처럼
말이다.
따라서 지금 PostMessage()를 하더라도
그 순간 메세지 큐에서 GetMessage()해서 DispatchMessage()
가 일어나지 않는다는 점이다.
가끔가다가
WndProc()를 스레드로 착각하는 경우를 보는데
절대 그렇지 않다. 절대 아니다.!!
PostMessage()는 메세지큐에 메세지를 넣었을뿐
아직 프로그램 실행은 PostMessage() 다음줄에 와있을 뿐이다.
64번 행에서 return 0;가 호출되어
WndProc가 종료한후 드디어 DispatchMessage()가 리턴되고
다시 GetMessage() 호출되는 것이다.
절대 스레드 처럼 동시에 실행 된다고 생각하면 안된다.
사실 이게 API로 짤때는 모두 당연하게 이해 했지만
MFC에서는 메세지 핸들러 라는 함수로만 프로그래밍 하는
습관이 있어 메세지루프의 구조가 감추어 졌기 때문에
어디에선가 메세지가 오면 무조건 메세지 핸들러가
실행된다고 생각하는 것 때문에 마치 스레드 처럼
동시에 여러개의 메세지 핸들러가 실행될지도 모른다는
착각을 유발하는 거다.
후.. 절대 그렇지 않으니 주의합시다.
자 이제 SendMessage().
SendMessage()는 틀리다.
사실 이놈은 아주 특별한 함수다.
이놈은 메세지를 메세지큐에 넣지 않는다.
헉!
메세지큐에 넣지 않고 어떻게 메세지처리를 기대하냐고?
후후..
SendMessage()는 마치 세치기와 같은원리다.
즉 GetMessage()는 메세지큐에서 들어온 순서데로 메세지를
가져오는데 SendMessage()는 현재 처리되고 있는 수행을
모두 일시 정지 시키고 SendMessage()로 보낸 메세지를
수행하게 한다.
예를 들어보면 쉽겠지..
63번행을 이번에는 SendMessage( hWnd,WM_DESTROY,0,0)로
바꾸어보자.
그러면 무슨일이 벌어지는가.?
SendMessage()를 호출한 순간 WndProc()가 재호출 된다.
즉 프로그램 실행이 48번행으로 옮겨지는 거다!
그리고 switch()문을따라 다시 62번 행으로 온다.
왜냐면 switch()의 message변수에는 SendMessage()가 보낸
WM_DESTROY가 들어 있으니 말이다.
그리고 다시 63번 행이 실행되겠지..
즉 위와같이 하면 영원히 반복한다는 것을 알수 있겠지?
즉 차이점이 무엇인가...
SendMessage()는 PostMessage()와 달리 메세지큐에 메세지를
넣고 임무를 완수하는게 아니고
무조건 WNdProc를 호출해서 보낸 메세지를 처리한뒤에야
임무를 완수한다는 거다.
만약 63번행에서 WM_DESTROY를 보내지 않고 WM_PAINT 를 보낸
다면 결국 60번 행에서 SendMessage() 때문에 호출된 WndProc가
리턴될때
비로소 SendMessage() 다음줄로 실행을 계속 한다는 거다.
후..
간추리자면 이렇다.
SendMessage()는 해당메세지를 처리하기 위해 WndProc를 직접
호출하는거나 마찬가지란 뜻이다.
즉 이렇게 WndProc()안에서 SendMessage()한 결과로 다시
WndProc()가 호출되는 경우를 "WndProc로의 코드 재진입"
이라고 한다.
우리는 여기서 전역변수(MFC에선 멤버변수가 될수 있겠지) 사용에
중대한 위험요소를 발견할수 있다.
즉 전역변수 int a;가 있다고 치자.
최초에 63번 행에서 SendMessage()를 호출하기 직전
a에 0를 넣어 놓았다고 하자.
그리고 54번행의 WM_PAINT메세지 처리부에서 a를 +1 시킨다고
하자.
그러면 SendMessage(WM_PAINT) 이전에 a==0 이던것이
SendMessage()가 리턴한 후엔 a=1 이 된다는 점을
눈치 챘는가??
MFC로 본다면
::OnDestroy()
{
m_a=0;
SendMessage(WM_PAINT);
MessageBox( m_a );
}
::OnPaint()
{
m_a=9;
}
위에서 SendMessage()호출후 m_a값은
0가 아니고 9가 된다는 점..
이게 PostMessage()일때는 0가 될것을..
O_O; 음...
이건 아주 중요한 문제가 될수 있으니 잘 생각해야 한다.
특히 MFC에서는 메세지핸들러로 메세지 처리가 각각의
함수로 분리되어 있기 때문에 SendMessage()로 인해
전역변수 혹은 멤버변수의 값이 항상 변질될수 있음을
간과해선 안된다.
그래서 말이다.
대부분의 메세지 처리는 PostMessage()로 하는게 좋다.
메세지의 흐름을 부드럽게 하고 위와같은 위험에서
자유로우니까 말이다.
SendMessage()는 즉시 실행이 되는 긴급을 요하거나
로직상으로 꼭 필요할때만 쓰는게 좋다.
음.. 이걸 이야기 하고 나니 좀 안심이 되는군.
^_^;
자. 드디어 본격적으로 MFC쪽으로 시야를 돌려보자.
이제 우리는 MFC가 어떻게 Hello World SDK버전(API로짠거)
을 C++화 시켜 감추었는지, 그 MFC 라는 옷을 확! 벋겨보자.
-_-; (음..여기서 부터 미성년자 관람불가 인가...)
VC++를 설치했다면 파일찾기로 Winmain.cpp 이란 파일을
찾을수 있을거다.
이놈을 보면 MFC가 WinMain()과 같은 작업을 어떻게
처리하는지 알수있다.
// 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는 CWinApp를 구현하므로서 하나의 응용프로그램이 된다.
CWinApp는 CWinThread에서 상속 받았음을 잊지 말도록.
위에서 호출되는 InitInstance()를 주목하시라~
우리는 이 함수에서 윈도우를 만들고 m_pMainWnd에
프로그램의 최상위 부모가될 윈도우 포인터를 세트해야 된다.
SDK에서 CreateWindow() 하는 부분이 바로 여기다!
역시 InitInstance()에서 Create()를 호출해야 한다.
우리는 실제작업에서 대부분 위자드를 사용해서 윈도우를
만드는데 도큐멘트/뷰 구조가 아닌 다이얼로그 베이스로
만들면 이 구조를 쉽게 이해할수 있다.
거기에 보면 CWinApp에서 상속한 우리프로그램의 App클래스에서
MFC위자드가 InitInstance()를 오버라이드해 거기에서
DoModal()로 윈도우를 만들고 그 윈도포인터를 m_pMainWnd에
복사하는걸 볼수 있을거다.
만약 m_pMainWnd에 윈도포인터를 지정하지 않으면
WinMain()이 리턴될꺼란걸 위 소스를 보면 알수있지.
즉 InitInstance()는 윈도우를 만들어라고 생긴 함수다.
우리는 여기서 무조건 윈도우를 맨들어야 한다.
그리고
위 소스에서 보면 알겠지만 InitInstance가 false를
리턴하면 ExitInstance()가 실행되고 Run()을 실행하지
않고 WinMain()이 종료됨을 알수 있다.
그럼 또 감이 팍팍! 오는군..
바로 Run()이 SDK에서 메세지 루프를 구현한 부분이다. 후후후.
앞에서 PeekMessage() 설명하면서 잠시 선을 뵈었지만
우리는 Run을 오버라이드 해서 메세지 루프 처리를
수동으로 할수도 있다. ^^
어쨋든 이정도로 일단 감만 잡아도 일반적인 상황에선
충분하다. 이제 SDK의 WndProc() 부분이 MFC에서 어떻게
구현되어 있는지 볼까?
음...
Wincore.cpp 이란 파일을 찾아보시라.
그러면 CWnd의 소스를 볼수 있을 것이다.
쭉 내려가다 보면
CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
이런 놈을 발견하게 될텐데 이놈이 바로 SDK에서
WndProc와 같은 놈이다!!!! 우하하.
이놈이 뭘 호출하는지 보시라.
바로 아래줄에 있는
CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
함수를 호출한다.
이 부분이 메세지맵을 이용해서 메세지를 우리가 만든 메세지 핸들에
처리되게끔 하는 함수다.
음.. 이 함수를 모두 분석하는건 좀 무리일수도 있겠군..
핵심만 간추리면 이렇다.
윈도우즈에서 발생하는 메세지는 수백가지에 이른다.
우리는 SDK에서 WndProc안의 switch case 문으로 메세지를
처리하는 대신에 메세지핸들러 라는 함수로 이걸 처리하는데
어떻게 메세지가 왔을때 메세지핸들러가 실행되냐면
쉽게말해
발생되는 메세지를 처리할 메세지 핸들러는 메세지엔트리
라는 어떤 배열에 저장되고(함수포인터가 저장되겠지..)
해당메세지가 발생했을때 OnWndMsg()에서는 그 메세지엔트리
배열에서 해당 함수를 실행시키는 거다.
음..
쉽게 말한다고 했는데 어려운가...쩝.
예를 보는게 최고지.
우리는 메세지 맵을 아래와 같이 정의해 쓴다.
// 선언부
DECLARE_MESSAGE_MAP()
// 구현부
BEGIN_MESSAGE_MAP(..)
ON_WM_...()
END_MESSAGE_MAP()
이런 코드..
이건 아시다시피 C++언어도 아니다.
이건 정의된 매크로다.
이 매크로는 컴파일 할때 변경된다!!
그 변경되는 코드들은 GetMessageMap() 함수를 구현시킨다.
그리고 이함수의 구현부는 앞서말한 메세지엔트리 배열을
가져올수 있도록 만든다.
즉 우리는!
메세지엔트리 라는 배열에 다가 원하는 메세지 핸들러를
채워주게 함으로써 MFC 소스코드(OnWndMsg())가 GetMessageMap()
을 통해 우리의 메세지엔트리 배열을 가져갈수 있게끔
해주는 거다.
마치 함수를 오버라이드 해서 구현부를 작성하는 원리처럼.~
헥헥헥..
사실 매세지맵의 내부원리나 그 처리과정은 실제 작업에서
그다지 중요한 지식은 아니다.
그냥 "이정도군.." 정도로 감만 잡고 있어도 된다.
나또한 이정도 밖에 모르고 사실 더 깊이는 알고 싶지도 않다.
알아봤자 MFC 소스코드를 바꾸지도 못하니 우리가 응용할수
있는 범위안에서만 그 교양의 지식으로 남겨 놓는게 좋을것
같다.
뭐 MFC 같은 클래스집합을 손수 한번 만들어 보시겠다는
야심찬 선수가 등장한다면 그 선수는 모두 알아야 겠지만..
메세지..
이놈이야말로 윈도 프로그래밍에서 가장 중요한 놈이자 가장
기본이 되는 놈이다.
이놈을 잘쓰면 윈도 프로그램 잘 만드는 거고 그렇지 못하면
못많드는 정도가 아니라 뒤죽박죽 날날이 프로그램이 된다.
win16에서는 메세지 루프와 관련해서 멀티테스킹을 했다.
그만큼 메세지의 처리와 사용은 성능면에서나 구조면에서
지대한 영향을 미친다. 그것보다 무서운건 버그를 만드는
주범이 될 확률도 높다는 것이다.
메세지에 대해서는 언제나 자신 있도록 관련서적도 참고해서
마스터 하는것이 좋지 않을까...
-감사합니다.-
--------------------------------------------------------------
휴...
다적고 보니 좀 길군요. ^^;
다시 읽어보며 좀 줄이긴 했지만 지루하지 않았는지 걱정됩니다.
늦은밤에 끄적거리는 거라 또 오타나 실수가 있을것
같아 미안하군요. -_-;
또 뭔가 많은걸 담아보려고 나름데로 고민 했는데 다시 읽어보니
다른분들에게 별 도움될만한게 많이 없는것 같아 죄송하구요.
앞으로 스레드에 대해 이야기할 기회가 있으면 그때 나머지
부분까지 이야기 할수 있도록 하겠습니다.
아무쪼록 이글을 읽으시는 모든분에게 도움이 되었으면 하는
바램 입니다. 감사합니다.
-태권브이-
2000/2/4
--------------------------------------------------------------
[출처] ◆◆ V 의 노트 -4편- ◆◆ |작성자 절쉐미남
'API' 카테고리의 다른 글
확장 dll 만들기 및 lib 파일 존재 이유 (0) | 2008.08.29 |
---|---|
DLL 테스트할 샘플 코드 만들기 - 프로젝트 병합 살펴볼 것 (1) | 2008.08.29 |
스레드 V의 노트 (0) | 2008.08.15 |
## SendMessage & PostMessage ## (0) | 2008.08.15 |
윈도우 프로시져 (1) | 2008.08.15 |
함수,변수 표기법(헝가리안) (2) | 2008.08.15 |
레지스트리 쓰기, 삭제 함수 (1) | 2008.08.13 |