API

모달리스 다이얼로그에 탭키 포커스 이동처럼 편리한 다이얼로그 키 처리 적용하기

디버그정 2020. 6. 21. 13:46

Modeless.txt
0.02MB

 

모달리스 다이얼로그에 탭키 포커스 이동처럼 편리한 다이얼로그 키 처리 적용하기

모달 다이얼로그의 탭키로 컨트롤을 이동하고 스페이스로 선택하거나 에디트에 포커스 존재시 엔터를 누르면 디폴트 버튼이 눌리는 기능은 편리하다고 한다.;
탭키이동은 많이 해봤어도 마지막 에디트에 커서 있는 상태에서 엔터치면 디폴트 버튼이 작동한다는건 모르는 사람이 훨씬 많을거다.ㅎㅎ
아무튼 일단 모달리스 다이얼로그를 CreateDialog, ShowWindow만 해놓고 아무런 처리를 하지 않으면 오로지 마우스로만 작동한다.
탭키를 눌러도 도무지 변화가 없으니 무빙이 답답한 고구마 같은 느낌이랄까...

위 문제 두가지 다른 형태로 나타나는데 각각의 코드를 작성해 해결할 수 있다.
먼저 호출부 윈도우를 CreateWindow로 생성한 경우 메시지 루프 소스코드에 아래와 같이 다이얼로그 관련 메시지 처리 명령문을 넣으면 된다.
while (GetMessage(&msg, NULL, 0, 0))
{
if (!IsDialogMessage(g_hModelessDlg, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
겟메시지로 메시지큐에서 꺼낸 후 다이얼로그의 메시지이면 해당 다이얼로그의 프로시저에서 처리하고 아니면 일반적인 과정을 수행한다.
한줄만 작성하면 되니 초간단하다.

다음의 경우가 다소 어려운데 모달 다이얼로그에서 클릭 등의 행위로 모달리스 다이얼로그를 생성한 경우이다.
모달 다이얼로그의 겟메시지 이후에 저런 식으로 코드를 넣으면 가능할 것 같은데 도무지 다이얼로그의 메시지 루프는 찾을 수 없다.
왜냐하면 다이얼로그는 MS에서 만들어 놓은 완성된 컨트롤이고 유저는 사용만 하므로
사용자가 직접적으로 메시지 루프 코드를 작성하거나 작성된 소스 코드에 접근할 수 없다.
유저는 이미 기계어로 컴파일된 API 함수 주소를 얻어서 사용할 뿐이다.

이 같은 경우 등을 대비해서 OS에서 겟메시지 처리시 콜백함수의 형태로 사용자에 물어 처리할 수 있게 하는 가능성을 열어 놓았다.
이런 요구가 하나 뿐 아니고 여러 개 존재할 수 있으므로 어떤 콜백 처리부 -> 다음 콜백 처리부 -> 또 다음 콜백 처리부.... 등으로
체인형태의 과정을 마친 후 비로소 원래의 작업을 수행한다.

SetWindowsHookEx 함수를 수행해 훜(낚아채기)을 걸어놓고 OS가 GetMessage 처리 후 사용자가 지정한 콜백함수를 수행하므로
콜백함수에 IsDialogMessage 수행 코드를 넣어주면 된다.
전역변수는 2개 사용된다. 훜핸들과 모달리스 다이얼로그 핸들이다.
HWND g_hModelessDlg;
HHOOK g_hHookForModelessDlgKey;
// 훜 설치
case WM_INITDIALOG: // 모달리스 다이얼로그의 생성부
g_hHookForModelessDlgKey = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProcForModelessDlgKey, NULL, GetCurrentThreadId());
return TRUE;
// 훜 해제. 윈도우 파괴시 정리작업을 해주면 된다.
case WM_DESTROY: // 모달리스 다이얼로그의 파괴부
UnhookWindowsHookEx(g_hHookForModelessDlgKey);
g_hModelessDlg = NULL;
return TRUE;

훜을 설치하는 SetWindowsHookEx에 주목하자.
1번째 인수 WH_GETMESSAGE는 목표 대상이며 겟메시지에 훜을 걸겠다는 의미이다.
2번째 인수 GetMsgProcForModelessDlgKey는 OS가 사용자에게 어떤 작업을 할 것인지 물어보는 콜백함수이다. 사용자가 HOOKPROC 형식에 맞춰 작성해줘야 된다.
3번째 인수는 콜백함수가 존재하는 인스턴스 핸들로 주로 DLL 형태로 전역 훜 작업을 하는 경우 DLL 핸들을 지정하는 용도로 사용된다.
DLL에 콜백함수 존재시 DLL 핸들로부터 콜백함수 주소를 구하기 때문이다. 현재 프로세스에서 별도의 다른 DLL에 작성한 것이 아닌 경우 NULL로 지정하면 된다.
4번째 인수는 스레드 아이디이다. 스레드 별로 각종 훜들이 수행된다. 여기서는 현재 스레드에서 작업하므로 현재 스레드 아이디를 지정한다.
이 부분을 0으로 지정하면 전역 훜으로 작동하는데 이렇게 수행하기 위해서는 별도의 DLL을 작성해야 한다. 다른 프로세스 공간을 함부로 침범할 수 없는 게 원칙이기 때문이다

.
별도의 콜백함수를 담은 조그만 DLL을 작성하고 이 DLL을 전 프로세스에 로딩하라는 명령을 내려서 처리한다.

가장 중요한 콜백함수 작성 부분이 남았다. 다음과 같이 작성한다.

LRESULT CALLBACK GetMsgProcForModelessDlgKey(int code, WPARAM wParam, LPMSG lpMsg)
{
if (code >= 0 && // code가 음수인 경우 바로 CallNextHookEx를 수행해야 한다.(MSDN 참조)
    PM_REMOVE == wParam && // 메시지가 실제로 메시지큐에서 꺼내져 제거되었음을 의미한다. PeekMessage는 존재여부 체크만 하고 리턴하는 경우도 있기 때문이

다.
    lpMsg->message >= WM_KEYFIRST && lpMsg->message <= WM_KEYLAST) // 메시지 범위 지정
{
if (IsDialogMessage(g_hModelessDlg, lpMsg)) // 드디어 이 API를 수행한다.
{
lpMsg->message = WM_NULL; // 호출부에서 훜 체인이 끝난 후 계속 진행하므로 반드시 널 메시지로 교체해 어떤 수행을 하지 않게 한다. 안 그

러면 띵! 경고음이 울렸다.
lpMsg->wParam = 0;
lpMsg->lParam = 0;
return 0; // 원칙은 아래 함수를 호출해 다음 훜 프로시저로 진행하는 것이지만 WM_NULL 메시지로 바꿔버렸으므로 더 진행하는 게 의미 없다.
// 다시 말하지만 이 경우만 예외적인 상황이고 원칙은 아래 함수를 수행해 다른 훜 프로시저들에게도 처리의 기회를 줘야 한다.
}
}
return CallNextHookEx(g_hHookForModelessDlgKey, code, wParam, (LPARAM)lpMsg); // 다음 훜 프로시저(콜백함수)로 간다. 체인의 형태로 구성된다.
}

주석 설명으로 충분하다.

SetWindowsHookEx 훜 API는 겟메시지 뿐 아니라 키보드, 마우스, 프로시저 등을 낚아채어 살펴볼 수 있으므로 보안에 상당히 주의를 요한다.
검색하다 알게된 사실인데 UAC가 도입된 윈도우 7부터는 해당 API를 사용시 프로그램 보안관련 메시지 창을 띄우거나
윈도우 설치 디렉토리 등에 영향을 주는 훜 작업은 금지되기도 한다고 한다.


//=========================================================
// 하나의 전역 다이얼로그 아닌 다수의 다이얼로그에 적용하기 
//---------------------------------------------------------

위의 경우는 g_hModelessDlg라는 하나의 전역변수 다이얼로그에 적용된다.
여러 다이얼로그에 모두 적용시키려면 GetMsgProc의 if (IsDialogMessage(g_hModelessDlg, lpMsg)) 조건문에서 모든 다이얼로그를 체크해야 가능하다.
이를 위해서는 다이얼로그 핸들을 전체적으로 관리할 자료구조가 필요하다.
Linked List(연결 리스트)를 사용하기로 한다.

연결 리스트에서 head 아닌 tail을 검색의 기준점으로 삼아 가장 최근에 열린 다이얼로그부터 탐색한다.
tail을 기준점으로 삼으면 추가시에도 노드 순회 없이 바로 가능하다.

위의 구성을 위해 필요한 메모리의 양은 다음과 같다.(32bit OS 기준)
전역변수: 훜 전역변수 4바이트, 탐색의 기준점이 되는 노드 포인터 4바이트
동적 메모리 할당: 각 다이얼로그 생성시 노드 구조체 크기인 8바이트씩 할당

참고로 동적 배열로 구성할 수도 있다.
배열로 구성한다면 추가시 배열 크기를 초과하면 메모리를 재할당해서 이전 데이터를 전부 옮겨야 하고 삭제시 뒷부분의 요소들을 전체 이동시켜야 한다.
처리할 코드가 많아지는 대신 배열의 장점은 빠른 접근, 검색인데 이게 수천개, 수만개 이상의 데이터를 검색하는 형태가 아니라서 딱히 메리트는 없다.
한 프로그램에서 다이얼로그를 띄워봤자 5개 이상 띄우고 작업하는 경우도 거의 없을 것이다.
아무튼 배열로 구성한다면 재할당이 거의 일어나지 않게 32개 정도로 충분한 공간을 마련하고, 삭제로 인한 이동시 memmove 같은 함수로 빠르게 처리하자.
그리고 탐색시 연결 리스트에서 tail을 기준으로 삼은 것처럼 가장 밑에서부터 즉 나중에 추가한 요소부터 시작하는게 효율적일 것이다.
사람의 특성상 가장 나중에 띄운 최근 다이얼로그에서 작업하는 게 가장 빈번하고 일반적이기 때문이다. 마치 스택 구조와 같은 느낌!

링크드 리스트로 구성한 코드는 다음과 같다.

// 다이얼로그 핸들을 Linked List(연결 리스트)로 관리한다.
// 맨 앞(head) 아닌 맨 끝(tail)을 탐색의 기준점으로 지정하는게 효율적이다.
// 대부분 이전 윈도우보다 최근 윈도우에서 작업하므로 탐색 시간이 절약되고 노드 추가시에도 순회할 필요 없다.
typedef struct _MODELESSDIALOGNODE {
HWND hDlg;
_MODELESSDIALOGNODE *prev;//next;
} MODELESSDIALOGNODE, *LPMODELESSDIALOGNODE;
MODELESSDIALOGNODE *g_MDtail;//g_MDhead

HHOOK g_hMDKHook;

LRESULT CALLBACK MDKGetMsgProc(int code, WPARAM wParam, LPMSG lpMsg)
{
MODELESSDIALOGNODE *node;

if (code >= 0 && // code가 음수인 경우 바로 CallNextHookEx를 수행해야 한다.(MSDN 참조)
PM_REMOVE == wParam && // 메시지가 실제로 메시지큐에서 꺼내져 제거되었음을 의미한다. PeekMessage는 존재여부 체크만 하고 리턴하는 경우도 있기 때문이

다.
lpMsg->message >= WM_KEYFIRST && lpMsg->message <= WM_KEYLAST) // 메시지 범위 지정
{
//for (node = g_MDhead; node; node = node->next)
for (node = g_MDtail; node; node = node->prev) // 노드 순회를 통해 해당 다이얼로그 메시지인지 검사한다.
{
if (IsDialogMessage(node->hDlg, lpMsg)) // 드디어 이 API를 수행한다.
{
lpMsg->message = WM_NULL; // 호출부에서 훜 체인이 끝난 후 계속 진행하므로 반드시 널 메시지로 교체해 어떤 수행을 하지 않게 한다

. 안 그러면 띵! 경고음이 울렸다.
lpMsg->wParam = 0;
lpMsg->lParam = 0;
return 0; // 원칙은 아래 함수를 호출해 다음 훜 프로시저로 진행하는 것이지만 WM_NULL 메시지로 바꿔버렸으므로 더 진행하는 게 의

미 없다.
// 다시 말하지만 이 경우만 예외적인 상황이고 원칙은 아래 함수를 수행해 다른 훜 프로시저들에게도 처리의 기회

를 줘야 한다.
}
}
}

return CallNextHookEx(g_hMDKHook, code, wParam, (LPARAM)lpMsg); // 다음 훜 프로시저로 진행한다. 체인 형태로 구성된다.
}

HHOOK __stdcall MDKSetHook(HWND hDlg)
{
MODELESSDIALOGNODE *new_node;//*node;

if (!g_hMDKHook) // 아직 훜이 설치 안 된 경우 노드를 생성하고 훜을 설치한다.
{
//if (!(g_MDhead = (MODELESSDIALOGNODE *)malloc(sizeof(MODELESSDIALOGNODE))))
//g_MDhead->hDlg = hDlg;
//g_MDhead->next = NULL;
if (!(g_MDtail = (MODELESSDIALOGNODE *)malloc(sizeof(MODELESSDIALOGNODE))))
return NULL;
g_MDtail->hDlg = hDlg;
g_MDtail->prev = NULL;
//MessageBoxW(hDlg, L"노드를 생성하였습니다.", L"MDKSetHook", MB_OK);
g_hMDKHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)MDKGetMsgProc, NULL, GetCurrentThreadId());
//MessageBoxW(hDlg, L"훜을 설치하였습니다.", L"MDKSetHook", MB_OK);
}
else // 이전 다이어얼로그에 의해 훜이 이미 설치된 경우 노드만 추가해 연결하면 된다.
{
// head가 기준점인 경우 노드 순회해 가장 뒤에 붙인다.
//for (node = g_MDhead; ; node = node->next)
//{
// if (node->next == NULL) // 연결된 노드가 없는 경우 즉 노드의 끝인 경우
// {
// if (!(new_node = (MODELESSDIALOGNODE *)malloc(sizeof(MODELESSDIALOGNODE))))
// return NULL;
// new_node->hDlg = hDlg;
// new_node->next = NULL;
// node->next = new_node;
// //MessageBoxW(hDlg, L"노드를 추가하였습니다.", L"MDKSetHook", MB_OK);
// break;
// }
//}

// tail이 기준점인 경우 이미 맨 끝이므로 노드 순회할 필요 없다. 
if (!(new_node = (MODELESSDIALOGNODE *)malloc(sizeof(MODELESSDIALOGNODE))))
return NULL;
new_node->hDlg = hDlg;
new_node->prev = g_MDtail; // 새로 추가한 노드의 pre에 기존 tail 노드를 입력한다.
g_MDtail = new_node; // 새로 추가한 노드를 tail 노드로 지정한다.
//MessageBoxW(hDlg, L"노드를 추가하였습니다.", L"MDKSetHook", MB_OK);
}

return g_hMDKHook;
}

