마. 훅의 활용

전역 훅 프로시저는 다른 프로세스의 메시지 흐름을 들여다 볼 수 있다는 점에서 활용성이 높다. 특정 윈도우로 입력되는 메시지의 흐름을 살펴보면 이 윈도우가 입력된 메시지에 대해 어떻게 반응할 것인가를 예측할 수 있으며 따라서 윈도우에게 사건이 발생하는 정확한 시점을 알 수 있다. 훅 프로시저는 메시지 흐름을 감시하고 있다가 관심있는 이벤트가 발생했을 때 원하는 어떤 조치를 취할 수 있을 것이다.

다음 예제는 메모장의 키 입력을 감시하는 훅 프로시저를 설치하고 사용자가 babo라는 키를 연속으로 입력하면 이 키 입력을 모두 취소하고 chunjae로 바꿔 버린다. babo라는 연속된 키입력 이벤트에 반응하여 메모장의 동작을 원하는 방식으로 제어할 수 있는 것이다. 훅 DLL은 앞서 작성했던 KeyBeepDll과 동일하므로 따로 살펴볼 필요가 없으며 훅 서버 프로그램이 키 입력을 감시하고 이벤트에 대응하는 방법만 보도록 하자.

 

#include "../HookNotePadDll/HookNotePadDll.h"

TCHAR Mes[]="메모장에서 BABO 입력하면 chaunjae 변경합니다.";

TCHAR szSrc[]="BABO";

TCHAR szDest[]="CHUNJAE";

int idx;

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

{

   HDC hdc;

   PAINTSTRUCT ps;

   HWND hFGWnd;

   TCHAR szClass[32];

   int i;

 

   switch(iMessage) {

   case WM_CREATE:

      InstallHook(hWnd);

      return 0;

   case WM_USER+1:

      hFGWnd=GetForegroundWindow();

      GetClassName(hFGWnd,szClass,32);

      if (lstrcmpi(szClass,"NotePad")==0 && (lParam & 0x80000000)==0) {

          if (wParam == (WPARAM)szSrc[idx]) {

             idx++;

          } else {

             idx=0;

          }

          if (szSrc[idx]==0) {

             for (i=0;i<lstrlen(szSrc);i++) {

                keybd_event(VK_BACK,0,0,0);

                keybd_event(VK_BACK,0,KEYEVENTF_KEYUP,0);

             }

             for (i=0;i<lstrlen(szDest);i++) {

                keybd_event(szDest[i],0,0,0);

                keybd_event(szDest[i],0,KEYEVENTF_KEYUP,0);

             }

          }

      }

      return 0;

   case WM_PAINT:

      hdc=BeginPaint(hWnd, &ps);

      TextOut(hdc,10,10,Mes,lstrlen(Mes));

      EndPaint(hWnd, &ps);

      return 0;

   case WM_DESTROY:

      UninstallHook();

      PostQuitMessage(0);

      return 0;

   }

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

}

 

이 프로그램은 메모장의 동작에 대해서만 관여하므로 훅 프로시저로부터 WM_USER+1 메시지를 받았을 때 활성화된 윈도우가 메모장이 맞는지를 먼저 살펴 본다. 활성 윈도우의 윈도우 클래스명이 "NotePad"가 아니라면 메모장이 아니므로 아무런 동작도 하지 않으며 키가 떨어질 때도 역시 아무 동작도 하지 않는다. 오직 메모장에서 키를 누를 때(WM_KEYDOWN)만 동작하도록 했는데 이 조건을 변경하면 모든 프로그램에 대해 동작하도록 바꿀 수도 있다.

메모장에서 키가 눌러졌을 때, 즉 메모장 윈도우가 WM_KEYDOWN 메시지를 받았을 때 wParam이 babo의 연속인지 아닌지를 항상 감시하고 있다가 만약 babo가 연속으로 입력되면 4개의 BS키를 보내 babo를 지우고 chunjae키를 차례대로 누름으로써 babo를 chunjae로 바꾸어 준다. keybd_event 는 마치 사용자가 키보드를 누른 것처럼 키 이벤트를 발생시키는 함수이다.

