Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 21개 있습니다.)
Windows: 226. Win32 C/C++ - Dialog에서 값을 반환하는 방법
; https://www.sysnet.pe.kr/2/0/13284

Windows: 227. Win32 C/C++ - Dialog Procedure를 재정의하는 방법
; https://www.sysnet.pe.kr/2/0/13285

Windows: 228. Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
; https://www.sysnet.pe.kr/2/0/13286

Windows: 229. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법
; https://www.sysnet.pe.kr/2/0/13287

Windows: 230. Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법
; https://www.sysnet.pe.kr/2/0/13288

Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법
; https://www.sysnet.pe.kr/2/0/13289

Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage
; https://www.sysnet.pe.kr/2/0/13292

Windows: 233.  Win32 - modeless 대화창을 modal처럼 동작하게 만드는 방법
; https://www.sysnet.pe.kr/2/0/13295

Windows: 234. IsDialogMessage와 협업하는 WM_GETDLGCODE Win32 메시지
; https://www.sysnet.pe.kr/2/0/13296

Windows: 235. Win32 - Code Modal과 UI Modal
; https://www.sysnet.pe.kr/2/0/13297

Windows: 237. Win32 - 모든 메시지 루프를 탈출하는 WM_QUIT 메시지
; https://www.sysnet.pe.kr/2/0/13299

Windows: 238. Win32 - Modal UI 창에 올바른 Owner(HWND)를 설정해야 하는 이유
; https://www.sysnet.pe.kr/2/0/13300

Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)
; https://www.sysnet.pe.kr/2/0/13305

Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
; https://www.sysnet.pe.kr/2/0/13306

Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)
; https://www.sysnet.pe.kr/2/0/13312

Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의
; https://www.sysnet.pe.kr/2/0/13315

Windows: 246. Win32 C/C++ - 직접 띄운 대화창 템플릿을 위한 Modal 메시지 루프 생성
; https://www.sysnet.pe.kr/2/0/13329

Windows: 247. Win32 C/C++ - CS_GLOBALCLASS 설명
; https://www.sysnet.pe.kr/2/0/13330

Windows: 248. Win32 C/C++ - 대화창을 위한 메시지 루프 사용자 정의
; https://www.sysnet.pe.kr/2/0/13332

Windows: 249. Win32 C/C++ - 대화창 템플릿을 런타임에 코딩해서 사용
; https://www.sysnet.pe.kr/2/0/13333

Windows: 250. Win32 C/C++ - Modal 메시지 루프 내에서 SetWindowsHookEx를 이용한 Thread 메시지 처리 방법
; https://www.sysnet.pe.kr/2/0/13334




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;                            // title이 문자열인 경우 유니코드 + 소문자 사용 시 주의 ("Issue with Win32 API Loading of PE Resources Containing Lowercase Letters")
                                                // non-ASCII 문자열인 경우, 대문자로 변환해 검색하지 않음.
}

// 초기 윈도우 3.x 시절에는 progman.exe 파일에 MS-DOS 시절의 프로그램을 위한 아이콘이 들어 있었습니다.
// What were the MS-DOS programs that Windows used the progman.exe stock icons for?
// ; https://devblogs.microsoft.com/oldnewthing/20250506-00/?p=111149

// 하지만 점차로 지원하는 아이콘이 늘어나면서 별도로 분리해야 할 필요가 생겨 리소스 전용의 "moricons.dll" 파일이 추가됩니다.

// 재미있게도 progman.exe는 이제 파일 탐색기로 대체되면서 삭제되었지만, 
// moricons.dll은 (현재 64비트 윈도우가 MS-DOS 환경을 지원하지 않는데도) 살아남아 "C:\Windows\System32" 디렉터리에 존재하고 있습니다.
// 그리고 해당 아이콘들은 비주얼 스튜디오 등의 프로그램으로 DLL을 로드해 확인하는 것도 가능하지만, 아래의 글에 잘 정리가 돼 있으니 참고하세요. ^^
// What were the MS-DOS programs that the moricons.dll icons were intended for?
// ; https://devblogs.microsoft.com/oldnewthing/20250507-00/?p=111157

// Why does Windows have trouble finding my Win32 resource if it contains an accented character?
// ; https://devblogs.microsoft.com/oldnewthing/20250430-00/?p=111129
// FindResource 함수가 대소문자 구분은 하지 않는다고 합니다. 
// 재미있는 건 accent 기호가 있는 문자를 별도로 구분하고 있으며, 그것의 대소문자도 구분한다고 합니다.
// 달리 말해, _wcsupr 함수의 유니코드 지원이 완벽하지 않다는 것입니다.

// Exploring possible solutions to the inconsistency in how Windows searches case-insensitively for named resources
// ; https://devblogs.microsoft.com/oldnewthing/20250723-00/?p=111403
// PE 사양에는 리소스 문자열 이름에 대해 대소문자를 구분하지 않는다고만 할 뿐, 구체적으로 어떤 알고리즘을 사용할지는 지정하지 않는다는 것에서 모호성을 두고 있습니다.
// 이로 인해, RC 컴파일러는 C 로케일을 사용하는 반면, Windows의 리소스 서브 시스템은 사용자의 현재 로케일을 사용하는 불일치가 발생해버렸습니다.
// 나머지 내용은 이것을 해결할 수 있는 제안과, 왜 그 제안이 타당하지 않는지에 대한 설명입니다.


