출처: http://wiki.rabidus.net/ow.asp?p=WindowsMessageFlow&revision=5

WindowsMessageFlow

FrontPage | RecentChanges | TitleIndex | UserPreferences | FindPage | Edit this page (last edited 2005-2-28)(diff)


Description

윈도우즈 프로그래밍에서 Message가 어떻게 발생되서 프로시저로 전달되는지에 대한 설명이다.

Window와 Message queue는 쓰레드 종속적이다.

윈도우즈에서 모든 객체의 소유권은 쓰레드가 가지고 있다. 윈도우를 생성한 쓰레드가 종료되면 윈도우도 따라 종료된다 따라서 윈도우에 메세지를 주는 메세지 큐는 해당 윈도우를 가지고 있는 쓰레드가 가지고 있다. 각 쓰레드는 한 쓰레드만 실행하는 것같은 환경에서 실행되어야 한다. 각 쓰레드는 다른 쓰레드에 영향을 받지 않는 메세지 큐를 가지고 있어야 하면 각 쓰레드는 키보드 포커스, 윈도우 활성화, 마우스 캡처등의 개념을 유지하는 모의 환경을 가지고 있어야 한다.

Windows의 기본적인 메세지 종류

윈도우즈는 기본적으로 메세지를 끊임없이 주고 받는 운영체제이다. 각종 이벤트(키보드, 마우스 입출력, 윈도우 사태변경등등)에 메세지가 발생하며 전달되고 처리된다. 메세지는 시스템 메세지 큐와 쓰레드 메세지 큐를 거치는 queued message와 해당 윈도우즈의 WNDCLASS에 등록된 프로시저에 전달되는 non-queued message로 구분된다.

함수로는 같은 쓰레드의 윈도우즈에 대해 호출되는 SendMessage?()가 non-queued message이며 PostXXXMessage?()류와 다른 쓰레드의 윈도우에 사용되는 SendMessage?()가 queued message이다.

메세지의 종류로는 queued message의 경우 키스트로크 (WM_KEYDOWN / WM_KEYUP등)과 키스트로크에 의한 문자(WM_CHAR), 마우스 이동(WM_MOUSEMOVE), 마우스 클릭(WM_LBUTTONDOWN), 타이머 메세지(WM_TIMER), 그리기 메세지 (WM_PAINT), 종료 메세지(WM_QUIT)가 있으며 non-queued message는 나머지 다른 메세지들이다. 종류로는 윈도우즈의 특정함수의 대한 메세지나 윈도우의 상태인 WM_CREATE, WM_SIZE, WM_SHOWWINDOW등이며 메뉴아이템의 선택결과인 WM_COMMAND메세지도 큐에 들어가지 않는다.

기본적인 메세지의 흐름도

base_message_flow.jpg

Windows는 쓰레드가 UI관련작업을 할 경우 THREADINFO를 생성하여 메세지큐를 쓰레드에 할당한다.

초기 쓰레드 생성시에는 UI에 필요한 리소스 생성작업을 하지 않는다. 하지만 쓰레드가 UI관련작업(메세지큐 체킹(GetMessage?()), 윈도우를 생성(CreateWindow?))을 할경우 THREADINFO라는 Undocumened 구조체를 할당한다. 만약 하나의 프로세스가 세개의 쓰레드를 생성하고 각 쓰레드가 CreateWindow?를 할경우 쓰레드별로 메세지큐가 생성된다.

다음 구조로 되어 있으며 아래 표와 그림을 참고하자.

threadinfo_struct.jpg

Posted-Message queue PostMessage?(), PostThreadMessage?()에 의해 Posted-Message queue에 메세지가 등록된다
Virtualized Input queue ...
Send-Message queue pointer 다른 쓰레드에서 SendMessageXXX?()함수에 의해 메세지가 등록된다.
Reply-Messsage queue pointer SendMessageXXX?()에 대한 다른쓰레드의 응답이나 ReplyMessage?()함수에 의해 메세지가 등록된다.
Wake Flags 메세지가 포스트 될때 어떤 메세지가 큐에 셋팅되어 있는지 알리는 flag이다. PostMessage?()의 경우 QS_POSTMESSAGE에 해당하는 비트셋을 셋팅한다. 만일 아무런 비트가 셋팅되어 있지 않다면 CPU는 스케줄링을 조절한다.

해당 윈도우에 메세지 Post하기

기본적으로 post를 하여 메세지를 보내는 방법은 비동기적이다. 즉 메세지를 post하고 바로 리턴하는 모델이다.