이 예제는 연속된 키입력만 감시하기 때문에 한글, 대소문자 등은 구분하지 않으며 중간에 BS나 커서 이동키로 이동, 편집한 경우도 연속된 문자열로 인정하지 않는다. IME 상태나 대소문자 구분 등을 판별하고 BS, Del 등의 간단한 편집키를 처리한다면 좀 더 완벽한 동작을 할 수도 있다. 이런 방식으로 다른 프로그램의 키입력을 감시, 변경하면 백그라운드 맞춤법 검사기나 상용구 입력기 등을 만들 수 있을 것이다.

다음 예제는 지역 훅을 사용하여 메시지 박스를 부모 윈도우의 중앙에 출력한다. MessageBox 함수는 자신의 위치를 지정하는 플래그가 없으며 무조건 화면 중앙에 나타나도록 되어 있다. 이 함수는 호출하는 즉시 메시지 박스를 띄우고 확인 버튼을 누를 때까지 리턴하지 않기 때문에 호출원에서 윈도우의 위치를 옮길 수 있는 기회가 없다. 위치를 옮길 때는 MoveWindow나 SetWindowPos 함수를 사용한다는 것은 알고 있지만 이 함수를 호출할 마땅한 시점이 없는 것이다.

메시지 박스가 생성되는 시점, 그러니까 WM_CREATE 메시지를 받을 때 이 윈도우의 위치를 옮겨야 하는데 그 시점이 운영체제 내부에 있기 때문에 응용 프로그램이 자신의 코드를 실행할  기회가 없는 것이다. 메시지 박스는 일종의 대화상자이고 이 대화상자의 윈도우 프로시저는 운영체제에 내장되어 있어 프로그래밍할 수 있는 대상이 아니다. 메시지 박스가 생성되는 시점을 구하기 위해 훅을 설치하고 윈도우가 생성될 때 보내지는 메시지를 가로채야 한다.

이때 사용하는 훅이 WH_CBT이다. CBT(Computer Based Training) 훅은 초보자들의 컴퓨터 조작 훈련을 위해 제공되는데 윈도우가 생성, 파괴, 이동 및 크기 변경시의 메시지를 감시하도록 한다. CBT 프로그램은 사용자에게 윈도우 조작 방법을 알려주고 실습을 유도하는데 이때 사용자들이 지시대로 윈도우의 생성, 이동, 종료 등을 제대로 하는지 감시하기 위해 WH_CBT 훅을 사용한다. 이 훅의 원래 목적은 사용자 교육용이지만 일반 응용 프로그램도 윈도우 관련 메시지를 가로채기 위해 이 훅을 사용할 수 있다.

이 훅을 사용하면 특정 윈도우가 생성되는 시점을 응용 프로그램이 알 수 있고 이때 원하는 처리, 예를 들어 위치를 옮기거나 크기를 바꾸거나 스타일을 변경할 수 있다. 메시지 박스처럼 사용자가 직접 만든 윈도우가 아닐지라도 말이다. 다음 예제는 WH_CBT훅과 서브클래싱을 사용하여 메시지 박스를 부모 윈도우의 중앙에 오도록 한다.

 

HHOOK hCbtHook;

void MoveToParentCenter(HWND hWnd)

{

   RECT wrt,crt;

   HWND hParent;

 

   hParent=GetParent(hWnd);

   if (IsIconic(hParent)) {

      ShowWindow(hParent,SW_RESTORE);

   }

 

   GetWindowRect(hParent,&wrt);

   GetWindowRect(hWnd,&crt);

   SetWindowPos(hWnd,HWND_NOTOPMOST,wrt.left+(wrt.right-wrt.left)/2-(crt.right-crt.left)/2,

      wrt.top+(wrt.bottom-wrt.top)/2-(crt.bottom-crt.top)/2,0,0,SWP_NOSIZE);

}

 

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

{

   WNDPROC OldProc;

   OldProc=(WNDPROC)GetProp(hWnd,"OldProp");

 

   switch(iMessage) {

   case WM_CREATE:

      MoveToParentCenter(hWnd);

      break;

   case WM_NCDESTROY:

      SetWindowLong(hWnd,GWL_WNDPROC,(DWORD)OldProc);

      RemoveProp(hWnd,"OldProp");

      break;

   }

   return CallWindowProc(OldProc,hWnd,iMessage,wParam,lParam);

}

 

