LeeWonTae 2014. 9. 18. 17:28

1.윈도우 프로시저

 

윈도우 프로시저(Window Procedure)

윈도우 프로시저는 윈도우 클래스당 하나씩(윈도우당 하나씩이 아니라) 배정되며

메시지에 대응하는 방식을 정의함으로써 윈도우의 행동양식을 결정한다.

윈도우 프로시저는 다음과 같은 원형을 가는 함수이다.

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

윈도우 프로시저는 재진입(ReEntrant)이 가능한 함수이다.

재진입이 가능하다는 것은 WndProc 실행 중에 또 WndProc이 호출 될 수 있다는 뜻이다.

 

메시지 큐

메시지는 시스템이나 사용자에 의해 발생한다.

메시지는 크게 메시지 큐로 들어가는 큐(Queued) 메시지와 큐에 들어가지 않고 곧바로 윈도우 프로시저로 보내지는

비큐(Non Queued) 메시지로 구분된다.

큐 메시지의 가장 큰 특징은 입력된 순서대로 큐에 쌓여 있다가 차례대로 처리된다는 점이다.

입력된 키를 바로 처리하지 못하는 일은 종종 발생하는데 이때 처리하지 못한 키를 대기시키기 위한

완충 장치로 메시지 큐가 존재하는 것이다.

비큐 메시지는 윈도우에게 특정 사실을 알리거나 명령을 보내기 위해 큐를 통하지 않고

바로 윈도우 프로시저로 보내지는 메시지이며 대부분의 메시지들은 비큐 메시지이다.

메시지 큐와 메시지 루프를 거치지 않으므로 신속하게 처리된다.

운영체제는 하나의 시스템 메시지 큐를 관리하며 또한 각 스레드별로 하나씩 메시지 큐를 생성한다.

잘 알겠지만 큐는 선입 선출(FIFO)의 원칙에 따라 운영되는 자료 구조이다.

즉, 메시지큐는 들어온 순서대로 메시지를 쌓아놓는 곳이다.

시스템 메시지 큐는 시스템 전체에 유일한 메시지 큐이며 모든 큐 메시지는 먼저 이곳에 저장된다.

시스템은 큐의 메시지를 하나씩 꺼내 어떤 스레드로 보낼 메시지인지 판단하여 스레드 메시지 큐로 보내고

시스템 메시지 큐에서 메시지를 지운다.

이 작업을 하는 프로세스를 시스템 아이들(System Idle)이라고 하는데 작업 관리자로 확인해 보면 항상

백그라운드에서 실행중이라는 것을 알 수 있다.

다음은 메시지가 처리되는 전체적인 과정이다.

이 그림에서 스레드 메시지 큐의 왼쪽은 디바이스 드라이버와 운영체제가 알아서 처리하는 부분이므로

개발자는 신경쓸 필요가 거의 없다.

스레드 메시지 큐에 쌓인 메시지만 순서대로 꺼내서 잘 처리하면 된다.

메시지는 최종적으로 윈도우에게 전달되지만 윈도우 하나당 메시지 큐가 하나씩 있는 것이 아님을 유의하자

메시지 큐는 스레드당 하나씩 생성되는 것이다.

그렇다고 해서 모든 스레드가 메시지 큐를 가지는 것은 아니다.

 

메시지 루프

사용자에 의해 입력된 메시지는 시스템 메시지 큐에 일단 저장되고 스레드 메시지 큐로 이동했다가

메시지 루프에 의해 해당 윈도우의 윈도우 프로시저로 보내져 처리된다.

메시지는 최종 처리 직전까지 계속 큐에 유지되는데 이때 메시지는 다음과 같이 정의된 구조체의 형태로 존재한다.

typedef struct tagMSG