PostMessage(HWND, UIMT, WPARAM, LPARAM);

(1) 먼저 PostMessage?()에 전달하고자 하는 윈도우의 HWND와 MSG 그리고 파라미터를 셋팅하여 호출한다.

(2) 시스템은 다음 MSG구조체를 저장할 공간을 할당하고 파라미터로 넘어온 데이터를 시스템 메세지 큐에 저장한다.

typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

(3) 시스템은 hwnd에 해당하는 윈도우를 가진 쓰레드를 알아낸다

(4) 해당 쓰레드의 포스트 메시지 큐에 블록의 주소를 추가하고 THREADINFO의 Wake Flags의 QS_POSTMESSAGE의 비트를 셋팅한다. 후에 바로 리턴하며 해당 프로시저가 이 메세지를 처리할지는 알수없다.

(5) 해당 쓰레드에서 GetMessage?()나 PeekMessage?()를 호출하여 MSG구조체에 데이터를 채운후에 DispatchMessage?()를 통하여 해당 윈도우의 프로시저로 메세지를 보낸다.

DWORD PostThreadMessage(DWORD, UINT, WPARAM, LPARAM);

(1) 첫번째 파라미터인 쓰레드 아이디의 메세지큐에 HWND값은 NULL인체로 포스트된다. HWND가 NULL인경우는 특정윈도우에게 보내는 것이 아니라 해당 쓰레드가 가지고 있는 윈도우와 프로시저등에 메세지를 전달하는 것으로 반드시 윈도우를 생성하지 않더라도 쓰레드가 메세지 기반으로 작동중이면 이 함수를 이용해서 메세지를 전달 할 수 있다. 대표적인 예가 MFC의 CWindThread?이며 이 클래스는 UI가 있는 쓰레드와 어떤 특정 잡을 처리하는 Walker 쓰레드로 구분된다.

(2) 큐를 가지지 않는 Walker 쓰레드는 메세지를 받지 못한다.

DWORD PostQuitMessage(int);

(1) VOID PostThreadMessage(GetCurrentThreadId(), WM_QUIT, nExitCode, 0)과 같은 코드이다.

(2) 해당 쓰레드의 모든 윈도우는 종료 메세지를 받는다. HWND가 NULL임으로 GetMessage?루프에서 처리하는 것이 보통이다.