void __stdcall MDKUnhook(HWND hDlg)
{
MODELESSDIALOGNODE *node, *next_node;//*prev_node;

// 현재 다이얼로그 노드를 찾아 링크를 재설정하고 현재 노드를 제거한다.
//prev_node = NULL;
//for (node = g_MDhead; node; node = node->next)
next_node = NULL;
for (node = g_MDtail; node; node = node->prev)
{
if (node->hDlg == hDlg)
{
// head가 기준점인 경우
//if (prev_node) // 이전 노드의 next에 다음 노드를 지정한다.
// prev_node->next = node->next;
//else // 이전 노드가 없으면 현재 노드가 헤드 노드란 의미이므로 헤드 노드에 다음 노드를 지정한다.
// g_MDhead = node->next;

// tail이 기준점인 경우
if (next_node) // 다음 노드의 pre에 이전 노드를 지정한다.
next_node->prev = node->prev;
else // 다음 노드가 없으면 현재 노드가 테일 노드란 의미이므로 테일 노드에 이전 노드를 지정한다.
g_MDtail = node->prev;

free(node);
//MessageBoxW(hDlg, L"노드를 제거하였습니다.", L"MDKUnhook", MB_OK);
break;
}
//prev_node = node; // 다음 회에서 현재의 노드가 이전 노드가 되므로 반복 전에 저장한다.
next_node = node; // 다음 회에서 현재의 노드가 다음 노드가 되므로 반복 전에 저장한다.
}

// 노드가 모두 제거된 경우 즉 이 다이얼로그를 끝으로 모두 닫힌 상황이면 훜을 제거한다.
if (!g_MDtail)//(!g_MDhead)
{
UnhookWindowsHookEx(g_hMDKHook);
g_hMDKHook = NULL;
//MessageBoxW(hDlg, L"훜을 제거하였습니다.", L"MDKUnhook", MB_OK);
}
}

