WWolves Den

DirectX 11 · 약 5분

WndProc에서 내 객체 찾기: 창마다 상태 연결하기

정적 전역 포인터 대신 창 인스턴스 데이터에 객체를 연결하고, 64비트 포인터와 객체 수명을 안전하게 다룹니다.

Wolfgadeok · 업데이트

콜백에는 this가 자동으로 전달되지 않는다

C++ 클래스 안에 그리기와 입력 함수를 모아 두어도 Windows가 호출하는 WndProc는 정해진 함수 형식을 따라야 합니다. 일반 멤버 함수에는 객체를 나타내는 숨은 this가 필요하므로 그 포인터를 그대로 창 프로시저로 등록할 수 없습니다. 자유 함수 또는 정적 멤버 함수를 진입점으로 사용하고, 적절한 객체를 찾아 실제 처리를 위임하는 구조가 필요합니다.

정적 포인터 하나에 창 객체를 저장하면 창이 하나일 때는 동작할 수 있지만, 창이 늘어날 때 마지막 객체가 앞의 객체를 덮어쓸 수 있습니다. 여러 창을 지원하려면 콜백이 받은 HWND에 해당하는 객체를 찾을 수 있어야 합니다. 전역 객체 하나로 모든 창을 처리하기보다 각 HWND와 해당 객체의 관계를 창별로 보관하는 구조를 생각해 봅시다.

생성 인수와 GWLP_USERDATA

CreateWindowEx의 마지막 인수에 객체 포인터를 전달하면 생성 메시지의 CREATESTRUCT에서 이를 받을 수 있습니다. WM_NCCREATE 시점에 GWLP_USERDATA에 저장하고 이후 메시지에서는 GetWindowLongPtrW로 가져오는 방식이 대표적입니다. 아래 코드는 연결 방식의 핵심만 나타냅니다. App과 HandleMessage의 구현, 창 생성, 오류 보고 등은 별도로 필요합니다.

예제 · cpp

// App은 HandleMessage(HWND, UINT, WPARAM, LPARAM)를 제공한다고 가정합니다.
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    App* app = reinterpret_cast<App*>(
        GetWindowLongPtrW(hwnd, GWLP_USERDATA));
    if (msg == WM_NCCREATE) {
        const auto* create = reinterpret_cast<const CREATESTRUCTW*>(lp);
        app = static_cast<App*>(create->lpCreateParams);
        if (!app) return FALSE;
        SetLastError(0);
        if (SetWindowLongPtrW(hwnd, GWLP_USERDATA,
                reinterpret_cast<LONG_PTR>(app)) == 0 && GetLastError() != 0)
            return FALSE;
    }
    if (msg == WM_NCDESTROY) {
        SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
        return DefWindowProcW(hwnd, msg, wp, lp);
    }
    return app ? app->HandleMessage(hwnd, msg, wp, lp)
               : DefWindowProcW(hwnd, msg, wp, lp);
}

64비트 대응과 소유권은 별개의 문제

포인터를 LONG 같은 32비트 정수에 저장하면 x64에서 값이 잘릴 수 있습니다. 포인터 크기에 맞는 LONG_PTR와 Get/SetWindowLongPtr 계열을 사용해야 합니다. 64비트 환경에서 연결이 끊긴다면 객체 포인터를 저장하고 꺼내는 과정에 잘리는 형 변환이 없는지 먼저 확인합니다. 반환값 0은 이전 값이 0이었다는 뜻일 수도 있으므로 설정 오류 판정도 API 규약에 맞춥니다.

GWLP_USERDATA에 저장한 포인터는 소유권을 자동으로 가져오거나 객체를 살려 두지 않습니다. 창이 살아 있는 동안 객체도 유효해야 하며, 객체를 지우기 전에 콜백이 더 이상 참조하지 않도록 설계합니다. 위 예제는 종료 시 연결만 지우고 객체 해제는 외부 소유자가 담당한다는 가정입니다. 리소스 해제까지 창 메시지에 맡기는 설계라면 해당 종료 경로를 별도로 구현해야 합니다.

  1. 같은 창 클래스로 두 창을 만들었을 때 입력 상태가 섞이지 않는지 검사합니다.
  2. 창 생성 실패 경로에서도 객체가 누수되지 않는지 확인합니다.
  3. 창을 닫은 뒤 오래된 포인터로 콜백이나 렌더링이 실행되지 않도록 정리 순서를 확인합니다.

참고 자료

예제는 학습용이며, 모든 환경에서 실행 검증된 완성 프로젝트는 아닙니다. 적용 전 사용 중인 버전과 프로젝트 환경에서 동작을 확인하세요.