I'm on your side, when times get rough.

2009-02-26

Win32 Control: “SetWindowsHookEx”

Filed under: Programming — Peter_KIM @ 11:34

시스템의 어떤 기본 윈도우(컨트롤)을 확장하여, 또 다른 윈도우(컨트롤)을 생성하는 것이 가능한 것은, 시스템의 해당 기본 컨트롤에 접근할 수 있도록 API 함수들이 존재하기 때문에 가능할 것입니다. 그러나, 무수히 많은 윈도우(컨트롤)이 친절하게 API 함수를 제공하지는 않습니다.

 

지난번에 사용한 “#32770″(대화 상자) 클래스를 살펴보겠습니다. 해당 클래스를 MessageBox 함수를 이용하여 생성하여, 생성된 대화 상자를 제어하기 위하여, 시스템에서 제공되는 API 함수를 찾아보겠습니다. Button, ListView, ComboBox 등등의 컨트롤에서 풍부한 API 함수가 제공되는 데에 비하여, “#32770” 클래스와 관련된 함수는 MSDN 문서를 아무리 찾아봐도 없습니다.

프로그램적으로 대화 상자의 위치를 조정하려고 해도 문제가 될 수 있습니다. 물론, 해당 윈도우의 핸들 값과 윈도우 API 함수를 이용하면 가능하다고 생각할 수도 있지만, MessageBox 함수로 생성된 대화 상자는 Modal 상태이며, 운영 체제 내부에 이벤트 처리 프로시저가 연결되어 있으므로, 최초의 시작 상태를 제어하기란 거의 불가능 수준으로 보입니다.

 

HOOK, 사전적인 의미는 갈고리”, 미국 속어로는 소매치기, 도둑이라는 의미로도 사용됩니다. 아래의 코드에서, MessageBox 함수로 생성된 대화 상자의 이벤트를 가로채 낼 것입니다. 그런 다음 기본 이벤트 처리 프로시저를 새로운 이벤트 처리 프로시저로 대체할 것입니다.

 

SetWindowsHookEx 함수를 MSDN 문서에서 찾아보면, 많은 설명이 나옵니다. 여러 가지 HOOK 프로시저 형태 중에서 WH_CBT 형식을 살펴보겠습니다. Hook 프로시저는 윈도우의 생성, 소멸, 활성화, 마우스 버튼 메시지, 키보드 메시지 등을 가로채는 역할을 담당합니다.

우선, 이 함수를 사용하기 전에 몇 가지를 더 살펴보겠습니다. MFC 라이브러리는 상당히 유용한 소스 코드를 많이 제공합니다. 사실 MFC 라이브러리도 Win32 API 함수를 포장한 것이므로, 어떤 소스를 작성하기 전에, MFC 라이브러리에서는 어떻게 구현했을까를 살펴보는 것도 좋은 일입니다.

이렇게 다른 사람이 작성한 코드를 보면서, 자신의 것으로 이해하고 활용하는 능력을 키우는 것이 좋습니다. , 실전에서 코드를 작성할 때, 남들이 이해하기 쉽도록 처리하는 것도 매우 중요합니다. 어떻게 보면, 어려운 이론 설명보다는 이런 방식도 반복 해보면, 자연스레 이론도 이해할 수 있고, 코드 구현 능력도 발전할 수 있습니다. 🙂

 

MFC 라이브러리 소스 파일 중에 wincore.cpp 파일을 보면, Window creation hooks 구역이 있습니다. _AfxCbtFilterHook 함수가 보입니다. 함수 이름으로 보아하니 WH_CBT 형식의 HOOK 처리 코드가 구현되어있나 봅니다. 이 함수를 C 코드로 작성하면 뭔가 해 볼 수 있는 느낌이 듭니다. 역시 MFC 라이브러리도 Win32(C) 프로그램에서 유용합니다.

_AfxCbtFilterHook 함수의 큰 기능은 다음과 같습니다.

1.  HOOK 처리될 윈도우(컨트롤)를 클래스 이름을 이용하여 걸러냅니다. (Filtering)

2.  Win32 윈도우(컨트롤)의 기본 메시지 처리 프로시저를 MFC 라이브러리에서 구현한 프로시저로 교체하는 것입니다. (Sub-Classing).

 

지난 글에서 사용한 코드에 추가적으로 간단한 코드(굵은 부분)를 작성해 보겠습니다. 어렵지 않은 코드이므로, 주석과 설명은 생략합니다.

우선 Hook 기능을 사용하기 위하여, 다음과 같은 코드를 작성합니다.

#include “TestWin32.H”

HINSTANCE hInst;

HHOOK hhookSysMsg;

HWND hWnd;

……

LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)

{

    PAINTSTRUCT ps;

    HDC hDC;

    switch(msg)

    {

    case WM_CREATE:

    hhookSysMsg = SetWindowsHookEx(WH_CBT, CBTProc, NULL, GetCurrentThreadId());

     break;

    }

……

        return 0;

}

이것으로 이벤트를 가로채는 준비가 완료되었습니다. SetWindowsHookEx 함수의 두 번째 인자인, 처리 함수(CBTProc)를 작성해 보겠습니다. 다음의 코드를 보기 전에 MSDN 문서를 살펴보길 바랍니다.

다음의 코드는 _AfxCbtFilterHook 함수에서 구현된 것을 따라서 작성한 것입니다. 코드에서 나타나는 여러 함수들에 대해서는 역시, MSDN 문서를 참조하시길 바랍니다.

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