위 함수들을 모달리스 다이얼로그의 생성부와 파괴부에서 수행하도록 지정한다.
case WM_INITDIALOG: 
MDKSetHook(hDlg);
...
return TRUE;

case WM_DESTROY:
MDKUnhook(hDlg);
...
return TRUE;


//===================================
// 스택 형태의 동적 배열로 구성해보기
//-----------------------------------

// 스택을 동적 배열로 구성한다.
HWND *g_phMDBase;
size_t g_uMDCapacity;
size_t g_uMDTop;

HHOOK g_hMDKHook;

LRESULT CALLBACK MDKGetMsgProc(int code, WPARAM wParam, LPMSG lpMsg)
{
size_t i;

if (code >= 0 && // code가 음수인 경우 바로 CallNextHookEx를 수행해야 한다.(MSDN 참조)
PM_REMOVE == wParam && // 메시지가 실제로 메시지큐에서 꺼내져 제거되었음을 의미한다. PeekMessage는 존재여부 체크만 하고 리턴하는 경우도 있기 때문이

다.
lpMsg->message >= WM_KEYFIRST && lpMsg->message <= WM_KEYLAST) // 메시지 범위 지정
{
for (i = g_uMDTop; /*i >= 0*/; --i) // 주의) size_t 타입 i는 항상 0 또는 양수이므로 i >= 0 조건식을 사용하면 안된다. 끝에서 0과 비교한다.
{
if (IsDialogMessage(g_phMDBase[i], lpMsg)) // 드디어 이 API를 수행한다.
{
lpMsg->message = WM_NULL; // 호출부에서 훜 체인이 끝난 후 계속 진행하므로 반드시 널 메시지로 교체해 어떤 수행을 하지 않게 한다

. 안 그러면 띵! 경고음이 울렸다.
lpMsg->wParam = 0;
lpMsg->lParam = 0;
return 0; // 원칙은 아래 함수를 호출해 다음 훜 프로시저로 진행하는 것이지만 WM_NULL 메시지로 바꿔버렸으므로 더 진행하는 게 의

미 없다.
// 다시 말하지만 이 경우만 예외적인 상황이고 원칙은 아래 함수를 수행해 다른 훜 프로시저들에게도 처리의 기회

를 줘야 한다.
}
if (!i)
break;
}
}

return CallNextHookEx(g_hMDKHook, code, wParam, (LPARAM)lpMsg); // 다음 훜 프로시저로 진행한다. 체인 형태로 구성된다.
}

