자원 파일 없이 구성하는 Win32 단일 소스 윈도우 애플리케이션

일반적인 Microsoft Visual C++ 프로젝트에서는 메뉴와 대화상자를 정의하기 위해 별도의 리소스 스크립트 파일 (.rc) 을 사용하여 컴파일러로 생성된 .res 파일을 링크합니다. 그러나 특정 제약 조건이 있거나 단순한 유틸리티를 만들 때, 모든 설정을 소스 코드 내에서 직접 처리하는 방법이 필요합니다.

이 문서에서는 Win32 API 를 활용하여 별도의 리소스 파일 없이도 메인 창, 메뉴바, 단축키, 그리고 모달 입력 대화상자를 완전히 코드만으로 구현하는 방법을 다룹니다. 특히 대화상자 생성 시 복잡한 메모리 템플릿 조작 대신, 창 초기화 과정에서 컨트롤을 동적으로 생성하는 더 직관적인 방식을 적용합니다.

메뉴 및 단축키의 코드 기반 구현

리소스 식별자를 사용하지 않으므로 메뉴 ID 와 단축키 조합은 상수로 정의해야 합니다. CreateMenuAppendMenu 함수를 호출하여 힌트를 얻은 메모리에 메뉴 구조를 직접 구축하고, SetMenu 를 통해 메인 윈도우에 적용합니다.

단축키는 ACCEL 구조체 배열을 정의한 후 CreateAcceleratorTable 로 변환합니다. 메시지 루프 내부에서 TranslateAccelerator 가 해당 테이블을 참조하여 키 입력을 명령 메시지로 변환하도록 설정하면 됩니다.

모달 대화상자의 메모리 템플릿 방식

대부분의 경우 DialogBox 는 리소스 ID 를 필요로 하지만, DialogBoxIndirectParam 를 사용하면 Heap 이나 글로벌 메모리에 할당된 DLGTEMPLATE 구조체를 전달할 수 있습니다. 과거에는 각 컨트롤의 위치와 속성을 바이트 단위로 계산해 메모리에 쓰는 방식이 선호되었으나, 이는 오류 가능성이 높고 유지보수가 어렵습니다.

따라서 본 예제에서는 대용량 메모리 할당과 정렬 계산만 필요한 최소한의 템플릿 헤더만을 작성한 뒤, 실제 입력창과 버튼은 대화상자의 WM_INITDIALOG 메시지 핸들러 내부에서 CreateWindow 함수를 호출하여 추가하는 방식을 사용합니다. 이를 통해 시각적 레이아웃 관리가 훨씬 용이해집니다.

include <windows.h>
include <tchar.h>
include <malloc.h>

// 전역 변수 및 상수 정의
HINSTANCE g_hInstance = NULL;
const int APP_WIDTH = 600;
const int APP_HEIGHT = 400;

// 메뉴 명령 ID
#define CMD_NEW       1001
#define CMD_EXIT      1002
#define CMD_ABOUT     1003

// 대화상자 컨트롤 ID
#define DlgEditCtrl   2001
#define DlgBtnOk      IDOK
#define DlgBtnCancel  IDCANCEL

// 함수 선언
ATOM RegisterAppClass(HINSTANCE hInst);
BOOL InitWindow(HINSTANCE hInst, int nCmdShow);
INT_PTR CreateCustomDialog(HWND hOwner, LPVOID lParam);
LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK DialogMsgHandler(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int);

다음은 전체 응용 프로그램을 하나의 C++ 파일로 통합한 예제입니다. 주요 변경점은 리소스 로딩 제거, 메모리 관리 최적화, 그리고 대화상자 내부 컨트롤 생성 로직의 개선입니다.

// 다이얼로그 템플릿 생성기
INT_PTR CreateCustomDialog(HWND hOwner, LPVOID lParamPtr)
{
    INT_PTR result = -1;
    HGLOBAL memBlock = GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, 1024);
    LPDLGTEMPLATE pTemplate = (LPDLGTEMPLATE)memBlock;

    // 기본 템플릿 정보 세팅 (컨트롤 개수는 0 으로)
    pTemplate->style = WS_POPUP | WS_CAPTION | WS_SYSMENU | DS_MODALFRAME;
    pTemplate->x = CW_USEDEFAULT;
    pTemplate->y = CW_USEDEFAULT;
    pTemplate->cx = 300;
    pTemplate->cy = 200;
    pTemplate->cdit = 0; // 컨트럴은 런타임에 생성하므로 0

    // 메뉴 없음, 클래스는 대화상자 기본 클래스 사용
    WORD* pw = (WORD*)(pTemplate + 1);
    *pw++ = 0; 
    *pw++ = 0; 

    // 클래스명 설정 (wchar_t 기준 8 바이트 이상 정렬 권장)
    wcsncpy((LPWSTR)pw, L"SimpleInputDlg", 15);
    
    GlobalUnlock(memBlock);

    // 파라미터가 있는 인덱스 대화상자 호출
    result = DialogBoxIndirectParam(g_hInstance, (LPDLGTEMPLATE)memBlock, hOwner, DialogMsgHandler, (LPARAM)lParamPtr);

    GlobalFree(memBlock);
    return result;
}