LRESULT CALLBACK CbtHookProc(int nCode,WPARAM wParam,LPARAM lParam)

{

   CBT_CREATEWND *pCbt;

   HWND hWnd;

   TCHAR szClassName[32];

   WNDPROC OldProc;

 

   if (nCode == HCBT_CREATEWND) {

      hWnd=(HWND)wParam;

      pCbt=(CBT_CREATEWND *)lParam;

 

      if (HIWORD(pCbt->lpcs->lpszClass)) {

          lstrcpy(szClassName,pCbt->lpcs->lpszClass);

      } else {

          GlobalGetAtomName((ATOM)pCbt->lpcs->lpszClass,szClassName,32);

      }

 

      if (lstrcmpi(szClassName,"#32770")==0 && ((pCbt->lpcs->style & WS_CHILD)==0)) {

          OldProc=(WNDPROC)GetWindowLong(hWnd,GWL_WNDPROC);

          SetProp(hWnd,"OldProp",OldProc);

          SetWindowLong(hWnd,GWL_WNDPROC,(DWORD)NewWndProc);

      }

   }

   return CallNextHookEx(hCbtHook,nCode,wParam,lParam);

}

 

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

{

   HDC hdc;

   PAINTSTRUCT ps;

 

   switch(iMessage) {

   case WM_CREATE:

      hCbtHook=SetWindowsHookEx(WH_CBT,CbtHookProc,NULL,GetCurrentThreadId());

      return 0;

   case WM_LBUTTONDOWN:

      MessageBox(hWnd," 메시지 박스는 부모 윈도우의 중앙에 나타납니다","알림",MB_OK);

      return 0;

   case WM_PAINT:

      hdc=BeginPaint(hWnd, &ps);

      EndPaint(hWnd, &ps);

      return 0;

   case WM_DESTROY:

      UnhookWindowsHookEx(hCbtHook);

      PostQuitMessage(0);

      return 0;

   }

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

}

 

WM_CRAETE에서 WH_CBT 훅을 설치하는데 메시지 박스는 같은 스레드내에서 생성되는 윈도우이므로 지역 훅을 설치하면 된다. WM_PAINT에서는 간단한 안내 메시지를 출력하고 WM_DESTROY에서는 훅을 제거했다. 마우스 왼쪽 버튼을 누르면 MessageBox 함수를 호출하여 메시지 박스를 띄운다. 별다른 처리를 하지 않는다면 메시지 박스는 항상 화면 중앙에 열리게 될 것이다.

CBT 훅 프로시저의 nCode로는 윈도우에 어떤 일이 발생했는지를 알려주는 다음과 같은 값이 전달되며 이때 wParam으로는 윈도우의 핸들이 전달되며 lParam으로는 메시지의 부가 정보가 전달된다. 다음 도표는 CBT 훅의 nCode값과 lParam 인수를 정리한 것인데 더 자세한 정보는 레퍼런스를 참고하기 바란다.

 

코드

설명

lParam

HCBT_ACTIVATE

윈도우 활성화

CBTACTIVATESTRUCT 구조체

HCBT_CREATEWND

윈도우 생성

CBT_CREATEWND 구조체

HCBT_DESTROYWND

윈도우 파괴

0

HCBT_MINMAX

최소 또는 최대화

하위 워드에 현재 상태(SW_*)

HCBT_MOVESIZE

이동 또는 크기 변경

윈도우의 현재 위치값을 가지는 RECT 구조체

HCBT_SYSCOMMAND

시스템 명령 실행

WM_SYSCOMMAND와 동일

 

리턴값으로는 해당 동작의 허가 여부를 리턴하는데 0을 리턴하면 동작을 허가하는 것이고 1을 리턴하면 금지하는 것이다. CBT 훅은 항상 해당 동작이 일어나기 전에 훅 프로시저에게 먼저 전달된다. 예를 들어 윈도우가 생성될 때 HCBT_CREATEWND 코드를 먼저 보낸 후 이 훅 프로시저가 0을 리턴하면 목표 윈도우로 WM_NCCREATE, WM_CREATE 메시지가 전달되며 윈도우가 파괴될 때도 WM_DESTROY 메시지를 보내기 전에 CBT훅의 HCBT_DESTROYWND 코드가 먼저 전달된다.