HHOOK __stdcall MDKSetHook(HWND hDlg)
{
if (!g_hMDKHook) // 아직 훜이 설치 안 된 경우
{
g_uMDCapacity = 32; // 초기 할당 개수 지정
if (!(g_phMDBase = (HWND *)malloc(sizeof(g_phMDBase[0]) * g_uMDCapacity)))
return NULL;
g_uMDTop = 0;
*g_phMDBase = hDlg;
g_hMDKHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)MDKGetMsgProc, NULL, GetCurrentThreadId());
//MessageBoxW(hDlg, L"배열을 생성, push하고 훜을 설치하였습니다.", L"MDKLLSetHook", MB_OK);
}
else // 이전 다이어얼로그에 의해 훜이 이미 설치된 경우 push만 하면 된다.
{
if (g_uMDTop == g_uMDCapacity - 1) // 용량 초과시 재할당한다.
{
g_uMDCapacity *= 2; // 기존의 2배만큼 더 할당한다.
if (!(g_phMDBase = (HWND *)realloc(g_phMDBase, sizeof(g_phMDBase[0]) * g_uMDCapacity)))
return NULL;
//MessageBoxW(hDlg, L"배열 메모리를 재할당하였습니다.", L"MDKSetHook", MB_OK);
}
g_phMDBase[++g_uMDTop] = hDlg;
//MessageBoxW(hDlg, L"배열 요소를 push하였습니다.", L"MDKSetHook", MB_OK);
}