while(GetMessage(&msg, 0, 0, 0)) {
if (msg.hwnd == NULL) {
// 쓰레드에 보내진 메세지다.
}
else {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

해당 윈도우에 메세지 Send하기

메세지를 Send하는 모델은 동기적인 방법이다. 따라서 함수를 호출한 후에 해당 처리 프로시저가 메세지를 처리하면 리턴되어 다음 실행을 하는 모델이다. 그래서 같은 쓰레드의 다른 윈도우의 프로시저에 메세지를 send하는것과 다른 프로시저의 쓰레드에 메세지를 send하는것은 동기화 문제를 가지고 있는데 윈도우즈는 다음과 같이 처리한다.

같은 쓰레드의 윈도우에 메세지 Send하기

같은 쓰레드의 윈도우에 SendMessage?()를 호출하면 Send message queue에 추가되는 것이 아니라 해당 윈도우의 프로시저를 직접 호출하여 프로시저가 처리를 다하면 리턴한다.

다른 쓰레드의 윈도우에 메세지 Send하기

BOOL SendMessage(HWND, UINT, WPARAM, LPARAM)

다른 쓰레드의 윈도우의 경우 다른 프로세스의 윈도우의 경우 직접적으로 SendMessage?()가 해당 윈도우의 프로시저를 호출할 수 없다. 그래서 이경우에는 다른 방법으로 동기화를 맞춘다.

(1) 일단 시스템 메세지 큐를 거쳐 해당 윈도우의 쓰레드의 Send message queue에 메세지가 추가되고 Wake Flag에 QS_SENDMESSAGE 비트를 셋팅한다. 중요한것은 SendMessage?()의 동기를 맞추기 위해서 시스템은 인터럽트를 사용하지 않으며 GetQueueStatus?()를 이용한 쓰레드의 메세지 큐에서 메세지를 꺼내는 알고리즘에 따라 작동한다. QS_SENDMESSAGE의 경우 제1순위임으로 Send message queue에 쌓여 있는 메세지를 해당 프로시저가 처리하게 된다.

(2) 이때 SendMessage?()를 호출한 쓰레드는 바로 리턴하지 않으며 SendMessage?()로 메세지를 전달한 대상 프로시저가 메세지를 처리한후에 자신의 쓰레드의reply message queue에 처리되었다는 메세지와 리턴값을 기다리며 실행중이다.

(3) 응답메세지가 상대 프로시저에 의해서 reply send queue에 들어오면 리턴값을 리턴하며 호출을 종료한다.

(4) 중요한것은 블럭킹이 걸려 있는 동안에도 시스템은 다른 쓰레드에서 보내어진 메세지는 send message queue에 쌓인다.

BOOL SendMessageTimeout(HWND, UINT, WPARAM, LPARAM, UINT flags, UINT timeout, PDWORD_PTR result)

기다릴 최대 시간과 기다리는 방법을 지정하여 SendMessage?를 할 수 있다.

BOOL SendMessageCallBack(HWND, UINT, WPARAM, LPARAM, SENDASYNCHPROC, ULONG_PYT)

쓰레드간의 메세지를 (interthread message) 가능하게 하는 함수로 SendMessageCallBack?을 호출하면 리턴되며 타켓 프로시저가 작업이 끝나고 reply message queue에 응답이 오면 등록된 콜백 함수를 호출한다. 콜백 함수의 프로토타입은 다음과 같다. VOID CALLBACK ResultCallBack(HWND, UINT, ULONG_PTR, LRESULT);

BOOL SendNotifyMessage(HWND, UINT, WPARAM, LPARAM)

메세지를 받는 쓰레드의 메세지 큐에 메세지를 추가하고 바로 리턴하지만 PostMessage?와는 2가지 경우에 따라 다르다.

 - 메세지 처리 우선순위가 높다.
 - 같은 쓰레드의 윈도우에게 보낼경우 SendMessage?()와 동일한 작업을 한다.

SendMessageXXX()를 통한 프로시저 호출에 대해 응답하기

SendMessageXXX?()의 함수등에서 호출당한 프로시저에서 BOOL ReplayMessage(LRESULT lResult);를 호출하면 바로 SendMessageXXX?()를 호출한 쓰레드로 작업완료를 알려주어 블럭킹을 풀리게 한다. 만약 동일한 쓰레드에서 호출된 SendMessageXXX?()의 경우 아무런 일도 일어나지 않는다.

SendMessageXXX()에서 보내진 메세지의 종류 알기

BOOL IsSendMessage();

같은 쓰레드에서 보내진 경우라면 TRUE, 다른 쓰레드라면 FALSE

BOOL IsSendMessageEx(PVOID)

파라미터의 리턴값으로 동일 쓰레드에서 호출한 SendMessageXXX?()인지 다른 쓰레드라면 어떤 종류의 SendMessageXXX?()인지를 알 수 있다.

모든 윈도우에 메세지 보내기

모든 윈도우에게 메세지를 보내는 방법은 long BroadcastSystemMessage(DWORD, LPDWORD, UINT, WPARAM, LPARAM)를 사용하여 수신자 종류를 지정해서 할수 있으며 SendMessageXXX?()함수에서 HWND_BROADCAST를 윈도우 핸들로 넘김으로서 가능하다.

하드웨어 입력을 통한 메세지 전달

쓰레드 큐에서 메세지를 꺼내는 알고리즘

기본적으로 메세지 큐를 가지고 있는 쓰레드에서 메세지 큐에 메세지가 없다면, 즉 THREADINFO구조체의 Wake Flag에 아무런 메세지 셋팅이 없다면 쓰레드는 스케줄링이 되지 않는다. 즉 메세지루프 내의 GetMessage?와 PeekMessage?함수는 UI관련된 작업을 수행하기전까지 Sleep상태에 빠진다.

만약 메세지가 큐에 들어온다면 GetMessage?와 PeekMessage?

DWORD GetQueueStatus(UINT flags)함수를 이용하여 정해진 알고리즘에 따라 메세지를 처리한다. 리턴값의 HIWORD는 현재 남아 있는 메세지의 타입의 플래그 값이다. 따라서 다음의 코드의 경우 WM_TIMER값이 있는지 확인하고 WM_PAINT값이 있는지도 리턴값을 통해 확인한다.
{{{BOOL IsPaintMsg = HIWORD(GetQueueStatus(QS_TIMER)) & QS_PAINT

리턴값의 LOWORD는 GetQueueStatus?, GetMessage?, PeekMessage?의 호출이후 진행되지 않았던 메세지의 타입을 가르킨다. 다음 표의 플래그는 어떤 메세지가 큐에 있는지를 나타내준다.

QS_KEY WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP, or WM_SYSKEYDOWN
QS_MOUSEMOVE WM_MOUSEMOVE
QS_MOUSEBUTTON WM_?BUTTON* (Where ? is L, M, or R, and * is DOWN, UP, or DBLCLK)
QS_MOUSE QS_MOUSEMOVE | QS_MOUSEBUTTON와 같다.
QS_INPUT QS_MOUSE | QS_KEY와 같다.
QS_PAINT WM_PAINT
QS_TIMER WM_TIMER
QS_HOTKEY WM_HOTKEY
QS_POSTMESSAGE 포스트된 메세지
QS_ALLPOSTMESSAGE 포스트된메세지
QS_ALLEVENTS QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY와 같다
QS_QUIT PostQuitMessage?에 의해 이 플래그 켜진다.
QS_SENDMESSAGE 다른쓰레드에서 Send된 메세지
QS_ALLINPUT QS_ALLEVENTS | QS_SENDMESSAGE와 같다

시스템은 웨이크 플래그의 성격에 따라 다르게 다룬다. 다음은 웨이크 플래그와 다루는 방법이다.

QS_MOUSEMOVE, QS_KEY, QS_MOUSEBUTTON, QS_HOTKEY의 입력관련 플래그 큐안에 메세지가 있는한 플래그는 유지되며 각 입력장치별로 메시자가 없다면 각 플래그별로 꺼진다
QS_PAINT 이 플래그는 해당 쓰레드의 윈도우가 Invalidate(무효)영역을 가질때 켜지며 ValidateRect?(), ValidateRegion?, BeginPaint?()등에 의해서 보여지는 윈도우가 모두 Validate가 되면 플래그는 꺼진다
QS_POSTMESSAGE 하나이상의 Post된 메세지가 있는 경우 켜지며 없을경우 사라진다
QS_TIMER 타이머가 설정될떄 켜지며 WM_TIMER가 꺼내어진후 플래그가 꺼지며 타이머는 설정된다.
QS_SENDMESSAGE 외부 쓰레드에서 온 Send메세지가 있다면 켜진다

GetMessage?나 PeekMessage?함수가 이런한 웨이크 플래그를 체킹하여 어떤 메세지가 실행해야 될지는 다음 알고리즘 처리 순서(우선순위)에 따르며 그 방법을 도식화한 그림이다.

(1) QS_SENDMESSAGE검사하여 적절한 윈도우 프로시저로 보내고 더이상의 Send된 메세지가 없다면 해당플래그는 꺼지고 GetMessage?리턴되지 않으며 다음 플래그를 검사한다.

(2) QS_POSTMESSAGE를 검사하여 있다면 MSG구조체에 데이터를 복사하고 더이상의 Post된 메세지가 없다면 플래그는 꺼지고 GetMessage?함수는 TRUE로 리턴된다.

(3) QS_QUIT를 검사하여 있다면 MSG구조체에 데이터를 복사하고 해당 플래그는 꺼지고 GetMessage?함수는 FALSE로 리턴되어 메세지 루프를 빠져나간다.

(4) QS_INPUT를 검사하여 관련 메세지가 있다면 MSG구조체를 채우고 키보드 메세지가 더 이상없다면 QS_KEY플래그가 꺼지고, 마우스 비턴 관련 메세지가 큐에 없다면 QS_MOUSEBUTTON이 꺼지고 마우스 이동관련 메세지가 없다면 QS_MOUSEMOVE가 꺼지고 GetMessage?함수는 TRUE로 리턴한다. 이때 WM_CHAR의 경우에는 다음과 같은 과정을 거쳐서 읽혀지게 된다.

1. 키보드를 누르면 WM_KEYDOWN / WM_KEYUP이나 WM_SYSKEYDOWN / WM_SYSKEYUP이 VIQ queue에 넣어지게 되고 QS_KEY플래그가 셋팅된다.
2. GetMessage가 WM_XXXKEYDOWN 메세지 정보를 가지고 리턴하면 DispatchWindow전에 TranslateMessage를 통하여 검사되고 가상키정보가 문자정보로 번역되어 Post큐에 WM_CHAR / WM_SYSCHAR로 입력된다.
3. 다음 GetMessage함수는 Post큐에서 WM_CHAR / WM_SYSCHAR를 읽게되고
4. 다음 GetMessage함수는 마지막 입력 이벤트 메세지인 WM_XXXKEYUP을 읽게 되어 프로시저에게 WM_XXXKEYDOWN, WM_CHAR / WM_SYSCHAR -> WM_XXXKEYUP을 전달하게 된다.

(5) QS_PAINT를 검사하여 켜져 있으면 MSG구조체를 데이터가 복사되고 해당 쓰레드의 윈도우 영역이 Validate상태가 되면 꺼지고 GetMessage?함수는 TRUE로 리턴된다.

(6) QS_TIMER를 검사하여 켜져 있으면 MSG구조체에 데이터가 복사되고 타이머는 리셋되며 QS_TIMER 플래그는 꺼진고 GetMessage?함수는 TRUE로 리턴된다.

msg_proc_algo.jpg

Windows 종료시에 발생하는 Message 순서

WM_QUIT, WM_CLOSE, WM_DESTORY메세지나, 마우스를 이용한 시스템메뉴의 종료버튼을 누르거나, 키보드의 ALT-F4에 의해서 생기는 종료에 대한 시퀀스다.

(1) 시스템 종료 이벤트 발생하여 WM_SYSCOMMAND메세지를 프로시저에 전달하고 프로시저는 DefWindowsProc()으로 전달한다.

(2) DefWindowsProc()은 응답으로 WM_CLOSE를 프로시저에 전달하고 프로시저는 DefWindowsProc()에 다시 전달한다.

(3) DefWindowsProc()WM_CLOSE에 대한 응답으로 DestroyWindow()호출하고 이 함수는 WM_DESTROY를 프로시저에 전달한다.

(4) 프로시저는 이 메세지가 발생할 경우 PostQuitMessage()를 호출하게 작성하며 함수가 호출될 경우 WM_QUIT가 전달되어 메세지 루프가 종료된다.

따라서 운도우즈에서 종료 확인을 검사할 경우 WM_CLOSE에서 하는 것이 좋다.

Walker쓰레드에 메세지큐와 커널오브젝트(이벤트)를 사용하여 잡처리루프 만들기

GetMessage?나 PeekMessage?의 경우 UI관련된 작업외에는 모두 Sleep상태에 둔다 하지만 메세지에 기반에 UI작업외에 다른 방법도 같이 사용하여 쓰레드를 컨트롤 할 수 있는데 이떄 사용하는 것이 다음의 함수이다. 이벤트 시그널과 메세지 풀링을 동시에 할수 있으며 GetMessage?와 PeekMessage?를 모두 사용 할 수 있다. 더 자세한것은 MSDN을 참고하자.

DWORD MsgWaitForMultipleObjectsEx(DWORD, PHANDLE, DWORD, DWORD, DWORD)

다음은 적절히 사용하는 코드이다.

BOOL fQuit = TRUE;

while(fQuit) {
DWORD dwResult = MsgWaitForMultipleObjectEx(1, &hEvent, INFINITE, QS_ALLEVENTS, MWMO_INPUTAVAILABLE);
switch(dwResult) {
case WAIT_OBJECT_0: // 이벤트가 시그널되었다.
break;
case WAIT_OBJECT_0 + 1: // 메시지가 큐에 있다.
{
MSG msg;
while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
fQuie = FALSE;
}
else {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
break;
}
}
}

메세지로 데이터 보내기

메세지의 파라미터로 특정 데이터의 주소를 넘겨주는 경우 다른 프로세스라 할지라도 반영이 되는 경우가 있다. 기본적으로 자기 프로세스의 공강에 공유된 메모리 매핑파일을 설정함으로서 가능한데 이때 아무런 설정없이 기본메세지로만 가능한 경우는 메세지의 종류가 알려진 경우이며(WM_SETTEXT, WM_GETTEXT) 다른 사용자 정의 메세지의 경우 시스템은 메모리 매핑파일을 위한인지 포인터를 전달하는건지 모르게 된다. 이런 경우에는 WM_COPYDATA메세지를 이용해서 가능해진다.

다음과 같이 사용하며, 반드시 SendMessage?를 사용해야 하며 사용시 다른 프로세스의 주소 공간에 복사본을 만드는 작업을 한다.

COPYDATASTRCUT cds;
//cds.dwData; // 이 멤버는 보내는 사람이 아무 값이 넣어 유용하게 사용하려는 뜻의 변수이다. 데이터의 형이나 정보등을 넣을수 있다.
cds.cbData = sizeof(send_data);
cds.lpData = send_data_ptr;
SendMessage(hwnd_receiver, WM_COPYDATA, (WPARAM) hwnd_sender, (LPARAM) &cds);

Reference

Documents

Website

Programming Application for window 5th

Win32 API Programming With Visual Basic

Books

AND