Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법
지난 글에 다이얼로그 리소스 형식을 알아봤으니,
Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
; https://www.sysnet.pe.kr/2/0/13286
이번에는 그 리소스를 읽어 처리하는 "Dialog manager" 관점에서의 이야기를 풀어보겠습니다. 이것 역시 Raymond Chen의 oldnewthing 블로그에 실려 있는 글을 제 맘대로 정리한 것입니다. ^^
The dialog manager, part 1: Warm-ups
; https://devblogs.microsoft.com/oldnewthing/20050329-00/?p=36043
Modeless 대화창을 생성하는 CreateDialogXxx 유의 함수들을 볼까요?
// Modeless 대화창 생성하는 함수
#define CreateDialogA(hInstance, lpName, hWndParent, lpDialogFunc) CreateDialogParamA(hInstance, lpName, hWndParent, lpDialogFunc, 0L)
#define CreateDialogW(hInstance, lpName, hWndParent, lpDialogFunc) CreateDialogParamW(hInstance, lpName, hWndParent, lpDialogFunc, 0L)
#define CreateDialogIndirectA(hInstance, lpTemplate, hWndParent, lpDialogFunc) CreateDialogIndirectParamA(hInstance, lpTemplate, hWndParent, lpDialogFunc, 0L)
#define CreateDialogIndirectW(hInstance, lpTemplate, hWndParent, lpDialogFunc) CreateDialogIndirectParamW(hInstance, lpTemplate, hWndParent, lpDialogFunc, 0L)
위에서
CreateDialog 매크로가 연결된
CreateDialogParam의 경우 내부 코드에서
CreateDialogIndirectParam을 호출하게 됩니다.
HWND WINAPI CreateDialogParam(HINSTANCE hinst,
LPCTSTR pszTemplate, HWND hwndParent,
DLGPROC lpDlgProc, LPARAM dwInitParam)
{
HWND hdlg = NULL;
HRSRC hrsrc = FindResource(hinst, pszTemplate, RT_DIALOG);
if (hrsrc) {
HGLOBAL hglob = LoadResource(hinst, hrsrc);
if (hglob) {
LPVOID pTemplate = LockResource(hglob); // 여기서 읽힌 데이터는 대화창 Template을 나타내는 이진 코드
if (pTemplate) {
hdlg = CreateDialogIndirectParam(hinst, pTemplate, hwndParent, lpDlgProc, dwInitParam);
}
FreeResource(hglob);
}
}
return hdlg;
}
보는 바와 같이, 단지 대화창 리소스를 찾아 해당 바이너리 스트림을 메모리에 로드하는 기능을 살짝 래핑해서 이후의 실질적인 해석 절차는
CreateDialogIndirectParam 함수 내에서 하게 되는 것입니다.
그러니까, 최종적으로는 모든 CreateDialogXxx 유의 함수들이 결국 CreateDialogIndirectParam 호출로 연결됩니다.
두 번째 글도 마저 살펴보겠습니다. ^^
The dialog manager, part 2: Creating the frame window
; https://devblogs.microsoft.com/oldnewthing/20050330-00/?p=36023
이전 글에서 설명했듯이 근래에는 (Windows 2000부터 지원하기 시작한) "32-bit Extended Templates"을 사용하고 있는데요, 이에 대한 구조체는 표준 Win32 헤더 파일에 포함되지는 않았지만 공식 문서로는 나와 있습니다. (아마도, 중간의 필드가 가변 길이를 갖기 때문에 C/C++ 헤더 파일에 실을 수는 없었을 것입니다.)
DLGTEMPLATEEX structure
; https://learn.microsoft.com/en-us/windows/win32/dlgbox/dlgtemplateex
DLGITEMTEMPLATEEX structure
; https://learn.microsoft.com/en-us/windows/win32/dlgbox/dlgitemtemplateex
따라서 첫 번째 할 일은, LockResource로 구한 포인터로부터 아래의 구조체를 채워 넣으면 됩니다.
typedef struct {
WORD dlgVer;
WORD signature;
DWORD helpID;
DWORD exStyle;
DWORD style;
WORD cDlgItems;
short x;
short y;
short cx;
short cy;
sz_Or_Ord menu;
sz_Or_Ord windowClass;
WCHAR title[titleLen];
WORD pointsize;
WORD weight;
BYTE italic;
BYTE charset;
WCHAR typeface[stringLen];
} DLGTEMPLATEEX;
중간에 문자열 처리가 가변적으로 돼 코드가 길어지므로 구체적인 코드는 첨부 파일에 실었으니 참고하시고 간단하게 다음의 메서드로 처리한다고 가정하겠습니다.
{
// ...[생략]...
LPVOID lpv = ::LockResource(hglobal);
DWORD resSize = SizeofResource(hInst, hrsrc);
DLGTEMPLATEEX* item = DLGTEMPLATEEX::ReadDialog(hInst, hWndMainWindow, AboutModeless, lpv);
// 처리
delete item;
}
INT_PTR CALLBACK AboutModeless(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
{
WORD cmdId = LOWORD(wParam);
if (cmdId == IDOK || cmdId == IDCANCEL)
{
::DestroyWindow(hDlg);
return (INT_PTR)TRUE;
}
}
break;
}
return (INT_PTR)FALSE;
}
이후 가장 먼저 할 일은 DS_* 상수 스타일을 WS_* 또는 WS_EX_* 상수 스타일로 바꾸는 건데, 이에 대해 다음과 같이 정리하고 있습니다.
DS_MODALFRAME
==> Extended windw style: WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE
DS_CONTEXTHELP
==> Extended windw style: WS_EX_CONTEXTHELP
DS_CONTROL
==> window style에서 제거: WS_CAPTION, WS_SYSMENU
==> Extended windw style: WS_EX_CONTROLPARENT
(* DS_CONTROL 옵션을 WS_POPUP과는 배타적으로, WS_CHILD와는 함께 사용)
이전 글에서 만든 About 대화상자는 다음과 같은 스타일을 가지고 있기 때문에,
dwStyle == DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
대충 이런 식으로 처리해 주면 됩니다.
if ((style & DS_MODALFRAME) == DS_MODALFRAME)
{
style &= ~DS_MODALFRAME;
exStyle |= WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE;
}
// ...[생략: DS_CONTEXTHELP, DS_CONTROL]...
그다음, menu가 지정되었다면 로드하는 것도 추가하고,
if (menu != nullptr && menu->HasValue())
{
if (menu->ordinal != 0)
{
_hMenu = ::LoadMenu(NULL, MAKEINTRESOURCE(menu->ordinal));
}
else
{
_hMenu = ::LoadMenu(NULL, menu->name);
}
}
폰트 정보도 DS_SETFONT의 유무에 따라 다음과 같이 읽어들여 준비해 둡니다.
/*
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About Project1"
FONT 8, "MS Shell Dlg"
*/
if ((style & DS_SETFONT) == DS_SETFONT)
{
HDC dc = GetDC(nullptr);
int pixels = MulDiv(this->pointsize, GetDeviceCaps(dc, LOGPIXELSY), 72); // MM_TEXT 모드인 경우
ReleaseDC(0, dc);
_hFont = ::CreateFont(
-pixels,
0,
0,
0,
weight,
italic,
0,
0,
DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH,
typeface);
}
else if ((style & DS_FIXEDSYS) == DS_FIXEDSYS) // DS_SETFONT보다 우선순위가 낮음(NT 4 운영체제 이하에서 실행되는 것을 고려)
{
_hFont = (HFONT)GetStockObject(SYSTEM_FIXED_FONT);
}
else
{
_hFont = (HFONT)GetStockObject(SYSTEM_FONT);
}
자, 그다음은 대화창이 보일 위치를 정하는 건데요,
IDD_ABOUTBOX DIALOGEX 0, 0, 170, 62
이게 좀 재미있습니다.
WPF가 pixel에 독립적인 단위를 썼던 것처럼, Dialog도 0, 0, 170, 62라고 기록한 값이 픽셀이 아닌 DLU라는 단위를 사용하고 있습니다.
따라서
DLU 단위를 픽셀로 변환해야 하는데요, 아래는 그 방법을 보여줍니다.
long baseUinits = GetDialogBaseUnits();
DWORD baseUinitsX = LOWORD(baseUinits); // 필자의 시스템에서, 8
DWORD baseUinitsY = HIWORD(baseUinits); // 필자의 시스템에서, 16
_dlgPosition.left = MulDiv(x, baseUinitsX, 4);
_dlgPosition.right = _dlgPosition.left + MulDiv(cx, baseUinitsX, 4); // 4 x-DLU
_dlgPosition.top = MulDiv(y, baseUinitsY, 8);
_dlgPosition.bottom = _dlgPosition.top + MulDiv(cy, baseUinitsY, 8); // 8 y-DLU
하지만 해당 API 문서 및 Raymond Chen도 언급하듯이,
GetDialogBaseUnits is a crock
; https://devblogs.microsoft.com/oldnewthing/20040217-00/?p=40573
GetDialogBaseUnits 함수는 대화창이 시스템 폰트를 사용하는 경우를 위한 것이라고 합니다. 따라서 폰트가 지정되었다면
GetTextMetrics와 GetTextExtentPoint32를 이용해 baseUinitsX, baseUinitsY을 구해야 하는데 아쉽게도 2개 모두 HDC 타입의 인자를 필요로 합니다.
윈도우가 아직 생성 전인데, HDC 타입으로 구했다는 것은, 아마도 어차피 Font에 대한 평균 width, height를 구하면 되기 때문에 빈 HDC를 하나 생성해서 구한 것이 아닌가... 예상해 봅니다. ^^ 따라서 다음과 같은 식으로 구하는 것도 가능합니다.
HDC hDC = GetDC(nullptr);
::SelectObject(hDC, _hFont);
TEXTMETRIC tm = { 0 };
GetTextMetrics(hDC, &tm);
int baseUnitsY = tm.tmHeight;
SIZE size = { 0 };
GetTextExtentPoint32(hDC, L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 52, &size);
int baseUnitsX = (size.cx / 26 + 1) / 2;
int cxDlg = MulDiv(cx, baseUnitsX, 4);
int cyDlg = MulDiv(cy, baseUnitsY, 8);
::DeleteDC(hDC);
참고로, About 상자의 기본 폰트 정보로는,
FONT 8, "MS Shell Dlg"
baseUinitsX = 4, baseUinitsY = 8이 나오기 때문에 대화상자에 지정한 "0, 0, 170, 62" 크기가 그대로 반환됩니다. (마지막에 언급하겠지만, 위와 같이 구한 값은 정상적으로 대화창의 크기를 정하지 못했습니다.) (업데이트:
Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법)
DLU에서 pixel로 변환한 크기는 윈도우의 Title Bar, Frame 등의 크기를 제외한 순수 client area에 해당합니다. 따라서, Window style에 따라 이 영역에 대한 크기 조정까지 해야 하는데요, AdjustWindowRectEx를 이용해 이 작업을 다음과 같이 처리할 수 있습니다.
RECT rcAdjust = { 0, 0, cxDlg, cyDlg };
AdjustWindowRectEx(&rcAdjust, dwStyle, hmenu != NULL, dwExStyle);
cxDlg = rcAdjust.right - rcAdjust.left;
cyDlg = rcAdjust.bottom - rcAdjust.top;
마지막으로, 윈도우의 생성 위치는 부모 윈도우를 기준으로 하므로, 만약 DS_ABSALIGN 설정이 없다면 x, y의 위치도 함께 조정해 줍니다.
if ((style & DS_ABSALIGN) != DS_ABSALIGN)
{
POINT pt = { MulDiv(x, baseUnitsX, 4), MulDiv(y, baseUnitsY, 8) };
ClientToScreen(_hwndParent, &pt);
_dlgPosition.left = pt.x;
_dlgPosition.top = pt.y;
}
_dlgPosition.right = _dlgPosition.left + cxDlg;
_dlgPosition.bottom = _dlgPosition.top + cyDlg;
위치를 잡았으니 이제 화면에 보여줘야 하는데요, 대화창의 경우 생성과 함께 보이도록 만들지는 않았다고 합니다. 따라서 윈도우 style에 VISIBLE 설정이 있으면 이를 제거하는 것으로 최종적으로는 다음과 같은 코드로 대화창 윈도우를 생성하게 됩니다.
BOOL fWasVisible = style & WS_VISIBLE;
style &= ~WS_VISIBLE;
LPCWSTR className = nullptr;
if (windowClass != nullptr && windowClass->HasValue())
{
if (windowClass->ordinal != 0)
{
className = MAKEINTRESOURCE(windowClass->ordinal);
}
else
{
className = windowClass->name;
}
}
if (className == nullptr)
{
// 기본 대화창의 window classname == #32770
className = MAKEINTRESOURCE(32770);
}
_hDlg = CreateWindowExW(
exStyle,
className,
title,
style & 0xFFFF0000, // DS_* 스타일을 제거
_dlgPosition.left,
_dlgPosition.top,
_dlgPosition.right - _dlgPosition.left,
_dlgPosition.bottom - _dlgPosition.top,
_hwndParent,
_hMenu,
_hInstance,
nullptr);
::SetWindowLongPtr(_hDlg, DWLP_DLGPROC, (LPARAM)_dlgProc); // 사용자가 구현한 Dialog Procedure 호출을 위해
::SendMessageW(_hDlg, WM_SETFONT, (WPARAM)_hFont, FALSE); // 폰트 설정
::ShowWindow(_hDlg, SW_SHOW); // 원래는 이 단계에서 보여주진 않지만, 테스트를 위해 추가
위의 단계를 보면, 대화창과 관련된 몇 가지 의문이 풀립니다. 1) 사용자의 Dialog Procedure에서 WM_CREATE 이벤트를 받지 못하는 것은 윈도우를 생성 후 DWLP_DLGPROC 값을 설정하기 때문이고, 2) 마찬가지로 가장 먼저 받게 되는 이벤트가 WM_SETFONT인 것은 DWLP_DLGPROC 설정 후 폰트 설정이 뒤따르기 때문입니다.
실행 결과를 보면,
아직 내부 컨트롤들에 대한 처리가 없기 때문에 저렇게 빈 윈도우만 뜨게 됩니다. ^^
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
뒷이야기를 들여다볼까요? ^^
아래의 글을 보면,
What's so special about the desktop window?
; https://devblogs.microsoft.com/oldnewthing/20040224-00/?p=40493
Desktop 윈도우를 부모로 한 Modal 대화창을 띄우는 경우 바탕 화면 및 기타 다른 윈도우들이 먹통이 될 수 있다고 합니다.
- A modal dialog disables its owner.
- Every window is a descendant of the desktop.
- When a window is disabled, all its descendants are also disabled.
하지만 실제로 테스트를 해보면,
HWND hDesktopWnd = ::GetDesktopWindow();
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hDesktopWnd, About);
먹통이 되지 않았는데요, 아마도 2004년 저 당시의 윈도우에서 그랬을 것이고 이후로는 저 부분이 개선이 된 듯합니다.
또 하나는, 왜 대화창이 CreateWindowEx 호출 순간에는 보이지 않도록 했는지 설명하는 글이 있습니다.
Why are dialog boxes initially created hidden?
; https://devblogs.microsoft.com/oldnewthing/20040311-00/?p=40303
한 마디로, 저 당시에는 시스템 성능이 너무 좋지 않아서 그랬다고 합니다. 그런 탓에, 대화창이 뜨기도 전에 이미 대화창이 요구하는 입력을 타이핑한 후 Enter 키를 치면 대화창을 곧바로 종료시키게 만드는 묘기같은 사용법도 가능했다고 합니다.
시스템 성능이 나아진 지금도, 위의 처리가 유지된 것은 그것 나름대로 유용한 면이 있었다고 합니다. 일례로 CreateWindowEx 시점에 대화창이 보인 후, 사용자가 WM_INITDIALOG에서 넣어 두었던 컨트롤에 대한 조정 코드들이 수행되는 것이 보기에 좋지가 않았고, 또한 대화창이 보였을 때 체크박스 상자가 있어 체크를 했더니 이후에 실행된 WM_INITDIALOG 코드에서 체크 박스를 해제하는 동작이 뒤늦게 발생할 수 있다고... ^^;
마지막으로, 위에서 제가 작성한 코드의 대화창 크기가 올바르게 나오지 않았는데요, 수작업으로 맞춰 보니 각각 6과 13에서 정상적으로 대화창의 크기가 나왔습니다.
baseUnitsX = 6;
baseUnitsY = 13;
int cxDlg = MulDiv(this->cx, baseUnitsX, 4); // baseUnitsX:4 비율 == 1.5
int cyDlg = MulDiv(this->cy, baseUnitsY, 8); // baseUnitsY:8 비율 == 1.625
음... 저 값이 왜 저렇게 나왔는지 끼워 맞출 수가 없었습니다. ^^; 그나마 baseUnitsX의 경우 한글로 테스트를 했더니,
GetTextExtentPoint32(hDC, L"가나다", 3, &size); // size.cx == 18
int baseUnitsX = size.cx / 3.0;
6으로 나오긴 했지만, 암튼 정확한 공식을 찾을 수는 없었습니다. 혹시 아시는 분은 덧글 부탁드립니다. ^^
(업데이트:
Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]