return g_hMDKHook;
}


void __stdcall MDKUnhook(HWND hDlg)
{
size_t i;

// top이 0인 경우 이 다이얼로그를 끝으로 모두 닫힌 상황이므로 아래로 진행할 필요 없이 훜을 제거하고 메모리를 해제한다.
if (!g_uMDTop)
{
UnhookWindowsHookEx(g_hMDKHook);
g_hMDKHook = NULL;
free(g_phMDBase);
//MessageBoxW(hDlg, L"마지막 다이얼로그이므로 훜을 제거하고 배열 메모리를 해제하였습니다.", L"MDKUnhook", MB_OK);
return;
}

for (i = g_uMDTop; /*i >= 0*/; --i) // 주의) size_t 타입 i는 항상 0 또는 양수이므로 i >= 0 조건식을 사용하면 안된다. 끝에서 0과 비교한다.
{
if (g_phMDBase[i] == hDlg)
{
// 뒷 부분 전체를 현재 위치로 당기면 된다.
memmove(g_phMDBase + i, g_phMDBase + i + 1, sizeof(g_phMDBase[0]) * (g_uMDTop - i));
//MessageBoxW(hDlg, L"배열 요소를 제거하였습니다.", L"MDKUnhook", MB_OK);
--g_uMDTop;
return;
}
if (!i)
break;
}
}