이 예제는 메시지 박스가 생성될 때 윈도우의 위치를 옮기고자 하므로 nCode가 HCBT_CREATEWND일 때 원하는 코드를 실행해야 한다. 이때 lParam으로는 다음과 같이 정의된 구조체의 포인터가 전달된다.

 

typedef struct {

    LPCREATESTRUCT lpcs;

    HWND hwndInsertAfter;

} CBT_CREATEWND, *LPCBT_CREATEWND;

 

lpcs는 CREATESTRUCT 구조체이며 hwndInsertAfter는 이 윈도우 바로 앞의 Z 순서를 가지는 윈도우 핸들이다. 훅 프로시저에서 이 구조체의 값을 직접 변경하면 윈도우의 위치나 크기, Z 순서를 바꿀 수 있다. lpcs->x, lpcs->y값을 조정하면 메시지 박스가 생성될 위치를 지정할 수 있는데 예제에서는 이 값을 직접 변경하지 않고 서브클래싱만 하고 있다. 왜냐하면 부모의 중앙 좌표를 구해야 하는데 윈도우가 생성되는 이 시점에는 아직 부모가 누구인지를 알 수 없기 때문이다. CBT훅은 동작이 일어나기 직전에 보내지므로 아직 이 윈도우는 만들어지지 않았으며 부모 자식 관계도 설정되어 있지 않다.

그래서 훅 프로시저는 서브클래싱만 해 놓고 서브클래스 프로시저의 WM_CREATE(또는 WM_INITDIALOG)에서 부모의 위치를 참조하여 부모의 중앙 위치로 가도록 했다. 여기서 WM_CREATE 메시지는 부모를 알 수 있는 최초의 시점이며 또한 이 윈도우가 보이기 전이므로 위치를 옮길 수 있는 최적의 위치에 해당된다. 원래 윈도우 프로시저의 번지는 별도의 전역 변수에 저장할 수도 있지만 윈도우 스스로 기억하도록 하기 위해 윈도우 프로퍼티를 사용했다. 이 윈도우는 파괴되기 직전에 윈도우 프로퍼티로부터 원래 윈도우 프로시저를 구해 자신의 서브클래싱을 직접 해제한다. 요약하자면 CBT 훅 프로시저는 윈도우가 생성되는 시점을 가로채서 서브클래싱만 하고 위치를 옮기는 작업은 서브클래스 프로시저가 하고 있는 셈이다.

훅 프로시저는 서브클래싱할 윈도우를 정확하게 선정해야 한다. CBT 훅은 모든 윈도우의 생성, 파괴, 이동 메시지를 받기 때문에 조건 점검을 정밀하게 하지 않으면 대화상자뿐만 아니라 대화상자안의 버튼이나 스태틱같은 컨트롤까지도 위치 이동의 대상이 되어 버리기 때문이다. 예제에서는 윈도우 클래스가 #32770, 즉 대화상자이고 차일드가 아닌지만으로 메시지 박스인지를 점검하고 있다. 윈도우가 좀 더 많은 프로젝트에서는 이보다 더 정밀한 조건 점검을 해야 할 것이다. 그렇지 않으면 원치않은 윈도우까지 위치가 이동되는 부작용이 발생한다.

예제를 실행하고 마우스 왼쪽 버튼을 눌러 보면 메시지 박스가 부모의 중앙에 열리게 될 것이다. 코드를 이해하는 것은 어렵지 않은데 그렇다면 메시지 박스의 위치를 옮기는데 왜 이렇게 복잡한 과정을 거쳐야 하는 것일까? 그 이유는 MessageBox라는 함수가 모달 대화상자를 열고 이 대화상자의 운용 일체를 관장하고 있기 때문이다. 이 함수를 호출하면 대화상자를 생성, 표시, 파괴하는 동작이 모두 이 함수내에서 일어나며 응용 프로그램이 메시지 박스에 대해 조작을 할 수 있는 기회가 없다. 그래서 훅을 설치하고 서브클래싱해서 생성 시점을 가로채는 복잡한 과정을 거쳐야 하는 것이다. 이 방법은 메시지 박스뿐만 아니라 공통 대화상자나 프로퍼티 시트 등의 컨트롤에도 동일하게 적용된다.