Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)
우리가 만든 대화창의 경우, 일정 시간 동안 사용자가 입력을 하지 않는다면 Dialog Procedure에 코드를 추가해 자동으로 닫는 것이 가능합니다.
예를 들어 볼까요? ^^ Visual Studio로 만든 기본 Windows Application C++ 프로젝트에서 About 대화창의 코드를 다음과 같이 바꿀 수 있습니다.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
static UINT _idTimer;
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
_idTimer = SetTimer(hDlg, 0, 3000, nullptr);
return (INT_PTR)TRUE;
case WM_TIMER:
if (_idTimer)
{
::KillTimer(hDlg, _idTimer);
_idTimer = 0;
}
EndDialog(hDlg, 0);
break;
case WM_CLOSE: // modal 대화창의 우측 상단 'x' 버튼을 누른 경우,
::OutputDebugString(L"About Dialog - WM_CLOSE\n");
break;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
if (_idTimer)
{
::KillTimer(hDlg, _idTimer);
_idTimer = 0;
}
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
사용자가 OK/CANCEL 버튼이나 타이틀 바 영역의 'x' 버튼을 누르지 않는다면 3초 후 자동으로 대화창이 닫히게 됩니다.
자, 그렇다면 우리가 만든 대화창이 아닌 경우, 예를 들어
MessageBox라면 어떻게 할 수 있을까요? 여기서 관건은, MessageBox를 호출한 후 내부에서 실행되는 Message Loop를 일정 시간이 지났을 때 탈출하도록 만들어야 한다는 점입니다.
이것을 쉽게 구현하는 한 가지 방법을 다음의 글에서 소개하고 있습니다.
Modality, part 7: A timed MessageBox, the cheap version
; https://devblogs.microsoft.com/oldnewthing/20050301-00/?p=36333
방법은 간단합니다. SetTimer를 이용해 일정 시간 후
메시지 루프를 벗어나도록 WM_QUIT 메시지를 생성하는 것입니다. 그럼 당연히 MessageBox 내의 Message Loop는 벗어나게 될 것입니다. 하지만 그와 함께 다시 WM_QUIT 메시지가 메시지 큐에 저장이 되는데요, 그걸 그냥 두면 윈도우 응용 프로그램이 생성해 둔 메시지 루프 전체로 퍼지게 됩니다. 따라서, 이것을 방지하기 위해 메시지 큐에 있는 WM_QUIT를 그냥 제거하는 처리를 하면 됩니다.
바로 그 역할을 하는 코드가 "
Modality, part 7: A timed MessageBox, the cheap version" 글에 다음과 같이 실려 있습니다.
static BOOL s_fTimedOut;
static HWND s_hwndMBOwnerEnable;
void CALLBACK CheapMsgBoxTooLateProc(HWND hWnd, UINT uiMsg, UINT_PTR idEvent, DWORD dwTime)
{
s_fTimedOut = TRUE;
if (s_hwndMBOwnerEnable) EnableWindow(s_hwndMBOwnerEnable, TRUE); // 활성화 순서
PostQuitMessage(42); // value not important
}
int TimedMessageBox(HWND hwndOwner, LPCTSTR ptszText, LPCTSTR ptszCaption, UINT uType, DWORD dwTimeout)
{
s_fTimedOut = FALSE;
s_hwndMBOwnerEnable = NULL;
if (hwndOwner && IsWindowEnabled(hwndOwner)) {
s_hwndMBOwnerEnable = hwndOwner;
}
UINT idTimer = SetTimer(NULL, 0, dwTimeout, CheapMsgBoxTooLateProc);
int iResult = MessageBox(hwndOwner, ptszText, ptszCaption, uType);
if (idTimer) KillTimer(NULL, idTimer);
if (s_fTimedOut) { // We timed out
MSG msg;
// Eat the fake WM_QUIT message we generated
PeekMessage(&msg, NULL, WM_QUIT, WM_QUIT, PM_REMOVE);
iResult = -1;
}
return iResult;
}
보는 바와 같이 WM_QUIT를 생성해 MessageBox를 벗어난 다음, PeekMessage를 이용해 WM_QUIT를 제거하고 있습니다. 실제로 위의 함수를 이용하면,
TimedMessageBox(hWnd, L"TEXT", L"TITLE", MB_OK, 3000); // 3초
3초 후에 MessageBox가 종료되는 것을 확인할 수 있습니다. 그런데 위의 코드에는 문제가 있습니다. 바로 전역 변수를 사용하기 때문에 그로 인해 발생하는 충돌을 조심해서 사용해야 하는 것입니다.
근데, WM_QUIT를 사용하기 때문에 솔직히 좀 복잡합니다. ^^ 그냥 단순히 WM_CLOSE를 사용한다면 저런 후처리를 하지 않아도 되는데요, 여기서 문제는 WM_CLOSE를 보내기 위해 MessageBox의 윈도우 핸들(HWND)을 알아내야 한다는 것입니다.
재미있는 것은,
위의 방법을 지난 글에서 설명했다는 점입니다. 즉,
GetLastActivePopup을 사용하면 되는 건데요, 이것을 이용하면 위의 TimedMessageBox를 다음과 같이 축약시킬 수 있습니다.
#define IDT_TIMER 0x5000
void CALLBACK CheapMsgBoxTooLateProc(HWND hWnd, UINT uiMsg, UINT_PTR idEvent, DWORD dwTime)
{
HWND hDlg = ::GetLastActivePopup(hWnd);
if (IsWindow(hDlg))
{
::SendMessage(hDlg, WM_CLOSE, 0, 0);
}
}
int TimedMessageBox(HWND hwndOwner, LPCTSTR ptszText, LPCTSTR ptszCaption, UINT uType, DWORD dwTimeout)
{
UINT idTimer = SetTimer(hwndOwner, IDT_TIMER, dwTimeout, CheapMsgBoxTooLateProc);
int iResult = MessageBox(hwndOwner, ptszText, ptszCaption, uType);
if (idTimer != 0)
{
KillTimer(hwndOwner, IDT_TIMER);
}
return iResult;
}
하지만 위의 코드도 문제가 있습니다. hwndOwner를 CheapMsgBoxTooLateProc에 전달하기 위해 SetTimer의 첫 번째 인자에 전달했는데요, 만약 TimedMessageBox를 라이브러리 성격의 DLL에 추가시켜 재사용하는 경우라면 hwndOwner로 전달한 윈도우가 기존에 생성한 SetTimer의 ID 값들 중에 IDT_TIMER(0x5000)와 겹칠 수 있는 가능성이 (거의 낮은 확률이지만) 존재한다는 점입니다. (아쉽게도 SetTimer에는 CheapMsgBoxTooLateProc에 전달할 수 있는 LPARAM 등의 인자가 없습니다.)
따라서 timer id 조차도 인자로 받도록 구성하면 좀 나아질 것입니다.
이 정도면, 뭐 그런대로 원리는 아시겠죠? ^^ 다음에 다룰 part 8에서는 위에서 소개한 자잘한 문제들을 해결한, 약간 복잡한 코드를 소개합니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]