//=========================================
 더 간단히 전역 배열로 스택 구성해 처리하기
//-----------------------------------------

// 스택을 간단히 전역 배열로 구성하고 메모리 할당 코드를 제거한다.
HWND g_ahMDBase[32]; // 32개면 넘친다. 한 프로그램에서 32개 초과해서 다이얼로그를 띄우는 경우는 거의 없을 것이다.
size_t g_uMDTop;

HHOOK g_hMDKHook;

LRESULT CALLBACK MDKSimpleGetMsgProc(int code, WPARAM wParam, LPMSG lpMsg)
{
size_t i;

if (code >= 0 && // code가 음수인 경우 바로 CallNextHookEx를 수행해야 한다.(MSDN 참조)
PM_REMOVE == wParam && // 메시지가 실제로 메시지큐에서 꺼내져 제거되었음을 의미한다. PeekMessage는 존재여부 체크만 하고 리턴하는 경우도 있기 때문이

다.
lpMsg->message >= WM_KEYFIRST && lpMsg->message <= WM_KEYLAST) // 메시지 범위 지정
{
for (i = g_uMDTop; /*i >= 0*/; --i) // 주의) size_t 타입 i는 항상 0 또는 양수이므로 i >= 0 조건식을 사용하면 안된다. 끝에서 0과 비교한다.
{
if (IsDialogMessage(g_ahMDBase[i], lpMsg)) // 드디어 이 API를 수행한다.
{
lpMsg->message = WM_NULL; // 호출부에서 훜 체인이 끝난 후 계속 진행하므로 반드시 널 메시지로 교체해 어떤 수행을 하지 않게 한다

. 안 그러면 띵! 경고음이 울렸다.
lpMsg->wParam = 0;
lpMsg->lParam = 0;
return 0; // 원칙은 아래 함수를 호출해 다음 훜 프로시저로 진행하는 것이지만 WM_NULL 메시지로 바꿔버렸으므로 더 진행하는 게 의

미 없다.
// 다시 말하지만 이 경우만 예외적인 상황이고 원칙은 아래 함수를 수행해 다른 훜 프로시저들에게도 처리의 기회

를 줘야 한다.
}
if (!i)
break;
}
}

return CallNextHookEx(g_hMDKHook, code, wParam, (LPARAM)lpMsg); // 다음 훜 프로시저로 진행한다. 체인 형태로 구성된다.
}

