Game Dev/Article

Window Message Callback에 Class Member 함수 이용하기

AKer 2009. 9. 16. 22:37
반응형
대부분의 Win32 Application에서 Message Callback 함수에는 전역 함수가 사용된다. 하지만 이렇게 전역함수를 사용하는 것은 객체지향 패러다임에도 맞지 않을 뿐 아니라 Main Loop와 Message Proc 간의 원활한 연동이 힘들다. 이 둘간의 통신을 위해서 또다른 전역 함수나 전역 변수를 사용해야 하는 것이다. 

LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	// .....
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
	// Register Class
	WNDCLASSEX wc;
	ZeroMemory(&wc, sizeof(wc));
	wc.cbSize = sizeof(wc);
	wc.style = CS_CLASSDC;
	wc.lpfnWndProc = (WNDPROC)MsgProc;
	wc.hInstance = GetModuleHandle(NULL)
	wc.lpszClassName = L"Window Name";
	RegisterClassEx(&wc);

	// Create Window
	HWND window = CreateWindow(.....);
}

따라서 Message Loop 및  윈도우를 관리하는 Wrapper Class를 별도로 제작하여 lpfnWndProc에 Class Member 함수를 대입하는 방법이 필요하다. 하지만 여기에도 풀어야 할 난제가 있다. 얼핏 생각하면 Class의 Member 함수를 그대로 대입하면 될 것 같지만 WndProc의 Callback 형식 때문에 Class Member 함수의 주소를 넣는다면 컴파일 에러가 나기 때문이다. 

LRESULT WINAPI WindowWrapper::MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	// Make It static
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
	// Register Class
	WNDCLASSEX wc;
	// .....
	wc.lpfnWndProc = (WNDPROC)WindowWrapper::MsgProc; // <----- static  
	RegisterClassEx(&wc);

	// Create Window
	HWND window = CreateWindow(.....);
}

가장 간단한 방법은 해당 멤버 함수를 static으로 선언하는 것이다. static 함수의 Callback은 내부적으로 Class형이 붙지 않기 때문에 가능한 방법이지만 역시 Main Loop와 Message Proc간의 데이터 교환은 WrapperClass의 static 변수와 static 함수를 통해서만 가능하다. static 함수 내에는 Class의 일반 Member를 사용할 수 없기 때문이다. 

따라서 별도의 해법이 필요한데 그 중 하나는 Window Hook을 이용하는 것이다. Window의 Create Message를 이용하여 윈도우의 User Data에 특수한 정보를 써 넣은 후, 그 정보를 이용해서 다시 Messaga를 받을 윈도우를 추론하는 것이다. 먼저 CreateWindow를 하기 전 Hooking을 위한 함수를 등록한다. 아래 예제에서 WindowWrapper::CBTProc이 그러한 함수이며 static 함수이다.

// Set Hook for Member Functioned "WNDPROC"
HHOOK hHook = SetWindowsHookEx(WH_CBT, WindowWrapper::CBTProc, 0, GetCurrentThreadId());
if (hHook)
{
	// Create Window
	HWND window = CreateWindow(
		windowName,
		windowName,
		style,
		0,
		0,
		windowSize.cx,
		windowSize.cy,
		GetDesktopWindow(),
		NULL,
		wc.hInstance,
		this // <----- User Data에 등록할 Wrapper Class의 Instance 주소
	);
	UnhookWindowsHookEx(hHook);
}

이렇게 CreateWindow 전에 Hooking 함수를 등록하면 WM_CREATE가 발생하기 이전에 HCBT_CREATEWND라는 메세지가 CBTProc으로 전달된다. 이 때 이 메세지에 전달된 인자들을 분석하여 CreateWindow 함수의 맨 마지막 this 인자를 추론할 수 있다. 그리고 이 추론된 this(=WindowWrapper의 주소)를 SetWindowLong 등의 함수를 이용하여 별도의 User Data 영역에 기록하여 둔다.

LRESULT CALLBACK CApplication::CBTProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	if (HCBT_CREATEWND == nCode)
	{
		HWND window = (HWND)wParam;
		LPCBT_CREATEWND cbtInfo = (LPCBT_CREATEWND)lParam;
		LPCREATESTRUCT windowInfo = cbtInfo->lpcs;

		SetWindowLong(window, GWL_USERDATA, (LONG)windowInfo->lpCreateParams); // <-----
	}

	return 0;
}

그리고 CBTProc의 HCBT_CREATEWND 처리가 리턴되면 아까 등록한 MsgProc에 WM_CREATE 및 모든 메세지들이 들어오기 시작할 것이다. 이 때 우리의 목표는 static인 MsgProc에서 WindowWrapper 인스턴스로 메세지를 전달하는 것이므로, SetWindowLong으로 설정한 WindowWrapper 주소를 가지고 유효한 포인터를 찾아내야 한다.

LRESULT WINAPI WindowWrapper::Proc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	WindowWrapper* pWindow = (WindowWrapper*)GetWindowLong(hWnd, GWL_USERDATA);
	if (pWindow)
	{
		return pWindow->RealMsgProc(hWnd, msg, wParam, lParam); // Not Static
	}
	else
	{
		return DefWindowProc(hWnd, msg, wParam, lParam);
	}
}

하지만 보면 알겠지만 2개의 static 함수가 추가로 필요하고, 매번 메세지가 들어올 때마다 형변환 및 포인터의 유효성을 검사해야 하기 때문에 오버헤드가 있을 수 있다. 물론 RealMsgProc 내에서 필요할 때마다 WindowWrapper의 Member 함수 및 변수를 자유롭게 사용할 수 있다. 

요새 제작 중인 개인 프로젝트에 적용해 본 결과 크게 성능이 떨어지는 것 같지는 않으며, MFC나 ATL 등을 구현할 때 사용된 방법도 있다고 하는데 나중에 봐야겠다. 

어쨌건... 


반응형