Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법
지난 글에 이어,
Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법
; https://www.sysnet.pe.kr/2/0/13287
대화창 내의 컨트롤까지 생성하는 것을 다뤄보겠습니다. ^^
BEGIN
ICON IDR_MAINFRAME,IDC_STATIC,14,14,21,20
LTEXT "Project1, Version 1.0",IDC_STATIC,42,14,114,8,SS_NOPREFIX
LTEXT "Copyright (c) 2023",IDC_STATIC,42,26,114,8
DEFPUSHBUTTON "OK",IDOK,113,41,50,14,WS_GROUP
END
관련 내용은 시리즈의 3번째 글에서 잘 정리하고 있습니다.
The dialog manager, part 3: Creating the controls
; https://devblogs.microsoft.com/oldnewthing/20050331-00/?p=36003
이에 대한 구조체 정의도
DLGTEMPLATEEX와 마찬가지로 문서에만 있고 표준 Win32 헤더에는 없습니다.
DLGITEMTEMPLATEEX structure
; https://learn.microsoft.com/en-us/windows/win32/dlgbox/dlgitemtemplateex
typedef struct {
DWORD helpID;
DWORD exStyle;
DWORD style;
short x;
short y;
short cx;
short cy;
DWORD id;
sz_Or_Ord windowClass;
sz_Or_Ord title;
WORD extraCount;
} DLGITEMTEMPLATEEX;
문서의 구조체에서는 마지막에 extraCount로 맺고 있지만, 실제로는 extraCount 값이 0이 아닌 경우 이후의 데이터를 더 읽어들여 CreateWindowEx의 마지막 인자에 전달해야 하는 것으로 나옵니다. (하지만, 실제로는 거의 쓰이는 경우가 없다고 합니다.)
이 구조체 역시 중간에 sz_Or_Ord로 대표되는 가변 크기의 필드 때문에 읽어내는 것이 좀 복잡하므로 그 코드를 여기에 싣는 것은 생략합니다. (첨부 파일에는 모든 코드가 있습니다.)
암튼, 그래서 그 코드를 이용해 다음과 같이 읽어냈다고 가정합니다. ^^
BYTE* pNext = (BYTE*)lpv + forward; // DLGTEMPLATEEX을 읽은 후 DLGITEMTEMPLATEEX의 첫 번째 위치까지 이동
vector<DLGITEMTEMPLATEEX*> items; // 읽어낸 DLGITEMTEMPLATEEX를 보관할 vector list
for (int i = 0; i < pDialog->cDlgItems; i++)
{
size_t itemSize = 0;
DLGITEMTEMPLATEEX* item = DLGITEMTEMPLATEEX::ReadItem(pNext, itemSize);
pNext += itemSize;
items.push_back(item);
}
pDialog->_controls = items;
정상적으로 2진 데이터로부터 DLGITEMTEMPLATEEX를 컨트롤 수만큼 읽었다면 가장 먼저 처리해야 할 것은 x, y, cx, cy 필드에 대한 DLU 단위를 pixel로 처리하는 것입니다.
DLGITEMTEMPLATEEX* control = *it;
int x = DLUtoPixelX(control->x); // DLU를 pixel로 변환
int y = DLUtoPixelY(control->y);
int cx = DLUtoPixelX(control->cx);
int cy = DLUtoPixelY(control->cy);
그다음, windowClass 필드에 지정한 값(ordinal 또는 문자열)을 읽어야 하는데요, ordinal로 저장된 것이라도
미리 정의된 숫자들은 그에 해당하는 className으로 바꿔야 합니다.
LPCWSTR GetText(sz_Or_Ord* pOrdinal, BOOL resolveClassName)
{
WCHAR* pText = nullptr;
if (pOrdinal != nullptr && pOrdinal->HasValue())
{
if (pOrdinal->ordinal != 0)
{
if (resolveClassName == TRUE)
{
switch (pOrdinal->ordinal)
{
case 0x0080: return L"Button";
case 0x0081: return L"Edit";
case 0x0082: return L"Static";
case 0x0083: return L"List box";
case 0x0084: return L"Scroll bar";
case 0x0085: return L"Combo box";
}
}
else
{
pText = MAKEINTRESOURCE(pOrdinal->ordinal);
}
}
else
{
pText = pOrdinal->name;
}
}
return pText;
}
따라서 className과 title을 각각 다음과 같이 처리할 수 있습니다.
LPCWSTR className = GetText(control->windowClass, TRUE);
LPCWSTR title = GetText(control->title);
그런데, 여기서 한 가지 유의할 점이 ICON 형식의 static 컨트롤에 대한 처리입니다.
ICON IDR_MAINFRAME,IDC_STATIC,14,14,21,20
저렇게 rc 파일에서 ICON으로 정의되면 그것은 DLGITEMTEMPLATEEX의 style에 SS_ICON 필드가 설정됩니다. 또한 "title"에 해당하는 값은 윈도우 타이틀 문자열을 의미하지 않고 Icon 자원의 ordinal을 가리킵니다. 따라서 특별히 SS_ICON을 위해 다음과 같은 후처리를 필요로 합니다.
if ((control->style & SS_ICON) == SS_ICON)
{
control->_hIcon = LoadIcon(this->_hInstance, title); // 여기서의 title은 IDR_MAINFRAME
title = nullptr;
}
자, 끝입니다. ^^ 이제 DLGITEMTEMPLATEEX 정보에 기반해 윈도우를 생성하고 아이콘 설정, helpID 처리, font 설정만 하면 마무리가 됩니다. ^^
HWND hwndChild = CreateWindowEx(
control->exStyle | WS_EX_NOPARENTNOTIFY, // 대화창 컨트롤의 경우
className, title, control->style,
x, y, cx, cy, this->_hDlg, reinterpret_cast<HMENU>(control->id),
this->_hInstance, control->rgbExtra);
if (hwndChild == nullptr)
{
return FALSE;
}
if (control->_hIcon != nullptr)
{
SendMessage(hwndChild, STM_SETICON, (WPARAM)(HICON)(control->_hIcon), 0L);
}
if (control->helpID != 0)
{
SetWindowContextHelpId(hwndChild, control->helpID);
}
if (this->_hFont != nullptr)
{
::SendMessageW(hwndChild, WM_SETFONT, (WPARAM)this->_hFont, FALSE);
}
간단하죠. ^^ 위의 과정을 설명하면서 Raymond Chen은 대화창 생성이 실패하는 사례에 대해 설명합니다. 가령 해당 class name의 윈도우 클래스 등록이 이뤄지지 않았다면 위에서 대화창의 자식 컨트롤을 생성하는 과정에 실패해 대화창 생성이 안 된다는 것입니다. (Win32 common control을 사용했는데
InitCommonControlsEx 호출을 잊은 경우!)
그런데, 자식 컨트롤 생성에 실패해도 이를 무시하는 옵션으로
DS_NOFAILCREATE가 있다고 합니다. ^^
자... 어찌어찌해서 대화창 구성이 완료되었다면 이제서야 비로소 WM_INITDIALOG를 보낼 수 있게 됩니다.
BOOL ignoreFailCreate = ((this->style & DS_NOFAILCREATE) == DS_NOFAILCREATE);
if (CreateChildControls(ignoreFailCreate) == FALSE)
{
DestroyWindow(this->_hDlg);
this->_hDlg = nullptr;
return FALSE;
}
::SetWindowLongPtr(this->_hDlg, DWLP_DLGPROC, (LPARAM)this->_dlgProc);
::SendMessageW(this->_hDlg, WM_SETFONT, (WPARAM)this->_hFont, FALSE);
HDC hDC = GetDC(this->_hDlg);
TEXTMETRIC tm = { 0 };
GetTextMetrics(hDC, &tm);
// WM_INITDIALOG를 Dialog Procedure에 전송해서 초기화를 마무리
HWND hwndDefaultFocus = GetNextDlgTabItem(this->_hDlg, NULL, FALSE);
if (SendMessage(this->_hDlg, WM_INITDIALOG, (WPARAM)hwndDefaultFocus, 0)) {
// https://devblogs.microsoft.com/oldnewthing/20040802-00/?p=38283
SendMessage(this->_hDlg, WM_NEXTDLGCTL, (WPARAM)hwndDefaultFocus, TRUE);
}
::ShowWindow(this->_hDlg, SW_SHOW);
물론, 이 외에도 여러 가지 자잘한 작업들이 있는데, 일단 위와 같은 정도만 구현해도 제법 구색이 갖춰집니다. (실행해 보면 Win32 API로 수행한 대화창과 완전히 동일한 모습으로 생성되는 것을 확인할 수 있습니다.)
(첨부 파일은
지난 글의 예제 코드를
이번 글의 변경 사항을 적용해 수정한 버전입니다.)
잠시 뒷이야기를 다뤄볼까요? ^^
지금까지 리소스 파일을 읽어 대화창을 구성하는 것에 집중하느라 미처 하지 못한 작업이 있습니다. 바로 자원의 해제입니다.
만약 Modal 유형의 대화창을 처리하는 것이었다면, 위의 작업을 마무리한 후 대화창이 닫히면 자원 해제를 할 수 있습니다. 하지만 Modeless 유형이라면 어떨까요? 대화창을 닫는 작업을 Dialog Procedure 내에서 WM_DESTROY 메시지로 전송(DestroyWindow)할 텐데 그에 대한 처리를 할 수 있는 것은
DefDlgProc 함수가 됩니다.
그러니까, 우리에게 자원 해제를 할 기회가 주어지지 않는 것입니다. 이게 가능하려면 전에 설명한 것처럼,
Win32 C/C++ - Dialog Procedure를 재정의하는 방법
; https://www.sysnet.pe.kr/2/0/13285
DefDlgProc과 같은 Window Procedure를 지정한 우리의 대화창 class를 가져야 합니다. 다시 말해, Window 개발자들은
CreateDialog와 같은 Win32 API를 이 글에서 구현한 방식만으로 구현하는 것에 한계를 알고 있었던 것입니다.
아마도 그런 이유로 인해 #32770 window class가 등록되지 않았을까... 하는 예상을 해봅니다. ^^
또 한 가지 설명해야 할 것은, Visual Studio가 생성한 Windows Application의 기본 프로젝트에 있는 RC 파일의 About 대화상자에 지정한 아이콘의 리소스 ID가 실은 없는 자원을 나타낸다는 점입니다.
BEGIN
ICON IDR_MAINFRAME,IDC_STATIC,14,14,21,20
...[생략]...
END
저 IDR_MAINFRAME에 해당하는 ICON 리소스가 없기 때문에 실제로 제가 만든 함수든, 윈도우가 제공하는 CreateDialog든, 실행하게 되면 아이콘 영역이 비어 있습니다.
마이크로소프트가 사용자에게 의도적으로 재정의하라고 저렇게 한 것인지, 아니면 버그인지는 알 수 없으나 암튼 기본 생성한 RC 파일에는 다음의 2개 아이콘이 정의돼 있으므로,
IDI_PROJECT1 ICON "Project1.ico"
IDI_SMALL ICON "small.ico"
둘 중의 아무거나 하나를 지정해서 사용하시면 됩니다.
BEGIN
ICON IDI_PROJECT1,IDC_STATIC,14,14,21,20
...[생략]...
END
그럼 우리가 만든 대화창 소스코드로도 아래와 같이 정상적으로 ^^ 아이콘이 static 컨트롤 영역에 나타납니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]