HHOOK __stdcall MDKSimpleSetHook(HWND hDlg)
{
if (!g_hMDKHook) // 아직 훜이 설치 안 된 경우
{
g_uMDTop = 0;
*g_ahMDBase = hDlg;
g_hMDKHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)MDKSimpleGetMsgProc, NULL, GetCurrentThreadId());
//MessageBoxW(hDlg, L"배열 요소를 처음으로 push하고 훜을 설치하였습니다.", L"MDKSimpleSetHook", MB_OK);
}
else // 이전 다이어얼로그에 의해 훜이 이미 설치된 경우 push만 하면 된다.
{
if (g_uMDTop == sizeof(g_ahMDBase) / sizeof(g_ahMDBase[0]) - 1)
{
//MessageBoxW(hDlg, L"배열 용량을 초과했으므로 훜이 적용되지 않습니다.", L"MDKSimpleSetHook", MB_OK);
return NULL;
}
g_ahMDBase[++g_uMDTop] = hDlg;
//MessageBoxW(hDlg, L"배열 요소를 push하였습니다.", L"MDKSimpleSetHook", MB_OK);
}

return g_hMDKHook;
}

void __stdcall MDKSimpleUnhook(HWND hDlg)
{
size_t i;

// top이 0인 경우 이 다이얼로그를 끝으로 모두 닫힌 상황이므로 아래로 진행할 필요 없이 훜을 제거하고 메모리를 해제한다.
if (!g_uMDTop)
{
UnhookWindowsHookEx(g_hMDKHook);
g_hMDKHook = NULL;
//MessageBoxW(hDlg, L"마지막 다이얼로그이므로 훜을 제거하였습니다.", L"MDKSimpleUnhook", MB_OK);
return;
}

for (i = g_uMDTop; /*i >= 0*/; --i) // 주의) size_t 타입 i는 항상 0 또는 양수이므로 i >= 0 조건식을 사용하면 안된다. 끝에서 0과 비교한다.
{
if (g_ahMDBase[i] == hDlg)
{
// 뒷 부분 전체를 현재 위치로 당기면 된다.
memmove(g_ahMDBase + i, g_ahMDBase + i + 1, sizeof(g_ahMDBase[0]) * (g_uMDTop - i));
//MessageBoxW(hDlg, L"배열 요소를 제거하였습니다.", L"MDKSimpleUnhook", MB_OK);
--g_uMDTop;
return;
}
if (!i)
break;
}
}