Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
Window를 하나 생성하려면, 그 윈도우가 속한 "클래스"를 지정해야 하는데요, 일례로 신규 Windows Application 프로젝트를 생성하면 다음과 같은 식으로 기본 코드에 그 과정이 포함됩니다.
WCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
{
// ...[생략]...
LoadStringW(hInstance, IDC_MODALOWNER, szWindowClass, MAX_LOADSTRING);
// ...[생략]...
}
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_MODALOWNER));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_MODALOWNER);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // Store instance handle in our global variable
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
그리고, 위의 코드에서 cbClsExtra, cbWndExtra 필드가 바로 오늘 다룰 내용의 주제입니다. ^^
이름에서 의미하듯이 cbClsExtra는 윈도우 클래스 단위로 확보되는 메모리 크기이고, cbWndExtra는 윈도우 단위로 확보되는 메모리 크기입니다. 일반적인 OOP 개념에서 각각 클래스 필드, 인스턴스 필드에 대응하는 것으로 이해해도 무방합니다.
따라서, cbClsExtra에 지정하는 메모리 크기는 윈도우 클래스 레벨이므로 그 클래스로 생성되는 모든 윈도우에서 접근하는 공통 저장소 역할을 합니다. 반면, cbWndExtra의 경우라면 개별 윈도우 단위로 유지할 저장소를 제공합니다.
간단하게 예를 들어볼까요? ^^ cbClsExtra와 cbWndExtra에 대해 다음과 같이 포인터 크기만큼의 데이터를 확보해 보겠습니다.
wcex.cbClsExtra = sizeof(LONG_PTR);
wcex.cbWndExtra = sizeof(LONG_PTR);
이후 클래스 저장소를 접근하고 싶다면
GetClassLongPtr /
SetClassLongPtr API를 사용하면 됩니다. 이름에서 벌써 "Class"가 포함돼 있습니다.
반면 윈도우 레벨의 저장소를 접근하고 싶다면 (짐작할 수 있겠지만 "Window" 이름이 들어간)
GetWindowLongPtr /
SetWindowLongPtr API를 사용하면 됩니다.
이것을 이용해 다음과 같이 코딩해 보면,
// 예를 들기 위해 윈도우 2개 생성
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
HWND hWnd2 = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
// 윈도우 저장소 테스트
SetWindowLongPtr(hWnd, 0, 501); // hWnd 윈도우 공간에 501 기록
SetWindowLongPtr(hWnd2, 0, 601); // hWnd2 윈도우 공간에 602 기록
LONG_PTR v1 = GetWindowLongPtr(hWnd, 0); // v1 == 501
LONG_PTR v2 = GetWindowLongPtr(hWnd2, 0); // v2 == 601
// 클래스 저장소 테스트
SetClassLongPtr(hWnd, 0, 502); // 윈도우 클래스 공간에 502 기록
SetClassLongPtr(hWnd2, 0, 602); // 윈도우 클래스 공간에 602 기록
LONG_PTR v3 = ::GetClassLongPtr(hWnd, 0); // v3 == 602
LONG_PTR v4 = ::GetClassLongPtr(hWnd2, 0); // v4 == 602
cbClsExtra와 cbWndExtra 공간의 차이점을 확연히 알 수 있습니다. 윈도우가 다른 경우 cbWndExtra 공간도 달라지는 반면, 해당 윈도우들이 같은 클래스를 지정했다면 동일한 cbClsExtra 공간을 접근한 결과입니다.
위에서 cbWndExtra의 경우 포인터 공간을 확보하고 있는데요, 사실 Window Manager는 기본적으로 윈도우 생성 시 포인터 크기만큼의 공간을 사용자를 위해 1개 마련해 두고 있기 때문에 위와 같이 코딩할 필요가 없습니다.
해당 영역은 GWLP_USERDATA로 접근할 수 있는데요, 따라서 다음과 같이 코딩할 수 있습니다.
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
// ...[생략]...
wcex.cbWndExtra = 0;
// ...[생략]...
return RegisterClassExW(&wcex);
}
...[생략]...
{
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
HWND hWnd2 = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
SetWindowLongPtr(hWnd, GWLP_USERDATA, 500);
SetWindowLongPtr(hWnd2, GWLP_USERDATA, 600);
LONG_PTR v1 = GetWindowLongPtr(hWnd, GWLP_USERDATA); // v1 == 500
LONG_PTR v2 = GetWindowLongPtr(hWnd2, GWLP_USERDATA); // v2 == 600
return TRUE;
}
참고로, 웹상의 블로그 내용 중에는 cbWndExtra로 추가한 공간을 접근할 때 GWLP_USERDATA를 사용해야 한다고 설명하는 글이 있는데 올바르지 않습니다. GWLP_USERDATA는 기본적으로 생성된 공간이고, cbWndExtra로 추가한 공간은 (바이트 크기의) 0을 기준으로 GetWindowLongPtr, SetWindowLongPtr로 접근해야 합니다.
예를 들어, cbWndExtra에 2개 포인터만큼의 크기를 확보했다면,
wcex.cbWndExtra = sizeof(LONG_PTR) * 2;
해당 바이트의 크기만큼 offest을 지정해 다음과 같이 값을 다룰 수 있습니다.
SetWindowLongPtr(hWnd, 0, 501);
SetWindowLongPtr(hWnd2, 0, 601);
SetWindowLongPtr(hWnd, sizeof(LONG_PTR), 502);
SetWindowLongPtr(hWnd2, sizeof(LONG_PTR), 602);
LONG_PTR v1 = GetWindowLongPtr(hWnd, 0); // v1 == 501
LONG_PTR v2 = GetWindowLongPtr(hWnd2, 0); // v2 == 601
LONG_PTR v3 = GetWindowLongPtr(hWnd, sizeof(LONG_PTR)); // v3 == 502
LONG_PTR v4 = GetWindowLongPtr(hWnd2, sizeof(LONG_PTR)); // v4 == 602
마지막으로, 윈도우 단위로 저장하는 공간에서는 이 외에도 (
문서에 나오듯이) GWL_EXSTYLE, GWLP_HINSTANCE, GWLP_ID, GWL_STYLE, GWLP_WNDPROC 값이 있습니다.
그중에서, GWLP_HINSTANCE는 Window를 생성할 때 전달했던 HINSTANCE 값인데요,
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
대개의 경우, hInstance 값을 (편의상) 전역 변수에 저장해 다루곤 하는데 사실 윈도우 핸들(HWND)로부터 이 값을 구할 수 있기 때문에,
HINSTANCE hInst = (HINSTANCE)GetWindowLongPtr(hWnd, GWLP_HINSTANCE);
꼭 필요한 것은 아닙니다.
우리가 알고 있는 MFC 프레임워크는 GWLP_USERDATA 또는 cbWndExtra를 잘 활용한 하나의 예라고 보시면 됩니다. 이에 대해서는 마이크로소프트의 공식 문서에서 더 잘 설명하고 있으니 참고하시면 됩니다.
Managing Application State
; https://learn.microsoft.com/en-us/windows/win32/learnwin32/managing-application-state-
CreateWindowEx를 이용해 상태 정보를 마지막 인자 lpParam에 전달하고, WM_CREATE 메시지에 전달된 CREATESTRUCT로부터,
typedef struct tagCREATESTRUCTA {
LPVOID lpCreateParams; // CreateWindowEx에 전달한 lParam 값
HINSTANCE hInstance;
HMENU hMenu;
HWND hwndParent;
int cy;
int cx;
int y;
int x;
LONG style;
LPCSTR lpszName;
LPCSTR lpszClass;
DWORD dwExStyle;
} CREATESTRUCTA, *LPCREATESTRUCTA;
lParam에 전달했던 상태 정보를 lpCreateParams 필드로 구해 그것을 윈도우 저장소에 보관함으로써,
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
StateInfo *pState;
if (uMsg == WM_CREATE)
{
CREATESTRUCT *pCreate = reinterpret_cast(lParam);
pState = reinterpret_cast(pCreate->lpCreateParams);
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
}
else
{
pState = GetAppState(hwnd);
}
// ...[생략]...
}
inline StateInfo* GetAppState(HWND hwnd)
{
LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast(ptr);
return pState;
}
이후의 처리를 OOP 방식으로 매끄럽게 연결하는 것을 볼 수 있습니다. ^^
이번 글은, 아래의 글에 대한 장황한 버전이었습니다. ^^
The bonus window bytes at GWLP_USERDATA
; https://devblogs.microsoft.com/oldnewthing/20050303-00/?p=36293
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]