자, 끝입니다. ^^ 이제 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든, 실행하게 되면 아이콘 영역이 비어 있습니다.

dialog_manager_part2_1.png

마이크로소프트가 사용자에게 의도적으로 재정의하라고 저렇게 한 것인지, 아니면 버그인지는 알 수 없으나 암튼 기본 생성한 RC 파일에는 다음의 2개 아이콘이 정의돼 있으므로,

IDI_PROJECT1            ICON                    "Project1.ico"

IDI_SMALL               ICON                    "small.ico"

둘 중의 아무거나 하나를 지정해서 사용하시면 됩니다.

BEGIN
    ICON            IDI_PROJECT1,IDC_STATIC,14,14,21,20
    ...[생략]...
END

그럼 우리가 만든 대화창 소스코드로도 아래와 같이 정상적으로 ^^ 아이콘이 static 컨트롤 영역에 나타납니다.

dialog_manager_part2_2.png

(2025-04-24 업데이트)

아래의 문서를 보면,

What resource ID should I give my application’s main icon?
; https://devblogs.microsoft.com/oldnewthing/20250423-00/?p=111106

IDI_APPLICATION 상수가 있다고 하는데요, 아마도 이것이 (본문의 경우처럼) 근래에는 "IDI_[프로젝트명]"으로 바뀐 것 같습니다. (또한 그 상수값은 107입니다.) 따라서 "default icon"으로 나타내기 위해서는 이후의 아이콘에 대한 상수값을 "ID_[프로젝트명]"보다 크게 잡아야 합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 7/24/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... [136]  137  138  139  140  141  142  143  144  145  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1715정성태7/13/201432613기타: 47. 운영체제 종료 후에도 USB 외장 하드의 전원이 꺼지지 않는 경우 [3]
1714정성태7/11/201422324VS.NET IDE: 92. Visual Studio 2013을 지원하는 IL Support 확장 도구
1713정성태7/11/201446175Windows: 98. 윈도우 시스템 디스크 용량 확보를 위한 "Package Cache" 폴더 이동 [1]
1712정성태7/10/201435039.NET Framework: 450. 영문 윈도우에서 C# 콘솔 프로그램의 유니코드 출력 방법 [3]
1711정성태7/10/201439759Windows: 97. cmd.exe 창에서 사용할 폰트를 추가하는 방법 [1]
1710정성태7/8/201432499개발 환경 구성: 230. 유니코드의 Surrogate Pair, Supplementary Characters가 뭘까요?파일 다운로드2
1709정성태7/8/201429045VS.NET IDE: 91. Visual Studio에서 32/64비트 IIS Express 실행하는 방법
1708정성태7/7/201426396VS.NET IDE: 90. Visual Studio - 사용자 정의 정적 분석 규칙 만드는 방법 [3]파일 다운로드1
1707정성태7/4/201424356.NET Framework: 449. C#에서 C++로 VARIANT 넘겨주는 방법파일 다운로드1
1706정성태7/3/201422717.NET Framework: 448. .NET SmartClient 컨트롤을 윈도우 8/2012에서 활성화하는 방법파일 다운로드1
1705정성태7/2/201437798VC++: 78. 보이어-무어(Boyer-Moore) 알고리즘이 정말 빠를까? [6]파일 다운로드1
1704정성태7/2/201424091.NET Framework: 447. w3wp.exe AppPool 재생(recycle)하는 방법 정리
1703정성태7/2/201424976.NET Framework: 446. Assembly.Load를 이용해 GAC에 등록된 어셈블리를 로드하는 방법 [1]파일 다운로드1
1702정성태6/23/201425010Phone: 11. Xamarin.Forms - 2. XAML을 이용한 페이지 개발파일 다운로드1
1701정성태6/23/201437424개발 환경 구성: 229. .NET Reflector + Reflexil 도구를 이용해 DLL 코드 변경 [4]
1700정성태6/23/201423805VS.NET IDE: 89. Visual Studio에서 기본 제공되는 성능 프로파일 [2]
1699정성태6/22/201425682Phone: 10. Xamarin.Forms - 1. Forms 시작하기 [2]파일 다운로드1
1698정성태6/22/201427522.NET Framework: 445. [부연 설명] 쉬운 C# 코드를 어럽게 이해하기 [2]
1697정성태6/22/201423480VS.NET IDE: 88. Visual Studio에서 직접 컴파일하는 IL 언어 확장 도구 - IL Support
1696정성태6/22/201422717.NET Framework: 444. clojure와 C#을 통해 이해하는 Sequence와 Vector 형식의 차이점 [1]
1695정성태6/21/201421774개발 환경 구성: 228. PowerShell ISE에서 (입력 기능이 있는) 콘솔 응용 프로그램을 시작하는 방법
1694정성태6/21/201423762개발 환경 구성: 227. 닷넷 용 ClojureCLR 개발환경 설정
1693정성태6/20/201424373개발 환경 구성: 226. Clojure 언어의 윈도우 개발환경 설정
1692정성태6/19/201434621오류 유형: 231. Visual Studio 2013 한글 버전 설치 오류 - The form specified for the subject is not one supported or known by the specified trust provider
1691정성태6/18/201430308개발 환경 구성: 225. 유닉스 계열의 tail 명령어가 제공되는 PowerShell [1]
1690정성태6/18/201432926개발 환경 구성: 224. DirectShow 예제 구하는 방법 [3]
... [136]  137  138  139  140  141  142  143  144  145  146  147  148  149  150  ...