일반적인 Microsoft Visual C++ 프로젝트에서는 메뉴와 대화상자를 정의하기 위해 별도의 리소스 스크립트 파일 (.rc) 을 사용하여 컴파일러로 생성된 .res 파일을 링크합니다. 그러나 특정 제약 조건이 있거나 단순한 유틸리티를 만들 때, 모든 설정을 소스 코드 내에서 직접 처리하는 방법이 필요합니다.
이 문서에서는 Win32 API 를 활용하여 별도의 리소스 파일 없이도 메인 창, 메뉴바, 단축키, 그리고 모달 입력 대화상자를 완전히 코드만으로 구현하는 방법을 다룹니다. 특히 대화상자 생성 시 복잡한 메모리 템플릿 조작 대신, 창 초기화 과정에서 컨트롤을 동적으로 생성하는 더 직관적인 방식을 적용합니다.
메뉴 및 단축키의 코드 기반 구현
리소스 식별자를 사용하지 않으므로 메뉴 ID 와 단축키 조합은 상수로 정의해야 합니다. CreateMenu 와 AppendMenu 함수를 호출하여 힌트를 얻은 메모리에 메뉴 구조를 직접 구축하고, 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;
}