애플리케이션 상태 관리
창 프로시저는 모든 메시지에 대해 호출되는 함수이므로 기본적으로 상태 비저장입니다. 따라서 함수 호출 사이에 애플리케이션의 상태를 추적하는 방법이 필요합니다.
가장 간단한 접근 방식은 전역 변수에 모든 항목을 넣는 것입니다. 이 접근 방식은 소규모 프로그램에 충분히 제대로 적용되며 많은 SDK 샘플에서 이 접근 방식을 사용합니다. 그러나 큰 프로그램에서는 전역 변수가 많이 사용됩니다. 또한 창이 여러 개 있을 수 있으며 각각 고유한 창 프로시저가 있습니다. 어떤 창이 어떤 변수에 액세스해야 하는지 추적하는 것은 복잡하고 오류가 발생하기 쉽습니다.
CreateWindowEx 함수는 모든 데이터 구조를 창에 전달하는 방법을 제공합니다. 이 함수가 호출되면 창 프로시저에 다음 두 메시지를 전송합니다.
이 메시지는 나열된 순서대로 전송됩니다. CreateWindowEx 중에 이 두 메시지만 전송되는 것은 아니지만 여기서는 다른 메시지를 무시해도 됩니다.
WM_NCCREATE 및 WM_CREATE 메시지는 창이 표시되기 전에 전송됩니다. 따라서 창의 초기 레이아웃을 결정하는 경우처럼 UI를 초기화하는 데 위치입니다.
CreateWindowEx의 마지막 매개 변수는 void*형식의 포인터입니다. 이 매개 변수에서 원하는 포인터 값을 전달할 수 있습니다. 창 프로시저가 WM_NCCREATE 또는 WM_CREATE 메시지를 처리하면 메시지 데이터에서 이 값을 추출할 수 있습니다.
이 매개 변수를 사용하여 애플리케이션 데이터를 창에 전달하는 방법을 살펴보겠습니다. 먼저 상태 정보를 보유하는 클래스 또는 구조체를 정의합니다.
// Define a structure to hold some state information.
struct StateInfo {
// ... (struct members not shown)
};
CreateWindowEx를 호출할 때 최종 void* 매개 변수에서 이 구조체에 대한 포인터를 전달합니다.
StateInfo *pState = new (std::nothrow) StateInfo;
if (pState == NULL)
{
return 0;
}
// Initialize the structure members (not shown).
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
pState // Additional application data
);
WM_NCCREATE 및 WM_CREATE 메시지를 받으면 각 메시지의 lParam 매개 변수는 CREATESTRUCT 구조체에 대한 포인터입니다. CREATESTRUCT 구조체에는 CreateWindowEx에 전달한 포인터가 포함됩니다.
다음은 데이터 구조에 대한 포인터를 추출하는 방법입니다. 먼저 lParam 매개 변수를 캐스팅하여 CREATESTRUCT 구조체를 가져옵니다.
CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
CREATESTRUCT 구조체의 lpCreateParams 멤버는 CreateWindowEx에서 지정한 원래 void 포인터입니다. lpCreateParams를 캐스팅하여 사용자 고유의 데이터 구조에 대한 포인터를 가져옵니다.
pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);
다음으로 SetWindowLongPtr 함수를 호출하고 데이터 구조에 대한 포인터를 전달합니다.
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
이 마지막 함수 호출의 목적은 창에 대한 인스턴스 데이터에 StateInfo 포인터를 저장하는 것입니다. 이렇게 하면 GetWindowLongPtr을 호출하여 항상 창에서 포인터를 다시 가져올 수 있습니다.
LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
각 창에는 자체 인스턴스 데이터가 있으므로 여러 창을 만들고 각 창에 데이터 구조의 자체 인스턴스를 제공할 수 있습니다. 이 접근 방식은 사용자 지정 컨트롤 클래스를 만드는 경우처럼 창의 클래스를 정의하고 해당 클래스의 창을 두 개 이상 만드는 경우에 특히 유용합니다. 작은 도우미 함수에서 GetWindowLongPtr 호출을 래핑하는 것이 편리합니다.
inline StateInfo* GetAppState(HWND hwnd)
{
LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
return pState;
}
이제 다음과 같이 창 프로시저를 작성할 수 있습니다.
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
StateInfo *pState;
if (uMsg == WM_CREATE)
{
CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
}
else
{
pState = GetAppState(hwnd);
}
switch (uMsg)
{
// Remainder of the window procedure not shown ...
}
return TRUE;
}
개체 지향 접근 방식
이 접근 방식을 더 확장할 수 있습니다. 창에 대한 상태 정보를 저장할 데이터 구조를 이미 정의했습니다. 이 데이터 구조에 데이터에서 작동하는 멤버 함수(메서드)를 제공하는 것이 좋습니다. 이렇게 하면 자연스럽게 구조체(또는 클래스)가 창의 모든 작업을 담당하는 디자인으로 이어집니다. 그러면 창 프로시저가 클래스의 일부가 됩니다.
즉, 다음에서 이동할 수 있습니다.
// pseudocode
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
StateInfo *pState;
/* Get pState from the HWND. */
switch (uMsg)
{
case WM_SIZE:
HandleResize(pState, ...);
break;
case WM_PAINT:
HandlePaint(pState, ...);
break;
// And so forth.
}
}
값:
// pseudocode
LRESULT MyWindow::WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SIZE:
this->HandleResize(...);
break;
case WM_PAINT:
this->HandlePaint(...);
break;
}
}
유일한 문제는 MyWindow::WindowProc
메서드를 연결하는 방법입니다.
RegisterClass 함수는 창 프로시저가 함수 포인터가 될 것으로 예상합니다. 이 컨텍스트에서는 (비정적) 멤버 함수에 대한 포인터를 전달할 수 없습니다. 그러나 정적 멤버 함수에 대한 포인터를 전달한 다음, 멤버 함수에 위임할 수 있습니다. 다음은 이 접근 방식을 보여 주는 클래스 템플릿입니다.
template <class DERIVED_TYPE>
class BaseWindow
{
public:
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
DERIVED_TYPE *pThis = NULL;
if (uMsg == WM_NCCREATE)
{
CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam;
pThis = (DERIVED_TYPE*)pCreate->lpCreateParams;
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);
pThis->m_hwnd = hwnd;
}
else
{
pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
}
if (pThis)
{
return pThis->HandleMessage(uMsg, wParam, lParam);
}
else
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
BaseWindow() : m_hwnd(NULL) { }
BOOL Create(
PCWSTR lpWindowName,
DWORD dwStyle,
DWORD dwExStyle = 0,
int x = CW_USEDEFAULT,
int y = CW_USEDEFAULT,
int nWidth = CW_USEDEFAULT,
int nHeight = CW_USEDEFAULT,
HWND hWndParent = 0,
HMENU hMenu = 0
)
{
WNDCLASS wc = {0};
wc.lpfnWndProc = DERIVED_TYPE::WindowProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = ClassName();
RegisterClass(&wc);
m_hwnd = CreateWindowEx(
dwExStyle, ClassName(), lpWindowName, dwStyle, x, y,
nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this
);
return (m_hwnd ? TRUE : FALSE);
}
HWND Window() const { return m_hwnd; }
protected:
virtual PCWSTR ClassName() const = 0;
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0;
HWND m_hwnd;
};
BaseWindow
클래스는 특정 창 클래스가 파생되는 추상 기본 클래스입니다. 예를 들어 다음은 BaseWindow
에서 파생된 단순 클래스의 선언입니다.
class MainWindow : public BaseWindow<MainWindow>
{
public:
PCWSTR ClassName() const { return L"Sample Window Class"; }
LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
};
창을 만들려면 BaseWindow::Create
를 호출합니다.
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
MainWindow win;
if (!win.Create(L"Learn to Program Windows", WS_OVERLAPPEDWINDOW))
{
return 0;
}
ShowWindow(win.Window(), nCmdShow);
// Run the message loop.
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
순수 가상 BaseWindow::HandleMessage
메서드는 창 프로시저를 구현하는 데 사용됩니다. 예를 들어 다음 구현은 모듈 1의 시작 부분에 표시된 창 프로시저와 동일합니다.
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(m_hwnd, &ps);
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
EndPaint(m_hwnd, &ps);
}
return 0;
default:
return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}
return TRUE;
}
창 핸들은 멤버 변수(m_hwnd)에 저장되므로 매개 변수로 HandleMessage
에 전달할 필요가 없습니다.
MFC(Microsoft Foundation Classes) 및 ATL(액티브 템플릿 라이브러리) 같은 기존의 많은 Windows 프로그래밍 프레임워크는 기본적으로 여기에 표시된 것과 유사한 접근 방식을 사용합니다. 물론 MFC와 같은 완전히 일반화된 프레임워크는 비교적 단순한 이 예제보다 더 복잡합니다.