{

        LPCREATESTRUCT lpcs;

        LPCBT_CREATEWND lpcbtcw;

        HWND hNewhWnd;

        LPCTSTR pszClassName;

        CHAR szClassName[_MAX_PATH];

        switch (nCode)

        {

        case HCBT_CREATEWND:

               lpcbtcw = (LPCBT_CREATEWND)lParam;

               hNewhWnd = (HWND)wParam;

               lpcs = lpcbtcw->lpcs;

               if ((unsigned long)(lpcs->lpszClass) > 0xffff) {

                       pszClassName = lpcs->lpszClass;

               }

               else {

               *szClassName = 0x00;

             // GlobalGetAtomName((ATOM)lpcs->lpszClass,szClassName,_countof(szClassName));

               GetClassName(hNewhWnd, szClassName, _countof(szClassName));

               pszClassName = szClassName;

               }

               if (!lstrcmpi(pszClassName, “#32770”) && !(lpcs->style & WS_CHILD)) {

                       lpcs->cx = 200;

                       lpcs->cy = 200;

               }

               break;

        }

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

}

 

프로그램을 컴파일, 링크하고, 실행하여, 메뉴를 차례로 눌러보겠습니다. 다음과 같은 대화 상자들이 표시될 것입니다.

클래스의 이름이 “#32770”인 대화 상자는 모두, 200*200 크기로 변경되었습니다. 지난 글에서 CreateWindow 함수를 이용하여 만든 대화 상자는 “#32770” 클래스의 대부분의 속성을 가지지만, 클래스의 이름을 변경하였고, 기본 메시지 처리 프로시저를 변경한 것을 기억해야 합니다.

 

그럼, 이제 한가지 작업을 더 해보겠습니다. “#32770” 클래스의 이름을 갖는 대화 상자의 기본 이벤트 처리 프로시저를 교체하여 보겠습니다. 먼저, wincore.cpp 파일을 살펴 보겠습니다. 아래와 같은 코드가 보입니다.

if (bSubclass) { // subclass the window with the proc which does gray backgrounds

        oldWndProc = (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC);

        if (oldWndProc != NULL && GetProp(hWnd, _afxOldWndProc) == NULL) {

               SetProp(hWnd, _afxOldWndProc, oldWndProc);

               if ((WNDPROC)GetProp(hWnd, _afxOldWndProc) == oldWndProc) {

                       GlobalAddAtom(_afxOldWndProc);

                       SetWindowLongPtr(hWnd,GWLP_WNDPROC,(DWORD_PTR)_AfxActivationWndProc);

                       ASSERT(oldWndProc != NULL);

               }

        }

}

이 부분의 주요 내용은 기본 프로시저를 메모리의 특정 영역(Global Atom Table)에 저장하고, 특정 프로시저(_AfxActivationWndProc)를 연결하는 것입니다. 이렇게 하는 이유는 새로 연결한 프로시저가 기본 프로시저의 모든 이벤트를 처리하지 않으며, 특정한 이벤트에 대한 처리 코드만 가지고 있으므로, 해당 프로시저에서 처리되지 않은 이벤트를 다시 기본 프로시저를 이용하여 처리하기 위함입니다.

wincore.cpp 파일의 _AfxActivationWndProc 함수에 구현된 부분을 참고해서, 다음과 같이 코드를 작성하겠습니다. 대화 상자에 나타나는 확인버튼의 크기와 위치를 조정해보겠습니다.

 

const CHAR _afxOldWndProc[] = “AfxOldWndProc423”;

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

{

……

switch (nCode)

{

case HCBT_CREATEWND:

……

        if (!lstrcmpi(pszClassName, “#32770”) && !(lpcs->style & WS_CHILD)) {

……

               oldWndProc = (WNDPROC)GetWindowLongPtr(hNewhWnd, GWLP_WNDPROC);

               if (oldWndProc != NULL && GetProp(hNewhWnd, _afxOldWndProc) == NULL) {

                       SetProp(hNewhWnd, (LPCTSTR)_afxOldWndProc, (HANDLE)oldWndProc);

                       if ((WNDPROC)GetProp(hNewhWnd, _afxOldWndProc) == oldWndProc) {

                              GlobalAddAtom(_afxOldWndProc);

                              SetWindowLongPtr(hNewhWnd, GWLP_WNDPROC, (LONG)WndProc32770);

                       }

               }

        }

        break;

}

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

}

LRESULT CALLBACK WndProc32770(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)

{

WNDPROC oldWndProc;

HWND hWndBtn;

oldWndProc = (WNDPROC)GetProp(hDlg, _afxOldWndProc);

switch (message)

{

  case WM_INITDIALOG:

      hWndBtn = GetDlgItem(hDlg, IDOK);

      SetWindowText(hWndBtn, “TEST”);

      SetWindowPos(hWndBtn, HWND_TOP, 0, 130, 100, 40, SWP_DRAWFRAME);

  break;

  case WM_COMMAND:

       if (LOWORD(wParam) == IDOK) {

               EndDialog(hDlg, LOWORD(wParam));

               DestroyWindow(hDlg);

       }

       break;

  case WM_NCDESTROY:

       SetWindowLongPtr(hDlg, GWLP_WNDPROC, (LONG)oldWndProc);

       RemoveProp(hDlg, _afxOldWndProc);

       GlobalDeleteAtom(GlobalFindAtom(_afxOldWndProc));

       break;

  }

  return CallWindowProc(oldWndProc, hDlg, message, wParam, lParam);

}

 

코드를 컴파일, 링크하여, 실행하면 다음과 같은 결과를 얻을 수 있습니다.

 

 

 

Win32 Control 부분에서 사용한 소스 코드는 아래의 주소에서 다운로드 할 수 있습니다.

https://skydrive.live.com/embedicon.aspx/.Public/TestWin32.7z?cid=1bbcdfedee1c617e

Create a free website or blog at WordPress.com.