LRESULT CALLBACK DialogMsgHandler(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static HWND hEditControl = NULL;
    static HWND hBtnOk = NULL;
    static HWND hBtnCancel = NULL;
    static wchar_t outputBuffer[256];

    switch (msg)
    {
        case WM_INITDIALOG:
            // 사용자가 넘긴 초기 데이터 복사
            wcscpy_s(outputBuffer, (wchar_t*)lParam);
            
            // 동적으로 편집박스 생성
            hEditControl = CreateWindow(
                WC_EDIT, NULL,
                WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOVSCROLL | ES_MULTILINE,
                20, 20, 260, 100,
                hDlg, (HMENU)DlgEditCtrl, g_hInstance, NULL
            );
            
            SetWindowText(hEditControl, outputBuffer);

            // 버튼들 생성
            hBtnOk = CreateWindow(WC_BUTTON, _T("확인"),
                WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON,
                20, 140, 70, 25, hDlg, (HMENU)DlgBtnOk, g_hInstance, NULL);
                
            hBtnCancel = CreateWindow(WC_BUTTON, _T("취소"),
                WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
                110, 140, 70, 25, hDlg, (HMENU)DlgBtnCancel, g_hInstance, NULL);
                
            return TRUE;

        case WM_COMMAND:
            if (LOWORD(wParam) == DlgBtnOk || LOWORD(wParam) == DlgBtnCancel)
            {
                // 결과 값을 출력 버퍼로 가져옴 (정적 변수 활용으로 데이터 회피)
                GetWindowText(hEditControl, outputBuffer, 255);
                // 실제 애플리케이션에서는 lParam 으로 전달된 포인터에 직접 씁니다.
                // 여기서는 간소화를 위해 전역/정적 영역에 의존했으나 확장 시 주의 필요
                EndDialog(hDlg, LOWORD(wParam));
                return TRUE;
            }
            return FALSE;

        default:
            return DefWindowProc(hDlg, msg, wParam, lParam);
    }
}

// 메인 윈도우 절차
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HMENU hMainMenu = NULL;
    HACCEL hAccelTable = NULL;
    
    switch (uMsg)
    {
        case WM_CREATE:
            // 메뉴 생성
            hMainMenu = CreateMenu();
            AppendMenu(hMainMenu, MF_STRING, CMD_NEW, _T("&새로 만들기"));
            AppendMenu(hMainMenu, MF_SEPARATOR, 0, NULL);
            AppendMenu(hMainMenu, MF_STRING, CMD_EXIT, _T("종&료"));
            AppendMenu(hMainMenu, MF_STRING, CMD_ABOUT, _T("&정보"));
            SetMenu(hWnd, hMainMenu);
            break;

        case WM_COMMAND:
            switch (LOWORD(wParam))
            {
                case CMD_NEW:
                    MessageBox(hWnd, _T("새로운 작업을 시작합니다."), _T("메시지"), MB_OK);
                    break;
                case CMD_ABOUT:
                {
                    wchar_t buffer[256] = _T("초기값: 빈 입력란");
                    if (CreateCustomDialog(hWnd, (LPVOID)buffer) == IDOK)
                    {
                        MessageBox(hWnd, buffer, _T("입력 결과"), MB_OK);
                    }
                    break;
                }
                case CMD_EXIT:
                    DestroyWindow(hWnd);
                    break;
            }
            return 0;

        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;

        default:
            return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return 0;
}

실행 루프 및 초기화

마지막으로 인스턴스 등록과 창 생성을 담당하는 초기화 단계가 필요합니다. 여기서 가속기 테이블을 정의하여 Ctrl+W 나 Alt+F2 같은 단축키를 매핑할 수 있습니다.

ATOM RegisterAppClass(HINSTANCE hInst)
{
    WNDCLASSEX wcex = {};
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.lpfnWndProc = MainWndProc;
    wcex.hInstance = hInst;
    wcex.lpszClassName = _T("Win32PureSourceApp");
    wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.hIconSm = wcex.hIcon;

    return RegisterClassEx(&wcex);
}

HACCEL CreateShortcutTable()
{
    static ACCEL accels[] = {
        {FVIRTKEY | FCONTROL, VK_W, CMD_EXIT}, // Ctrl+W 종료
        {FVIRTKEY | FALT,      VK_F1, CMD_ABOUT}   // Alt+F1 정보
    };
    return CreateAcceleratorTable(accels, 2);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR cmdLine, int showCmd)
{
    g_hInstance = hInstance;
    MSG msg = {};
    HACCEL hAccel = CreateShortcutTable();

    if (!RegisterAppClass(hInstance))
    {
        return FALSE;
    }

    HWND hWnd = CreateWindow(
        _T("Win32PureSourceApp"), _T("리소스 없는 애플리케이션"),
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,
        NULL, NULL, hInstance, NULL
    );

    ShowWindow(hWnd, showCmd);
    UpdateWindow(hWnd);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccel, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int)msg.wParam;
}

태그: win32-api cplusplus windows-sdk dialog-procedure memory-allocation

8월 1일 07:43에 게시됨