{

    HWND            hwnd;

    UINT              message;

    WPARAM       wparam;

    LPARAM        lparam;

    DWORD          time;

    POINT            pt;

이 구조체에는 메시지에 관한 정보 외에도 메시지가 발생한 시간(time)과 발생시의 마우스 좌표(pt)가 들어있다.

그러나 시간과 마우스 좌표는 모든 메시지들이 필요로하는 정보가 아니므로 WndProc까지 전달되지는 않는다.

필요할 경우 다음 두 함수를 사용하여 직접 조사해야 한다.

DWORD GetMessagePos(VOID);

LONG GetMessageTime(VOID); 

메시지루프는 메시지 큐에서 메시지를 꺼내 메시지 처리 함수로 보내는 일을 한다.

보통 WinMain의 제일 끝 부분에 위치하며 다음과 같이 그 형태가 정형화되어 있다.

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

{

    TranslateMessage(&Message);

    DispatchMessage(&Message);

세개의 함수 호출문이 있고 while 루프로 싸여져 있는데 이 루프는 프로그램이 끝날 때까지(WM_QUIT) 계속 반복된다.

GetMessage 함수는 스레드 메시지 큐에 대기중인 메시지를 꺼내 첫 번째 인수로 전달된 MSG 구조체에 복사한다.

TranslateMessage 함수는 가상키 입력을 문자 입력(WM_CHAR)으로 바꾸는 역할을 한다.

DispatchMessage 함수는 메시지를 윈도우 프로시저로 보내 처리하도록 하는데 MSG 구조체의 hwnd 멤버를 보고

정확하게 목적 윈도우의 메시지 처리 함수로 배달한다.

 

PeekMessage

메시지 루프에서 제일 중요한 함수는 메시지를 가져오는 GetMessage 함수이다.

GetMessage 함수의 주요한 세가지 특징을 요약하면 ① 제거한다. ② 대기한다. ③ 양보한다. 로 정리 할 수 있다.

이렇게 GetMessage에서 놀고 있는 시간을 데드 타임(dead time)이라고 하는데 사람보다 CPU가 월등히 빠르기 때문에

데드 타임의 비율이 꽤 높은 편이다.

데드타임을 잘 활용하면 애니메이션이나 기타 틈틈이 해야 할 일을 다른 작업 시간에 영향을 주지 않고도 할 수 있다.

어차피 처리할 메시지가 없는 남는 시간이기 때문이다.

그런데 문제는 GetMessage 함수가 메시지를 받기 전에는 절대로 리턴하지 않기 때문에 데드 타임을 활용할 수 없다는 점이다.

이때는 다음 함수를 사용한다. 

BOOL PeekMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg); 

PeekMessage는 GetMessage의 세가지 특징과 완전히 반대되는 성질을 가지는데

메시지 큐에서 메시지를 꺼내거나 검사하되 메시지가 없더라도 즉각 리턴한다.

즉, 대기하지 않으며 따라서 양보도 할 줄 모른다.

이때 리턴값이 TRUE이면 메시지가 있다는 뜻이며 FALSE이면 메시지가 없다는 뜻이다.

PeekMessage를 사용한 메시지 펌프는 메시지 시스템을 이해하는데도 비중이 높은 주제이며

Win16 환경에서 백그라운드 작업을 처리하는 가장 효율적인 방법이었다.

그러나 Win32에서는 스레드라는 더 좋은 장치가 있어 요즘은 잘 사용되지 않는다.

 

아이들 타임

응용 프로그램이 아무 것도 하지 않고 노는 시간을 아이들 타임(Idle Time) 또는 데드 타임(Dead Time)이라고 한다.

이런 버려지는 아이들 탕미을 잘 활용하면 주기적으로 해야 하는 긴급하지 않은 작업을 틈틈이 나누어 할 수 있다.

아이들 타임 작업의 특징은 수시로 해야 하되 결코 긴급하지 않으며 속도를 희생해 가면서까지 정확할 필요도 없다는 점이다.

 

키 상태 조사하기

WM_KEYDOWN 메시지를 받으면 키가 입력되는 시점을 알 수 있으며 이때 wParam을 읽어 어떤 키가 눌러졌는지 조사한다.

그러나 이 메시지는 키가 입력되었다는 사실을 알릴 뿐이지 키의 현재 상태를 조사하는 것은 아니다.

두 개의 키를 동시에 누를 경우 먼저 누른 키에 대해서는 더 이상 WM_KEYDOWN 메시지가 전달되지 않는다.

WM_KEYDOWN은 키가 눌러질 때 보내지는 메시지이지 키를 계속 누르고 있다고 해서 전달되는 것은 아니다.

다만 한 키를 계속 누르고 있을 때 반복 입력 기능이 있을 뿐이다.

만약 이런 처리를 하려면 키보드 메시지를 받아서는 안되며 직접 키의 상태를 조사해서 적용해야 한다.

키의 현재 상태, 즉 키가 눌러졌는지 아니면 떨어져 있는지를 조사할 때는 다음 두 함수를 사용한다.

SHORT GetKeyState(int nVirtKey);

SHORT GetAsyncKeyState(int vKey); 

GetKeyState는 메시지가 발생했을 때의 상황을 조사하고, GetAsyncKeyState는 이 메시지가 처리될 때의 상황을 조사한다.

주기적으로 키의 상태를 조사하는 함수

BOOL GetKeyboardState(PBYTE lpKeyState); 

 

트리플 클릭

트리플 클릭이란 일정한 범위 내의 화면 좌표를 짧은 시간안에 연속적으로 세 번 누르는 동작을 의미한다.

화면좌표 범위나 시간은 시스템 설정에 따라 다르지만 보통 4픽셀 내외의 위치를

0.5초보다 짧은 간격으로 눌러야 연속 클릭으로 인정된다.

모든 메시지에는 메시지 발생 시간과 마우스 커서 위치가 기록되어 있으므로

이 정보를 읽어 조건을 만족하는지 점검해 보면 여러 번의 연속 클릭을 검출 할 수 있다.

 

 

2. 메시지 통신

 

메시지 보내기와 붙이기

메시지는 주로 사용자에 의해 발생되지만 프로그램 내부에서 윈도우간의 통신을 위해

의도적으로 다른 윈도우에게 메시지를 보낼 수도 있다.

이 때는 다음 두 함수를 사용한다.

BOOL PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

LRESULT SendMessage(HWND hWnd, UINT Msg,  WPARAM wParam, LPARAM lParam);

다른 스레드의 메시지 큐에 메시지를 붙일 때는 다음 함수가 사용된다.

 BOOL PostThreadMessage(DWORD idThread, UINT Msg, WPARAM wParam, LPARAM lParam);

윈도우간에 메시지를 교환할 때 어떤 함수를 사용할 것인가는 신중하게 결정해야 하는데

대부분의 경우는 SendMessage로 보내는 것이 정석이며 또 효율적이다.

또 WM_COPYDATA 같은 메시지는 그 특성상 반드시 SendMessage로만 보내야 하며 PostMessage로 붙여서는 안 된다.

SendMessage는 당장 어떤 일을 하라는 명령이며

PostMessage는 한가해질 때 어떤 일을 하라는 신호라고 할 수 있다.

 

메시지 데드락

SendMessage 함수는 메시지를 받은 윈도우 프로시저가 리턴하기 전에는 리턴하지 않는다.

PostMessage가 붙이는 메시지는 일종의 신호이기 때문에 천천히 처리해도 상관없지만

SendMessage가 보내는 메시지는 일종의 명령이기 때문에 곧바로 처리해야 하기 때문이다.

즉 SendMessage 호출은 곧 서브루틴 호출과 대등하며 실제로 WndProc의 case 하나를 호출하는 것과 같다.

이 특성은 유념해서 기억해 둘 만한 중요한 내용인데 이 특성을 잘 모르면 아주 골치아픈 버그의 원인이 되기도 한다.

다음과 같은 가상 코드를 보자.

SendMessage(hChild, WM_SOME ,0,0);

다음 명령 

hChild 윈도우에게 어떤 작업ㅇ르 명령하기 위해 WM_SOME이라는 메시지를 보냈다.

그런데 이 작업이 총 10초 정도의 시간이 필요하다면 SendMessage 다음의 명령은 결국 10초 후에나 실행된다.

작업 시간이 오래 걸리는 것은 어쩔 수 없지만 스레드간의 메시지 호출인 경우는

호출한 쪽이 상대편의 처리를 기다리는 일시적인 데드락 상황이 될 수도 있다.

 

BOOL ReplyMessage(LRESULT lResult); 

이 함수는 SendMessage로 전달된 메시지를 즉시 리턴하는데 lResult는 SendMessage로 리턴될 값이다.

 

BOOL SendNotifyMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

LRESULT SendMessageTimeout(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam, UINT fuFlags,

    UINT uTimeout, PDWORD_PTR lpdwResult);

SendNotifyMessage는 SendMessage와 유사하되 hWnd가 다른 스레드의 윈도우일 경우는 대기를 하지 않고 즉시 리턴한다.

이 함수는 메시지가 단순한 통지의 성격을 가지고 있을 때, 즉 끝까지 처리되기를 기다릴 필요가 없을 때 사용한다.

SendMessageTimeout 함수는 지정한 경과 시간 이상이 지나면 메시지의 처리 여부에 상관없이 즉시 리턴함으로써

메시지를 보낸 윈도우가 무한히 대기하지 않게 한다.

과거에는 메시지를 통한 통신이 중요했었고 메시지만으로도 대부분의 통신을 할 수 있었지만

32비트 이후의 환경에서는 메시지 통신을 대체할 수 있는 IPC 방법이 많이 개발되어 요즘은 이런 문제가 거의 발생하지 않는다.

 

메시지 콜백

SendMessage의 블록 특성은 문제가 되기도 하는데 때로는 문제가 없더라도 불편한 면도 있다.

이때는 다음 함수를 사용해서 블록 특성을 피해갈 수 있다.

BOOL SendMessageCallback(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam,

    SENDASYNCPROC lpCallBack, ULONG_PTR dwData); 

이 함수는 윈도우로 메시지를 보낸 후 즉시 리턴하되 콜백함수를 등록해 놓고

메시지 처리가 끝나면 콜백 함수를 호출하도록 시스템에게 부탁한다.

즉시 리턴하므로 메시지를 보내는 스레드는 곧바로 다른 작업을 할 수 있으며 메시지 처리가 끝난후 콜백함수가 호출되므로

처리 후의 시점도 정확하게 파악할 수 있다는 장점이 있다.

콜백함수는 다음과 같은 원형을 가진다. 

VOID CALLBACK SendAsyncProc(HWND hWnd, UINT uMsg, ULONG_PTR dwData, LRESULT lResult); 

 

브로드캐스팅(BroadCasting)

메시지는 일반적으로 한 윈도우에서 다른 윈도우로 직접 전달되는 것이 보통이다.

즉 1:1 통신에 사용된다.

브로드캐스팅이란 복수 개의 수신자에게 한꺼번에 메시지를 보내는 동작을 말하는데

말 그대로 실행중인 모든 윈도우에게 메시지로 방송을 하는 것이다.

이때 수신 대상 윈도우의 종류는 응용 프로그램은 물론, 시스템 디바이스 드라이버, 네트워크 드라이버,

기타 설치 가능한 디바이스 드라이버도 포함된다.

실행중인 모든 프로세스가 영향을 받는 중대한 변화가 생겼을 때 이 변화를 만든 프로그램은 관련자들에게

브로드캐스팅을 하여 제대로 변화를 감지 할 수 있도록 해야 한다.

브로드캐스팅을 할 때는 다음 함수를 호출한다.

long BroadcastSystemMessage(DWORD dwFlags, LPDWORD lpdwRecipients, UINT uiMessage,

    WPARAM wParam, LPARAM lParam);

대상 윈도우 핸들이 없으며 대신 수신자의 종류를 지정할 수 있는 인수가 있고 옵션을 지정할 수 있는 플래그가 있다.

가능한 수신자의 종류는 다음과 같으며 복수 개를 지정할 수도 있다.

 

 

3.서브클래싱

 

서브클래싱(SubClassing)이란

서브클래싱이란 윈도우 프로시저로 전달되는 메시지를 중간에 가로채는 기법이다.

중간에서 메시지를 조작함으로써 윈도우 모양을 변경하거나 동작을 감시할 수 있다.

새로운 윈도우 프로시저 함수를 만들어 두고 특정 윈도우의 윈도우 프로시저 번지를 새로만든 윈도우 프로시저의 번지로 변경하면

모든 메시지는 새로 만든 윈도우 프로시저로 전달된다.

이때 새로 만들어진 윈도우 프로시저를 서브클래스 프로시저(Subclass Procedure)라고 한다.

단 번지를 변경하기 전에 원래 윈도우 프로시저 번지를 보관해 두었다가 서브클래스 프로시저에서 처리하지 않은 메시지는

반드시 원래 윈도우 프로시저로 전달해야 한다.

도스의 인터럽터 가로채기와 개념적으로 동일하며 사용하는 목적이나 효과도 유사하다.

윈도우로 전달되는 메시지를 서브클래스 프로시저가 먼저 받는 것 외에는 기존 메시지 처리 메커니즘과 동일하다.

서브클래스 프로시저는 메시지를 먼저 받아서 다음 세가지 방법으로 이 메시지를 처리 할 수 있다.

통과

자신이 처리할 수 없거나 관심이 없는 메시지는 그냥 원래의 윈도우 프로시저로 전달한다.

원래의 윈도우 프로시저는 아무일 없었다는 듯이 자신의 방식대로 메시지를 디폴트 처리할 것이다.

서브클래스 프로시저가 단순히 중계역할을 할 뿐이며 이 경우는 윈도우에 어떠한 변화도 없다.

직접 처리

원하는 메시지가 왔을 때 자신이 직접 처리하며 윈도우 프로시저로는 보내지 않고 그냥 리턴해 버린다.

원래의 윈도우 프로시저가 메시지를 처리하는 방법과는  다르게 메시지를 처리함으로써

윈도우의 모양이나 동작이 변경 될 수 있다.

변형

메시지를 변경한 후 다시 원래의 윈도우 프로시저로 보낸다.

특정 메시지를 다른 메시지로 변경한다거나 메시지의 인수(wParam, lParam)를 다른 값으로 바꾼 후

윈도우 프로시저로 보내는 방법을 우선 생각할 수 있다.

이외에 메시지를 처리하기 전이나 후에 특별한 다른 처리를 할 수도 있다.

 

메시지를 마음대로 변경할 수 있는 서브클래싱은 아주 유용하고도 강력한 기법임에 틀림없다.

하지만 그 이면에는 항상 잠재적인 위험성이 도사리고 있어 아주 좃미스럽게 사용해야 한다.

꼭 필요하지 않는 한 서브클래싱은 하지 않는 것이 좋지만 불가피할 경우는 다음 주의 사항을 잘 숙지하고 안전하게 사용하자.

우선 윈도우의 원래 기능을 보존해야 한다.

두 번째로 주의할 점은 서브클래스 프로시저는 어떠한 일이 있어도 여분 메모리를 건드려서는 안된다.

여분 메모리가 꼭 필요한 상황이라면 윈도우 프로퍼티를 대신 사용하는 것이 좋다.

 

전역 서브클래싱

서브클래싱도 시점과 방법, 그리고 효과가 미치는 범위에 따라 다음 두가지로 분류한다.

 

인스턴스 서브클래싱

윈도우가 만들어진 후 그 윈도우 하나에 대해서만 윈도우 프로시저를 교체하는 것이다.

따라서 이후에 만들어지는 윈도우는 이 서브클래싱의 영향을 전혀 받지 않는다.

특정 윈도우 하나에 대해서만  윈도우 프로시저를 교체하는 것이므로 SetWindowLongPtr 함수가 사용된다.

전역 서브클래싱

특정 윈도우에 대해서 서브클래싱을 하는 것이 아니라 윈도우 클래스에 대해 서브클래싱을 하는 것이다.

윈도우 클래스의 WNDCLASS 구조체를 직접 변경하며 이때는 SetClassLongPtr 함수가 사용된다.

이미 만들어진 윈도우에 대해서는 전혀 효과가 없으며 앞으로 만들어질 윈도우만 영향을 받는다.

 

슈퍼클래싱(Superclassing)

서브클래싱과 비슷한 기법으로 슈퍼클래싱이라는 것도 있다.

서브클래싱은 윈도우 프로시저만 교체하는 것인데 비해

슈퍼클래싱은 기존 클래스(=베이스 클래스)의 정보를 바탕으로 하여 완전히 새로운 클래스를 만드는 것이다.

슈퍼클래싱에 의해 새로 만들어진 윈도우 클래스는 베이스 클래스와 동일하지만 여기에 변화를 주면

원하는대로  윈도우 클래스를 수정할 수 있다.

단 SCROLLBAR 클래스는 슈퍼클래싱을 해서는 안된다고 알려져 있다.

슈퍼클래싱의 핵심함수는 GetClassInfo(Ex) 함수이다.

BOOL GetClassInfo(HINSTANCE hInstance, LPCTSTR lpClassName, LPWNDCLASS lpWndClass); 

 

서브클래싱과 슈퍼클래싱은 둘 다 사용목적이 비슷하며 상호 대체성이 있다.

어떤 방법을 쓸 것인가는 상황에 따라 다르겠지만 대체로 슈퍼클래싱이 더 안전하고 부릴 수 있는 기교도 더 많다.

특정 윈도우 하나만 수정할 때는 인스턴스 서브클래싱을 사용하고

다량의 윈도우를 한꺼번에 바꿀 때는 전역 또는 슈퍼클래싱을 사용하는 것이 편리하다.

 

 

4. 메시지 크래커

 

메시지 크래커

메시지 크래커는 WndProc에 들어갈 함수 호출문과 메시지 처리 함수의 원형을 매크로로 잘 정의해 놓은 것이다.

 

Windowsx.h

메시지 크래커는 컴파일러가 지원하는 것도 아니고 C언어 명세에 있는 것도 아니다.

다만 Windowsx.h라는 헤더 파일에 저으이되어 있는 아주 단순한 매크로 구문일 뿐이다.

메시지 크래커를 실제 업무에 적용시키는 절차

 

① Wnd Proc에 처리하고자 하는 메시지에 대해 HANDLE_MSG 매크로 구문을 삽입한다.

    처리하고자 하는 메시지와 함수명을 짝짓기만 하면 된다.

② 함수의 본체를 만든다.

    메시지 처리 함수의 원형은 Windowsx.h에 주석으로 기록되어 있다.

    주석으로 처리된 부분을 복사해 와서 이름변경후 함수의 본체 코드를 작성한다.

③ 핸들러 함수의 원형을 선언하면 작업이 완료된다.

    물론 Windowsx.h는 소스 선두에 포함해야 한다.

 

 

 

- 윈도우즈 API 정복 中 -